mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-06 02:48:28 +00:00
feat(domain): update entities, services, repositories, and add shared exceptions
This commit is contained in:
parent
9d7282221c
commit
cfcdb545f0
47 changed files with 1012 additions and 338 deletions
|
|
@ -1,3 +1,3 @@
|
|||
package com.iflytek.skillhub.domain.event;
|
||||
|
||||
public record SkillPublishedEvent(Long skillId, Long versionId, Long publisherId) {}
|
||||
public record SkillPublishedEvent(Long skillId, Long versionId, String publisherId) {}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public class Namespace {
|
|||
private String avatarUrl;
|
||||
|
||||
@Column(name = "created_by")
|
||||
private Long createdBy;
|
||||
private String createdBy;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
|
@ -41,7 +41,7 @@ public class Namespace {
|
|||
|
||||
protected Namespace() {}
|
||||
|
||||
public Namespace(String slug, String displayName, Long createdBy) {
|
||||
public Namespace(String slug, String displayName, String createdBy) {
|
||||
this.slug = slug;
|
||||
this.displayName = displayName;
|
||||
this.createdBy = createdBy;
|
||||
|
|
@ -69,7 +69,7 @@ public class Namespace {
|
|||
public void setType(NamespaceType type) { this.type = type; }
|
||||
public String getAvatarUrl() { return avatarUrl; }
|
||||
public void setAvatarUrl(String avatarUrl) { this.avatarUrl = avatarUrl; }
|
||||
public Long getCreatedBy() { return createdBy; }
|
||||
public String getCreatedBy() { return createdBy; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public class NamespaceMember {
|
|||
private Long namespaceId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
private String userId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 32)
|
||||
|
|
@ -29,7 +29,7 @@ public class NamespaceMember {
|
|||
|
||||
protected NamespaceMember() {}
|
||||
|
||||
public NamespaceMember(Long namespaceId, Long userId, NamespaceRole role) {
|
||||
public NamespaceMember(Long namespaceId, String userId, NamespaceRole role) {
|
||||
this.namespaceId = namespaceId;
|
||||
this.userId = userId;
|
||||
this.role = role;
|
||||
|
|
@ -49,8 +49,8 @@ public class NamespaceMember {
|
|||
public Long getId() { return id; }
|
||||
public Long getNamespaceId() { return namespaceId; }
|
||||
public void setNamespaceId(Long namespaceId) { this.namespaceId = namespaceId; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public String getUserId() { return userId; }
|
||||
public void setUserId(String userId) { this.userId = userId; }
|
||||
public NamespaceRole getRole() { return role; }
|
||||
public void setRole(NamespaceRole role) { this.role = role; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import java.util.List;
|
|||
import java.util.Optional;
|
||||
|
||||
public interface NamespaceMemberRepository {
|
||||
Optional<NamespaceMember> findByNamespaceIdAndUserId(Long namespaceId, Long userId);
|
||||
List<NamespaceMember> findByUserId(Long userId);
|
||||
Optional<NamespaceMember> findByNamespaceIdAndUserId(Long namespaceId, String userId);
|
||||
List<NamespaceMember> findByUserId(String userId);
|
||||
Page<NamespaceMember> findByNamespaceId(Long namespaceId, Pageable pageable);
|
||||
NamespaceMember save(NamespaceMember member);
|
||||
void deleteByNamespaceIdAndUserId(Long namespaceId, Long userId);
|
||||
void deleteByNamespaceIdAndUserId(Long namespaceId, String userId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.iflytek.skillhub.domain.namespace;
|
||||
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
|
@ -11,19 +12,24 @@ import java.util.Optional;
|
|||
public class NamespaceMemberService {
|
||||
|
||||
private final NamespaceMemberRepository namespaceMemberRepository;
|
||||
private final NamespaceService namespaceService;
|
||||
|
||||
public NamespaceMemberService(NamespaceMemberRepository namespaceMemberRepository) {
|
||||
public NamespaceMemberService(NamespaceMemberRepository namespaceMemberRepository,
|
||||
NamespaceService namespaceService) {
|
||||
this.namespaceMemberRepository = namespaceMemberRepository;
|
||||
this.namespaceService = namespaceService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public NamespaceMember addMember(Long namespaceId, Long userId, NamespaceRole role) {
|
||||
public NamespaceMember addMember(Long namespaceId, String userId, NamespaceRole role, String operatorUserId) {
|
||||
namespaceService.assertAdminOrOwner(namespaceId, operatorUserId);
|
||||
|
||||
if (role == NamespaceRole.OWNER) {
|
||||
throw new IllegalArgumentException("Cannot directly assign OWNER role");
|
||||
throw new DomainBadRequestException("error.namespace.member.owner.assignDirect");
|
||||
}
|
||||
|
||||
if (namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId).isPresent()) {
|
||||
throw new IllegalArgumentException("User is already a member of this namespace");
|
||||
throw new DomainBadRequestException("error.namespace.member.alreadyExists");
|
||||
}
|
||||
|
||||
NamespaceMember member = new NamespaceMember(namespaceId, userId, role);
|
||||
|
|
@ -31,41 +37,45 @@ public class NamespaceMemberService {
|
|||
}
|
||||
|
||||
@Transactional
|
||||
public void removeMember(Long namespaceId, Long userId) {
|
||||
public void removeMember(Long namespaceId, String userId, String operatorUserId) {
|
||||
namespaceService.assertAdminOrOwner(namespaceId, operatorUserId);
|
||||
|
||||
NamespaceMember member = namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Member not found"));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.member.notFound"));
|
||||
|
||||
if (member.getRole() == NamespaceRole.OWNER) {
|
||||
throw new IllegalArgumentException("Cannot remove OWNER from namespace");
|
||||
throw new DomainBadRequestException("error.namespace.member.owner.remove");
|
||||
}
|
||||
|
||||
namespaceMemberRepository.deleteByNamespaceIdAndUserId(namespaceId, userId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public NamespaceMember updateMemberRole(Long namespaceId, Long userId, NamespaceRole newRole) {
|
||||
public NamespaceMember updateMemberRole(Long namespaceId, String userId, NamespaceRole newRole, String operatorUserId) {
|
||||
namespaceService.assertAdminOrOwner(namespaceId, operatorUserId);
|
||||
|
||||
if (newRole == NamespaceRole.OWNER) {
|
||||
throw new IllegalArgumentException("Cannot set OWNER role directly. Use transferOwnership instead");
|
||||
throw new DomainBadRequestException("error.namespace.member.owner.setDirect");
|
||||
}
|
||||
|
||||
NamespaceMember member = namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Member not found"));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.member.notFound"));
|
||||
|
||||
member.setRole(newRole);
|
||||
return namespaceMemberRepository.save(member);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void transferOwnership(Long namespaceId, Long currentOwnerId, Long newOwnerId) {
|
||||
public void transferOwnership(Long namespaceId, String currentOwnerId, String newOwnerId) {
|
||||
NamespaceMember currentOwner = namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, currentOwnerId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Current owner not found"));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.owner.current.notFound"));
|
||||
|
||||
if (currentOwner.getRole() != NamespaceRole.OWNER) {
|
||||
throw new IllegalArgumentException("User is not the current owner");
|
||||
throw new DomainBadRequestException("error.namespace.owner.current.invalid");
|
||||
}
|
||||
|
||||
NamespaceMember newOwner = namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, newOwnerId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("New owner not found in namespace"));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.owner.new.notFound"));
|
||||
|
||||
currentOwner.setRole(NamespaceRole.ADMIN);
|
||||
newOwner.setRole(NamespaceRole.OWNER);
|
||||
|
|
@ -74,7 +84,7 @@ public class NamespaceMemberService {
|
|||
namespaceMemberRepository.save(newOwner);
|
||||
}
|
||||
|
||||
public Optional<NamespaceRole> getMemberRole(Long namespaceId, Long userId) {
|
||||
public Optional<NamespaceRole> getMemberRole(Long namespaceId, String userId) {
|
||||
return namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId)
|
||||
.map(NamespaceMember::getRole);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ package com.iflytek.skillhub.domain.namespace;
|
|||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface NamespaceRepository {
|
||||
Optional<Namespace> findById(Long id);
|
||||
List<Namespace> findByIdIn(List<Long> ids);
|
||||
Optional<Namespace> findBySlug(String slug);
|
||||
Page<Namespace> findByStatus(NamespaceStatus status, Pageable pageable);
|
||||
Namespace save(Namespace namespace);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.iflytek.skillhub.domain.namespace;
|
||||
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
|
|
@ -16,11 +18,11 @@ public class NamespaceService {
|
|||
}
|
||||
|
||||
@Transactional
|
||||
public Namespace createNamespace(String slug, String displayName, String description, Long creatorUserId) {
|
||||
public Namespace createNamespace(String slug, String displayName, String description, String creatorUserId) {
|
||||
SlugValidator.validate(slug);
|
||||
|
||||
if (namespaceRepository.findBySlug(slug).isPresent()) {
|
||||
throw new IllegalArgumentException("Namespace with slug '" + slug + "' already exists");
|
||||
throw new DomainBadRequestException("error.namespace.slug.exists", slug);
|
||||
}
|
||||
|
||||
Namespace namespace = new Namespace(slug, displayName, creatorUserId);
|
||||
|
|
@ -35,9 +37,11 @@ public class NamespaceService {
|
|||
}
|
||||
|
||||
@Transactional
|
||||
public Namespace updateNamespace(Long namespaceId, String displayName, String description, String avatarUrl) {
|
||||
public Namespace updateNamespace(Long namespaceId, String displayName, String description, String avatarUrl,
|
||||
String operatorUserId) {
|
||||
Namespace namespace = namespaceRepository.findById(namespaceId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Namespace not found with id: " + namespaceId));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.id.notFound", namespaceId));
|
||||
assertAdminOrOwner(namespaceId, operatorUserId);
|
||||
|
||||
if (displayName != null) {
|
||||
namespace.setDisplayName(displayName);
|
||||
|
|
@ -54,6 +58,15 @@ public class NamespaceService {
|
|||
|
||||
public Namespace getNamespaceBySlug(String slug) {
|
||||
return namespaceRepository.findBySlug(slug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Namespace not found with slug: " + slug));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", slug));
|
||||
}
|
||||
|
||||
void assertAdminOrOwner(Long namespaceId, String userId) {
|
||||
NamespaceRole role = namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId)
|
||||
.map(NamespaceMember::getRole)
|
||||
.orElseThrow(() -> new DomainForbiddenException("error.namespace.membership.required"));
|
||||
if (role != NamespaceRole.OWNER && role != NamespaceRole.ADMIN) {
|
||||
throw new DomainForbiddenException("error.namespace.admin.required");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.iflytek.skillhub.domain.namespace;
|
||||
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
|
@ -15,22 +17,32 @@ public class SlugValidator {
|
|||
|
||||
public static void validate(String slug) {
|
||||
if (slug == null || slug.isBlank()) {
|
||||
throw new IllegalArgumentException("Slug cannot be null or blank");
|
||||
throw new DomainBadRequestException("error.slug.blank");
|
||||
}
|
||||
if (slug.length() < MIN_LENGTH || slug.length() > MAX_LENGTH) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Slug length must be between %d and %d characters", MIN_LENGTH, MAX_LENGTH));
|
||||
throw new DomainBadRequestException("error.slug.length", MIN_LENGTH, MAX_LENGTH);
|
||||
}
|
||||
if (!SLUG_PATTERN.matcher(slug).matches()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Slug must contain only lowercase alphanumeric characters and hyphens, " +
|
||||
"and must start and end with an alphanumeric character");
|
||||
throw new DomainBadRequestException("error.slug.pattern");
|
||||
}
|
||||
if (slug.contains("--")) {
|
||||
throw new IllegalArgumentException("Slug cannot contain consecutive hyphens");
|
||||
throw new DomainBadRequestException("error.slug.doubleHyphen");
|
||||
}
|
||||
if (RESERVED_SLUGS.contains(slug)) {
|
||||
throw new IllegalArgumentException("Slug '" + slug + "' is reserved and cannot be used");
|
||||
throw new DomainBadRequestException("error.slug.reserved", slug);
|
||||
}
|
||||
}
|
||||
|
||||
public static String slugify(String raw) {
|
||||
if (raw == null) {
|
||||
throw new DomainBadRequestException("error.slug.blank");
|
||||
}
|
||||
String slug = raw.trim().toLowerCase()
|
||||
.replaceAll("[^a-z0-9]+", "-")
|
||||
.replaceAll("^-+", "")
|
||||
.replaceAll("-+$", "")
|
||||
.replaceAll("-{2,}", "-");
|
||||
validate(slug);
|
||||
return slug;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ public class PromotionRequest {
|
|||
private Integer version = 1;
|
||||
|
||||
@Column(name = "submitted_by", nullable = false)
|
||||
private Long submittedBy;
|
||||
private String submittedBy;
|
||||
|
||||
@Column(name = "reviewed_by")
|
||||
private Long reviewedBy;
|
||||
private String reviewedBy;
|
||||
|
||||
@Column(name = "review_comment", columnDefinition = "TEXT")
|
||||
private String reviewComment;
|
||||
|
|
@ -49,7 +49,7 @@ public class PromotionRequest {
|
|||
protected PromotionRequest() {}
|
||||
|
||||
public PromotionRequest(Long sourceSkillId, Long sourceVersionId,
|
||||
Long targetNamespaceId, Long submittedBy) {
|
||||
Long targetNamespaceId, String submittedBy) {
|
||||
this.sourceSkillId = sourceSkillId;
|
||||
this.sourceVersionId = sourceVersionId;
|
||||
this.targetNamespaceId = targetNamespaceId;
|
||||
|
|
@ -76,11 +76,11 @@ public class PromotionRequest {
|
|||
|
||||
public Integer getVersion() { return version; }
|
||||
|
||||
public Long getSubmittedBy() { return submittedBy; }
|
||||
public String getSubmittedBy() { return submittedBy; }
|
||||
|
||||
public Long getReviewedBy() { return reviewedBy; }
|
||||
public String getReviewedBy() { return reviewedBy; }
|
||||
|
||||
public void setReviewedBy(Long reviewedBy) { this.reviewedBy = reviewedBy; }
|
||||
public void setReviewedBy(String reviewedBy) { this.reviewedBy = reviewedBy; }
|
||||
|
||||
public String getReviewComment() { return reviewComment; }
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@ public interface PromotionRequestRepository {
|
|||
Optional<PromotionRequest> findById(Long id);
|
||||
Optional<PromotionRequest> findBySourceVersionIdAndStatus(Long sourceVersionId, ReviewTaskStatus status);
|
||||
Page<PromotionRequest> findByStatus(ReviewTaskStatus status, Pageable pageable);
|
||||
int updateStatusWithVersion(Long id, ReviewTaskStatus status, Long reviewedBy,
|
||||
int updateStatusWithVersion(Long id, ReviewTaskStatus status, String reviewedBy,
|
||||
String reviewComment, Long targetSkillId, Integer expectedVersion);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ public class PromotionService {
|
|||
|
||||
@Transactional
|
||||
public PromotionRequest submitPromotion(Long sourceSkillId, Long sourceVersionId,
|
||||
Long targetNamespaceId, Long userId) {
|
||||
Long targetNamespaceId, String userId) {
|
||||
Skill sourceSkill = skillRepository.findById(sourceSkillId)
|
||||
.orElseThrow(() -> new DomainNotFoundException("skill.not_found", sourceSkillId));
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ public class PromotionService {
|
|||
}
|
||||
|
||||
@Transactional
|
||||
public PromotionRequest approvePromotion(Long promotionId, Long reviewerId,
|
||||
public PromotionRequest approvePromotion(Long promotionId, String reviewerId,
|
||||
String comment, Set<String> platformRoles) {
|
||||
PromotionRequest request = promotionRequestRepository.findById(promotionId)
|
||||
.orElseThrow(() -> new DomainNotFoundException("promotion.not_found", promotionId));
|
||||
|
|
@ -149,7 +149,7 @@ public class PromotionService {
|
|||
}
|
||||
|
||||
@Transactional
|
||||
public PromotionRequest rejectPromotion(Long promotionId, Long reviewerId,
|
||||
public PromotionRequest rejectPromotion(Long promotionId, String reviewerId,
|
||||
String comment, Set<String> platformRoles) {
|
||||
PromotionRequest request = promotionRequestRepository.findById(promotionId)
|
||||
.orElseThrow(() -> new DomainNotFoundException("promotion.not_found", promotionId));
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public class ReviewPermissionChecker {
|
|||
* @return true if the user is allowed to review
|
||||
*/
|
||||
public boolean canReview(ReviewTask task,
|
||||
Long userId,
|
||||
String userId,
|
||||
NamespaceType namespaceType,
|
||||
Map<Long, NamespaceRole> userNamespaceRoles,
|
||||
Set<String> platformRoles) {
|
||||
|
|
@ -49,7 +49,7 @@ public class ReviewPermissionChecker {
|
|||
*/
|
||||
public boolean canReviewPromotion(
|
||||
PromotionRequest request,
|
||||
Long userId,
|
||||
String userId,
|
||||
Set<String> platformRoles) {
|
||||
if (request.getSubmittedBy().equals(userId)) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public class ReviewService {
|
|||
}
|
||||
|
||||
@Transactional
|
||||
public ReviewTask submitReview(Long skillVersionId, Long namespaceId, Long userId) {
|
||||
public ReviewTask submitReview(Long skillVersionId, Long namespaceId, String userId) {
|
||||
SkillVersion skillVersion = skillVersionRepository.findById(skillVersionId)
|
||||
.orElseThrow(() -> new DomainNotFoundException("skill_version.not_found", skillVersionId));
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ public class ReviewService {
|
|||
}
|
||||
|
||||
@Transactional
|
||||
public ReviewTask approveReview(Long reviewTaskId, Long reviewerId, String comment,
|
||||
public ReviewTask approveReview(Long reviewTaskId, String reviewerId, String comment,
|
||||
Map<Long, NamespaceRole> userNamespaceRoles,
|
||||
Set<String> platformRoles) {
|
||||
ReviewTask task = reviewTaskRepository.findById(reviewTaskId)
|
||||
|
|
@ -111,7 +111,7 @@ public class ReviewService {
|
|||
}
|
||||
|
||||
@Transactional
|
||||
public ReviewTask rejectReview(Long reviewTaskId, Long reviewerId, String comment,
|
||||
public ReviewTask rejectReview(Long reviewTaskId, String reviewerId, String comment,
|
||||
Map<Long, NamespaceRole> userNamespaceRoles,
|
||||
Set<String> platformRoles) {
|
||||
ReviewTask task = reviewTaskRepository.findById(reviewTaskId)
|
||||
|
|
@ -144,7 +144,7 @@ public class ReviewService {
|
|||
}
|
||||
|
||||
@Transactional
|
||||
public void withdrawReview(Long skillVersionId, Long userId) {
|
||||
public void withdrawReview(Long skillVersionId, String userId) {
|
||||
ReviewTask task = reviewTaskRepository.findBySkillVersionIdAndStatus(
|
||||
skillVersionId, ReviewTaskStatus.PENDING)
|
||||
.orElseThrow(() -> new DomainNotFoundException("review_task.not_found_for_version", skillVersionId));
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ public class ReviewTask {
|
|||
private Integer version = 1;
|
||||
|
||||
@Column(name = "submitted_by", nullable = false)
|
||||
private Long submittedBy;
|
||||
private String submittedBy;
|
||||
|
||||
@Column(name = "reviewed_by")
|
||||
private Long reviewedBy;
|
||||
private String reviewedBy;
|
||||
|
||||
@Column(name = "review_comment", columnDefinition = "TEXT")
|
||||
private String reviewComment;
|
||||
|
|
@ -43,7 +43,7 @@ public class ReviewTask {
|
|||
protected ReviewTask() {}
|
||||
|
||||
public ReviewTask(Long skillVersionId, Long namespaceId,
|
||||
Long submittedBy) {
|
||||
String submittedBy) {
|
||||
this.skillVersionId = skillVersionId;
|
||||
this.namespaceId = namespaceId;
|
||||
this.submittedBy = submittedBy;
|
||||
|
|
@ -61,11 +61,11 @@ public class ReviewTask {
|
|||
|
||||
public Integer getVersion() { return version; }
|
||||
|
||||
public Long getSubmittedBy() { return submittedBy; }
|
||||
public String getSubmittedBy() { return submittedBy; }
|
||||
|
||||
public Long getReviewedBy() { return reviewedBy; }
|
||||
public String getReviewedBy() { return reviewedBy; }
|
||||
|
||||
public void setReviewedBy(Long reviewedBy) { this.reviewedBy = reviewedBy; }
|
||||
public void setReviewedBy(String reviewedBy) { this.reviewedBy = reviewedBy; }
|
||||
|
||||
public String getReviewComment() { return reviewComment; }
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ public interface ReviewTaskRepository {
|
|||
Optional<ReviewTask> findById(Long id);
|
||||
Optional<ReviewTask> findBySkillVersionIdAndStatus(Long skillVersionId, ReviewTaskStatus status);
|
||||
Page<ReviewTask> findByNamespaceIdAndStatus(Long namespaceId, ReviewTaskStatus status, Pageable pageable);
|
||||
Page<ReviewTask> findBySubmittedByAndStatus(Long submittedBy, ReviewTaskStatus status, Pageable pageable);
|
||||
Page<ReviewTask> findBySubmittedByAndStatus(String submittedBy, ReviewTaskStatus status, Pageable pageable);
|
||||
void delete(ReviewTask reviewTask);
|
||||
int updateStatusWithVersion(Long id, ReviewTaskStatus status, Long reviewedBy,
|
||||
int updateStatusWithVersion(Long id, ReviewTaskStatus status, String reviewedBy,
|
||||
String reviewComment, Integer expectedVersion);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package com.iflytek.skillhub.domain.shared.exception;
|
||||
|
||||
public class DomainBadRequestException extends LocalizedDomainException {
|
||||
|
||||
public DomainBadRequestException(String messageCode, Object... messageArgs) {
|
||||
super(messageCode, messageArgs);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.iflytek.skillhub.domain.shared.exception;
|
||||
|
||||
public class DomainForbiddenException extends LocalizedDomainException {
|
||||
|
||||
public DomainForbiddenException(String messageCode, Object... messageArgs) {
|
||||
super(messageCode, messageArgs);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.iflytek.skillhub.domain.shared.exception;
|
||||
|
||||
public abstract class LocalizedDomainException extends RuntimeException {
|
||||
|
||||
private final String messageCode;
|
||||
private final Object[] messageArgs;
|
||||
|
||||
protected LocalizedDomainException(String messageCode, Object... messageArgs) {
|
||||
super(messageCode);
|
||||
this.messageCode = messageCode;
|
||||
this.messageArgs = messageArgs == null ? new Object[0] : messageArgs;
|
||||
}
|
||||
|
||||
public String messageCode() {
|
||||
return messageCode;
|
||||
}
|
||||
|
||||
public Object[] messageArgs() {
|
||||
return messageArgs.clone();
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ public class Skill {
|
|||
private String summary;
|
||||
|
||||
@Column(name = "owner_id", nullable = false)
|
||||
private Long ownerId;
|
||||
private String ownerId;
|
||||
|
||||
@Column(name = "source_skill_id")
|
||||
private Long sourceSkillId;
|
||||
|
|
@ -54,13 +54,13 @@ public class Skill {
|
|||
private Integer ratingCount = 0;
|
||||
|
||||
@Column(name = "created_by")
|
||||
private Long createdBy;
|
||||
private String createdBy;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_by")
|
||||
private Long updatedBy;
|
||||
private String updatedBy;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
|
@ -68,7 +68,7 @@ public class Skill {
|
|||
protected Skill() {
|
||||
}
|
||||
|
||||
public Skill(Long namespaceId, String slug, Long ownerId, SkillVisibility visibility) {
|
||||
public Skill(Long namespaceId, String slug, String ownerId, SkillVisibility visibility) {
|
||||
this.namespaceId = namespaceId;
|
||||
this.slug = slug;
|
||||
this.ownerId = ownerId;
|
||||
|
|
@ -108,7 +108,7 @@ public class Skill {
|
|||
return summary;
|
||||
}
|
||||
|
||||
public Long getOwnerId() {
|
||||
public String getOwnerId() {
|
||||
return ownerId;
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +144,7 @@ public class Skill {
|
|||
return ratingCount;
|
||||
}
|
||||
|
||||
public Long getCreatedBy() {
|
||||
public String getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ public class Skill {
|
|||
return createdAt;
|
||||
}
|
||||
|
||||
public Long getUpdatedBy() {
|
||||
public String getUpdatedBy() {
|
||||
return updatedBy;
|
||||
}
|
||||
|
||||
|
|
@ -185,11 +185,11 @@ public class Skill {
|
|||
this.latestVersionId = latestVersionId;
|
||||
}
|
||||
|
||||
public void setCreatedBy(Long createdBy) {
|
||||
public void setCreatedBy(String createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
public void setUpdatedBy(Long updatedBy) {
|
||||
public void setUpdatedBy(String updatedBy) {
|
||||
this.updatedBy = updatedBy;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@ import java.util.List;
|
|||
public interface SkillFileRepository {
|
||||
List<SkillFile> findByVersionId(Long versionId);
|
||||
SkillFile save(SkillFile file);
|
||||
void saveAll(List<SkillFile> files);
|
||||
<S extends SkillFile> List<S> saveAll(Iterable<S> files);
|
||||
void deleteByVersionId(Long versionId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import java.util.Optional;
|
|||
|
||||
public interface SkillRepository {
|
||||
Optional<Skill> findById(Long id);
|
||||
List<Skill> findByIdIn(List<Long> ids);
|
||||
List<Skill> findAll();
|
||||
Optional<Skill> findByNamespaceIdAndSlug(Long namespaceId, String slug);
|
||||
List<Skill> findByNamespaceIdAndStatus(Long namespaceId, SkillStatus status);
|
||||
Skill save(Skill skill);
|
||||
List<Skill> findByOwnerId(Long ownerId);
|
||||
List<Skill> findByOwnerId(String ownerId);
|
||||
void incrementDownloadCount(Long skillId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public class SkillTag {
|
|||
private Long versionId;
|
||||
|
||||
@Column(name = "created_by", nullable = false)
|
||||
private Long createdBy;
|
||||
private String createdBy;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
|
@ -32,7 +32,7 @@ public class SkillTag {
|
|||
protected SkillTag() {
|
||||
}
|
||||
|
||||
public SkillTag(Long skillId, String tagName, Long versionId, Long createdBy) {
|
||||
public SkillTag(Long skillId, String tagName, Long versionId, String createdBy) {
|
||||
this.skillId = skillId;
|
||||
this.tagName = tagName;
|
||||
this.versionId = versionId;
|
||||
|
|
@ -67,7 +67,7 @@ public class SkillTag {
|
|||
return versionId;
|
||||
}
|
||||
|
||||
public Long getCreatedBy() {
|
||||
public String getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.iflytek.skillhub.domain.skill;
|
|||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
@Entity
|
||||
@Table(name = "skill_version")
|
||||
|
|
@ -14,7 +16,7 @@ public class SkillVersion {
|
|||
@Column(name = "skill_id", nullable = false)
|
||||
private Long skillId;
|
||||
|
||||
@Column(nullable = false, length = 50)
|
||||
@Column(nullable = false, length = 64)
|
||||
private String version;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
|
|
@ -24,10 +26,12 @@ public class SkillVersion {
|
|||
@Column(columnDefinition = "TEXT")
|
||||
private String changelog;
|
||||
|
||||
@Column(name = "parsed_metadata_json", columnDefinition = "TEXT")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Column(name = "parsed_metadata_json", columnDefinition = "jsonb")
|
||||
private String parsedMetadataJson;
|
||||
|
||||
@Column(name = "manifest_json", columnDefinition = "TEXT")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Column(name = "manifest_json", columnDefinition = "jsonb")
|
||||
private String manifestJson;
|
||||
|
||||
@Column(name = "file_count", nullable = false)
|
||||
|
|
@ -40,7 +44,7 @@ public class SkillVersion {
|
|||
private LocalDateTime publishedAt;
|
||||
|
||||
@Column(name = "created_by", nullable = false)
|
||||
private Long createdBy;
|
||||
private String createdBy;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
|
@ -48,7 +52,7 @@ public class SkillVersion {
|
|||
protected SkillVersion() {
|
||||
}
|
||||
|
||||
public SkillVersion(Long skillId, String version, Long createdBy) {
|
||||
public SkillVersion(Long skillId, String version, String createdBy) {
|
||||
this.skillId = skillId;
|
||||
this.version = version;
|
||||
this.createdBy = createdBy;
|
||||
|
|
@ -101,7 +105,7 @@ public class SkillVersion {
|
|||
return publishedAt;
|
||||
}
|
||||
|
||||
public Long getCreatedBy() {
|
||||
public String getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import java.util.Optional;
|
|||
|
||||
public interface SkillVersionRepository {
|
||||
Optional<SkillVersion> findById(Long id);
|
||||
List<SkillVersion> findByIdIn(List<Long> ids);
|
||||
Optional<SkillVersion> findBySkillIdAndVersion(Long skillId, String version);
|
||||
List<SkillVersion> findBySkillIdAndStatus(Long skillId, SkillVersionStatus status);
|
||||
SkillVersion save(SkillVersion version);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import java.util.Map;
|
|||
|
||||
public class VisibilityChecker {
|
||||
|
||||
public boolean canAccess(Skill skill, Long currentUserId, Map<Long, NamespaceRole> userNamespaceRoles) {
|
||||
public boolean canAccess(Skill skill, String currentUserId, Map<Long, NamespaceRole> userNamespaceRoles) {
|
||||
return switch (skill.getVisibility()) {
|
||||
case PUBLIC -> true;
|
||||
case NAMESPACE_ONLY -> userNamespaceRoles.containsKey(skill.getNamespaceId());
|
||||
|
|
@ -14,7 +14,7 @@ public class VisibilityChecker {
|
|||
};
|
||||
}
|
||||
|
||||
private boolean isOwner(Skill skill, Long currentUserId) {
|
||||
private boolean isOwner(Skill skill, String currentUserId) {
|
||||
return currentUserId != null && skill.getOwnerId().equals(currentUserId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.iflytek.skillhub.domain.skill.metadata;
|
||||
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import java.util.Map;
|
||||
|
|
@ -10,23 +11,23 @@ public class SkillMetadataParser {
|
|||
|
||||
public SkillMetadata parse(String content) {
|
||||
if (content == null || content.isBlank()) {
|
||||
throw new IllegalArgumentException("Content cannot be null or empty");
|
||||
throw new DomainBadRequestException("error.skill.metadata.content.empty");
|
||||
}
|
||||
|
||||
String trimmedContent = content.trim();
|
||||
|
||||
if (!trimmedContent.startsWith(FRONTMATTER_DELIMITER)) {
|
||||
throw new IllegalArgumentException("Missing frontmatter: content must start with '---'");
|
||||
throw new DomainBadRequestException("error.skill.metadata.frontmatter.missingStart");
|
||||
}
|
||||
|
||||
int firstDelimiterEnd = trimmedContent.indexOf('\n', FRONTMATTER_DELIMITER.length());
|
||||
if (firstDelimiterEnd == -1) {
|
||||
throw new IllegalArgumentException("Missing frontmatter: no content after first '---'");
|
||||
throw new DomainBadRequestException("error.skill.metadata.frontmatter.missingContent");
|
||||
}
|
||||
|
||||
int secondDelimiterStart = trimmedContent.indexOf(FRONTMATTER_DELIMITER, firstDelimiterEnd + 1);
|
||||
if (secondDelimiterStart == -1) {
|
||||
throw new IllegalArgumentException("Missing frontmatter: no closing '---' found");
|
||||
throw new DomainBadRequestException("error.skill.metadata.frontmatter.missingEnd");
|
||||
}
|
||||
|
||||
String yamlContent = trimmedContent.substring(firstDelimiterEnd + 1, secondDelimiterStart).trim();
|
||||
|
|
@ -37,18 +38,20 @@ public class SkillMetadataParser {
|
|||
Yaml yaml = new Yaml();
|
||||
Object parsed = yaml.load(yamlContent);
|
||||
if (!(parsed instanceof Map)) {
|
||||
throw new IllegalArgumentException("Invalid YAML: frontmatter must be a map");
|
||||
throw new DomainBadRequestException("error.skill.metadata.yaml.notMap");
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) parsed;
|
||||
frontmatter = map;
|
||||
} catch (DomainBadRequestException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Invalid YAML in frontmatter: " + e.getMessage(), e);
|
||||
throw new DomainBadRequestException("error.skill.metadata.yaml.invalid", e.getMessage());
|
||||
}
|
||||
|
||||
String name = extractRequiredField(frontmatter, "name");
|
||||
String description = extractRequiredField(frontmatter, "description");
|
||||
String version = extractRequiredField(frontmatter, "version");
|
||||
String version = extractOptionalField(frontmatter, "version");
|
||||
|
||||
return new SkillMetadata(name, description, version, body, frontmatter);
|
||||
}
|
||||
|
|
@ -56,8 +59,13 @@ public class SkillMetadataParser {
|
|||
private String extractRequiredField(Map<String, Object> frontmatter, String fieldName) {
|
||||
Object value = frontmatter.get(fieldName);
|
||||
if (value == null) {
|
||||
throw new IllegalArgumentException("Missing required field: " + fieldName);
|
||||
throw new DomainBadRequestException("error.skill.metadata.requiredField.missing", fieldName);
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
private String extractOptionalField(Map<String, Object> frontmatter, String fieldName) {
|
||||
Object value = frontmatter.get(fieldName);
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ 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.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import com.iflytek.skillhub.storage.ObjectStorageService;
|
||||
import com.iflytek.skillhub.storage.ObjectMetadata;
|
||||
|
|
@ -51,24 +53,24 @@ public class SkillDownloadService {
|
|||
public DownloadResult downloadLatest(
|
||||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
Long currentUserId,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Skill not found: " + skillSlug));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new SecurityException("Access denied to skill: " + skillSlug);
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skillSlug);
|
||||
}
|
||||
|
||||
if (skill.getLatestVersionId() == null) {
|
||||
throw new IllegalArgumentException("No published version available for skill: " + skillSlug);
|
||||
throw new DomainBadRequestException("error.skill.version.latest.unavailable", skillSlug);
|
||||
}
|
||||
|
||||
SkillVersion version = skillVersionRepository.findById(skill.getLatestVersionId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Latest version not found"));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.version.latest.notFound"));
|
||||
|
||||
return downloadVersion(skill, version);
|
||||
}
|
||||
|
|
@ -77,20 +79,20 @@ public class SkillDownloadService {
|
|||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
String versionStr,
|
||||
Long currentUserId,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Skill not found: " + skillSlug));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new SecurityException("Access denied to skill: " + skillSlug);
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skillSlug);
|
||||
}
|
||||
|
||||
SkillVersion version = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), versionStr)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Version not found: " + versionStr));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.version.notFound", versionStr));
|
||||
|
||||
return downloadVersion(skill, version);
|
||||
}
|
||||
|
|
@ -99,36 +101,39 @@ public class SkillDownloadService {
|
|||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
String tagName,
|
||||
Long currentUserId,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Skill not found: " + skillSlug));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new SecurityException("Access denied to skill: " + skillSlug);
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skillSlug);
|
||||
}
|
||||
|
||||
SkillTag tag = skillTagRepository.findBySkillIdAndTagName(skill.getId(), tagName)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Tag not found: " + tagName));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.tag.notFound", tagName));
|
||||
|
||||
if (tag.getVersionId() == null) {
|
||||
throw new IllegalArgumentException("Tag does not point to a version: " + tagName);
|
||||
throw new DomainBadRequestException("error.skill.tag.version.missing", tagName);
|
||||
}
|
||||
|
||||
SkillVersion version = skillVersionRepository.findById(tag.getVersionId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Version not found for tag: " + tagName));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.tag.version.notFound", tagName));
|
||||
|
||||
return downloadVersion(skill, version);
|
||||
}
|
||||
|
||||
private DownloadResult downloadVersion(Skill skill, SkillVersion version) {
|
||||
assertPublishedAccessible(skill);
|
||||
assertPublishedVersion(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");
|
||||
throw new DomainBadRequestException("error.skill.bundle.notFound");
|
||||
}
|
||||
|
||||
ObjectMetadata metadata = objectStorageService.getMetadata(storageKey);
|
||||
|
|
@ -144,6 +149,18 @@ public class SkillDownloadService {
|
|||
|
||||
private Namespace findNamespace(String slug) {
|
||||
return namespaceRepository.findBySlug(slug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Namespace not found: " + slug));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", slug));
|
||||
}
|
||||
|
||||
private void assertPublishedAccessible(Skill skill) {
|
||||
if (skill.getStatus() != SkillStatus.ACTIVE) {
|
||||
throw new DomainBadRequestException("error.skill.status.notActive");
|
||||
}
|
||||
}
|
||||
|
||||
private void assertPublishedVersion(SkillVersion version) {
|
||||
if (version.getStatus() != SkillVersionStatus.PUBLISHED) {
|
||||
throw new DomainBadRequestException("error.skill.version.notPublished", version.getVersion());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ 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.namespace.SlugValidator;
|
||||
import com.iflytek.skillhub.domain.review.ReviewTask;
|
||||
import com.iflytek.skillhub.domain.review.ReviewTaskRepository;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import com.iflytek.skillhub.domain.skill.metadata.SkillMetadata;
|
||||
import com.iflytek.skillhub.domain.skill.metadata.SkillMetadataParser;
|
||||
|
|
@ -18,15 +22,25 @@ import org.springframework.stereotype.Service;
|
|||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
@Service
|
||||
public class SkillPublishService {
|
||||
|
||||
public record PublishResult(
|
||||
Long skillId,
|
||||
String slug,
|
||||
SkillVersion version
|
||||
) {}
|
||||
|
||||
private final NamespaceRepository namespaceRepository;
|
||||
private final NamespaceMemberRepository namespaceMemberRepository;
|
||||
private final SkillRepository skillRepository;
|
||||
|
|
@ -38,6 +52,7 @@ public class SkillPublishService {
|
|||
private final PrePublishValidator prePublishValidator;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ReviewTaskRepository reviewTaskRepository;
|
||||
|
||||
public SkillPublishService(
|
||||
NamespaceRepository namespaceRepository,
|
||||
|
|
@ -50,7 +65,8 @@ public class SkillPublishService {
|
|||
SkillMetadataParser skillMetadataParser,
|
||||
PrePublishValidator prePublishValidator,
|
||||
ApplicationEventPublisher eventPublisher,
|
||||
ObjectMapper objectMapper) {
|
||||
ObjectMapper objectMapper,
|
||||
ReviewTaskRepository reviewTaskRepository) {
|
||||
this.namespaceRepository = namespaceRepository;
|
||||
this.namespaceMemberRepository = namespaceMemberRepository;
|
||||
this.skillRepository = skillRepository;
|
||||
|
|
@ -62,70 +78,79 @@ public class SkillPublishService {
|
|||
this.prePublishValidator = prePublishValidator;
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.objectMapper = objectMapper;
|
||||
this.reviewTaskRepository = reviewTaskRepository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SkillVersion publishFromEntries(
|
||||
public PublishResult publishFromEntries(
|
||||
String namespaceSlug,
|
||||
List<PackageEntry> entries,
|
||||
Long publisherId,
|
||||
String publisherId,
|
||||
SkillVisibility visibility) {
|
||||
|
||||
// 1. Find namespace by slug
|
||||
Namespace namespace = namespaceRepository.findBySlug(namespaceSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Namespace not found: " + namespaceSlug));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", namespaceSlug));
|
||||
|
||||
// 2. Check publisher is member
|
||||
namespaceMemberRepository.findByNamespaceIdAndUserId(namespace.getId(), publisherId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Publisher is not a member of namespace: " + namespaceSlug));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.publish.publisher.notMember", namespaceSlug));
|
||||
|
||||
// 3. Validate package
|
||||
ValidationResult packageValidation = skillPackageValidator.validate(entries);
|
||||
if (!packageValidation.passed()) {
|
||||
throw new IllegalArgumentException("Package validation failed: " + String.join(", ", packageValidation.errors()));
|
||||
throw new DomainBadRequestException(
|
||||
"error.skill.publish.package.invalid",
|
||||
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"));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.publish.skillMd.notFound"));
|
||||
|
||||
String skillMdContent = new String(skillMd.content());
|
||||
SkillMetadata metadata = skillMetadataParser.parse(skillMdContent);
|
||||
if (metadata.version() == null || metadata.version().isBlank()) {
|
||||
throw new DomainBadRequestException("error.skill.metadata.requiredField.missing", "version");
|
||||
}
|
||||
String skillSlug = SlugValidator.slugify(metadata.name());
|
||||
|
||||
// 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()));
|
||||
throw new DomainBadRequestException(
|
||||
"error.skill.publish.precheck.failed",
|
||||
String.join(", ", prePublishValidation.errors()));
|
||||
}
|
||||
|
||||
// 6. Find or create Skill record
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), metadata.name())
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseGet(() -> {
|
||||
Skill newSkill = new Skill(namespace.getId(), metadata.name(), publisherId, visibility);
|
||||
Skill newSkill = new Skill(namespace.getId(), skillSlug, 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());
|
||||
throw new DomainBadRequestException("error.skill.version.exists", metadata.version());
|
||||
}
|
||||
|
||||
// 8. Create SkillVersion
|
||||
SkillVersion version = new SkillVersion(skill.getId(), metadata.version(), publisherId);
|
||||
version.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
version.setPublishedAt(LocalDateTime.now());
|
||||
version.setStatus(SkillVersionStatus.PENDING_REVIEW);
|
||||
|
||||
// Store metadata as JSON
|
||||
try {
|
||||
String metadataJson = objectMapper.writeValueAsString(metadata);
|
||||
version.setParsedMetadataJson(metadataJson);
|
||||
version.setManifestJson(objectMapper.writeValueAsString(buildManifest(entries)));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to serialize metadata", e);
|
||||
throw new IllegalStateException("Failed to serialize metadata", e);
|
||||
}
|
||||
|
||||
version = skillVersionRepository.save(version);
|
||||
|
|
@ -168,12 +193,22 @@ public class SkillPublishService {
|
|||
digest.reset();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to process files", e);
|
||||
throw new IllegalStateException("Failed to process files", e);
|
||||
}
|
||||
|
||||
// 10. Save SkillFile records
|
||||
skillFileRepository.saveAll(skillFiles);
|
||||
|
||||
// 10.5 Build and upload bundle zip for download endpoints
|
||||
byte[] bundleZip = buildBundle(entries);
|
||||
String bundleKey = String.format("packages/%d/%d/bundle.zip", skill.getId(), version.getId());
|
||||
objectStorageService.putObject(
|
||||
bundleKey,
|
||||
new ByteArrayInputStream(bundleZip),
|
||||
bundleZip.length,
|
||||
"application/zip"
|
||||
);
|
||||
|
||||
// 11. Update version stats
|
||||
version.setFileCount(skillFiles.size());
|
||||
version.setTotalSize(totalSize);
|
||||
|
|
@ -189,7 +224,32 @@ public class SkillPublishService {
|
|||
// 13. Publish SkillPublishedEvent
|
||||
eventPublisher.publishEvent(new SkillPublishedEvent(skill.getId(), version.getId(), publisherId));
|
||||
|
||||
// 14. Return version
|
||||
return version;
|
||||
// 14. Return published identifiers
|
||||
return new PublishResult(skill.getId(), skill.getSlug(), version);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> buildManifest(List<PackageEntry> entries) {
|
||||
return entries.stream()
|
||||
.map(entry -> Map.<String, Object>of(
|
||||
"path", entry.path(),
|
||||
"size", entry.size(),
|
||||
"contentType", entry.contentType()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private byte[] buildBundle(List<PackageEntry> entries) {
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
|
||||
for (PackageEntry entry : entries) {
|
||||
ZipEntry zipEntry = new ZipEntry(entry.path());
|
||||
zipOutputStream.putNextEntry(zipEntry);
|
||||
zipOutputStream.write(entry.content());
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
zipOutputStream.finish();
|
||||
return outputStream.toByteArray();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to build bundle zip", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ 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.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import com.iflytek.skillhub.storage.ObjectStorageService;
|
||||
import org.springframework.data.domain.Page;
|
||||
|
|
@ -11,8 +13,15 @@ import org.springframework.data.domain.Pageable;
|
|||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Comparator;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
|
|
@ -22,6 +31,7 @@ public class SkillQueryService {
|
|||
private final SkillRepository skillRepository;
|
||||
private final SkillVersionRepository skillVersionRepository;
|
||||
private final SkillFileRepository skillFileRepository;
|
||||
private final SkillTagRepository skillTagRepository;
|
||||
private final ObjectStorageService objectStorageService;
|
||||
private final VisibilityChecker visibilityChecker;
|
||||
|
||||
|
|
@ -30,12 +40,14 @@ public class SkillQueryService {
|
|||
SkillRepository skillRepository,
|
||||
SkillVersionRepository skillVersionRepository,
|
||||
SkillFileRepository skillFileRepository,
|
||||
SkillTagRepository skillTagRepository,
|
||||
ObjectStorageService objectStorageService,
|
||||
VisibilityChecker visibilityChecker) {
|
||||
this.namespaceRepository = namespaceRepository;
|
||||
this.skillRepository = skillRepository;
|
||||
this.skillVersionRepository = skillVersionRepository;
|
||||
this.skillFileRepository = skillFileRepository;
|
||||
this.skillTagRepository = skillTagRepository;
|
||||
this.objectStorageService = objectStorageService;
|
||||
this.visibilityChecker = visibilityChecker;
|
||||
}
|
||||
|
|
@ -53,19 +65,42 @@ public class SkillQueryService {
|
|||
Long namespaceId
|
||||
) {}
|
||||
|
||||
public record SkillVersionDetailDTO(
|
||||
Long id,
|
||||
String version,
|
||||
String status,
|
||||
String changelog,
|
||||
Integer fileCount,
|
||||
Long totalSize,
|
||||
java.time.LocalDateTime publishedAt,
|
||||
String parsedMetadataJson,
|
||||
String manifestJson
|
||||
) {}
|
||||
|
||||
public record ResolvedVersionDTO(
|
||||
Long skillId,
|
||||
String namespace,
|
||||
String slug,
|
||||
String version,
|
||||
Long versionId,
|
||||
String fingerprint,
|
||||
Boolean matched,
|
||||
String downloadUrl
|
||||
) {}
|
||||
|
||||
public SkillDetailDTO getSkillDetail(
|
||||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
Long currentUserId,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Skill not found: " + skillSlug));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new SecurityException("Access denied to skill: " + skillSlug);
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skillSlug);
|
||||
}
|
||||
|
||||
String latestVersion = null;
|
||||
|
|
@ -92,7 +127,7 @@ public class SkillQueryService {
|
|||
|
||||
public Page<Skill> listSkillsByNamespace(
|
||||
String namespaceSlug,
|
||||
Long currentUserId,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles,
|
||||
Pageable pageable) {
|
||||
|
||||
|
|
@ -112,38 +147,91 @@ public class SkillQueryService {
|
|||
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));
|
||||
public SkillVersionDetailDTO getVersionDetail(
|
||||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
String version,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
Skill skill = findSkill(namespaceSlug, skillSlug);
|
||||
assertPublishedAccessible(skill, currentUserId, userNsRoles);
|
||||
SkillVersion skillVersion = findVersion(skill, version);
|
||||
assertPublishedVersion(skillVersion, version);
|
||||
|
||||
SkillVersion skillVersion = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), version)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Version not found: " + version));
|
||||
return new SkillVersionDetailDTO(
|
||||
skillVersion.getId(),
|
||||
skillVersion.getVersion(),
|
||||
skillVersion.getStatus().name(),
|
||||
skillVersion.getChangelog(),
|
||||
skillVersion.getFileCount(),
|
||||
skillVersion.getTotalSize(),
|
||||
skillVersion.getPublishedAt(),
|
||||
skillVersion.getParsedMetadataJson(),
|
||||
skillVersion.getManifestJson()
|
||||
);
|
||||
}
|
||||
|
||||
public List<SkillFile> listFiles(
|
||||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
String version,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
Skill skill = findSkill(namespaceSlug, skillSlug);
|
||||
assertPublishedAccessible(skill, currentUserId, userNsRoles);
|
||||
|
||||
SkillVersion skillVersion = findVersion(skill, version);
|
||||
assertPublishedVersion(skillVersion, 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));
|
||||
public List<SkillFile> listFilesByTag(
|
||||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
String tagName,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
Skill skill = findSkill(namespaceSlug, skillSlug);
|
||||
assertPublishedAccessible(skill, currentUserId, userNsRoles);
|
||||
SkillVersion skillVersion = resolveVersionEntity(skill, null, tagName, null);
|
||||
return skillFileRepository.findByVersionId(skillVersion.getId());
|
||||
}
|
||||
|
||||
SkillVersion skillVersion = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), version)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Version not found: " + version));
|
||||
public InputStream getFileContent(
|
||||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
String version,
|
||||
String filePath,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
Skill skill = findSkill(namespaceSlug, skillSlug);
|
||||
assertPublishedAccessible(skill, currentUserId, userNsRoles);
|
||||
|
||||
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));
|
||||
SkillVersion skillVersion = findVersion(skill, version);
|
||||
assertPublishedVersion(skillVersion, version);
|
||||
|
||||
SkillFile file = findFile(skillVersion, filePath);
|
||||
|
||||
return objectStorageService.getObject(file.getStorageKey());
|
||||
}
|
||||
|
||||
public InputStream getFileContentByTag(
|
||||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
String tagName,
|
||||
String filePath,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
Skill skill = findSkill(namespaceSlug, skillSlug);
|
||||
assertPublishedAccessible(skill, currentUserId, userNsRoles);
|
||||
SkillVersion skillVersion = resolveVersionEntity(skill, null, tagName, null);
|
||||
SkillFile file = findFile(skillVersion, 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));
|
||||
Skill skill = findSkill(namespaceSlug, skillSlug);
|
||||
|
||||
List<SkillVersion> publishedVersions = skillVersionRepository.findBySkillIdAndStatus(
|
||||
skill.getId(), SkillVersionStatus.PUBLISHED);
|
||||
|
|
@ -156,8 +244,145 @@ public class SkillQueryService {
|
|||
return new PageImpl<>(pageContent, pageable, publishedVersions.size());
|
||||
}
|
||||
|
||||
public ResolvedVersionDTO resolveVersion(
|
||||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
String version,
|
||||
String tag,
|
||||
String hash,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
if (version != null && !version.isBlank() && tag != null && !tag.isBlank()) {
|
||||
throw new DomainBadRequestException("error.skill.resolve.versionTag.conflict");
|
||||
}
|
||||
|
||||
Skill skill = findSkill(namespaceSlug, skillSlug);
|
||||
assertPublishedAccessible(skill, currentUserId, userNsRoles);
|
||||
SkillVersion resolved = resolveVersionEntity(skill, version, tag, hash);
|
||||
String fingerprint = computeFingerprint(resolved);
|
||||
Boolean matched = hash == null || hash.isBlank() ? null : Objects.equals(hash, fingerprint);
|
||||
|
||||
return new ResolvedVersionDTO(
|
||||
skill.getId(),
|
||||
namespaceSlug,
|
||||
skill.getSlug(),
|
||||
resolved.getVersion(),
|
||||
resolved.getId(),
|
||||
fingerprint,
|
||||
matched,
|
||||
String.format(
|
||||
"/api/v1/skills/%s/%s/versions/%s/download",
|
||||
encodePathSegment(namespaceSlug),
|
||||
encodePathSegment(skill.getSlug()),
|
||||
encodePathSegment(resolved.getVersion()))
|
||||
);
|
||||
}
|
||||
|
||||
private Namespace findNamespace(String slug) {
|
||||
return namespaceRepository.findBySlug(slug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Namespace not found: " + slug));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", slug));
|
||||
}
|
||||
|
||||
private Skill findSkill(String namespaceSlug, String skillSlug) {
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
return skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
}
|
||||
|
||||
private SkillVersion findVersion(Skill skill, String version) {
|
||||
return skillVersionRepository.findBySkillIdAndVersion(skill.getId(), version)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.version.notFound", version));
|
||||
}
|
||||
|
||||
private SkillFile findFile(SkillVersion skillVersion, String filePath) {
|
||||
return skillFileRepository.findByVersionId(skillVersion.getId()).stream()
|
||||
.filter(f -> f.getFilePath().equals(filePath))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.file.notFound", filePath));
|
||||
}
|
||||
|
||||
private SkillVersion resolveVersionEntity(Skill skill, String version, String tag, String hash) {
|
||||
if (version != null && !version.isBlank()) {
|
||||
SkillVersion exactVersion = findVersion(skill, version);
|
||||
assertPublishedVersion(exactVersion, version);
|
||||
return exactVersion;
|
||||
}
|
||||
|
||||
if (tag != null && !tag.isBlank()) {
|
||||
if ("latest".equalsIgnoreCase(tag)) {
|
||||
return resolveLatestVersion(skill);
|
||||
}
|
||||
SkillTag skillTag = skillTagRepository.findBySkillIdAndTagName(skill.getId(), tag)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.tag.notFound", tag));
|
||||
if (skillTag.getVersionId() == null) {
|
||||
throw new DomainBadRequestException("error.skill.tag.version.missing", tag);
|
||||
}
|
||||
SkillVersion taggedVersion = skillVersionRepository.findById(skillTag.getVersionId())
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.tag.version.notFound", tag));
|
||||
assertPublishedVersion(taggedVersion, taggedVersion.getVersion());
|
||||
return taggedVersion;
|
||||
}
|
||||
|
||||
List<SkillVersion> publishedVersions = skillVersionRepository.findBySkillIdAndStatus(
|
||||
skill.getId(), SkillVersionStatus.PUBLISHED);
|
||||
if (publishedVersions.isEmpty()) {
|
||||
throw new DomainBadRequestException("error.skill.version.latest.unavailable", skill.getSlug());
|
||||
}
|
||||
|
||||
if (hash != null && !hash.isBlank()) {
|
||||
Optional<SkillVersion> matchedVersion = publishedVersions.stream()
|
||||
.filter(candidate -> Objects.equals(hash, computeFingerprint(candidate)))
|
||||
.findFirst();
|
||||
if (matchedVersion.isPresent()) {
|
||||
return matchedVersion.get();
|
||||
}
|
||||
}
|
||||
|
||||
return resolveLatestVersion(skill);
|
||||
}
|
||||
|
||||
private SkillVersion resolveLatestVersion(Skill skill) {
|
||||
if (skill.getLatestVersionId() == null) {
|
||||
throw new DomainBadRequestException("error.skill.version.latest.unavailable", skill.getSlug());
|
||||
}
|
||||
SkillVersion latestVersion = skillVersionRepository.findById(skill.getLatestVersionId())
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.version.latest.notFound"));
|
||||
assertPublishedVersion(latestVersion, latestVersion.getVersion());
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
private String computeFingerprint(SkillVersion version) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
List<SkillFile> files = skillFileRepository.findByVersionId(version.getId()).stream()
|
||||
.sorted(Comparator.comparing(SkillFile::getFilePath))
|
||||
.toList();
|
||||
for (SkillFile file : files) {
|
||||
String line = file.getFilePath() + ":" + file.getSha256() + "\n";
|
||||
digest.update(line.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return "sha256:" + HexFormat.of().formatHex(digest.digest());
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to compute version fingerprint", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String encodePathSegment(String value) {
|
||||
return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20");
|
||||
}
|
||||
|
||||
private void assertPublishedAccessible(Skill skill, String currentUserId, Map<Long, NamespaceRole> userNsRoles) {
|
||||
if (skill.getStatus() != SkillStatus.ACTIVE) {
|
||||
throw new DomainBadRequestException("error.skill.status.notActive");
|
||||
}
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skill.getSlug());
|
||||
}
|
||||
}
|
||||
|
||||
private void assertPublishedVersion(SkillVersion version, String versionStr) {
|
||||
if (version.getStatus() != SkillVersionStatus.PUBLISHED) {
|
||||
throw new DomainBadRequestException("error.skill.version.notPublished", versionStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
package com.iflytek.skillhub.domain.skill.service;
|
||||
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
|
@ -14,16 +18,19 @@ public class SkillTagService {
|
|||
private static final String RESERVED_TAG_LATEST = "latest";
|
||||
|
||||
private final NamespaceRepository namespaceRepository;
|
||||
private final NamespaceMemberRepository namespaceMemberRepository;
|
||||
private final SkillRepository skillRepository;
|
||||
private final SkillVersionRepository skillVersionRepository;
|
||||
private final SkillTagRepository skillTagRepository;
|
||||
|
||||
public SkillTagService(
|
||||
NamespaceRepository namespaceRepository,
|
||||
NamespaceMemberRepository namespaceMemberRepository,
|
||||
SkillRepository skillRepository,
|
||||
SkillVersionRepository skillVersionRepository,
|
||||
SkillTagRepository skillTagRepository) {
|
||||
this.namespaceRepository = namespaceRepository;
|
||||
this.namespaceMemberRepository = namespaceMemberRepository;
|
||||
this.skillRepository = skillRepository;
|
||||
this.skillVersionRepository = skillVersionRepository;
|
||||
this.skillTagRepository = skillTagRepository;
|
||||
|
|
@ -32,9 +39,13 @@ public class SkillTagService {
|
|||
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));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
|
||||
return skillTagRepository.findBySkillId(skill.getId());
|
||||
List<SkillTag> tags = new java.util.ArrayList<>(skillTagRepository.findBySkillId(skill.getId()));
|
||||
if (skill.getLatestVersionId() != null) {
|
||||
tags.add(new SkillTag(skill.getId(), RESERVED_TAG_LATEST, skill.getLatestVersionId(), skill.getOwnerId()));
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
|
@ -43,24 +54,25 @@ public class SkillTagService {
|
|||
String skillSlug,
|
||||
String tagName,
|
||||
String targetVersion,
|
||||
Long operatorId) {
|
||||
String operatorId) {
|
||||
|
||||
// Reject "latest" tag
|
||||
if (RESERVED_TAG_LATEST.equalsIgnoreCase(tagName)) {
|
||||
throw new IllegalArgumentException("Tag name 'latest' is reserved");
|
||||
throw new DomainBadRequestException("error.skill.tag.latest.reserved");
|
||||
}
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
assertAdminOrOwner(namespace.getId(), operatorId);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Skill not found: " + skillSlug));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
|
||||
// Find target version
|
||||
SkillVersion version = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), targetVersion)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Version not found: " + targetVersion));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.version.notFound", targetVersion));
|
||||
|
||||
// Target must be PUBLISHED
|
||||
if (version.getStatus() != SkillVersionStatus.PUBLISHED) {
|
||||
throw new IllegalArgumentException("Target version must be PUBLISHED");
|
||||
throw new DomainBadRequestException("error.skill.tag.targetVersion.notPublished");
|
||||
}
|
||||
|
||||
// Check if tag exists
|
||||
|
|
@ -78,24 +90,34 @@ public class SkillTagService {
|
|||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteTag(String namespaceSlug, String skillSlug, String tagName, Long operatorId) {
|
||||
public void deleteTag(String namespaceSlug, String skillSlug, String tagName, String operatorId) {
|
||||
// Reject "latest" tag
|
||||
if (RESERVED_TAG_LATEST.equalsIgnoreCase(tagName)) {
|
||||
throw new IllegalArgumentException("Tag name 'latest' is reserved and cannot be deleted");
|
||||
throw new DomainBadRequestException("error.skill.tag.latest.delete");
|
||||
}
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
assertAdminOrOwner(namespace.getId(), operatorId);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Skill not found: " + skillSlug));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
|
||||
SkillTag tag = skillTagRepository.findBySkillIdAndTagName(skill.getId(), tagName)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Tag not found: " + tagName));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.tag.notFound", tagName));
|
||||
|
||||
skillTagRepository.delete(tag);
|
||||
}
|
||||
|
||||
private Namespace findNamespace(String slug) {
|
||||
return namespaceRepository.findBySlug(slug)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Namespace not found: " + slug));
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", slug));
|
||||
}
|
||||
|
||||
private void assertAdminOrOwner(Long namespaceId, String operatorId) {
|
||||
NamespaceRole role = namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, operatorId)
|
||||
.map(member -> member.getRole())
|
||||
.orElseThrow(() -> new DomainForbiddenException("error.namespace.membership.required"));
|
||||
if (role != NamespaceRole.OWNER && role != NamespaceRole.ADMIN) {
|
||||
throw new DomainForbiddenException("error.namespace.admin.required");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package com.iflytek.skillhub.domain.skill.validation;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class NoOpPrePublishValidator implements PrePublishValidator {
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ public interface PrePublishValidator {
|
|||
record SkillPackageContext(
|
||||
List<PackageEntry> entries,
|
||||
SkillMetadata metadata,
|
||||
Long publisherId,
|
||||
String publisherId,
|
||||
Long namespaceId
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.iflytek.skillhub.domain.skill.validation;
|
||||
|
||||
import com.iflytek.skillhub.domain.shared.exception.LocalizedDomainException;
|
||||
import com.iflytek.skillhub.domain.skill.metadata.SkillMetadataParser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -42,8 +43,11 @@ public class SkillPackageValidator {
|
|||
try {
|
||||
String content = new String(skillMd.content());
|
||||
metadataParser.parse(content);
|
||||
} catch (IllegalArgumentException e) {
|
||||
errors.add("Invalid SKILL.md frontmatter: " + e.getMessage());
|
||||
} catch (LocalizedDomainException e) {
|
||||
String detail = e.messageArgs().length == 0
|
||||
? e.messageCode()
|
||||
: e.messageCode() + " " + java.util.Arrays.toString(e.messageArgs());
|
||||
errors.add("Invalid SKILL.md frontmatter: " + detail);
|
||||
}
|
||||
|
||||
// 3. Check file count
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import java.time.LocalDateTime;
|
|||
@Table(name = "user_account")
|
||||
public class UserAccount {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
@Column(length = 128)
|
||||
private String id;
|
||||
|
||||
@Column(name = "display_name", nullable = false, length = 128)
|
||||
private String displayName;
|
||||
|
|
@ -24,7 +24,7 @@ public class UserAccount {
|
|||
private UserStatus status = UserStatus.ACTIVE;
|
||||
|
||||
@Column(name = "merged_to_user_id")
|
||||
private Long mergedToUserId;
|
||||
private String mergedToUserId;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
|
@ -34,7 +34,8 @@ public class UserAccount {
|
|||
|
||||
protected UserAccount() {}
|
||||
|
||||
public UserAccount(String displayName, String email, String avatarUrl) {
|
||||
public UserAccount(String id, String displayName, String email, String avatarUrl) {
|
||||
this.id = id;
|
||||
this.displayName = displayName;
|
||||
this.email = email;
|
||||
this.avatarUrl = avatarUrl;
|
||||
|
|
@ -52,7 +53,7 @@ public class UserAccount {
|
|||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public String getId() { return id; }
|
||||
public String getDisplayName() { return displayName; }
|
||||
public void setDisplayName(String displayName) { this.displayName = displayName; }
|
||||
public String getEmail() { return email; }
|
||||
|
|
@ -61,8 +62,8 @@ public class UserAccount {
|
|||
public void setAvatarUrl(String avatarUrl) { this.avatarUrl = avatarUrl; }
|
||||
public UserStatus getStatus() { return status; }
|
||||
public void setStatus(UserStatus status) { this.status = status; }
|
||||
public Long getMergedToUserId() { return mergedToUserId; }
|
||||
public void setMergedToUserId(Long mergedToUserId) { this.mergedToUserId = mergedToUserId; }
|
||||
public String getMergedToUserId() { return mergedToUserId; }
|
||||
public void setMergedToUserId(String mergedToUserId) { this.mergedToUserId = mergedToUserId; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public boolean isActive() { return this.status == UserStatus.ACTIVE; }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ package com.iflytek.skillhub.domain.user;
|
|||
import java.util.Optional;
|
||||
|
||||
public interface UserAccountRepository {
|
||||
Optional<UserAccount> findById(Long id);
|
||||
Optional<UserAccount> findById(String id);
|
||||
UserAccount save(UserAccount user);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
package com.iflytek.skillhub.domain.namespace;
|
||||
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
|
@ -22,6 +18,8 @@ class NamespaceMemberServiceTest {
|
|||
|
||||
@Mock
|
||||
private NamespaceMemberRepository namespaceMemberRepository;
|
||||
@Mock
|
||||
private NamespaceService namespaceService;
|
||||
|
||||
@InjectMocks
|
||||
private NamespaceMemberService namespaceMemberService;
|
||||
|
|
@ -29,7 +27,7 @@ class NamespaceMemberServiceTest {
|
|||
@Test
|
||||
void addMember_shouldAddMemberSuccessfully() {
|
||||
Long namespaceId = 1L;
|
||||
Long userId = 2L;
|
||||
String userId = "user-2";
|
||||
NamespaceRole role = NamespaceRole.MEMBER;
|
||||
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId))
|
||||
|
|
@ -37,7 +35,7 @@ class NamespaceMemberServiceTest {
|
|||
when(namespaceMemberRepository.save(any(NamespaceMember.class)))
|
||||
.thenReturn(new NamespaceMember(namespaceId, userId, role));
|
||||
|
||||
NamespaceMember result = namespaceMemberService.addMember(namespaceId, userId, role);
|
||||
NamespaceMember result = namespaceMemberService.addMember(namespaceId, userId, role, "user-99");
|
||||
|
||||
assertNotNull(result);
|
||||
verify(namespaceMemberRepository).save(any(NamespaceMember.class));
|
||||
|
|
@ -45,52 +43,60 @@ class NamespaceMemberServiceTest {
|
|||
|
||||
@Test
|
||||
void addMember_shouldThrowExceptionForOwnerRole() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
namespaceMemberService.addMember(1L, 2L, NamespaceRole.OWNER));
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceMemberService.addMember(1L, "user-2", NamespaceRole.OWNER, "user-99"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void addMember_shouldRequireAdminOrOwner() {
|
||||
doThrow(new DomainForbiddenException("error.namespace.admin.required")).when(namespaceService).assertAdminOrOwner(1L, "user-99");
|
||||
|
||||
assertThrows(DomainForbiddenException.class, () ->
|
||||
namespaceMemberService.addMember(1L, "user-2", NamespaceRole.MEMBER, "user-99"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void addMember_shouldThrowExceptionWhenMemberExists() {
|
||||
Long namespaceId = 1L;
|
||||
Long userId = 2L;
|
||||
String userId = "user-2";
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId))
|
||||
.thenReturn(Optional.of(new NamespaceMember()));
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
namespaceMemberService.addMember(namespaceId, userId, NamespaceRole.MEMBER));
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceMemberService.addMember(namespaceId, userId, NamespaceRole.MEMBER, "user-99"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeMember_shouldThrowExceptionForOwner() {
|
||||
Long namespaceId = 1L;
|
||||
Long userId = 2L;
|
||||
String userId = "user-2";
|
||||
NamespaceMember ownerMember = new NamespaceMember(namespaceId, userId, NamespaceRole.OWNER);
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId))
|
||||
.thenReturn(Optional.of(ownerMember));
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
namespaceMemberService.removeMember(namespaceId, userId));
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceMemberService.removeMember(namespaceId, userId, "user-99"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeMember_shouldThrowExceptionWhenMemberNotFound() {
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, 2L))
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, "user-2"))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
namespaceMemberService.removeMember(1L, 2L));
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceMemberService.removeMember(1L, "user-2", "user-99"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateMemberRole_shouldUpdateRoleSuccessfully() {
|
||||
Long namespaceId = 1L;
|
||||
Long userId = 2L;
|
||||
String userId = "user-2";
|
||||
NamespaceMember member = new NamespaceMember(namespaceId, userId, NamespaceRole.MEMBER);
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId))
|
||||
.thenReturn(Optional.of(member));
|
||||
when(namespaceMemberRepository.save(any(NamespaceMember.class))).thenReturn(member);
|
||||
|
||||
NamespaceMember result = namespaceMemberService.updateMemberRole(namespaceId, userId, NamespaceRole.ADMIN);
|
||||
NamespaceMember result = namespaceMemberService.updateMemberRole(namespaceId, userId, NamespaceRole.ADMIN, "user-99");
|
||||
|
||||
assertNotNull(result);
|
||||
verify(namespaceMemberRepository).save(member);
|
||||
|
|
@ -99,26 +105,26 @@ class NamespaceMemberServiceTest {
|
|||
@Test
|
||||
void updateMemberRole_shouldThrowExceptionForOwnerRole() {
|
||||
Long namespaceId = 1L;
|
||||
Long userId = 2L;
|
||||
String userId = "user-2";
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
namespaceMemberService.updateMemberRole(namespaceId, userId, NamespaceRole.OWNER));
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceMemberService.updateMemberRole(namespaceId, userId, NamespaceRole.OWNER, "user-99"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateMemberRole_shouldThrowExceptionWhenMemberNotFound() {
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, 2L))
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, "user-2"))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
namespaceMemberService.updateMemberRole(1L, 2L, NamespaceRole.ADMIN));
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceMemberService.updateMemberRole(1L, "user-2", NamespaceRole.ADMIN, "user-99"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferOwnership_shouldTransferOwnershipSuccessfully() {
|
||||
Long namespaceId = 1L;
|
||||
Long currentOwnerId = 2L;
|
||||
Long newOwnerId = 3L;
|
||||
String currentOwnerId = "user-2";
|
||||
String newOwnerId = "user-3";
|
||||
|
||||
NamespaceMember currentOwner = new NamespaceMember(namespaceId, currentOwnerId, NamespaceRole.OWNER);
|
||||
NamespaceMember newOwner = new NamespaceMember(namespaceId, newOwnerId, NamespaceRole.ADMIN);
|
||||
|
|
@ -137,30 +143,30 @@ class NamespaceMemberServiceTest {
|
|||
|
||||
@Test
|
||||
void transferOwnership_shouldThrowExceptionWhenCurrentOwnerNotFound() {
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, 2L))
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, "user-2"))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
namespaceMemberService.transferOwnership(1L, 2L, 3L));
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceMemberService.transferOwnership(1L, "user-2", "user-3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferOwnership_shouldThrowExceptionWhenCurrentUserIsNotOwner() {
|
||||
Long namespaceId = 1L;
|
||||
Long currentOwnerId = 2L;
|
||||
String currentOwnerId = "user-2";
|
||||
NamespaceMember notOwner = new NamespaceMember(namespaceId, currentOwnerId, NamespaceRole.ADMIN);
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, currentOwnerId))
|
||||
.thenReturn(Optional.of(notOwner));
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
namespaceMemberService.transferOwnership(namespaceId, currentOwnerId, 3L));
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceMemberService.transferOwnership(namespaceId, currentOwnerId, "user-3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferOwnership_shouldThrowExceptionWhenNewOwnerNotFound() {
|
||||
Long namespaceId = 1L;
|
||||
Long currentOwnerId = 2L;
|
||||
Long newOwnerId = 3L;
|
||||
String currentOwnerId = "user-2";
|
||||
String newOwnerId = "user-3";
|
||||
NamespaceMember currentOwner = new NamespaceMember(namespaceId, currentOwnerId, NamespaceRole.OWNER);
|
||||
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, currentOwnerId))
|
||||
|
|
@ -168,14 +174,14 @@ class NamespaceMemberServiceTest {
|
|||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, newOwnerId))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceMemberService.transferOwnership(namespaceId, currentOwnerId, newOwnerId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getMemberRole_shouldReturnRole() {
|
||||
Long namespaceId = 1L;
|
||||
Long userId = 2L;
|
||||
String userId = "user-2";
|
||||
NamespaceMember member = new NamespaceMember(namespaceId, userId, NamespaceRole.ADMIN);
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId))
|
||||
.thenReturn(Optional.of(member));
|
||||
|
|
@ -188,10 +194,10 @@ class NamespaceMemberServiceTest {
|
|||
|
||||
@Test
|
||||
void getMemberRole_shouldReturnEmptyWhenMemberNotFound() {
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, 2L))
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, "user-2"))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
Optional<NamespaceRole> result = namespaceMemberService.getMemberRole(1L, 2L);
|
||||
Optional<NamespaceRole> result = namespaceMemberService.getMemberRole(1L, "user-2");
|
||||
|
||||
assertFalse(result.isPresent());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.iflytek.skillhub.domain.namespace;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
|
|
@ -30,7 +31,7 @@ class NamespaceServiceTest {
|
|||
String slug = "test-namespace";
|
||||
String displayName = "Test Namespace";
|
||||
String description = "Test description";
|
||||
Long creatorUserId = 1L;
|
||||
String creatorUserId = "user-1";
|
||||
|
||||
Namespace savedNamespace = new Namespace(slug, displayName, creatorUserId);
|
||||
when(namespaceRepository.findBySlug(slug)).thenReturn(Optional.empty());
|
||||
|
|
@ -51,24 +52,33 @@ class NamespaceServiceTest {
|
|||
String slug = "existing-slug";
|
||||
when(namespaceRepository.findBySlug(slug)).thenReturn(Optional.of(new Namespace()));
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
namespaceService.createNamespace(slug, "Name", "Desc", 1L));
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceService.createNamespace(slug, "Name", "Desc", "user-1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createNamespace_shouldThrowExceptionForInvalidSlug() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
namespaceService.createNamespace("INVALID", "Name", "Desc", 1L));
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceService.createNamespace("INVALID", "Name", "Desc", "user-1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateNamespace_shouldUpdateFields() {
|
||||
Long namespaceId = 1L;
|
||||
Namespace namespace = new Namespace("slug", "Old Name", 1L);
|
||||
String operatorUserId = "user-1";
|
||||
Namespace namespace = new Namespace("slug", "Old Name", "user-1");
|
||||
when(namespaceRepository.findById(namespaceId)).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, operatorUserId))
|
||||
.thenReturn(Optional.of(new NamespaceMember(namespaceId, operatorUserId, NamespaceRole.OWNER)));
|
||||
when(namespaceRepository.save(any(Namespace.class))).thenReturn(namespace);
|
||||
|
||||
Namespace result = namespaceService.updateNamespace(namespaceId, "New Name", "New Desc", "http://avatar.url");
|
||||
Namespace result = namespaceService.updateNamespace(
|
||||
namespaceId,
|
||||
"New Name",
|
||||
"New Desc",
|
||||
"http://avatar.url",
|
||||
operatorUserId
|
||||
);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(namespaceRepository).save(namespace);
|
||||
|
|
@ -78,14 +88,27 @@ class NamespaceServiceTest {
|
|||
void updateNamespace_shouldThrowExceptionWhenNotFound() {
|
||||
when(namespaceRepository.findById(1L)).thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
namespaceService.updateNamespace(1L, "Name", "Desc", null));
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceService.updateNamespace(1L, "Name", "Desc", null, "user-1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateNamespace_shouldThrowExceptionWhenOperatorLacksPrivilege() {
|
||||
Long namespaceId = 1L;
|
||||
String operatorUserId = "user-2";
|
||||
Namespace namespace = new Namespace("slug", "Old Name", "user-1");
|
||||
when(namespaceRepository.findById(namespaceId)).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, operatorUserId))
|
||||
.thenReturn(Optional.of(new NamespaceMember(namespaceId, operatorUserId, NamespaceRole.MEMBER)));
|
||||
|
||||
assertThrows(DomainForbiddenException.class, () ->
|
||||
namespaceService.updateNamespace(namespaceId, "Name", "Desc", null, operatorUserId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNamespaceBySlug_shouldReturnNamespace() {
|
||||
String slug = "test-slug";
|
||||
Namespace namespace = new Namespace(slug, "Name", 1L);
|
||||
Namespace namespace = new Namespace(slug, "Name", "user-1");
|
||||
when(namespaceRepository.findBySlug(slug)).thenReturn(Optional.of(namespace));
|
||||
|
||||
Namespace result = namespaceService.getNamespaceBySlug(slug);
|
||||
|
|
@ -98,7 +121,7 @@ class NamespaceServiceTest {
|
|||
void getNamespaceBySlug_shouldThrowExceptionWhenNotFound() {
|
||||
when(namespaceRepository.findBySlug("nonexistent")).thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceService.getNamespaceBySlug("nonexistent"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.iflytek.skillhub.domain.namespace;
|
||||
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
|
|
@ -13,45 +14,45 @@ class SlugValidatorTest {
|
|||
}
|
||||
@Test
|
||||
void shouldRejectTooShort() {
|
||||
Exception ex = assertThrows(IllegalArgumentException.class, () -> SlugValidator.validate("a"));
|
||||
assertTrue(ex.getMessage().contains("length"));
|
||||
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class, () -> SlugValidator.validate("a"));
|
||||
assertEquals("error.slug.length", ex.messageCode());
|
||||
}
|
||||
@Test
|
||||
void shouldRejectTooLong() {
|
||||
String longSlug = "a".repeat(65);
|
||||
Exception ex = assertThrows(IllegalArgumentException.class, () -> SlugValidator.validate(longSlug));
|
||||
assertTrue(ex.getMessage().contains("length"));
|
||||
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class, () -> SlugValidator.validate(longSlug));
|
||||
assertEquals("error.slug.length", ex.messageCode());
|
||||
}
|
||||
@Test
|
||||
void shouldRejectUpperCase() {
|
||||
Exception ex = assertThrows(IllegalArgumentException.class, () -> SlugValidator.validate("MyNamespace"));
|
||||
assertTrue(ex.getMessage().contains("lowercase"));
|
||||
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class, () -> SlugValidator.validate("MyNamespace"));
|
||||
assertEquals("error.slug.pattern", ex.messageCode());
|
||||
}
|
||||
@Test
|
||||
void shouldRejectStartingWithHyphen() {
|
||||
Exception ex = assertThrows(IllegalArgumentException.class, () -> SlugValidator.validate("-namespace"));
|
||||
assertTrue(ex.getMessage().contains("alphanumeric"));
|
||||
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class, () -> SlugValidator.validate("-namespace"));
|
||||
assertEquals("error.slug.pattern", ex.messageCode());
|
||||
}
|
||||
@Test
|
||||
void shouldRejectEndingWithHyphen() {
|
||||
Exception ex = assertThrows(IllegalArgumentException.class, () -> SlugValidator.validate("namespace-"));
|
||||
assertTrue(ex.getMessage().contains("alphanumeric"));
|
||||
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class, () -> SlugValidator.validate("namespace-"));
|
||||
assertEquals("error.slug.pattern", ex.messageCode());
|
||||
}
|
||||
@Test
|
||||
void shouldRejectDoubleHyphen() {
|
||||
Exception ex = assertThrows(IllegalArgumentException.class, () -> SlugValidator.validate("my--namespace"));
|
||||
assertTrue(ex.getMessage().contains("consecutive"));
|
||||
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class, () -> SlugValidator.validate("my--namespace"));
|
||||
assertEquals("error.slug.doubleHyphen", ex.messageCode());
|
||||
}
|
||||
@Test
|
||||
void shouldRejectReservedWords() {
|
||||
assertThrows(IllegalArgumentException.class, () -> SlugValidator.validate("admin"));
|
||||
assertThrows(IllegalArgumentException.class, () -> SlugValidator.validate("api"));
|
||||
assertThrows(IllegalArgumentException.class, () -> SlugValidator.validate("global"));
|
||||
assertThrows(IllegalArgumentException.class, () -> SlugValidator.validate("system"));
|
||||
assertThrows(DomainBadRequestException.class, () -> SlugValidator.validate("admin"));
|
||||
assertThrows(DomainBadRequestException.class, () -> SlugValidator.validate("api"));
|
||||
assertThrows(DomainBadRequestException.class, () -> SlugValidator.validate("global"));
|
||||
assertThrows(DomainBadRequestException.class, () -> SlugValidator.validate("system"));
|
||||
}
|
||||
@Test
|
||||
void shouldRejectSpecialCharacters() {
|
||||
Exception ex = assertThrows(IllegalArgumentException.class, () -> SlugValidator.validate("my_namespace"));
|
||||
assertTrue(ex.getMessage().contains("lowercase") || ex.getMessage().contains("alphanumeric"));
|
||||
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class, () -> SlugValidator.validate("my_namespace"));
|
||||
assertEquals("error.slug.pattern", ex.messageCode());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ class PromotionServiceTest {
|
|||
private static final Long SOURCE_SKILL_ID = 10L;
|
||||
private static final Long SOURCE_VERSION_ID = 20L;
|
||||
private static final Long TARGET_NAMESPACE_ID = 30L;
|
||||
private static final Long USER_ID = 100L;
|
||||
private static final Long REVIEWER_ID = 200L;
|
||||
private static final String USER_ID = "user-100";
|
||||
private static final String REVIEWER_ID = "user-200";
|
||||
private static final Long PROMOTION_ID = 1L;
|
||||
private static final Long NEW_SKILL_ID = 50L;
|
||||
private static final Long NEW_VERSION_ID = 60L;
|
||||
|
|
@ -84,14 +84,14 @@ class PromotionServiceTest {
|
|||
}
|
||||
|
||||
private Namespace createGlobalNamespace() {
|
||||
Namespace ns = new Namespace("global", "Global", 1L);
|
||||
Namespace ns = new Namespace("global", "Global", "user-1");
|
||||
setField(ns, "id", TARGET_NAMESPACE_ID);
|
||||
ns.setType(NamespaceType.GLOBAL);
|
||||
return ns;
|
||||
}
|
||||
|
||||
private Namespace createTeamNamespace() {
|
||||
Namespace ns = new Namespace("team-a", "Team A", 1L);
|
||||
Namespace ns = new Namespace("team-a", "Team A", "user-1");
|
||||
setField(ns, "id", TARGET_NAMESPACE_ID);
|
||||
// default type is TEAM
|
||||
return ns;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class ReviewPermissionCheckerTest {
|
|||
|
||||
@Test
|
||||
void cannotReviewOwnSubmission() {
|
||||
Long userId = 1L;
|
||||
String userId = "user-1";
|
||||
ReviewTask task = new ReviewTask(1L, 10L, userId);
|
||||
assertFalse(checker.canReview(task, userId,
|
||||
NamespaceType.TEAM, Map.of(), Set.of()));
|
||||
|
|
@ -25,56 +25,56 @@ class ReviewPermissionCheckerTest {
|
|||
|
||||
@Test
|
||||
void teamAdminCanReviewTeamSkill() {
|
||||
ReviewTask task = new ReviewTask(1L, 10L, 2L);
|
||||
assertTrue(checker.canReview(task, 1L,
|
||||
ReviewTask task = new ReviewTask(1L, 10L, "user-2");
|
||||
assertTrue(checker.canReview(task, "user-1",
|
||||
NamespaceType.TEAM,
|
||||
Map.of(10L, NamespaceRole.ADMIN), Set.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void teamOwnerCanReviewTeamSkill() {
|
||||
ReviewTask task = new ReviewTask(1L, 10L, 2L);
|
||||
assertTrue(checker.canReview(task, 1L,
|
||||
ReviewTask task = new ReviewTask(1L, 10L, "user-2");
|
||||
assertTrue(checker.canReview(task, "user-1",
|
||||
NamespaceType.TEAM,
|
||||
Map.of(10L, NamespaceRole.OWNER), Set.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void teamMemberCannotReviewTeamSkill() {
|
||||
ReviewTask task = new ReviewTask(1L, 10L, 2L);
|
||||
assertFalse(checker.canReview(task, 1L,
|
||||
ReviewTask task = new ReviewTask(1L, 10L, "user-2");
|
||||
assertFalse(checker.canReview(task, "user-1",
|
||||
NamespaceType.TEAM,
|
||||
Map.of(10L, NamespaceRole.MEMBER), Set.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void skillAdminCanReviewGlobalSkill() {
|
||||
ReviewTask task = new ReviewTask(1L, 1L, 2L);
|
||||
assertTrue(checker.canReview(task, 1L,
|
||||
ReviewTask task = new ReviewTask(1L, 1L, "user-2");
|
||||
assertTrue(checker.canReview(task, "user-1",
|
||||
NamespaceType.GLOBAL,
|
||||
Map.of(), Set.of("SKILL_ADMIN")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void superAdminCanReviewGlobalSkill() {
|
||||
ReviewTask task = new ReviewTask(1L, 1L, 2L);
|
||||
assertTrue(checker.canReview(task, 1L,
|
||||
ReviewTask task = new ReviewTask(1L, 1L, "user-2");
|
||||
assertTrue(checker.canReview(task, "user-1",
|
||||
NamespaceType.GLOBAL,
|
||||
Map.of(), Set.of("SUPER_ADMIN")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void skillAdminCannotReviewTeamSkill() {
|
||||
ReviewTask task = new ReviewTask(1L, 10L, 2L);
|
||||
assertFalse(checker.canReview(task, 1L,
|
||||
ReviewTask task = new ReviewTask(1L, 10L, "user-2");
|
||||
assertFalse(checker.canReview(task, "user-1",
|
||||
NamespaceType.TEAM,
|
||||
Map.of(), Set.of("SKILL_ADMIN")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonAdminCannotReviewGlobalSkill() {
|
||||
ReviewTask task = new ReviewTask(1L, 1L, 2L);
|
||||
assertFalse(checker.canReview(task, 1L,
|
||||
ReviewTask task = new ReviewTask(1L, 1L, "user-2");
|
||||
assertFalse(checker.canReview(task, "user-1",
|
||||
NamespaceType.GLOBAL,
|
||||
Map.of(), Set.of()));
|
||||
}
|
||||
|
|
@ -83,28 +83,28 @@ class ReviewPermissionCheckerTest {
|
|||
|
||||
@Test
|
||||
void skillAdminCanReviewPromotion() {
|
||||
PromotionRequest req = new PromotionRequest(1L, 1L, 1L, 2L);
|
||||
assertTrue(checker.canReviewPromotion(req, 1L,
|
||||
PromotionRequest req = new PromotionRequest(1L, 1L, 1L, "user-2");
|
||||
assertTrue(checker.canReviewPromotion(req, "user-1",
|
||||
Set.of("SKILL_ADMIN")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void superAdminCanReviewPromotion() {
|
||||
PromotionRequest req = new PromotionRequest(1L, 1L, 1L, 2L);
|
||||
assertTrue(checker.canReviewPromotion(req, 1L,
|
||||
PromotionRequest req = new PromotionRequest(1L, 1L, 1L, "user-2");
|
||||
assertTrue(checker.canReviewPromotion(req, "user-1",
|
||||
Set.of("SUPER_ADMIN")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void regularUserCannotReviewPromotion() {
|
||||
PromotionRequest req = new PromotionRequest(1L, 1L, 1L, 2L);
|
||||
assertFalse(checker.canReviewPromotion(req, 1L,
|
||||
PromotionRequest req = new PromotionRequest(1L, 1L, 1L, "user-2");
|
||||
assertFalse(checker.canReviewPromotion(req, "user-1",
|
||||
Set.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cannotReviewOwnPromotion() {
|
||||
Long userId = 2L;
|
||||
String userId = "user-2";
|
||||
PromotionRequest req = new PromotionRequest(1L, 1L, 1L, userId);
|
||||
assertFalse(checker.canReviewPromotion(req, userId,
|
||||
Set.of("SKILL_ADMIN")));
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ class ReviewServiceTest {
|
|||
|
||||
private static final Long SKILL_VERSION_ID = 10L;
|
||||
private static final Long NAMESPACE_ID = 20L;
|
||||
private static final Long USER_ID = 100L;
|
||||
private static final Long REVIEWER_ID = 200L;
|
||||
private static final String USER_ID = "user-100";
|
||||
private static final String REVIEWER_ID = "user-200";
|
||||
private static final Long REVIEW_TASK_ID = 1L;
|
||||
private static final Long SKILL_ID = 30L;
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ class ReviewServiceTest {
|
|||
}
|
||||
|
||||
private Namespace createTeamNamespace() {
|
||||
Namespace ns = new Namespace("team-a", "Team A", 1L);
|
||||
Namespace ns = new Namespace("team-a", "Team A", "user-1");
|
||||
setField(ns, "id", NAMESPACE_ID);
|
||||
return ns;
|
||||
}
|
||||
|
|
@ -347,7 +347,7 @@ class ReviewServiceTest {
|
|||
when(reviewTaskRepository.findBySkillVersionIdAndStatus(SKILL_VERSION_ID, ReviewTaskStatus.PENDING))
|
||||
.thenReturn(Optional.of(task));
|
||||
|
||||
Long otherUserId = 999L;
|
||||
String otherUserId = "user-999";
|
||||
assertThrows(DomainForbiddenException.class,
|
||||
() -> reviewService.withdrawReview(SKILL_VERSION_ID, otherUserId));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ class VisibilityCheckerTest {
|
|||
private Skill privateSkill;
|
||||
|
||||
private static final Long NAMESPACE_ID = 1L;
|
||||
private static final Long OWNER_ID = 100L;
|
||||
private static final Long OTHER_USER_ID = 200L;
|
||||
private static final Long ADMIN_USER_ID = 300L;
|
||||
private static final Long NAMESPACE_OWNER_ID = 400L;
|
||||
private static final String OWNER_ID = "user-100";
|
||||
private static final String OTHER_USER_ID = "user-200";
|
||||
private static final String ADMIN_USER_ID = "user-300";
|
||||
private static final String NAMESPACE_OWNER_ID = "user-400";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.iflytek.skillhub.domain.skill.metadata;
|
||||
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
|
@ -62,11 +63,11 @@ class SkillMetadataParserTest {
|
|||
void testThrowsWhenNoFrontmatter() {
|
||||
String content = "# Just a markdown file without frontmatter";
|
||||
|
||||
IllegalArgumentException exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
DomainBadRequestException exception = assertThrows(
|
||||
DomainBadRequestException.class,
|
||||
() -> parser.parse(content)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("Missing frontmatter"));
|
||||
assertEquals("error.skill.metadata.frontmatter.missingStart", exception.messageCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -79,11 +80,12 @@ class SkillMetadataParserTest {
|
|||
Body
|
||||
""";
|
||||
|
||||
IllegalArgumentException exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
DomainBadRequestException exception = assertThrows(
|
||||
DomainBadRequestException.class,
|
||||
() -> parser.parse(content)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("name"));
|
||||
assertEquals("error.skill.metadata.requiredField.missing", exception.messageCode());
|
||||
assertEquals("name", exception.messageArgs()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -96,15 +98,16 @@ class SkillMetadataParserTest {
|
|||
Body
|
||||
""";
|
||||
|
||||
IllegalArgumentException exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
DomainBadRequestException exception = assertThrows(
|
||||
DomainBadRequestException.class,
|
||||
() -> parser.parse(content)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("description"));
|
||||
assertEquals("error.skill.metadata.requiredField.missing", exception.messageCode());
|
||||
assertEquals("description", exception.messageArgs()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testThrowsWhenMissingVersion() {
|
||||
void testAllowsMissingVersion() {
|
||||
String content = """
|
||||
---
|
||||
name: test-skill
|
||||
|
|
@ -113,11 +116,11 @@ class SkillMetadataParserTest {
|
|||
Body
|
||||
""";
|
||||
|
||||
IllegalArgumentException exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> parser.parse(content)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("version"));
|
||||
SkillMetadata metadata = parser.parse(content);
|
||||
|
||||
assertEquals("test-skill", metadata.name());
|
||||
assertEquals("Test description", metadata.description());
|
||||
assertNull(metadata.version());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -131,11 +134,11 @@ class SkillMetadataParserTest {
|
|||
Body
|
||||
""";
|
||||
|
||||
IllegalArgumentException exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
DomainBadRequestException exception = assertThrows(
|
||||
DomainBadRequestException.class,
|
||||
() -> parser.parse(content)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("Invalid YAML"));
|
||||
assertEquals("error.skill.metadata.yaml.invalid", exception.messageCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -147,10 +150,10 @@ class SkillMetadataParserTest {
|
|||
version: 1.0.0
|
||||
""";
|
||||
|
||||
IllegalArgumentException exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
DomainBadRequestException exception = assertThrows(
|
||||
DomainBadRequestException.class,
|
||||
() -> parser.parse(content)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("Missing frontmatter"));
|
||||
assertEquals("error.skill.metadata.frontmatter.missingEnd", exception.messageCode());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import com.iflytek.skillhub.storage.ObjectMetadata;
|
||||
import com.iflytek.skillhub.storage.ObjectStorageService;
|
||||
|
|
@ -63,17 +64,19 @@ class SkillDownloadServiceTest {
|
|||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
Long userId = 100L;
|
||||
String userId = "user-100";
|
||||
Map<Long, NamespaceRole> userNsRoles = Map.of(1L, NamespaceRole.MEMBER);
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, userId, SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
skill.setLatestVersionId(10L);
|
||||
|
||||
SkillVersion version = new SkillVersion(1L, "1.0.0", userId);
|
||||
setId(version, 10L);
|
||||
version.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
String storageKey = "packages/1/10/bundle.zip";
|
||||
InputStream content = new ByteArrayInputStream("test".getBytes());
|
||||
ObjectMetadata metadata = new ObjectMetadata(1000L, "application/zip", Instant.now());
|
||||
|
|
@ -102,16 +105,18 @@ class SkillDownloadServiceTest {
|
|||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
String tagName = "stable";
|
||||
Long userId = 100L;
|
||||
String userId = "user-100";
|
||||
Map<Long, NamespaceRole> userNsRoles = Map.of(1L, NamespaceRole.MEMBER);
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, userId, SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
SkillTag tag = new SkillTag(1L, tagName, 10L, userId);
|
||||
SkillVersion version = new SkillVersion(1L, "1.0.0", userId);
|
||||
setId(version, 10L);
|
||||
version.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
String storageKey = "packages/1/10/bundle.zip";
|
||||
InputStream content = new ByteArrayInputStream("test".getBytes());
|
||||
ObjectMetadata metadata = new ObjectMetadata(1000L, "application/zip", Instant.now());
|
||||
|
|
@ -134,6 +139,32 @@ class SkillDownloadServiceTest {
|
|||
verify(eventPublisher).publishEvent(any(SkillDownloadedEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadVersion_ShouldRejectDraftVersion() throws Exception {
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
String versionStr = "1.0.0";
|
||||
String userId = "user-100";
|
||||
Map<Long, NamespaceRole> userNsRoles = Map.of(1L, NamespaceRole.MEMBER);
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, userId, SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
SkillVersion version = new SkillVersion(1L, versionStr, userId);
|
||||
setId(version, 10L);
|
||||
version.setStatus(SkillVersionStatus.DRAFT);
|
||||
|
||||
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.findBySkillIdAndVersion(1L, versionStr)).thenReturn(Optional.of(version));
|
||||
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
service.downloadVersion(namespaceSlug, skillSlug, versionStr, userId, userNsRoles));
|
||||
}
|
||||
|
||||
private void setId(Object entity, Long id) throws Exception {
|
||||
Field idField = entity.getClass().getDeclaredField("id");
|
||||
idField.setAccessible(true);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ 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.review.ReviewTaskRepository;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import com.iflytek.skillhub.domain.skill.metadata.SkillMetadata;
|
||||
import com.iflytek.skillhub.domain.skill.metadata.SkillMetadataParser;
|
||||
|
|
@ -54,6 +56,8 @@ class SkillPublishServiceTest {
|
|||
private PrePublishValidator prePublishValidator;
|
||||
@Mock
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
@Mock
|
||||
private ReviewTaskRepository reviewTaskRepository;
|
||||
|
||||
private SkillPublishService service;
|
||||
private ObjectMapper objectMapper;
|
||||
|
|
@ -72,7 +76,8 @@ class SkillPublishServiceTest {
|
|||
skillMetadataParser,
|
||||
prePublishValidator,
|
||||
eventPublisher,
|
||||
objectMapper
|
||||
objectMapper,
|
||||
reviewTaskRepository
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -80,14 +85,14 @@ class SkillPublishServiceTest {
|
|||
void testPublishFromEntries_Success() throws Exception {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
Long publisherId = 100L;
|
||||
String publisherId = "user-100";
|
||||
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);
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
NamespaceMember member = mock(NamespaceMember.class);
|
||||
SkillMetadata metadata = new SkillMetadata("test-skill", "Test", "1.0.0", "Body", Map.of());
|
||||
|
|
@ -108,10 +113,18 @@ class SkillPublishServiceTest {
|
|||
when(skillRepository.save(any())).thenReturn(skill);
|
||||
|
||||
// Act
|
||||
SkillVersion result = service.publishFromEntries(namespaceSlug, entries, publisherId, SkillVisibility.PUBLIC);
|
||||
SkillPublishService.PublishResult result = service.publishFromEntries(
|
||||
namespaceSlug,
|
||||
entries,
|
||||
publisherId,
|
||||
SkillVisibility.PUBLIC
|
||||
);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
assertEquals(1L, result.skillId());
|
||||
assertEquals("test-skill", result.slug());
|
||||
assertEquals("1.0.0", result.version().getVersion());
|
||||
verify(eventPublisher).publishEvent(any(SkillPublishedEvent.class));
|
||||
verify(skillFileRepository).saveAll(anyList());
|
||||
verify(objectStorageService, atLeastOnce()).putObject(anyString(), any(), anyLong(), anyString());
|
||||
|
|
@ -124,8 +137,8 @@ class SkillPublishServiceTest {
|
|||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.empty());
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
service.publishFromEntries(namespaceSlug, List.of(), 100L, SkillVisibility.PUBLIC)
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
service.publishFromEntries(namespaceSlug, List.of(), "user-100", SkillVisibility.PUBLIC)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -133,15 +146,15 @@ class SkillPublishServiceTest {
|
|||
void testPublishFromEntries_NotAMember() throws Exception {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
Long publisherId = 100L;
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
String publisherId = "user-100";
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
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, () ->
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
service.publishFromEntries(namespaceSlug, List.of(), publisherId, SkillVisibility.PUBLIC)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ 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.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import com.iflytek.skillhub.storage.ObjectStorageService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
|
@ -37,6 +39,8 @@ class SkillQueryServiceTest {
|
|||
@Mock
|
||||
private SkillFileRepository skillFileRepository;
|
||||
@Mock
|
||||
private SkillTagRepository skillTagRepository;
|
||||
@Mock
|
||||
private ObjectStorageService objectStorageService;
|
||||
@Mock
|
||||
private VisibilityChecker visibilityChecker;
|
||||
|
|
@ -50,6 +54,7 @@ class SkillQueryServiceTest {
|
|||
skillRepository,
|
||||
skillVersionRepository,
|
||||
skillFileRepository,
|
||||
skillTagRepository,
|
||||
objectStorageService,
|
||||
visibilityChecker
|
||||
);
|
||||
|
|
@ -60,10 +65,10 @@ class SkillQueryServiceTest {
|
|||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
Long userId = 100L;
|
||||
String userId = "user-100";
|
||||
Map<Long, NamespaceRole> userNsRoles = Map.of(1L, NamespaceRole.MEMBER);
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, userId, SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
|
|
@ -94,12 +99,12 @@ class SkillQueryServiceTest {
|
|||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
Long userId = 100L;
|
||||
String userId = "user-100";
|
||||
Map<Long, NamespaceRole> userNsRoles = Map.of();
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, 200L, SkillVisibility.PRIVATE);
|
||||
Skill skill = new Skill(1L, skillSlug, "user-200", SkillVisibility.PRIVATE);
|
||||
setId(skill, 1L);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
|
|
@ -107,7 +112,7 @@ class SkillQueryServiceTest {
|
|||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(false);
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(SecurityException.class, () ->
|
||||
assertThrows(DomainForbiddenException.class, () ->
|
||||
service.getSkillDetail(namespaceSlug, skillSlug, userId, userNsRoles)
|
||||
);
|
||||
}
|
||||
|
|
@ -116,11 +121,11 @@ class SkillQueryServiceTest {
|
|||
void testListSkillsByNamespace() throws Exception {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
Long userId = 100L;
|
||||
String userId = "user-100";
|
||||
Map<Long, NamespaceRole> userNsRoles = Map.of(1L, NamespaceRole.MEMBER);
|
||||
Pageable pageable = PageRequest.of(0, 10);
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill1 = new Skill(1L, "skill1", userId, SkillVisibility.PUBLIC);
|
||||
setId(skill1, 1L);
|
||||
|
|
@ -147,27 +152,167 @@ class SkillQueryServiceTest {
|
|||
String skillSlug = "test-skill";
|
||||
String version = "1.0.0";
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, 100L, SkillVisibility.PUBLIC);
|
||||
Skill skill = new Skill(1L, skillSlug, "user-100", SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
SkillVersion skillVersion = new SkillVersion(1L, version, 100L);
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
SkillVersion skillVersion = new SkillVersion(1L, version, "user-100");
|
||||
setId(skillVersion, 1L);
|
||||
skillVersion.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
SkillFile file1 = new SkillFile(1L, "file1.txt", 100L, "text/plain", "hash1", "key1");
|
||||
Map<Long, NamespaceRole> userNsRoles = Map.of(1L, NamespaceRole.MEMBER);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true);
|
||||
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);
|
||||
List<SkillFile> result = service.listFiles(namespaceSlug, skillSlug, version, "user-100", userNsRoles);
|
||||
|
||||
// Assert
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("file1.txt", result.get(0).getFilePath());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListFiles_ShouldRejectDraftVersion() throws Exception {
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
String version = "1.0.0";
|
||||
Map<Long, NamespaceRole> userNsRoles = Map.of(1L, NamespaceRole.MEMBER);
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, "user-100", SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
SkillVersion skillVersion = new SkillVersion(1L, version, "user-100");
|
||||
setId(skillVersion, 1L);
|
||||
skillVersion.setStatus(SkillVersionStatus.DRAFT);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(skillVersion));
|
||||
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
service.listFiles(namespaceSlug, skillSlug, version, "user-100", userNsRoles));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetVersionDetail_ShouldReturnMetadataPayload() throws Exception {
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
String version = "1.0.0";
|
||||
Map<Long, NamespaceRole> userNsRoles = Map.of(1L, NamespaceRole.MEMBER);
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, "user-100", SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
SkillVersion skillVersion = new SkillVersion(1L, version, "user-100");
|
||||
setId(skillVersion, 10L);
|
||||
skillVersion.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
skillVersion.setParsedMetadataJson("{\"name\":\"test-skill\"}");
|
||||
skillVersion.setManifestJson("[{\"path\":\"SKILL.md\"}]");
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(skillVersion));
|
||||
|
||||
SkillQueryService.SkillVersionDetailDTO result = service.getVersionDetail(
|
||||
namespaceSlug,
|
||||
skillSlug,
|
||||
version,
|
||||
"user-100",
|
||||
userNsRoles
|
||||
);
|
||||
|
||||
assertEquals("{\"name\":\"test-skill\"}", result.parsedMetadataJson());
|
||||
assertEquals("[{\"path\":\"SKILL.md\"}]", result.manifestJson());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListFilesByTag_ShouldResolveLatestTag() throws Exception {
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
Map<Long, NamespaceRole> userNsRoles = Map.of(1L, NamespaceRole.MEMBER);
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, "user-100", SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
skill.setLatestVersionId(11L);
|
||||
SkillVersion latestVersion = new SkillVersion(1L, "1.1.0", "user-100");
|
||||
setId(latestVersion, 11L);
|
||||
latestVersion.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
SkillFile file = new SkillFile(11L, "README.md", 12L, "text/markdown", "hash", "storage-key");
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findById(11L)).thenReturn(Optional.of(latestVersion));
|
||||
when(skillFileRepository.findByVersionId(11L)).thenReturn(List.of(file));
|
||||
|
||||
List<SkillFile> result = service.listFilesByTag(namespaceSlug, skillSlug, "latest", "user-100", userNsRoles);
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("README.md", result.get(0).getFilePath());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResolveVersion_ShouldReturnLatestWhenHashDoesNotMatch() throws Exception {
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
Map<Long, NamespaceRole> userNsRoles = Map.of(1L, NamespaceRole.MEMBER);
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, "user-100", SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
skill.setLatestVersionId(10L);
|
||||
|
||||
SkillVersion version100 = new SkillVersion(1L, "1.0.0", "user-100");
|
||||
setId(version100, 9L);
|
||||
version100.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
SkillVersion version110 = new SkillVersion(1L, "1.1.0", "user-100");
|
||||
setId(version110, 10L);
|
||||
version110.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
|
||||
SkillFile version100File = new SkillFile(9L, "SKILL.md", 10L, "text/markdown", "hash100", "key100");
|
||||
SkillFile version110File = new SkillFile(10L, "SKILL.md", 10L, "text/markdown", "hash110", "key110");
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PUBLISHED))
|
||||
.thenReturn(List.of(version100, version110));
|
||||
when(skillVersionRepository.findById(10L)).thenReturn(Optional.of(version110));
|
||||
when(skillFileRepository.findByVersionId(9L)).thenReturn(List.of(version100File));
|
||||
when(skillFileRepository.findByVersionId(10L)).thenReturn(List.of(version110File));
|
||||
|
||||
SkillQueryService.ResolvedVersionDTO result = service.resolveVersion(
|
||||
namespaceSlug,
|
||||
skillSlug,
|
||||
null,
|
||||
null,
|
||||
"sha256:does-not-match",
|
||||
"user-100",
|
||||
userNsRoles
|
||||
);
|
||||
|
||||
assertEquals("1.1.0", result.version());
|
||||
assertEquals(Boolean.FALSE, result.matched());
|
||||
assertTrue(result.downloadUrl().contains("/versions/1.1.0/download"));
|
||||
}
|
||||
|
||||
private void setId(Object entity, Long id) throws Exception {
|
||||
Field idField = entity.getClass().getDeclaredField("id");
|
||||
idField.setAccessible(true);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
package com.iflytek.skillhub.domain.skill.service;
|
||||
|
||||
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.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
|
@ -23,6 +28,8 @@ class SkillTagServiceTest {
|
|||
@Mock
|
||||
private NamespaceRepository namespaceRepository;
|
||||
@Mock
|
||||
private NamespaceMemberRepository namespaceMemberRepository;
|
||||
@Mock
|
||||
private SkillRepository skillRepository;
|
||||
@Mock
|
||||
private SkillVersionRepository skillVersionRepository;
|
||||
|
|
@ -35,6 +42,7 @@ class SkillTagServiceTest {
|
|||
void setUp() {
|
||||
service = new SkillTagService(
|
||||
namespaceRepository,
|
||||
namespaceMemberRepository,
|
||||
skillRepository,
|
||||
skillVersionRepository,
|
||||
skillTagRepository
|
||||
|
|
@ -48,9 +56,9 @@ class SkillTagServiceTest {
|
|||
String skillSlug = "test-skill";
|
||||
String tagName = "stable";
|
||||
String targetVersion = "1.0.0";
|
||||
Long operatorId = 100L;
|
||||
String operatorId = "user-100";
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, operatorId, SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
|
|
@ -60,6 +68,8 @@ class SkillTagServiceTest {
|
|||
SkillTag tag = new SkillTag(1L, tagName, 1L, operatorId);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, operatorId))
|
||||
.thenReturn(Optional.of(new NamespaceMember(1L, operatorId, NamespaceRole.OWNER)));
|
||||
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());
|
||||
|
|
@ -80,10 +90,10 @@ class SkillTagServiceTest {
|
|||
String skillSlug = "test-skill";
|
||||
String tagName = "latest";
|
||||
String targetVersion = "1.0.0";
|
||||
Long operatorId = 100L;
|
||||
String operatorId = "user-100";
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
service.createOrMoveTag(namespaceSlug, skillSlug, tagName, targetVersion, operatorId)
|
||||
);
|
||||
}
|
||||
|
|
@ -94,15 +104,17 @@ class SkillTagServiceTest {
|
|||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
String tagName = "stable";
|
||||
Long operatorId = 100L;
|
||||
String operatorId = "user-100";
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
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(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, operatorId))
|
||||
.thenReturn(Optional.of(new NamespaceMember(1L, operatorId, NamespaceRole.ADMIN)));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillTagRepository.findBySkillIdAndTagName(1L, tagName)).thenReturn(Optional.of(tag));
|
||||
|
||||
|
|
@ -119,26 +131,45 @@ class SkillTagServiceTest {
|
|||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
String tagName = "latest";
|
||||
Long operatorId = 100L;
|
||||
String operatorId = "user-100";
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
service.deleteTag(namespaceSlug, skillSlug, tagName, operatorId)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateTag_RequiresAdminOrOwner() throws Exception {
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
String tagName = "stable";
|
||||
String targetVersion = "1.0.0";
|
||||
String operatorId = "user-100";
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, operatorId))
|
||||
.thenReturn(Optional.of(new NamespaceMember(1L, operatorId, NamespaceRole.MEMBER)));
|
||||
|
||||
assertThrows(DomainForbiddenException.class, () ->
|
||||
service.createOrMoveTag(namespaceSlug, skillSlug, tagName, targetVersion, operatorId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListTags() throws Exception {
|
||||
// Arrange
|
||||
String namespaceSlug = "test-ns";
|
||||
String skillSlug = "test-skill";
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", 1L);
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, 100L, SkillVisibility.PUBLIC);
|
||||
Skill skill = new Skill(1L, skillSlug, "user-100", SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
SkillTag tag1 = new SkillTag(1L, "stable", 1L, 100L);
|
||||
SkillTag tag2 = new SkillTag(1L, "beta", 2L, 100L);
|
||||
SkillTag tag1 = new SkillTag(1L, "stable", 1L, "user-100");
|
||||
SkillTag tag2 = new SkillTag(1L, "beta", 2L, "user-100");
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue