fix(review): enforce archived mutation guard

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-09-02 02:58:46 +08:00
parent 56e9e0ebd9
commit 222fdd643f
3 changed files with 42 additions and 1 deletions

View file

@ -8,6 +8,7 @@ import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException;
import com.iflytek.skillhub.domain.skill.Skill;
import com.iflytek.skillhub.domain.skill.SkillRepository;
import com.iflytek.skillhub.domain.skill.SkillStatus;
import com.iflytek.skillhub.domain.skill.SkillVersionRepository;
import com.iflytek.skillhub.domain.skill.SkillVersionStatus;
import com.iflytek.skillhub.domain.skill.VisibilityChecker;
@ -142,7 +143,7 @@ public class SkillReviewAppService {
&& skillVersionRepository.findById(skill.getLatestVersionId())
.map(version -> version.getStatus() == SkillVersionStatus.PUBLISHED)
.orElse(false);
if (!published) {
if (skill.getStatus() != SkillStatus.ACTIVE || !published) {
throw new DomainBadRequestException("error.skillReview.notInteractable");
}
return skill;

View file

@ -13,6 +13,7 @@ import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
import com.iflytek.skillhub.domain.skill.Skill;
import com.iflytek.skillhub.domain.skill.SkillRepository;
import com.iflytek.skillhub.domain.skill.SkillStatus;
import com.iflytek.skillhub.domain.skill.SkillVersion;
import com.iflytek.skillhub.domain.skill.SkillVersionRepository;
import com.iflytek.skillhub.domain.skill.SkillVersionStatus;
@ -125,6 +126,23 @@ class SkillReviewAppServiceTest {
verify(ratingService, never()).upsertReview(any(), any(), any(Short.class), any());
}
@Test
void archivedSkillReviewMutationIsRejectedServerSide() {
Skill skill = publishedSkill(SkillVisibility.PUBLIC);
skill.setStatus(SkillStatus.ARCHIVED);
SkillVersion published = new SkillVersion(10L, "1.0.0", "owner");
published.setStatus(SkillVersionStatus.PUBLISHED);
when(skillRepository.findById(10L)).thenReturn(Optional.of(skill));
when(skillVersionRepository.findById(100L)).thenReturn(Optional.of(published));
assertThatThrownBy(() -> service.upsert(
10L, "owner", (short) 5, "archived", Map.of(), Set.of()))
.isInstanceOf(DomainBadRequestException.class)
.hasMessage("error.skillReview.notInteractable");
verify(ratingService, never()).upsertReview(any(), any(), any(Short.class), any());
}
@Test
void authorCanClearReviewAfterSkillStopsBeingInteractable() {
SkillRating review = new SkillRating(10L, "author", (short) 4);

View file

@ -1241,6 +1241,28 @@ class SkillQueryServiceTest {
assertFalse(result.canInteract());
}
@Test
void testGetSkillDetail_ShouldKeepInteractionForActiveSkillWithoutHeadlineVersion() throws Exception {
String namespaceSlug = "test-ns";
String skillSlug = "test-skill";
String ownerId = "owner-1";
Namespace namespace = new Namespace(namespaceSlug, "Test NS", ownerId);
setId(namespace, 1L);
Skill skill = new Skill(1L, skillSlug, ownerId, SkillVisibility.PUBLIC);
setId(skill, 1L);
skill.setStatus(SkillStatus.ACTIVE);
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
SkillQueryService.SkillDetailDTO result = service.getSkillDetail(
namespaceSlug, skillSlug, ownerId, Map.of());
assertNull(result.headlineVersion());
assertTrue(result.canInteract());
}
@Test
void testGetSkillDetail_ShouldIncludeRejectedOwnerPreviewComment() throws Exception {
String namespaceSlug = "test-ns";