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
This commit is contained in:
vsxd 2026-03-12 20:11:32 +08:00
parent cab627be02
commit 4dfa17dc4e
4 changed files with 107 additions and 3 deletions

View file

@ -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();
}

View file

@ -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<SkillRating> existing = ratingRepository.findBySkillIdAndUserId(skillId, userId);
if (existing.isPresent()) {

View file

@ -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<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("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<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", 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

View file

@ -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<Long, NamespaceRole> 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);