mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-10 03:27:54 +00:00
feat(domain): add skill publish, query, download, and tag services with tests
This commit is contained in:
parent
c9f9ef03f2
commit
28040f36a2
9 changed files with 1248 additions and 0 deletions
|
|
@ -31,6 +31,15 @@
|
|||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.iflytek.skillhub</groupId>
|
||||
<artifactId>skillhub-storage</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
package com.iflytek.skillhub.domain.skill.service;
|
||||
|
||||
import com.iflytek.skillhub.domain.event.SkillDownloadedEvent;
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import com.iflytek.skillhub.storage.ObjectStorageService;
|
||||
import com.iflytek.skillhub.storage.ObjectMetadata;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class SkillDownloadService {
|
||||
|
||||
private final NamespaceRepository namespaceRepository;
|
||||
private final SkillRepository skillRepository;
|
||||
private final SkillVersionRepository skillVersionRepository;
|
||||
private final SkillTagRepository skillTagRepository;
|
||||
private final ObjectStorageService objectStorageService;
|
||||
private final VisibilityChecker visibilityChecker;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
public SkillDownloadService(
|
||||
NamespaceRepository namespaceRepository,
|
||||
SkillRepository skillRepository,
|
||||
SkillVersionRepository skillVersionRepository,
|
||||
SkillTagRepository skillTagRepository,
|
||||
ObjectStorageService objectStorageService,
|
||||
VisibilityChecker visibilityChecker,
|
||||
ApplicationEventPublisher eventPublisher) {
|
||||
this.namespaceRepository = namespaceRepository;
|
||||
this.skillRepository = skillRepository;
|
||||
this.skillVersionRepository = skillVersionRepository;
|
||||
this.skillTagRepository = skillTagRepository;
|
||||
this.objectStorageService = objectStorageService;
|
||||
this.visibilityChecker = visibilityChecker;
|
||||
this.eventPublisher = eventPublisher;
|
||||
}
|
||||
|
||||
public record DownloadResult(
|
||||
InputStream content,
|
||||
String filename,
|
||||
long contentLength,
|
||||
String contentType
|
||||
) {}
|
||||
|
||||
public DownloadResult downloadLatest(
|
||||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
Long currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Skill not found: " + skillSlug));
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new SecurityException("Access denied to skill: " + skillSlug);
|
||||
}
|
||||
|
||||
if (skill.getLatestVersionId() == null) {
|
||||
throw new IllegalArgumentException("No published version available for skill: " + skillSlug);
|
||||
}
|
||||
|
||||
SkillVersion version = skillVersionRepository.findById(skill.getLatestVersionId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Latest version not found"));
|
||||
|
||||
return downloadVersion(skill, version);
|
||||
}
|
||||
|
||||
public DownloadResult downloadVersion(
|
||||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
String versionStr,
|
||||
Long currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Skill not found: " + skillSlug));
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new SecurityException("Access denied to skill: " + skillSlug);
|
||||
}
|
||||
|
||||
SkillVersion version = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), versionStr)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Version not found: " + versionStr));
|
||||
|
||||
return downloadVersion(skill, version);
|
||||
}
|
||||
|
||||
public DownloadResult downloadByTag(
|
||||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
String tagName,
|
||||
Long currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Skill not found: " + skillSlug));
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new SecurityException("Access denied to skill: " + skillSlug);
|
||||
}
|
||||
|
||||
SkillTag tag = skillTagRepository.findBySkillIdAndTagName(skill.getId(), tagName)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Tag not found: " + tagName));
|
||||
|
||||
if (tag.getVersionId() == null) {
|
||||
throw new IllegalArgumentException("Tag does not point to a version: " + tagName);
|
||||
}
|
||||
|
||||
SkillVersion version = skillVersionRepository.findById(tag.getVersionId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Version not found for tag: " + tagName));
|
||||
|
||||
return downloadVersion(skill, version);
|
||||
}
|
||||
|
||||
private DownloadResult downloadVersion(Skill skill, SkillVersion version) {
|
||||
String storageKey = String.format("packages/%d/%d/bundle.zip", skill.getId(), version.getId());
|
||||
|
||||
if (!objectStorageService.exists(storageKey)) {
|
||||
throw new IllegalArgumentException("Bundle not found in storage");
|
||||
}
|
||||
|
||||
ObjectMetadata metadata = objectStorageService.getMetadata(storageKey);
|
||||
InputStream content = objectStorageService.getObject(storageKey);
|
||||
|
||||
// Publish download event
|
||||
eventPublisher.publishEvent(new SkillDownloadedEvent(skill.getId(), version.getId()));
|
||||
|
||||
String filename = String.format("%s-%s.zip", skill.getSlug(), version.getVersion());
|
||||
|
||||
return new DownloadResult(content, filename, metadata.size(), metadata.contentType());
|
||||
}
|
||||
|
||||
private Namespace findNamespace(String slug) {
|
||||
return namespaceRepository.findBySlug(slug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Namespace not found: " + slug));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
package com.iflytek.skillhub.domain.skill.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.iflytek.skillhub.domain.event.SkillPublishedEvent;
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import com.iflytek.skillhub.domain.skill.metadata.SkillMetadata;
|
||||
import com.iflytek.skillhub.domain.skill.metadata.SkillMetadataParser;
|
||||
import com.iflytek.skillhub.domain.skill.validation.PackageEntry;
|
||||
import com.iflytek.skillhub.domain.skill.validation.PrePublishValidator;
|
||||
import com.iflytek.skillhub.domain.skill.validation.SkillPackageValidator;
|
||||
import com.iflytek.skillhub.domain.skill.validation.ValidationResult;
|
||||
import com.iflytek.skillhub.storage.ObjectStorageService;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class SkillPublishService {
|
||||
|
||||
private final NamespaceRepository namespaceRepository;
|
||||
private final NamespaceMemberRepository namespaceMemberRepository;
|
||||
private final SkillRepository skillRepository;
|
||||
private final SkillVersionRepository skillVersionRepository;
|
||||
private final SkillFileRepository skillFileRepository;
|
||||
private final ObjectStorageService objectStorageService;
|
||||
private final SkillPackageValidator skillPackageValidator;
|
||||
private final SkillMetadataParser skillMetadataParser;
|
||||
private final PrePublishValidator prePublishValidator;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public SkillPublishService(
|
||||
NamespaceRepository namespaceRepository,
|
||||
NamespaceMemberRepository namespaceMemberRepository,
|
||||
SkillRepository skillRepository,
|
||||
SkillVersionRepository skillVersionRepository,
|
||||
SkillFileRepository skillFileRepository,
|
||||
ObjectStorageService objectStorageService,
|
||||
SkillPackageValidator skillPackageValidator,
|
||||
SkillMetadataParser skillMetadataParser,
|
||||
PrePublishValidator prePublishValidator,
|
||||
ApplicationEventPublisher eventPublisher,
|
||||
ObjectMapper objectMapper) {
|
||||
this.namespaceRepository = namespaceRepository;
|
||||
this.namespaceMemberRepository = namespaceMemberRepository;
|
||||
this.skillRepository = skillRepository;
|
||||
this.skillVersionRepository = skillVersionRepository;
|
||||
this.skillFileRepository = skillFileRepository;
|
||||
this.objectStorageService = objectStorageService;
|
||||
this.skillPackageValidator = skillPackageValidator;
|
||||
this.skillMetadataParser = skillMetadataParser;
|
||||
this.prePublishValidator = prePublishValidator;
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SkillVersion publishFromEntries(
|
||||
String namespaceSlug,
|
||||
List<PackageEntry> entries,
|
||||
Long publisherId,
|
||||
SkillVisibility visibility) {
|
||||
|
||||
// 1. Find namespace by slug
|
||||
Namespace namespace = namespaceRepository.findBySlug(namespaceSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Namespace not found: " + namespaceSlug));
|
||||
|
||||
// 2. Check publisher is member
|
||||
namespaceMemberRepository.findByNamespaceIdAndUserId(namespace.getId(), publisherId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Publisher is not a member of namespace: " + namespaceSlug));
|
||||
|
||||
// 3. Validate package
|
||||
ValidationResult packageValidation = skillPackageValidator.validate(entries);
|
||||
if (!packageValidation.passed()) {
|
||||
throw new IllegalArgumentException("Package validation failed: " + String.join(", ", packageValidation.errors()));
|
||||
}
|
||||
|
||||
// 4. Parse SKILL.md
|
||||
PackageEntry skillMd = entries.stream()
|
||||
.filter(e -> e.path().equals("SKILL.md"))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("SKILL.md not found"));
|
||||
|
||||
String skillMdContent = new String(skillMd.content());
|
||||
SkillMetadata metadata = skillMetadataParser.parse(skillMdContent);
|
||||
|
||||
// 5. Run PrePublishValidator
|
||||
PrePublishValidator.SkillPackageContext context = new PrePublishValidator.SkillPackageContext(
|
||||
entries, metadata, publisherId, namespace.getId());
|
||||
ValidationResult prePublishValidation = prePublishValidator.validate(context);
|
||||
if (!prePublishValidation.passed()) {
|
||||
throw new IllegalArgumentException("Pre-publish validation failed: " + String.join(", ", prePublishValidation.errors()));
|
||||
}
|
||||
|
||||
// 6. Find or create Skill record
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), metadata.name())
|
||||
.orElseGet(() -> {
|
||||
Skill newSkill = new Skill(namespace.getId(), metadata.name(), publisherId, visibility);
|
||||
newSkill.setCreatedBy(publisherId);
|
||||
return skillRepository.save(newSkill);
|
||||
});
|
||||
|
||||
// 7. Check version doesn't already exist
|
||||
if (skillVersionRepository.findBySkillIdAndVersion(skill.getId(), metadata.version()).isPresent()) {
|
||||
throw new IllegalArgumentException("Version already exists: " + metadata.version());
|
||||
}
|
||||
|
||||
// 8. Create SkillVersion
|
||||
SkillVersion version = new SkillVersion(skill.getId(), metadata.version(), publisherId);
|
||||
version.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
version.setPublishedAt(LocalDateTime.now());
|
||||
|
||||
// Store metadata as JSON
|
||||
try {
|
||||
String metadataJson = objectMapper.writeValueAsString(metadata);
|
||||
version.setParsedMetadataJson(metadataJson);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to serialize metadata", e);
|
||||
}
|
||||
|
||||
version = skillVersionRepository.save(version);
|
||||
|
||||
// 9. Upload each file to storage and compute SHA-256
|
||||
List<SkillFile> skillFiles = new ArrayList<>();
|
||||
long totalSize = 0;
|
||||
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
HexFormat hexFormat = HexFormat.of();
|
||||
|
||||
for (PackageEntry entry : entries) {
|
||||
String storageKey = String.format("skills/%d/%d/%s", skill.getId(), version.getId(), entry.path());
|
||||
|
||||
// Upload to storage
|
||||
objectStorageService.putObject(
|
||||
storageKey,
|
||||
new ByteArrayInputStream(entry.content()),
|
||||
entry.size(),
|
||||
entry.contentType()
|
||||
);
|
||||
|
||||
// Compute SHA-256
|
||||
byte[] hash = digest.digest(entry.content());
|
||||
String sha256 = hexFormat.formatHex(hash);
|
||||
|
||||
// Create SkillFile record
|
||||
SkillFile skillFile = new SkillFile(
|
||||
version.getId(),
|
||||
entry.path(),
|
||||
entry.size(),
|
||||
entry.contentType(),
|
||||
sha256,
|
||||
storageKey
|
||||
);
|
||||
skillFiles.add(skillFile);
|
||||
totalSize += entry.size();
|
||||
|
||||
digest.reset();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to process files", e);
|
||||
}
|
||||
|
||||
// 10. Save SkillFile records
|
||||
skillFileRepository.saveAll(skillFiles);
|
||||
|
||||
// 11. Update version stats
|
||||
version.setFileCount(skillFiles.size());
|
||||
version.setTotalSize(totalSize);
|
||||
skillVersionRepository.save(version);
|
||||
|
||||
// 12. Update skill
|
||||
skill.setLatestVersionId(version.getId());
|
||||
skill.setDisplayName(metadata.name());
|
||||
skill.setSummary(metadata.description());
|
||||
skill.setUpdatedBy(publisherId);
|
||||
skillRepository.save(skill);
|
||||
|
||||
// 13. Publish SkillPublishedEvent
|
||||
eventPublisher.publishEvent(new SkillPublishedEvent(skill.getId(), version.getId(), publisherId));
|
||||
|
||||
// 14. Return version
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
package com.iflytek.skillhub.domain.skill.service;
|
||||
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import com.iflytek.skillhub.storage.ObjectStorageService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class SkillQueryService {
|
||||
|
||||
private final NamespaceRepository namespaceRepository;
|
||||
private final SkillRepository skillRepository;
|
||||
private final SkillVersionRepository skillVersionRepository;
|
||||
private final SkillFileRepository skillFileRepository;
|
||||
private final ObjectStorageService objectStorageService;
|
||||
private final VisibilityChecker visibilityChecker;
|
||||
|
||||
public SkillQueryService(
|
||||
NamespaceRepository namespaceRepository,
|
||||
SkillRepository skillRepository,
|
||||
SkillVersionRepository skillVersionRepository,
|
||||
SkillFileRepository skillFileRepository,
|
||||
ObjectStorageService objectStorageService,
|
||||
VisibilityChecker visibilityChecker) {
|
||||
this.namespaceRepository = namespaceRepository;
|
||||
this.skillRepository = skillRepository;
|
||||
this.skillVersionRepository = skillVersionRepository;
|
||||
this.skillFileRepository = skillFileRepository;
|
||||
this.objectStorageService = objectStorageService;
|
||||
this.visibilityChecker = visibilityChecker;
|
||||
}
|
||||
|
||||
public record SkillDetailDTO(
|
||||
Long id,
|
||||
String slug,
|
||||
String displayName,
|
||||
String summary,
|
||||
String visibility,
|
||||
String status,
|
||||
Long downloadCount,
|
||||
Integer starCount,
|
||||
String latestVersion,
|
||||
Long namespaceId
|
||||
) {}
|
||||
|
||||
public SkillDetailDTO getSkillDetail(
|
||||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
Long currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Skill not found: " + skillSlug));
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new SecurityException("Access denied to skill: " + skillSlug);
|
||||
}
|
||||
|
||||
String latestVersion = null;
|
||||
if (skill.getLatestVersionId() != null) {
|
||||
SkillVersion version = skillVersionRepository.findById(skill.getLatestVersionId()).orElse(null);
|
||||
if (version != null) {
|
||||
latestVersion = version.getVersion();
|
||||
}
|
||||
}
|
||||
|
||||
return new SkillDetailDTO(
|
||||
skill.getId(),
|
||||
skill.getSlug(),
|
||||
skill.getDisplayName(),
|
||||
skill.getSummary(),
|
||||
skill.getVisibility().name(),
|
||||
skill.getStatus().name(),
|
||||
skill.getDownloadCount(),
|
||||
skill.getStarCount(),
|
||||
latestVersion,
|
||||
skill.getNamespaceId()
|
||||
);
|
||||
}
|
||||
|
||||
public Page<Skill> listSkillsByNamespace(
|
||||
String namespaceSlug,
|
||||
Long currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles,
|
||||
Pageable pageable) {
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
List<Skill> allSkills = skillRepository.findByNamespaceIdAndStatus(namespace.getId(), SkillStatus.ACTIVE);
|
||||
|
||||
// Filter by visibility
|
||||
List<Skill> accessibleSkills = allSkills.stream()
|
||||
.filter(skill -> visibilityChecker.canAccess(skill, currentUserId, userNsRoles))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Manual pagination
|
||||
int start = (int) pageable.getOffset();
|
||||
int end = Math.min(start + pageable.getPageSize(), accessibleSkills.size());
|
||||
List<Skill> pageContent = accessibleSkills.subList(start, end);
|
||||
|
||||
return new PageImpl<>(pageContent, pageable, accessibleSkills.size());
|
||||
}
|
||||
|
||||
public List<SkillFile> listFiles(String namespaceSlug, String skillSlug, String version) {
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Skill not found: " + skillSlug));
|
||||
|
||||
SkillVersion skillVersion = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), version)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Version not found: " + version));
|
||||
|
||||
return skillFileRepository.findByVersionId(skillVersion.getId());
|
||||
}
|
||||
|
||||
public InputStream getFileContent(String namespaceSlug, String skillSlug, String version, String filePath) {
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Skill not found: " + skillSlug));
|
||||
|
||||
SkillVersion skillVersion = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), version)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Version not found: " + version));
|
||||
|
||||
List<SkillFile> files = skillFileRepository.findByVersionId(skillVersion.getId());
|
||||
SkillFile file = files.stream()
|
||||
.filter(f -> f.getFilePath().equals(filePath))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("File not found: " + filePath));
|
||||
|
||||
return objectStorageService.getObject(file.getStorageKey());
|
||||
}
|
||||
|
||||
public Page<SkillVersion> listVersions(String namespaceSlug, String skillSlug, Pageable pageable) {
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Skill not found: " + skillSlug));
|
||||
|
||||
List<SkillVersion> publishedVersions = skillVersionRepository.findBySkillIdAndStatus(
|
||||
skill.getId(), SkillVersionStatus.PUBLISHED);
|
||||
|
||||
// Manual pagination
|
||||
int start = (int) pageable.getOffset();
|
||||
int end = Math.min(start + pageable.getPageSize(), publishedVersions.size());
|
||||
List<SkillVersion> pageContent = publishedVersions.subList(start, end);
|
||||
|
||||
return new PageImpl<>(pageContent, pageable, publishedVersions.size());
|
||||
}
|
||||
|
||||
private Namespace findNamespace(String slug) {
|
||||
return namespaceRepository.findBySlug(slug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Namespace not found: " + slug));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package com.iflytek.skillhub.domain.skill.service;
|
||||
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class SkillTagService {
|
||||
|
||||
private static final String RESERVED_TAG_LATEST = "latest";
|
||||
|
||||
private final NamespaceRepository namespaceRepository;
|
||||
private final SkillRepository skillRepository;
|
||||
private final SkillVersionRepository skillVersionRepository;
|
||||
private final SkillTagRepository skillTagRepository;
|
||||
|
||||
public SkillTagService(
|
||||
NamespaceRepository namespaceRepository,
|
||||
SkillRepository skillRepository,
|
||||
SkillVersionRepository skillVersionRepository,
|
||||
SkillTagRepository skillTagRepository) {
|
||||
this.namespaceRepository = namespaceRepository;
|
||||
this.skillRepository = skillRepository;
|
||||
this.skillVersionRepository = skillVersionRepository;
|
||||
this.skillTagRepository = skillTagRepository;
|
||||
}
|
||||
|
||||
public List<SkillTag> listTags(String namespaceSlug, String skillSlug) {
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Skill not found: " + skillSlug));
|
||||
|
||||
return skillTagRepository.findBySkillId(skill.getId());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SkillTag createOrMoveTag(
|
||||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
String tagName,
|
||||
String targetVersion,
|
||||
Long operatorId) {
|
||||
|
||||
// Reject "latest" tag
|
||||
if (RESERVED_TAG_LATEST.equalsIgnoreCase(tagName)) {
|
||||
throw new IllegalArgumentException("Tag name 'latest' is reserved");
|
||||
}
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Skill not found: " + skillSlug));
|
||||
|
||||
// Find target version
|
||||
SkillVersion version = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), targetVersion)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Version not found: " + targetVersion));
|
||||
|
||||
// Target must be PUBLISHED
|
||||
if (version.getStatus() != SkillVersionStatus.PUBLISHED) {
|
||||
throw new IllegalArgumentException("Target version must be PUBLISHED");
|
||||
}
|
||||
|
||||
// Check if tag exists
|
||||
SkillTag existingTag = skillTagRepository.findBySkillIdAndTagName(skill.getId(), tagName).orElse(null);
|
||||
|
||||
if (existingTag != null) {
|
||||
// Move tag
|
||||
existingTag.setVersionId(version.getId());
|
||||
return skillTagRepository.save(existingTag);
|
||||
} else {
|
||||
// Create new tag
|
||||
SkillTag newTag = new SkillTag(skill.getId(), tagName, version.getId(), operatorId);
|
||||
return skillTagRepository.save(newTag);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteTag(String namespaceSlug, String skillSlug, String tagName, Long operatorId) {
|
||||
// Reject "latest" tag
|
||||
if (RESERVED_TAG_LATEST.equalsIgnoreCase(tagName)) {
|
||||
throw new IllegalArgumentException("Tag name 'latest' is reserved and cannot be deleted");
|
||||
}
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Skill not found: " + skillSlug));
|
||||
|
||||
SkillTag tag = skillTagRepository.findBySkillIdAndTagName(skill.getId(), tagName)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Tag not found: " + tagName));
|
||||
|
||||
skillTagRepository.delete(tag);
|
||||
}
|
||||
|
||||
private Namespace findNamespace(String slug) {
|
||||
return namespaceRepository.findBySlug(slug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Namespace not found: " + slug));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.iflytek.skillhub.domain.skill.service;
|
||||
|
||||
import com.iflytek.skillhub.domain.event.SkillDownloadedEvent;
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import com.iflytek.skillhub.storage.ObjectMetadata;
|
||||
import com.iflytek.skillhub.storage.ObjectStorageService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SkillDownloadServiceTest {
|
||||
|
||||
@Mock
|
||||
private NamespaceRepository namespaceRepository;
|
||||
@Mock
|
||||
private SkillRepository skillRepository;
|
||||
@Mock
|
||||
private SkillVersionRepository skillVersionRepository;
|
||||
@Mock
|
||||
private SkillTagRepository skillTagRepository;
|
||||
@Mock
|
||||
private ObjectStorageService objectStorageService;
|
||||
@Mock
|
||||
private VisibilityChecker visibilityChecker;
|
||||
@Mock
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
private SkillDownloadService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new SkillDownloadService(
|
||||
namespaceRepository,
|
||||
skillRepository,
|
||||
skillVersionRepository,
|
||||
skillTagRepository,
|
||||
objectStorageService,
|
||||
visibilityChecker,
|
||||
eventPublisher
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadLatest_Success() throws Exception {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
Long userId = 100L;
|
||||
Map<Long, NamespaceRole> userNsRoles = Map.of(1L, NamespaceRole.MEMBER);
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, userId, SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
skill.setLatestVersionId(10L);
|
||||
|
||||
SkillVersion version = new SkillVersion(1L, "1.0.0", userId);
|
||||
setId(version, 10L);
|
||||
String storageKey = "packages/1/10/bundle.zip";
|
||||
InputStream content = new ByteArrayInputStream("test".getBytes());
|
||||
ObjectMetadata metadata = new ObjectMetadata(1000L, "application/zip", Instant.now());
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findById(10L)).thenReturn(Optional.of(version));
|
||||
when(objectStorageService.exists(storageKey)).thenReturn(true);
|
||||
when(objectStorageService.getMetadata(storageKey)).thenReturn(metadata);
|
||||
when(objectStorageService.getObject(storageKey)).thenReturn(content);
|
||||
|
||||
// Act
|
||||
SkillDownloadService.DownloadResult result = service.downloadLatest(namespaceSlug, skillSlug, userId, userNsRoles);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
assertEquals("test-skill-1.0.0.zip", result.filename());
|
||||
assertEquals(1000L, result.contentLength());
|
||||
verify(eventPublisher).publishEvent(any(SkillDownloadedEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadByTag_Success() throws Exception {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
String tagName = "stable";
|
||||
Long userId = 100L;
|
||||
Map<Long, NamespaceRole> userNsRoles = Map.of(1L, NamespaceRole.MEMBER);
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, userId, SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
SkillTag tag = new SkillTag(1L, tagName, 10L, userId);
|
||||
SkillVersion version = new SkillVersion(1L, "1.0.0", userId);
|
||||
setId(version, 10L);
|
||||
String storageKey = "packages/1/10/bundle.zip";
|
||||
InputStream content = new ByteArrayInputStream("test".getBytes());
|
||||
ObjectMetadata metadata = new ObjectMetadata(1000L, "application/zip", Instant.now());
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
when(skillTagRepository.findBySkillIdAndTagName(1L, tagName)).thenReturn(Optional.of(tag));
|
||||
when(skillVersionRepository.findById(10L)).thenReturn(Optional.of(version));
|
||||
when(objectStorageService.exists(storageKey)).thenReturn(true);
|
||||
when(objectStorageService.getMetadata(storageKey)).thenReturn(metadata);
|
||||
when(objectStorageService.getObject(storageKey)).thenReturn(content);
|
||||
|
||||
// Act
|
||||
SkillDownloadService.DownloadResult result = service.downloadByTag(namespaceSlug, skillSlug, tagName, userId, userNsRoles);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
assertEquals("test-skill-1.0.0.zip", result.filename());
|
||||
verify(eventPublisher).publishEvent(any(SkillDownloadedEvent.class));
|
||||
}
|
||||
|
||||
private void setId(Object entity, Long id) throws Exception {
|
||||
Field idField = entity.getClass().getDeclaredField("id");
|
||||
idField.setAccessible(true);
|
||||
idField.set(entity, id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
package com.iflytek.skillhub.domain.skill.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.iflytek.skillhub.domain.event.SkillPublishedEvent;
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMember;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import com.iflytek.skillhub.domain.skill.metadata.SkillMetadata;
|
||||
import com.iflytek.skillhub.domain.skill.metadata.SkillMetadataParser;
|
||||
import com.iflytek.skillhub.domain.skill.validation.PackageEntry;
|
||||
import com.iflytek.skillhub.domain.skill.validation.PrePublishValidator;
|
||||
import com.iflytek.skillhub.domain.skill.validation.SkillPackageValidator;
|
||||
import com.iflytek.skillhub.domain.skill.validation.ValidationResult;
|
||||
import com.iflytek.skillhub.storage.ObjectStorageService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SkillPublishServiceTest {
|
||||
|
||||
@Mock
|
||||
private NamespaceRepository namespaceRepository;
|
||||
@Mock
|
||||
private NamespaceMemberRepository namespaceMemberRepository;
|
||||
@Mock
|
||||
private SkillRepository skillRepository;
|
||||
@Mock
|
||||
private SkillVersionRepository skillVersionRepository;
|
||||
@Mock
|
||||
private SkillFileRepository skillFileRepository;
|
||||
@Mock
|
||||
private ObjectStorageService objectStorageService;
|
||||
@Mock
|
||||
private SkillPackageValidator skillPackageValidator;
|
||||
@Mock
|
||||
private SkillMetadataParser skillMetadataParser;
|
||||
@Mock
|
||||
private PrePublishValidator prePublishValidator;
|
||||
@Mock
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
private SkillPublishService service;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
objectMapper = new ObjectMapper();
|
||||
service = new SkillPublishService(
|
||||
namespaceRepository,
|
||||
namespaceMemberRepository,
|
||||
skillRepository,
|
||||
skillVersionRepository,
|
||||
skillFileRepository,
|
||||
objectStorageService,
|
||||
skillPackageValidator,
|
||||
skillMetadataParser,
|
||||
prePublishValidator,
|
||||
eventPublisher,
|
||||
objectMapper
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublishFromEntries_Success() throws Exception {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
Long publisherId = 100L;
|
||||
String skillMdContent = "---\nname: test-skill\ndescription: Test\nversion: 1.0.0\n---\nBody";
|
||||
|
||||
PackageEntry skillMd = new PackageEntry("SKILL.md", skillMdContent.getBytes(), skillMdContent.length(), "text/markdown");
|
||||
PackageEntry file1 = new PackageEntry("file1.txt", "content".getBytes(), 7, "text/plain");
|
||||
List<PackageEntry> entries = List.of(skillMd, file1);
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
setId(namespace, 1L);
|
||||
NamespaceMember member = mock(NamespaceMember.class);
|
||||
SkillMetadata metadata = new SkillMetadata("test-skill", "Test", "1.0.0", "Body", Map.of());
|
||||
|
||||
Skill skill = new Skill(1L, "test-skill", publisherId, SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
SkillVersion version = new SkillVersion(1L, "1.0.0", publisherId);
|
||||
setId(version, 10L);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(any(), eq(publisherId))).thenReturn(Optional.of(member));
|
||||
when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass());
|
||||
when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata);
|
||||
when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass());
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(Optional.of(skill));
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(any(), eq("1.0.0"))).thenReturn(Optional.empty());
|
||||
when(skillVersionRepository.save(any())).thenReturn(version);
|
||||
when(skillRepository.save(any())).thenReturn(skill);
|
||||
|
||||
// Act
|
||||
SkillVersion result = service.publishFromEntries(namespaceSlug, entries, publisherId, SkillVisibility.PUBLIC);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
verify(eventPublisher).publishEvent(any(SkillPublishedEvent.class));
|
||||
verify(skillFileRepository).saveAll(anyList());
|
||||
verify(objectStorageService, atLeastOnce()).putObject(anyString(), any(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublishFromEntries_NamespaceNotFound() {
|
||||
// Arrange
|
||||
String namespaceSlug = "nonexistent";
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.empty());
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
service.publishFromEntries(namespaceSlug, List.of(), 100L, SkillVisibility.PUBLIC)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublishFromEntries_NotAMember() throws Exception {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
Long publisherId = 100L;
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
setId(namespace, 1L);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(any(), eq(publisherId))).thenReturn(Optional.empty());
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
service.publishFromEntries(namespaceSlug, List.of(), publisherId, SkillVisibility.PUBLIC)
|
||||
);
|
||||
}
|
||||
|
||||
private void setId(Object entity, Long id) throws Exception {
|
||||
Field idField = entity.getClass().getDeclaredField("id");
|
||||
idField.setAccessible(true);
|
||||
idField.set(entity, id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
package com.iflytek.skillhub.domain.skill.service;
|
||||
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import com.iflytek.skillhub.storage.ObjectStorageService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SkillQueryServiceTest {
|
||||
|
||||
@Mock
|
||||
private NamespaceRepository namespaceRepository;
|
||||
@Mock
|
||||
private SkillRepository skillRepository;
|
||||
@Mock
|
||||
private SkillVersionRepository skillVersionRepository;
|
||||
@Mock
|
||||
private SkillFileRepository skillFileRepository;
|
||||
@Mock
|
||||
private ObjectStorageService objectStorageService;
|
||||
@Mock
|
||||
private VisibilityChecker visibilityChecker;
|
||||
|
||||
private SkillQueryService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new SkillQueryService(
|
||||
namespaceRepository,
|
||||
skillRepository,
|
||||
skillVersionRepository,
|
||||
skillFileRepository,
|
||||
objectStorageService,
|
||||
visibilityChecker
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetSkillDetail_Success() throws Exception {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
Long userId = 100L;
|
||||
Map<Long, NamespaceRole> userNsRoles = Map.of(1L, NamespaceRole.MEMBER);
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, userId, SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
skill.setDisplayName("Test Skill");
|
||||
skill.setSummary("Test Summary");
|
||||
skill.setLatestVersionId(10L);
|
||||
|
||||
SkillVersion version = new SkillVersion(1L, "1.0.0", userId);
|
||||
setId(version, 10L);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findById(10L)).thenReturn(Optional.of(version));
|
||||
|
||||
// Act
|
||||
SkillQueryService.SkillDetailDTO result = service.getSkillDetail(namespaceSlug, skillSlug, userId, userNsRoles);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
assertEquals(skillSlug, result.slug());
|
||||
assertEquals("Test Skill", result.displayName());
|
||||
assertEquals("1.0.0", result.latestVersion());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetSkillDetail_AccessDenied() throws Exception {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
Long userId = 100L;
|
||||
Map<Long, NamespaceRole> userNsRoles = Map.of();
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, 200L, SkillVisibility.PRIVATE);
|
||||
setId(skill, 1L);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(false);
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(SecurityException.class, () ->
|
||||
service.getSkillDetail(namespaceSlug, skillSlug, userId, userNsRoles)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListSkillsByNamespace() throws Exception {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
Long userId = 100L;
|
||||
Map<Long, NamespaceRole> userNsRoles = Map.of(1L, NamespaceRole.MEMBER);
|
||||
Pageable pageable = PageRequest.of(0, 10);
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
setId(namespace, 1L);
|
||||
Skill skill1 = new Skill(1L, "skill1", userId, SkillVisibility.PUBLIC);
|
||||
setId(skill1, 1L);
|
||||
Skill skill2 = new Skill(1L, "skill2", userId, SkillVisibility.PRIVATE);
|
||||
setId(skill2, 2L);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndStatus(1L, SkillStatus.ACTIVE)).thenReturn(List.of(skill1, skill2));
|
||||
when(visibilityChecker.canAccess(skill1, userId, userNsRoles)).thenReturn(true);
|
||||
when(visibilityChecker.canAccess(skill2, userId, userNsRoles)).thenReturn(false);
|
||||
|
||||
// Act
|
||||
Page<Skill> result = service.listSkillsByNamespace(namespaceSlug, userId, userNsRoles, pageable);
|
||||
|
||||
// Assert
|
||||
assertEquals(1, result.getTotalElements());
|
||||
assertEquals("skill1", result.getContent().get(0).getSlug());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListFiles() throws Exception {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
String version = "1.0.0";
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, 100L, SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
SkillVersion skillVersion = new SkillVersion(1L, version, 100L);
|
||||
setId(skillVersion, 1L);
|
||||
SkillFile file1 = new SkillFile(1L, "file1.txt", 100L, "text/plain", "hash1", "key1");
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(skillVersion));
|
||||
when(skillFileRepository.findByVersionId(1L)).thenReturn(List.of(file1));
|
||||
|
||||
// Act
|
||||
List<SkillFile> result = service.listFiles(namespaceSlug, skillSlug, version);
|
||||
|
||||
// Assert
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("file1.txt", result.get(0).getFilePath());
|
||||
}
|
||||
|
||||
private void setId(Object entity, Long id) throws Exception {
|
||||
Field idField = entity.getClass().getDeclaredField("id");
|
||||
idField.setAccessible(true);
|
||||
idField.set(entity, id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
package com.iflytek.skillhub.domain.skill.service;
|
||||
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SkillTagServiceTest {
|
||||
|
||||
@Mock
|
||||
private NamespaceRepository namespaceRepository;
|
||||
@Mock
|
||||
private SkillRepository skillRepository;
|
||||
@Mock
|
||||
private SkillVersionRepository skillVersionRepository;
|
||||
@Mock
|
||||
private SkillTagRepository skillTagRepository;
|
||||
|
||||
private SkillTagService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new SkillTagService(
|
||||
namespaceRepository,
|
||||
skillRepository,
|
||||
skillVersionRepository,
|
||||
skillTagRepository
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateTag_Success() throws Exception {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
String tagName = "stable";
|
||||
String targetVersion = "1.0.0";
|
||||
Long operatorId = 100L;
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, operatorId, SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
SkillVersion version = new SkillVersion(1L, targetVersion, operatorId);
|
||||
setId(version, 1L);
|
||||
version.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
SkillTag tag = new SkillTag(1L, tagName, 1L, operatorId);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, targetVersion)).thenReturn(Optional.of(version));
|
||||
when(skillTagRepository.findBySkillIdAndTagName(1L, tagName)).thenReturn(Optional.empty());
|
||||
when(skillTagRepository.save(any())).thenReturn(tag);
|
||||
|
||||
// Act
|
||||
SkillTag result = service.createOrMoveTag(namespaceSlug, skillSlug, tagName, targetVersion, operatorId);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
verify(skillTagRepository).save(any(SkillTag.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateTag_RejectLatest() {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
String tagName = "latest";
|
||||
String targetVersion = "1.0.0";
|
||||
Long operatorId = 100L;
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
service.createOrMoveTag(namespaceSlug, skillSlug, tagName, targetVersion, operatorId)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteTag_Success() throws Exception {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
String tagName = "stable";
|
||||
Long operatorId = 100L;
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, operatorId, SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
SkillTag tag = new SkillTag(1L, tagName, 1L, operatorId);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillTagRepository.findBySkillIdAndTagName(1L, tagName)).thenReturn(Optional.of(tag));
|
||||
|
||||
// Act
|
||||
service.deleteTag(namespaceSlug, skillSlug, tagName, operatorId);
|
||||
|
||||
// Assert
|
||||
verify(skillTagRepository).delete(tag);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteTag_RejectLatest() {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
String tagName = "latest";
|
||||
Long operatorId = 100L;
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
service.deleteTag(namespaceSlug, skillSlug, tagName, operatorId)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListTags() throws Exception {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, 100L, SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
SkillTag tag1 = new SkillTag(1L, "stable", 1L, 100L);
|
||||
SkillTag tag2 = new SkillTag(1L, "beta", 2L, 100L);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillTagRepository.findBySkillId(1L)).thenReturn(List.of(tag1, tag2));
|
||||
|
||||
// Act
|
||||
List<SkillTag> result = service.listTags(namespaceSlug, skillSlug);
|
||||
|
||||
// Assert
|
||||
assertEquals(2, result.size());
|
||||
}
|
||||
|
||||
private void setId(Object entity, Long id) throws Exception {
|
||||
Field idField = entity.getClass().getDeclaredField("id");
|
||||
idField.setAccessible(true);
|
||||
idField.set(entity, id);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue