mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-07 02:57:51 +00:00
fix(auth): enforce owner isolation for skill publishing
- Change skill uniqueness constraint from (namespace_id, slug) to (namespace_id, slug, owner_id) to support per-user skill records - Reject publish when another owner has a published skill with same slug - Reject review approval when same-slug conflict exists - Auto-withdraw pending review versions when submitting a new version - Resolve visible skill by preferring published skill, then current user's own skill, to fix wrong skill returned for same-slug queries - Invalidate all skill query cache on publish to prevent stale data
This commit is contained in:
parent
94a1e95bc7
commit
cb717fd5f4
22 changed files with 427 additions and 92 deletions
|
|
@ -304,8 +304,7 @@ public class ClawHubCompatController {
|
|||
|
||||
Namespace ns = namespaceRepository.findBySlug(coord.namespace())
|
||||
.orElseThrow(() -> new DomainNotFoundException("error.namespace.notFound", coord.namespace()));
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(ns.getId(), coord.slug())
|
||||
.orElseThrow(() -> new DomainNotFoundException("error.skill.notFound", coord.slug()));
|
||||
Skill skill = resolveVisibleSkill(ns.getId(), coord.slug(), userId);
|
||||
|
||||
SkillVersion latestVersionEntity = null;
|
||||
if (skill.getLatestVersionId() != null) {
|
||||
|
|
@ -382,8 +381,7 @@ public class ClawHubCompatController {
|
|||
SkillCoordinate coord = mapper.fromCanonical(canonicalSlug);
|
||||
Namespace ns = namespaceRepository.findBySlug(coord.namespace())
|
||||
.orElseThrow(() -> new DomainNotFoundException("error.namespace.notFound", coord.namespace()));
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(ns.getId(), coord.slug())
|
||||
.orElseThrow(() -> new DomainNotFoundException("error.skill.notFound", canonicalSlug));
|
||||
Skill skill = resolveVisibleSkill(ns.getId(), coord.slug(), principal.userId());
|
||||
|
||||
boolean alreadyStarred = skillStarService.isStarred(skill.getId(), principal.userId());
|
||||
skillStarService.star(skill.getId(), principal.userId());
|
||||
|
|
@ -399,8 +397,7 @@ public class ClawHubCompatController {
|
|||
SkillCoordinate coord = mapper.fromCanonical(canonicalSlug);
|
||||
Namespace ns = namespaceRepository.findBySlug(coord.namespace())
|
||||
.orElseThrow(() -> new DomainNotFoundException("error.namespace.notFound", coord.namespace()));
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(ns.getId(), coord.slug())
|
||||
.orElseThrow(() -> new DomainNotFoundException("error.skill.notFound", canonicalSlug));
|
||||
Skill skill = resolveVisibleSkill(ns.getId(), coord.slug(), principal.userId());
|
||||
|
||||
boolean alreadyUnstarred = !skillStarService.isStarred(skill.getId(), principal.userId());
|
||||
skillStarService.unstar(skill.getId(), principal.userId());
|
||||
|
|
@ -482,4 +479,26 @@ public class ClawHubCompatController {
|
|||
principal.avatarUrl()
|
||||
);
|
||||
}
|
||||
|
||||
private Skill resolveVisibleSkill(Long namespaceId, String slug, String currentUserId) {
|
||||
java.util.List<Skill> skills = skillRepository.findByNamespaceIdAndSlug(namespaceId, slug);
|
||||
if (skills.isEmpty()) {
|
||||
throw new DomainNotFoundException("error.skill.notFound", slug);
|
||||
}
|
||||
java.util.Optional<Skill> published = skills.stream()
|
||||
.filter(s -> s.getLatestVersionId() != null)
|
||||
.findFirst();
|
||||
if (published.isPresent()) {
|
||||
return published.get();
|
||||
}
|
||||
if (currentUserId != null) {
|
||||
java.util.Optional<Skill> ownSkill = skills.stream()
|
||||
.filter(s -> currentUserId.equals(s.getOwnerId()))
|
||||
.findFirst();
|
||||
if (ownSkill.isPresent()) {
|
||||
return ownSkill.get();
|
||||
}
|
||||
}
|
||||
return skills.get(0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ public class SkillLifecycleController extends BaseApiController {
|
|||
@RequestAttribute("userId") String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
|
||||
HttpServletRequest httpRequest) {
|
||||
Skill skill = findSkill(namespace, slug);
|
||||
Skill skill = findSkill(namespace, slug, userId);
|
||||
Skill archived = skillGovernanceService.archiveSkill(
|
||||
skill.getId(),
|
||||
userId,
|
||||
|
|
@ -86,7 +86,7 @@ public class SkillLifecycleController extends BaseApiController {
|
|||
@RequestAttribute("userId") String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
|
||||
HttpServletRequest httpRequest) {
|
||||
Skill skill = findSkill(namespace, slug);
|
||||
Skill skill = findSkill(namespace, slug, userId);
|
||||
Skill restored = skillGovernanceService.unarchiveSkill(
|
||||
skill.getId(),
|
||||
userId,
|
||||
|
|
@ -106,7 +106,7 @@ public class SkillLifecycleController extends BaseApiController {
|
|||
@RequestAttribute("userId") String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
|
||||
HttpServletRequest httpRequest) {
|
||||
Skill skill = findSkill(namespace, slug);
|
||||
Skill skill = findSkill(namespace, slug, userId);
|
||||
SkillVersion skillVersion = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), version)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.version.notFound", version));
|
||||
skillGovernanceService.deleteVersion(
|
||||
|
|
@ -128,7 +128,7 @@ public class SkillLifecycleController extends BaseApiController {
|
|||
@PathVariable String version,
|
||||
@RequestAttribute("userId") String userId,
|
||||
HttpServletRequest httpRequest) {
|
||||
Skill skill = findSkill(namespace, slug);
|
||||
Skill skill = findSkill(namespace, slug, userId);
|
||||
SkillVersion skillVersion = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), version)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.version.notFound", version));
|
||||
reviewService.withdrawReview(skillVersion.getId(), userId);
|
||||
|
|
@ -155,7 +155,7 @@ public class SkillLifecycleController extends BaseApiController {
|
|||
@RequestAttribute("userId") String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
|
||||
HttpServletRequest httpRequest) {
|
||||
Skill skill = findSkill(namespace, slug);
|
||||
Skill skill = findSkill(namespace, slug, userId);
|
||||
SkillVersion skillVersion = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), version)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.version.notFound", version));
|
||||
SkillPublishService.PublishResult result = skillPublishService.rereleasePublishedVersion(
|
||||
|
|
@ -181,11 +181,32 @@ public class SkillLifecycleController extends BaseApiController {
|
|||
new SkillLifecycleMutationResponse(result.skillId(), result.version().getId(), "RERELEASE_VERSION", result.version().getStatus().name()));
|
||||
}
|
||||
|
||||
private Skill findSkill(String namespaceSlug, String skillSlug) {
|
||||
private Skill findSkill(String namespaceSlug, String skillSlug, String currentUserId) {
|
||||
String cleanNamespace = namespaceSlug.startsWith("@") ? namespaceSlug.substring(1) : namespaceSlug;
|
||||
Namespace namespace = namespaceRepository.findBySlug(cleanNamespace)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", cleanNamespace));
|
||||
return skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
return resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
}
|
||||
|
||||
private Skill resolveVisibleSkill(Long namespaceId, String slug, String currentUserId) {
|
||||
java.util.List<Skill> skills = skillRepository.findByNamespaceIdAndSlug(namespaceId, slug);
|
||||
if (skills.isEmpty()) {
|
||||
throw new DomainBadRequestException("error.skill.notFound", slug);
|
||||
}
|
||||
java.util.Optional<Skill> published = skills.stream()
|
||||
.filter(s -> s.getLatestVersionId() != null)
|
||||
.findFirst();
|
||||
if (published.isPresent()) {
|
||||
return published.get();
|
||||
}
|
||||
if (currentUserId != null) {
|
||||
java.util.Optional<Skill> ownSkill = skills.stream()
|
||||
.filter(s -> currentUserId.equals(s.getOwnerId()))
|
||||
.findFirst();
|
||||
if (ownSkill.isPresent()) {
|
||||
return ownSkill.get();
|
||||
}
|
||||
}
|
||||
return skills.get(0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ public class SkillReportController extends BaseApiController {
|
|||
@RequestBody SkillReportSubmitRequest request,
|
||||
@RequestAttribute("userId") String userId,
|
||||
HttpServletRequest httpRequest) {
|
||||
Skill skill = findSkill(namespace, slug);
|
||||
Skill skill = findSkill(namespace, slug, userId);
|
||||
var report = skillReportService.submitReport(
|
||||
skill.getId(),
|
||||
userId,
|
||||
|
|
@ -55,11 +55,32 @@ public class SkillReportController extends BaseApiController {
|
|||
return ok("response.success.created", new SkillReportMutationResponse(report.getId(), report.getStatus().name()));
|
||||
}
|
||||
|
||||
private Skill findSkill(String namespaceSlug, String skillSlug) {
|
||||
private Skill findSkill(String namespaceSlug, String skillSlug, String currentUserId) {
|
||||
String cleanNamespace = namespaceSlug.startsWith("@") ? namespaceSlug.substring(1) : namespaceSlug;
|
||||
Namespace namespace = namespaceRepository.findBySlug(cleanNamespace)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", cleanNamespace));
|
||||
return skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
return resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
}
|
||||
}
|
||||
|
||||
private Skill resolveVisibleSkill(Long namespaceId, String slug, String currentUserId) {
|
||||
java.util.List<Skill> skills = skillRepository.findByNamespaceIdAndSlug(namespaceId, slug);
|
||||
if (skills.isEmpty()) {
|
||||
throw new DomainBadRequestException("error.skill.notFound", slug);
|
||||
}
|
||||
java.util.Optional<Skill> published = skills.stream()
|
||||
.filter(s -> s.getLatestVersionId() != null)
|
||||
.findFirst();
|
||||
if (published.isPresent()) {
|
||||
return published.get();
|
||||
}
|
||||
if (currentUserId != null) {
|
||||
java.util.Optional<Skill> ownSkill = skills.stream()
|
||||
.filter(s -> currentUserId.equals(s.getOwnerId()))
|
||||
.findFirst();
|
||||
if (ownSkill.isPresent()) {
|
||||
return ownSkill.get();
|
||||
}
|
||||
}
|
||||
return skills.get(0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
-- V12__skill_owner_uniqueness.sql
|
||||
-- Change skill uniqueness from (namespace_id, slug) to (namespace_id, slug, owner_id)
|
||||
-- to support owner-isolated skill records with the same name
|
||||
|
||||
ALTER TABLE skill DROP CONSTRAINT skill_namespace_id_slug_key;
|
||||
ALTER TABLE skill ADD CONSTRAINT skill_namespace_id_slug_owner_id_key UNIQUE(namespace_id, slug, owner_id);
|
||||
|
|
@ -119,3 +119,5 @@ error.admin.user.role.invalid=Invalid role: {0}
|
|||
error.admin.user.role.superAdmin.assignDenied=Only SUPER_ADMIN can assign SUPER_ADMIN role
|
||||
error.admin.user.status.invalid=Invalid user status: {0}
|
||||
error.admin.user.status.unsupported=Only ACTIVE or DISABLED status can be managed here
|
||||
error.skill.publish.nameConflict=A published skill with name ''{0}'' already exists in this namespace
|
||||
error.skill.approve.nameConflict=Cannot approve: a published skill with name ''{0}'' already exists in this namespace
|
||||
|
|
|
|||
|
|
@ -119,3 +119,5 @@ error.admin.user.role.invalid=无效的角色:{0}
|
|||
error.admin.user.role.superAdmin.assignDenied=只有 SUPER_ADMIN 可以分配 SUPER_ADMIN 角色
|
||||
error.admin.user.status.invalid=无效的用户状态:{0}
|
||||
error.admin.user.status.unsupported=这里只允许管理 ACTIVE 或 DISABLED 状态的用户
|
||||
error.skill.publish.nameConflict=该命名空间下已存在名为"{0}"的已发布技能,无法提交
|
||||
error.skill.approve.nameConflict=无法通过审核:该命名空间下已存在名为"{0}"的已发布技能
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ class SkillLifecycleControllerTest {
|
|||
setSkillId(skill, 1L);
|
||||
|
||||
given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace));
|
||||
given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.Optional.of(skill));
|
||||
given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.List.of(skill));
|
||||
given(skillGovernanceService.archiveSkill(eq(1L), eq("usr_1"), anyMap(), nullable(String.class), nullable(String.class), eq("cleanup")))
|
||||
.willReturn(skillWithStatus(skill, com.iflytek.skillhub.domain.skill.SkillStatus.ARCHIVED));
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ class SkillLifecycleControllerTest {
|
|||
skill.setStatus(com.iflytek.skillhub.domain.skill.SkillStatus.ARCHIVED);
|
||||
|
||||
given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace));
|
||||
given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.Optional.of(skill));
|
||||
given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.List.of(skill));
|
||||
given(skillGovernanceService.unarchiveSkill(eq(1L), eq("usr_1"), anyMap(), nullable(String.class), nullable(String.class)))
|
||||
.willReturn(skillWithStatus(skill, com.iflytek.skillhub.domain.skill.SkillStatus.ACTIVE));
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ class SkillLifecycleControllerTest {
|
|||
version.setStatus(SkillVersionStatus.DRAFT);
|
||||
|
||||
given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace));
|
||||
given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.Optional.of(skill));
|
||||
given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.List.of(skill));
|
||||
given(skillVersionRepository.findBySkillIdAndVersion(1L, "1.0.0")).willReturn(java.util.Optional.of(version));
|
||||
|
||||
mockMvc.perform(delete("/api/web/skills/global/demo-skill/versions/1.0.0")
|
||||
|
|
@ -162,7 +162,7 @@ class SkillLifecycleControllerTest {
|
|||
version.setStatus(SkillVersionStatus.PENDING_REVIEW);
|
||||
|
||||
given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace));
|
||||
given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.Optional.of(skill));
|
||||
given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.List.of(skill));
|
||||
given(skillVersionRepository.findBySkillIdAndVersion(1L, "1.0.0")).willReturn(java.util.Optional.of(version));
|
||||
|
||||
mockMvc.perform(post("/api/web/skills/global/demo-skill/versions/1.0.0/withdraw-review")
|
||||
|
|
@ -188,7 +188,7 @@ class SkillLifecycleControllerTest {
|
|||
newVersion.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
|
||||
given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace));
|
||||
given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.Optional.of(skill));
|
||||
given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.List.of(skill));
|
||||
SkillVersion sourceVersion = new SkillVersion(1L, "1.2.3", "owner");
|
||||
setSkillVersionId(sourceVersion, 2L);
|
||||
sourceVersion.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class SkillReportControllerTest {
|
|||
ReflectionTestUtils.setField(report, "id", 99L);
|
||||
|
||||
given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace));
|
||||
given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.Optional.of(skill));
|
||||
given(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).willReturn(java.util.List.of(skill));
|
||||
given(skillReportService.submitReport(eq(10L), eq("user-1"), eq("Spam"), eq("details"), nullable(String.class), nullable(String.class)))
|
||||
.willReturn(report);
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ConcurrentModificationException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
|
|
@ -153,12 +154,27 @@ public class ReviewService {
|
|||
|
||||
SkillVersion skillVersion = skillVersionRepository.findById(task.getSkillVersionId())
|
||||
.orElseThrow(() -> new DomainNotFoundException("skill_version.not_found", task.getSkillVersionId()));
|
||||
|
||||
Skill skill = skillRepository.findById(skillVersion.getSkillId())
|
||||
.orElseThrow(() -> new DomainNotFoundException("skill.not_found", skillVersion.getSkillId()));
|
||||
|
||||
// Check no other owner has a published skill with the same slug
|
||||
List<Skill> sameSlugSkills = skillRepository.findByNamespaceIdAndSlug(skill.getNamespaceId(), skill.getSlug());
|
||||
for (Skill other : sameSlugSkills) {
|
||||
if (!other.getId().equals(skill.getId())) {
|
||||
boolean otherHasPublished = !skillVersionRepository
|
||||
.findBySkillIdAndStatus(other.getId(), SkillVersionStatus.PUBLISHED)
|
||||
.isEmpty();
|
||||
if (otherHasPublished) {
|
||||
throw new DomainBadRequestException("error.skill.approve.nameConflict", skill.getSlug());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
skillVersion.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
skillVersion.setPublishedAt(LocalDateTime.now());
|
||||
skillVersionRepository.save(skillVersion);
|
||||
|
||||
Skill skill = skillRepository.findById(skillVersion.getSkillId())
|
||||
.orElseThrow(() -> new DomainNotFoundException("skill.not_found", skillVersion.getSkillId()));
|
||||
skill.setLatestVersionId(skillVersion.getId());
|
||||
applyPublishedMetadata(skill, skillVersion);
|
||||
skill.setUpdatedBy(reviewerId);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ 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> findByNamespaceIdAndSlug(Long namespaceId, String slug);
|
||||
Optional<Skill> findByNamespaceIdAndSlugAndOwnerId(Long namespaceId, String slug, String ownerId);
|
||||
List<Skill> findByNamespaceIdAndStatus(Long namespaceId, SkillStatus status);
|
||||
Skill save(Skill skill);
|
||||
void delete(Skill skill);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ import org.springframework.stereotype.Service;
|
|||
|
||||
import java.io.InputStream;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class SkillDownloadService {
|
||||
|
|
@ -59,8 +61,7 @@ public class SkillDownloadService {
|
|||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
|
|
@ -85,8 +86,7 @@ public class SkillDownloadService {
|
|||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
|
|
@ -107,8 +107,7 @@ public class SkillDownloadService {
|
|||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
|
|
@ -155,6 +154,28 @@ public class SkillDownloadService {
|
|||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", slug));
|
||||
}
|
||||
|
||||
private Skill resolveVisibleSkill(Long namespaceId, String slug, String currentUserId) {
|
||||
List<Skill> skills = skillRepository.findByNamespaceIdAndSlug(namespaceId, slug);
|
||||
if (skills.isEmpty()) {
|
||||
throw new DomainBadRequestException("error.skill.notFound", slug);
|
||||
}
|
||||
Optional<Skill> published = skills.stream()
|
||||
.filter(s -> s.getLatestVersionId() != null)
|
||||
.findFirst();
|
||||
if (published.isPresent()) {
|
||||
return published.get();
|
||||
}
|
||||
if (currentUserId != null) {
|
||||
Optional<Skill> ownSkill = skills.stream()
|
||||
.filter(s -> currentUserId.equals(s.getOwnerId()))
|
||||
.findFirst();
|
||||
if (ownSkill.isPresent()) {
|
||||
return ownSkill.get();
|
||||
}
|
||||
}
|
||||
return skills.get(0);
|
||||
}
|
||||
|
||||
private void assertPublishedAccessible(Skill skill) {
|
||||
if (skill.getStatus() != SkillStatus.ACTIVE) {
|
||||
throw new DomainBadRequestException("error.skill.status.notActive");
|
||||
|
|
|
|||
|
|
@ -188,8 +188,23 @@ public class SkillPublishService {
|
|||
String.join(", ", prePublishValidation.errors()));
|
||||
}
|
||||
|
||||
// 6. Find or create Skill record
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
// 6. Find or create Skill record (with owner isolation)
|
||||
List<Skill> existingSkills = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug);
|
||||
|
||||
// Check if any other owner's skill has published versions
|
||||
for (Skill existing : existingSkills) {
|
||||
if (!existing.getOwnerId().equals(publisherId)) {
|
||||
boolean hasPublished = !skillVersionRepository
|
||||
.findBySkillIdAndStatus(existing.getId(), SkillVersionStatus.PUBLISHED)
|
||||
.isEmpty();
|
||||
if (hasPublished) {
|
||||
throw new DomainBadRequestException("error.skill.publish.nameConflict", skillSlug);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find or create skill for current user
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlugAndOwnerId(namespace.getId(), skillSlug, publisherId)
|
||||
.orElseGet(() -> {
|
||||
Skill newSkill = new Skill(namespace.getId(), skillSlug, publisherId, visibility);
|
||||
newSkill.setCreatedBy(publisherId);
|
||||
|
|
@ -200,6 +215,14 @@ public class SkillPublishService {
|
|||
throw new DomainBadRequestException("error.skill.publish.archived", skillSlug);
|
||||
}
|
||||
|
||||
// 6c. Auto-withdraw pending review versions
|
||||
List<SkillVersion> pendingVersions = skillVersionRepository
|
||||
.findBySkillIdAndStatus(skill.getId(), SkillVersionStatus.PENDING_REVIEW);
|
||||
for (SkillVersion pending : pendingVersions) {
|
||||
pending.setStatus(SkillVersionStatus.DRAFT);
|
||||
skillVersionRepository.save(pending);
|
||||
}
|
||||
|
||||
// 7. Check version doesn't already exist
|
||||
if (skillVersionRepository.findBySkillIdAndVersion(skill.getId(), metadata.version()).isPresent()) {
|
||||
throw new DomainBadRequestException("error.skill.version.exists", metadata.version());
|
||||
|
|
|
|||
|
|
@ -114,8 +114,7 @@ public class SkillQueryService {
|
|||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
|
|
@ -182,7 +181,7 @@ public class SkillQueryService {
|
|||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = findSkill(namespace, skillSlug);
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles);
|
||||
SkillVersion skillVersion = findVersion(skill, version);
|
||||
assertPreviewAccessible(skill, skillVersion, version, currentUserId);
|
||||
|
|
@ -207,7 +206,7 @@ public class SkillQueryService {
|
|||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = findSkill(namespace, skillSlug);
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles);
|
||||
|
||||
SkillVersion skillVersion = findVersion(skill, version);
|
||||
|
|
@ -223,7 +222,7 @@ public class SkillQueryService {
|
|||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = findSkill(namespace, skillSlug);
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles);
|
||||
SkillVersion skillVersion = resolveVersionEntity(skill, null, tagName, null);
|
||||
return skillFileRepository.findByVersionId(skillVersion.getId());
|
||||
|
|
@ -237,7 +236,7 @@ public class SkillQueryService {
|
|||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = findSkill(namespace, skillSlug);
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles);
|
||||
|
||||
SkillVersion skillVersion = findVersion(skill, version);
|
||||
|
|
@ -256,7 +255,7 @@ public class SkillQueryService {
|
|||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = findSkill(namespace, skillSlug);
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles);
|
||||
SkillVersion skillVersion = resolveVersionEntity(skill, null, tagName, null);
|
||||
SkillFile file = findFile(skillVersion, filePath);
|
||||
|
|
@ -269,7 +268,7 @@ public class SkillQueryService {
|
|||
Map<Long, NamespaceRole> userNsRoles,
|
||||
Pageable pageable) {
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = findSkill(namespace, skillSlug);
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles);
|
||||
List<SkillVersion> visibleVersions;
|
||||
if (canManageRestrictedSkill(skill, currentUserId, userNsRoles)) {
|
||||
|
|
@ -313,7 +312,7 @@ public class SkillQueryService {
|
|||
}
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = findSkill(namespace, skillSlug);
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles);
|
||||
SkillVersion resolved = resolveVersionEntity(skill, version, tag, hash);
|
||||
String fingerprint = computeFingerprint(resolved);
|
||||
|
|
@ -340,14 +339,30 @@ public class SkillQueryService {
|
|||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", slug));
|
||||
}
|
||||
|
||||
private Skill findSkill(String namespaceSlug, String skillSlug) {
|
||||
private Skill findSkill(String namespaceSlug, String skillSlug, String currentUserId) {
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
return findSkill(namespace, skillSlug);
|
||||
return resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
}
|
||||
|
||||
private Skill findSkill(Namespace namespace, String skillSlug) {
|
||||
return skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
private Skill resolveVisibleSkill(Long namespaceId, String slug, String currentUserId) {
|
||||
List<Skill> skills = skillRepository.findByNamespaceIdAndSlug(namespaceId, slug);
|
||||
if (skills.isEmpty()) {
|
||||
throw new DomainBadRequestException("error.skill.notFound", slug);
|
||||
}
|
||||
|
||||
if (currentUserId != null) {
|
||||
Optional<Skill> ownSkill = skills.stream()
|
||||
.filter(s -> currentUserId.equals(s.getOwnerId()))
|
||||
.findFirst();
|
||||
if (ownSkill.isPresent()) {
|
||||
return ownSkill.get();
|
||||
}
|
||||
}
|
||||
|
||||
return skills.stream()
|
||||
.filter(s -> s.getLatestVersionId() != null)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", slug));
|
||||
}
|
||||
|
||||
private SkillVersion findVersion(Skill skill, String version) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import org.springframework.stereotype.Service;
|
|||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class SkillTagService {
|
||||
|
|
@ -44,8 +45,7 @@ public class SkillTagService {
|
|||
String currentUserId,
|
||||
java.util.Map<Long, NamespaceRole> userNamespaceRoles) {
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNamespaceRoles)) {
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skillSlug);
|
||||
}
|
||||
|
|
@ -76,8 +76,7 @@ public class SkillTagService {
|
|||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
assertAdminOrOwner(namespace.getId(), operatorId);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, operatorId);
|
||||
|
||||
// Find target version
|
||||
SkillVersion version = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), targetVersion)
|
||||
|
|
@ -111,8 +110,7 @@ public class SkillTagService {
|
|||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
assertAdminOrOwner(namespace.getId(), operatorId);
|
||||
Skill skill = skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, operatorId);
|
||||
|
||||
SkillTag tag = skillTagRepository.findBySkillIdAndTagName(skill.getId(), tagName)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.tag.notFound", tagName));
|
||||
|
|
@ -125,6 +123,28 @@ public class SkillTagService {
|
|||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", slug));
|
||||
}
|
||||
|
||||
private Skill resolveVisibleSkill(Long namespaceId, String slug, String currentUserId) {
|
||||
List<Skill> skills = skillRepository.findByNamespaceIdAndSlug(namespaceId, slug);
|
||||
if (skills.isEmpty()) {
|
||||
throw new DomainBadRequestException("error.skill.notFound", slug);
|
||||
}
|
||||
Optional<Skill> published = skills.stream()
|
||||
.filter(s -> s.getLatestVersionId() != null)
|
||||
.findFirst();
|
||||
if (published.isPresent()) {
|
||||
return published.get();
|
||||
}
|
||||
if (currentUserId != null) {
|
||||
Optional<Skill> ownSkill = skills.stream()
|
||||
.filter(s -> currentUserId.equals(s.getOwnerId()))
|
||||
.findFirst();
|
||||
if (ownSkill.isPresent()) {
|
||||
return ownSkill.get();
|
||||
}
|
||||
}
|
||||
return skills.get(0);
|
||||
}
|
||||
|
||||
private void assertAdminOrOwner(Long namespaceId, String operatorId) {
|
||||
NamespaceRole role = namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, operatorId)
|
||||
.map(member -> member.getRole())
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import org.springframework.context.ApplicationEventPublisher;
|
|||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
|
||||
import java.util.ConcurrentModificationException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
|
@ -233,6 +234,7 @@ class ReviewServiceTest {
|
|||
.thenReturn(1);
|
||||
when(skillVersionRepository.findById(SKILL_VERSION_ID)).thenReturn(Optional.of(sv));
|
||||
when(skillRepository.findById(SKILL_ID)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(NAMESPACE_ID, "my-skill")).thenReturn(List.of(skill));
|
||||
when(reviewTaskRepository.findById(REVIEW_TASK_ID)).thenReturn(Optional.of(task));
|
||||
|
||||
ReviewTask result = reviewService.approveReview(
|
||||
|
|
@ -263,6 +265,7 @@ class ReviewServiceTest {
|
|||
when(reviewTaskRepository.updateStatusWithVersion(any(), any(), any(), any(), any())).thenReturn(1);
|
||||
when(skillVersionRepository.findById(SKILL_VERSION_ID)).thenReturn(Optional.of(sv));
|
||||
when(skillRepository.findById(SKILL_ID)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(NAMESPACE_ID, "my-skill")).thenReturn(List.of(skill));
|
||||
when(reviewTaskRepository.findById(REVIEW_TASK_ID)).thenReturn(Optional.of(task));
|
||||
|
||||
reviewService.approveReview(REVIEW_TASK_ID, REVIEWER_ID, "ok",
|
||||
|
|
@ -356,6 +359,7 @@ class ReviewServiceTest {
|
|||
.thenReturn(1);
|
||||
when(skillVersionRepository.findById(SKILL_VERSION_ID)).thenReturn(Optional.of(sv));
|
||||
when(skillRepository.findById(SKILL_ID)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(NAMESPACE_ID, "my-skill")).thenReturn(List.of(skill));
|
||||
when(reviewTaskRepository.findById(REVIEW_TASK_ID)).thenReturn(Optional.of(task));
|
||||
|
||||
ReviewTask result = reviewService.approveReview(
|
||||
|
|
@ -379,6 +383,33 @@ class ReviewServiceTest {
|
|||
() -> reviewService.approveReview(REVIEW_TASK_ID, REVIEWER_ID, "ok",
|
||||
Map.of(NAMESPACE_ID, NamespaceRole.ADMIN), Set.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectApproveWhenOtherOwnerHasPublishedSameSlug() {
|
||||
ReviewTask task = createPendingReviewTask();
|
||||
Namespace ns = createTeamNamespace();
|
||||
SkillVersion sv = createPendingReviewSkillVersion();
|
||||
Skill skill = createSkill(); // owned by USER_ID
|
||||
|
||||
// Another owner's skill with same slug that has a published version
|
||||
Skill otherSkill = new Skill(NAMESPACE_ID, "my-skill", "other-user", SkillVisibility.PUBLIC);
|
||||
setField(otherSkill, "id", 99L);
|
||||
SkillVersion otherPublished = new SkillVersion(99L, "1.0.0", "other-user");
|
||||
otherPublished.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
|
||||
when(reviewTaskRepository.findById(REVIEW_TASK_ID)).thenReturn(Optional.of(task));
|
||||
when(namespaceRepository.findById(NAMESPACE_ID)).thenReturn(Optional.of(ns));
|
||||
when(permissionChecker.canReview(any(), any(), any(), anyMap(), anySet())).thenReturn(true);
|
||||
when(reviewTaskRepository.updateStatusWithVersion(any(), any(), any(), any(), any())).thenReturn(1);
|
||||
when(skillVersionRepository.findById(SKILL_VERSION_ID)).thenReturn(Optional.of(sv));
|
||||
when(skillRepository.findById(SKILL_ID)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(NAMESPACE_ID, "my-skill")).thenReturn(List.of(skill, otherSkill));
|
||||
when(skillVersionRepository.findBySkillIdAndStatus(99L, SkillVersionStatus.PUBLISHED)).thenReturn(List.of(otherPublished));
|
||||
|
||||
assertThrows(DomainBadRequestException.class,
|
||||
() -> reviewService.approveReview(REVIEW_TASK_ID, REVIEWER_ID, "ok",
|
||||
Map.of(NAMESPACE_ID, NamespaceRole.ADMIN), Set.of()));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import java.io.ByteArrayInputStream;
|
|||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
|
|
@ -82,7 +83,7 @@ class SkillDownloadServiceTest {
|
|||
ObjectMetadata metadata = new ObjectMetadata(1000L, "application/zip", Instant.now());
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findById(10L)).thenReturn(Optional.of(version));
|
||||
when(objectStorageService.exists(storageKey)).thenReturn(true);
|
||||
|
|
@ -124,7 +125,7 @@ class SkillDownloadServiceTest {
|
|||
ObjectMetadata metadata = new ObjectMetadata(1000L, "application/zip", Instant.now());
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
when(skillTagRepository.findBySkillIdAndTagName(1L, tagName)).thenReturn(Optional.of(tag));
|
||||
when(skillVersionRepository.findById(10L)).thenReturn(Optional.of(version));
|
||||
|
|
@ -164,7 +165,7 @@ class SkillDownloadServiceTest {
|
|||
ObjectMetadata metadata = new ObjectMetadata(1000L, "application/zip", Instant.now());
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, versionStr)).thenReturn(Optional.of(version));
|
||||
when(objectStorageService.exists(storageKey)).thenReturn(true);
|
||||
|
|
@ -196,7 +197,7 @@ class SkillDownloadServiceTest {
|
|||
version.setStatus(SkillVersionStatus.DRAFT);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, versionStr)).thenReturn(Optional.of(version));
|
||||
|
||||
|
|
|
|||
|
|
@ -109,7 +109,8 @@ class SkillPublishServiceTest {
|
|||
when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass());
|
||||
when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata);
|
||||
when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass());
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(List.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("test-skill"), eq(publisherId))).thenReturn(Optional.of(skill));
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(any(), eq("1.0.0"))).thenReturn(Optional.empty());
|
||||
when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> {
|
||||
SkillVersion saved = invocation.getArgument(0);
|
||||
|
|
@ -162,7 +163,8 @@ class SkillPublishServiceTest {
|
|||
when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass());
|
||||
when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata);
|
||||
when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass());
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("smoke-skill-two"))).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("smoke-skill-two"))).thenReturn(List.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("smoke-skill-two"), eq(publisherId))).thenReturn(Optional.of(skill));
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(any(), eq("0.2.0"))).thenReturn(Optional.empty());
|
||||
when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> {
|
||||
SkillVersion saved = invocation.getArgument(0);
|
||||
|
|
@ -205,7 +207,8 @@ class SkillPublishServiceTest {
|
|||
when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass());
|
||||
when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata);
|
||||
when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass());
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("auto-skill"))).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("auto-skill"))).thenReturn(List.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("auto-skill"), eq(publisherId))).thenReturn(Optional.of(skill));
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(any(), eq("1.0.0"))).thenReturn(Optional.empty());
|
||||
when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> {
|
||||
SkillVersion saved = invocation.getArgument(0);
|
||||
|
|
@ -253,7 +256,8 @@ class SkillPublishServiceTest {
|
|||
when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass());
|
||||
when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata);
|
||||
when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass());
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(Optional.of(archivedSkill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(List.of(archivedSkill));
|
||||
when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("test-skill"), eq(publisherId))).thenReturn(Optional.of(archivedSkill));
|
||||
|
||||
assertThrows(DomainBadRequestException.class, () -> service.publishFromEntries(
|
||||
namespaceSlug,
|
||||
|
|
@ -283,7 +287,8 @@ class SkillPublishServiceTest {
|
|||
when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass());
|
||||
when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata);
|
||||
when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass());
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(Optional.of(new Skill(1L, "test-skill", publisherId, SkillVisibility.PUBLIC)));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(List.of(new Skill(1L, "test-skill", publisherId, SkillVisibility.PUBLIC)));
|
||||
when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("test-skill"), eq(publisherId))).thenReturn(Optional.of(new Skill(1L, "test-skill", publisherId, SkillVisibility.PUBLIC)));
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(any(), anyString())).thenReturn(Optional.empty());
|
||||
when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> {
|
||||
SkillVersion saved = invocation.getArgument(0);
|
||||
|
|
@ -367,7 +372,8 @@ class SkillPublishServiceTest {
|
|||
when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass());
|
||||
when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata);
|
||||
when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass());
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("admin-skill"))).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("admin-skill"))).thenReturn(List.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("admin-skill"), eq(publisherId))).thenReturn(Optional.of(skill));
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(any(), eq("1.0.0"))).thenReturn(Optional.empty());
|
||||
when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> {
|
||||
SkillVersion saved = invocation.getArgument(0);
|
||||
|
|
@ -413,7 +419,8 @@ class SkillPublishServiceTest {
|
|||
|
||||
Skill skill = new Skill(namespace.getId(), "too-long-skill", publisherId, SkillVisibility.PUBLIC);
|
||||
setId(skill, 10L);
|
||||
when(skillRepository.findByNamespaceIdAndSlug(namespace.getId(), "too-long-skill")).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(namespace.getId(), "too-long-skill")).thenReturn(List.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(namespace.getId(), "too-long-skill", publisherId)).thenReturn(Optional.of(skill));
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(skill.getId(), "1.0.0")).thenReturn(Optional.empty());
|
||||
when(skillVersionRepository.save(any())).thenAnswer(invocation -> {
|
||||
SkillVersion version = invocation.getArgument(0);
|
||||
|
|
@ -537,6 +544,129 @@ class SkillPublishServiceTest {
|
|||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublishFromEntries_ShouldRejectWhenOtherOwnerHasPublishedSkill() throws Exception {
|
||||
String namespaceSlug = "test-ns";
|
||||
String publisherId = "user-200";
|
||||
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");
|
||||
List<PackageEntry> entries = List.of(skillMd);
|
||||
|
||||
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());
|
||||
|
||||
// Existing skill owned by another user with a published version
|
||||
Skill existingSkill = new Skill(1L, "test-skill", "user-100", SkillVisibility.PUBLIC);
|
||||
setId(existingSkill, 1L);
|
||||
SkillVersion publishedVersion = new SkillVersion(1L, "0.1.0", "user-100");
|
||||
publishedVersion.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(any(), eq(publisherId))).thenReturn(Optional.of(member));
|
||||
when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass());
|
||||
when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata);
|
||||
when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass());
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(List.of(existingSkill));
|
||||
when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PUBLISHED)).thenReturn(List.of(publishedVersion));
|
||||
|
||||
assertThrows(DomainBadRequestException.class, () -> service.publishFromEntries(
|
||||
namespaceSlug, entries, publisherId, SkillVisibility.PUBLIC, Set.of()
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublishFromEntries_ShouldAllowWhenOtherOwnerHasNonPublishedSkill() throws Exception {
|
||||
String namespaceSlug = "test-ns";
|
||||
String publisherId = "user-200";
|
||||
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");
|
||||
List<PackageEntry> entries = List.of(skillMd);
|
||||
|
||||
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());
|
||||
|
||||
// Existing skill owned by another user with NO published version
|
||||
Skill existingSkill = new Skill(1L, "test-skill", "user-100", SkillVisibility.PUBLIC);
|
||||
setId(existingSkill, 1L);
|
||||
|
||||
Skill newSkill = new Skill(1L, "test-skill", publisherId, SkillVisibility.PUBLIC);
|
||||
setId(newSkill, 2L);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(any(), eq(publisherId))).thenReturn(Optional.of(member));
|
||||
when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass());
|
||||
when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata);
|
||||
when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass());
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(List.of(existingSkill));
|
||||
when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PUBLISHED)).thenReturn(List.of());
|
||||
when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("test-skill"), eq(publisherId))).thenReturn(Optional.empty());
|
||||
when(skillRepository.save(any(Skill.class))).thenReturn(newSkill);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(any(), eq("1.0.0"))).thenReturn(Optional.empty());
|
||||
when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> {
|
||||
SkillVersion saved = invocation.getArgument(0);
|
||||
if (saved.getId() == null) setId(saved, 10L);
|
||||
return saved;
|
||||
});
|
||||
|
||||
SkillPublishService.PublishResult result = service.publishFromEntries(
|
||||
namespaceSlug, entries, publisherId, SkillVisibility.PUBLIC, Set.of()
|
||||
);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("test-skill", result.slug());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublishFromEntries_ShouldAutoWithdrawPendingVersions() throws Exception {
|
||||
String namespaceSlug = "test-ns";
|
||||
String publisherId = "user-100";
|
||||
String skillMdContent = "---\nname: test-skill\ndescription: Test\nversion: 2.0.0\n---\nBody";
|
||||
|
||||
PackageEntry skillMd = new PackageEntry("SKILL.md", skillMdContent.getBytes(), skillMdContent.length(), "text/markdown");
|
||||
List<PackageEntry> entries = List.of(skillMd);
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
NamespaceMember member = mock(NamespaceMember.class);
|
||||
SkillMetadata metadata = new SkillMetadata("test-skill", "Test", "2.0.0", "Body", Map.of());
|
||||
|
||||
Skill skill = new Skill(1L, "test-skill", publisherId, SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
|
||||
// Existing pending version
|
||||
SkillVersion pendingV1 = new SkillVersion(1L, "1.0.0", publisherId);
|
||||
pendingV1.setStatus(SkillVersionStatus.PENDING_REVIEW);
|
||||
setId(pendingV1, 5L);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(any(), eq(publisherId))).thenReturn(Optional.of(member));
|
||||
when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass());
|
||||
when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata);
|
||||
when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass());
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(List.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("test-skill"), eq(publisherId))).thenReturn(Optional.of(skill));
|
||||
when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PENDING_REVIEW)).thenReturn(List.of(pendingV1));
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(any(), eq("2.0.0"))).thenReturn(Optional.empty());
|
||||
when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> {
|
||||
SkillVersion saved = invocation.getArgument(0);
|
||||
if (saved.getId() == null) setId(saved, 10L);
|
||||
return saved;
|
||||
});
|
||||
when(skillRepository.save(any())).thenReturn(skill);
|
||||
|
||||
service.publishFromEntries(namespaceSlug, entries, publisherId, SkillVisibility.PUBLIC, Set.of());
|
||||
|
||||
// Verify pending version was withdrawn to DRAFT
|
||||
assertEquals(SkillVersionStatus.DRAFT, pendingV1.getStatus());
|
||||
verify(skillVersionRepository).save(pendingV1);
|
||||
}
|
||||
|
||||
private void setId(Object entity, Long id) throws Exception {
|
||||
Field idField = entity.getClass().getDeclaredField("id");
|
||||
idField.setAccessible(true);
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class SkillQueryServiceTest {
|
|||
setId(version, 10L);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findById(10L)).thenReturn(Optional.of(version));
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ class SkillQueryServiceTest {
|
|||
setId(skill, 1L);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(false);
|
||||
|
||||
// Act & Assert
|
||||
|
|
@ -189,7 +189,7 @@ class SkillQueryServiceTest {
|
|||
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(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.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));
|
||||
|
|
@ -219,7 +219,7 @@ class SkillQueryServiceTest {
|
|||
skillVersion.setStatus(SkillVersionStatus.DRAFT);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(skillVersion));
|
||||
|
||||
|
|
@ -246,7 +246,7 @@ class SkillQueryServiceTest {
|
|||
SkillFile file = new SkillFile(1L, filePath, 100L, "text/markdown", "hash1", "skills/1/1/SKILL.md");
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.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(file));
|
||||
|
|
@ -279,7 +279,7 @@ class SkillQueryServiceTest {
|
|||
skillVersion.setManifestJson("[{\"path\":\"SKILL.md\"}]");
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(skillVersion));
|
||||
|
||||
|
|
@ -313,7 +313,7 @@ class SkillQueryServiceTest {
|
|||
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(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.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));
|
||||
|
|
@ -351,7 +351,7 @@ class SkillQueryServiceTest {
|
|||
rejected.setStatus(SkillVersionStatus.REJECTED);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillId(1L)).thenReturn(List.of(pending, published, rejected));
|
||||
|
||||
|
|
@ -385,7 +385,7 @@ class SkillQueryServiceTest {
|
|||
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(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PUBLISHED))
|
||||
.thenReturn(List.of(version100, version110));
|
||||
|
|
@ -427,7 +427,7 @@ class SkillQueryServiceTest {
|
|||
SkillFile file = new SkillFile(11L, "SKILL.md", 10L, "text/markdown", "hash", "key");
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, null, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findById(11L)).thenReturn(Optional.of(version));
|
||||
when(skillVersionRepository.findBySkillIdAndStatus(3L, SkillVersionStatus.PUBLISHED)).thenReturn(List.of(version));
|
||||
|
|
@ -460,7 +460,7 @@ class SkillQueryServiceTest {
|
|||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
|
||||
SkillQueryService.SkillDetailDTO result = service.getSkillDetail(namespaceSlug, skillSlug, userId, userNsRoles);
|
||||
|
|
@ -574,7 +574,7 @@ class SkillQueryServiceTest {
|
|||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
|
||||
SkillQueryService.SkillDetailDTO result = service.getSkillDetail(namespaceSlug, skillSlug, userId, userNsRoles);
|
||||
|
|
@ -607,7 +607,7 @@ class SkillQueryServiceTest {
|
|||
pending.setStatus(SkillVersionStatus.PENDING_REVIEW);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PENDING_REVIEW))
|
||||
.thenReturn(List.of(pending));
|
||||
|
|
@ -642,7 +642,7 @@ class SkillQueryServiceTest {
|
|||
pending.setStatus(SkillVersionStatus.PENDING_REVIEW);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findById(11L)).thenReturn(Optional.of(published));
|
||||
|
||||
|
|
@ -674,7 +674,7 @@ class SkillQueryServiceTest {
|
|||
pending.setManifestJson("[{\"path\":\"SKILL.md\"}]");
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(pending));
|
||||
|
||||
|
|
@ -709,7 +709,7 @@ class SkillQueryServiceTest {
|
|||
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(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(pending));
|
||||
when(skillFileRepository.findByVersionId(11L)).thenReturn(List.of(file));
|
||||
|
|
@ -739,7 +739,7 @@ class SkillQueryServiceTest {
|
|||
pending.setStatus(SkillVersionStatus.PENDING_REVIEW);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, viewerId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(pending));
|
||||
|
||||
|
|
@ -771,7 +771,7 @@ class SkillQueryServiceTest {
|
|||
rejected.setStatus(SkillVersionStatus.REJECTED);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillId(1L)).thenReturn(List.of(rejected, draft, published));
|
||||
|
||||
|
|
@ -805,7 +805,7 @@ class SkillQueryServiceTest {
|
|||
published.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PUBLISHED)).thenReturn(List.of(published));
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class SkillTagServiceTest {
|
|||
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(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, targetVersion)).thenReturn(Optional.of(version));
|
||||
when(skillTagRepository.findBySkillIdAndTagName(1L, tagName)).thenReturn(Optional.empty());
|
||||
when(skillTagRepository.save(any())).thenReturn(tag);
|
||||
|
|
@ -118,7 +118,7 @@ class SkillTagServiceTest {
|
|||
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(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(skillTagRepository.findBySkillIdAndTagName(1L, tagName)).thenReturn(Optional.of(tag));
|
||||
|
||||
// Act
|
||||
|
|
@ -175,7 +175,7 @@ class SkillTagServiceTest {
|
|||
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));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(skillTagRepository.findBySkillId(1L)).thenReturn(List.of(tag1, tag2));
|
||||
when(visibilityChecker.canAccess(eq(skill), isNull(), eq(java.util.Map.of()))).thenReturn(true);
|
||||
|
||||
|
|
|
|||
|
|
@ -38,10 +38,15 @@ public class JpaSkillRepositoryAdapter implements SkillRepository {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Optional<Skill> findByNamespaceIdAndSlug(Long namespaceId, String slug) {
|
||||
public List<Skill> findByNamespaceIdAndSlug(Long namespaceId, String slug) {
|
||||
return delegate.findByNamespaceIdAndSlug(namespaceId, slug);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Skill> findByNamespaceIdAndSlugAndOwnerId(Long namespaceId, String slug, String ownerId) {
|
||||
return delegate.findByNamespaceIdAndSlugAndOwnerId(namespaceId, slug, ownerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Skill> findByNamespaceIdAndStatus(Long namespaceId, SkillStatus status) {
|
||||
return delegate.findByNamespaceIdAndStatus(namespaceId, status);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import java.util.Optional;
|
|||
@Repository
|
||||
public interface SkillJpaRepository extends JpaRepository<Skill, Long>, SkillRepository {
|
||||
List<Skill> findByIdIn(List<Long> ids);
|
||||
Optional<Skill> findByNamespaceIdAndSlug(Long namespaceId, String slug);
|
||||
List<Skill> findByNamespaceIdAndSlug(Long namespaceId, String slug);
|
||||
Optional<Skill> findByNamespaceIdAndSlugAndOwnerId(Long namespaceId, String slug, String ownerId);
|
||||
|
||||
@Override
|
||||
default List<Skill> findByNamespaceIdAndStatus(Long namespaceId, SkillStatus status) {
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ export function usePublishSkill() {
|
|||
skipGlobalErrorHandler: true,
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['skills', 'my'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['skills'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue