From 4dfa17dc4e62e864656fddaa3124926896fd00e1 Mon Sep 17 00:00:00 2001 From: vsxd Date: Thu, 12 Mar 2026 20:11:32 +0800 Subject: [PATCH] refactor(domain): improve SkillRating validation and add domain tests - Add rating value validation in SkillRating entity - Improve SkillRatingService error handling - Add SkillPublishService and SkillQueryService test coverage --- .../skillhub/domain/social/SkillRating.java | 5 +- .../domain/social/SkillRatingService.java | 3 +- .../service/SkillPublishServiceTest.java | 64 +++++++++++++++++++ .../skill/service/SkillQueryServiceTest.java | 38 +++++++++++ 4 files changed, 107 insertions(+), 3 deletions(-) diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillRating.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillRating.java index 92751d46..8d496c63 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillRating.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillRating.java @@ -1,5 +1,6 @@ package com.iflytek.skillhub.domain.social; +import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; import jakarta.persistence.*; import java.time.LocalDateTime; @@ -28,14 +29,14 @@ public class SkillRating { protected SkillRating() {} public SkillRating(Long skillId, String userId, short score) { - if (score < 1 || score > 5) throw new IllegalArgumentException("Score must be 1-5"); + if (score < 1 || score > 5) throw new DomainBadRequestException("error.rating.score.invalid"); this.skillId = skillId; this.userId = userId; this.score = score; } public void updateScore(short newScore) { - if (newScore < 1 || newScore > 5) throw new IllegalArgumentException("Score must be 1-5"); + if (newScore < 1 || newScore > 5) throw new DomainBadRequestException("error.rating.score.invalid"); this.score = newScore; this.updatedAt = LocalDateTime.now(); } diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillRatingService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillRatingService.java index 22d722d1..814c1ff7 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillRatingService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillRatingService.java @@ -1,5 +1,6 @@ package com.iflytek.skillhub.domain.social; +import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; import com.iflytek.skillhub.domain.social.event.SkillRatedEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -21,7 +22,7 @@ public class SkillRatingService { @Transactional public void rate(Long skillId, String userId, short score) { if (score < 1 || score > 5) { - throw new IllegalArgumentException("Score must be 1-5"); + throw new DomainBadRequestException("error.rating.score.invalid"); } Optional existing = ratingRepository.findBySkillIdAndUserId(skillId, userId); if (existing.isPresent()) { diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java index 9054c09d..416d441e 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java @@ -130,6 +130,70 @@ class SkillPublishServiceTest { verify(objectStorageService, atLeastOnce()).putObject(anyString(), any(), anyLong(), anyString()); } + @Test + void testPublishFromEntries_ShouldSlugifyNameBeforeLookupAndResponse() throws Exception { + String namespaceSlug = "test-ns"; + String publisherId = "user-100"; + String skillMdContent = "---\nname: Smoke Skill Two\ndescription: Test\nversion: 0.2.0\n---\nBody"; + + PackageEntry skillMd = new PackageEntry("SKILL.md", skillMdContent.getBytes(), skillMdContent.length(), "text/markdown"); + List 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("Smoke Skill Two", "Test", "0.2.0", "Body", Map.of()); + + Skill skill = new Skill(1L, "smoke-skill-two", publisherId, SkillVisibility.PUBLIC); + setId(skill, 2L); + SkillVersion version = new SkillVersion(2L, "0.2.0", publisherId); + setId(version, 20L); + + 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("smoke-skill-two"))).thenReturn(Optional.of(skill)); + when(skillVersionRepository.findBySkillIdAndVersion(any(), eq("0.2.0"))).thenReturn(Optional.empty()); + when(skillVersionRepository.save(any())).thenReturn(version); + when(skillRepository.save(any())).thenReturn(skill); + + SkillPublishService.PublishResult result = service.publishFromEntries( + namespaceSlug, + entries, + publisherId, + SkillVisibility.PUBLIC + ); + + assertEquals("smoke-skill-two", result.slug()); + verify(skillRepository).findByNamespaceIdAndSlug(1L, "smoke-skill-two"); + } + + @Test + void testPublishFromEntries_ShouldRejectMissingVersionBeforePersistence() throws Exception { + String namespaceSlug = "test-ns"; + String publisherId = "user-100"; + String skillMdContent = "---\nname: test-skill\ndescription: Test\n---\nBody"; + + PackageEntry skillMd = new PackageEntry("SKILL.md", skillMdContent.getBytes(), skillMdContent.length(), "text/markdown"); + List 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", null, "Body", Map.of()); + + 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); + + assertThrows(DomainBadRequestException.class, () -> + service.publishFromEntries(namespaceSlug, entries, publisherId, SkillVisibility.PUBLIC)); + verify(skillVersionRepository, never()).save(any()); + } + @Test void testPublishFromEntries_NamespaceNotFound() { // Arrange diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillQueryServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillQueryServiceTest.java index 549ce761..91b2cac9 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillQueryServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillQueryServiceTest.java @@ -313,6 +313,44 @@ class SkillQueryServiceTest { assertTrue(result.downloadUrl().contains("/versions/1.1.0/download")); } + @Test + void testResolveVersion_ShouldEncodeDownloadUrlPathSegments() throws Exception { + String namespaceSlug = "global"; + String skillSlug = "smoke-skill-two"; + Map userNsRoles = Map.of(); + + Namespace namespace = new Namespace(namespaceSlug, "Global", "user-1"); + setId(namespace, 1L); + Skill skill = new Skill(1L, skillSlug, "user-100", SkillVisibility.PUBLIC); + setId(skill, 3L); + skill.setStatus(SkillStatus.ACTIVE); + skill.setLatestVersionId(11L); + + SkillVersion version = new SkillVersion(3L, "1.0.0 beta", "user-100"); + setId(version, 11L); + version.setStatus(SkillVersionStatus.PUBLISHED); + 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(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)); + when(skillFileRepository.findByVersionId(11L)).thenReturn(List.of(file)); + + SkillQueryService.ResolvedVersionDTO result = service.resolveVersion( + namespaceSlug, + skillSlug, + null, + null, + null, + null, + userNsRoles + ); + + assertEquals("/api/v1/skills/global/smoke-skill-two/versions/1.0.0%20beta/download", result.downloadUrl()); + } + private void setId(Object entity, Long id) throws Exception { Field idField = entity.getClass().getDeclaredField("id"); idField.setAccessible(true);