mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-28 11:25:00 +00:00
fix(cli): require installable latest in search
Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
parent
32cc316b30
commit
f5259daa94
6 changed files with 416 additions and 27 deletions
|
|
@ -24,11 +24,13 @@ import org.mockito.Mock;
|
|||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
|
|
@ -98,8 +100,6 @@ class SkillSearchAppServiceTest {
|
|||
when(skillRepository.findByIdIn(List.of(11L))).thenReturn(List.of(visibleSkill));
|
||||
when(namespaceRepository.findByIdIn(List.of(2L))).thenReturn(List.of(activeNamespace));
|
||||
when(skillVersionRepository.findByIdIn(List.of(111L))).thenReturn(List.of());
|
||||
when(skillVersionRepository.findBySkillIdInAndStatus(List.of(11L), com.iflytek.skillhub.domain.skill.SkillVersionStatus.PUBLISHED))
|
||||
.thenReturn(List.of());
|
||||
|
||||
SkillSearchAppService.SearchResponse response = service.search("skill", null, "newest", 0, 1, null, null);
|
||||
|
||||
|
|
@ -147,8 +147,6 @@ class SkillSearchAppServiceTest {
|
|||
when(skillRepository.findByIdIn(List.of(10L))).thenReturn(List.of(visibleSkill));
|
||||
when(namespaceRepository.findByIdIn(List.of(1L))).thenReturn(List.of(namespace));
|
||||
when(skillVersionRepository.findByIdIn(List.of(101L))).thenReturn(List.of());
|
||||
when(skillVersionRepository.findBySkillIdInAndStatus(List.of(10L), com.iflytek.skillhub.domain.skill.SkillVersionStatus.PUBLISHED))
|
||||
.thenReturn(List.of());
|
||||
|
||||
SkillSearchAppService.SearchResponse response = service.search("skill", null, "newest", 0, 20, "user-9", Map.of());
|
||||
|
||||
|
|
@ -166,6 +164,9 @@ class SkillSearchAppServiceTest {
|
|||
setField(second, "id", 11L);
|
||||
second.setLatestVersionId(102L);
|
||||
|
||||
SkillVersion firstVersion = publishedVersion(10L, 101L, "1.0.0");
|
||||
SkillVersion secondVersion = publishedVersion(11L, 102L, "2.0.0");
|
||||
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
setField(namespace, "id", 1L);
|
||||
namespace.setStatus(NamespaceStatus.ACTIVE);
|
||||
|
|
@ -174,20 +175,80 @@ class SkillSearchAppServiceTest {
|
|||
.thenReturn(new SearchResult(List.of(10L, 11L), 2, 0, 20));
|
||||
when(skillRepository.findByIdIn(List.of(10L, 11L))).thenReturn(List.of(first, second));
|
||||
when(namespaceRepository.findByIdIn(List.of(1L))).thenReturn(List.of(namespace));
|
||||
when(skillVersionRepository.findByIdIn(List.of(101L, 102L))).thenReturn(List.of());
|
||||
when(skillVersionRepository.findBySkillIdInAndStatus(List.of(10L, 11L), com.iflytek.skillhub.domain.skill.SkillVersionStatus.PUBLISHED))
|
||||
.thenReturn(List.of());
|
||||
when(skillVersionRepository.findByIdIn(List.of(101L, 102L))).thenReturn(List.of(firstVersion, secondVersion));
|
||||
|
||||
SkillSearchAppService.SearchResponse response = service.search(null, null, "newest", 0, 20, null, null);
|
||||
|
||||
assertEquals(2, response.items().size());
|
||||
assertEquals("1.0.0", response.items().get(0).publishedVersion().version());
|
||||
assertEquals("2.0.0", response.items().get(1).publishedVersion().version());
|
||||
verify(skillVersionRepository, times(1)).findByIdIn(List.of(101L, 102L));
|
||||
verify(skillVersionRepository, times(1))
|
||||
verify(skillVersionRepository, times(0))
|
||||
.findBySkillIdInAndStatus(List.of(10L, 11L), com.iflytek.skillhub.domain.skill.SkillVersionStatus.PUBLISHED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void search_shouldNotExposeDownloadUnavailableVersionAsPublishedSummary() {
|
||||
void search_shouldNotFallbackToOlderPublishedVersionWhenLatestIsMissing() {
|
||||
Skill skill = new Skill(1L, "missing-latest", "owner-1", SkillVisibility.PUBLIC);
|
||||
setField(skill, "id", 10L);
|
||||
|
||||
SkillVersion oldInstallable = publishedVersion(10L, 100L, "0.9.0");
|
||||
|
||||
Namespace namespace = new Namespace("global", "Global", "owner-1");
|
||||
setField(namespace, "id", 1L);
|
||||
namespace.setStatus(NamespaceStatus.ACTIVE);
|
||||
|
||||
when(searchQueryService.search(any()))
|
||||
.thenReturn(new SearchResult(List.of(10L), 1, 0, 20));
|
||||
when(skillRepository.findByIdIn(List.of(10L))).thenReturn(List.of(skill));
|
||||
when(namespaceRepository.findByIdIn(List.of(1L))).thenReturn(List.of(namespace));
|
||||
org.mockito.Mockito.lenient()
|
||||
.when(skillVersionRepository.findBySkillIdInAndStatus(List.of(10L), SkillVersionStatus.PUBLISHED))
|
||||
.thenReturn(List.of(oldInstallable));
|
||||
|
||||
SkillSearchAppService.SearchResponse response = service.search(null, null, "newest", 0, 20, null, null);
|
||||
|
||||
assertEquals(1, response.items().size());
|
||||
assertEquals("missing-latest", response.items().getFirst().slug());
|
||||
assertNull(response.items().getFirst().publishedVersion());
|
||||
verify(skillVersionRepository, times(0))
|
||||
.findBySkillIdInAndStatus(List.of(10L), SkillVersionStatus.PUBLISHED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void search_shouldNotFallbackToOlderPublishedVersionWhenLatestIsYanked() {
|
||||
Skill skill = new Skill(1L, "yanked-latest", "owner-1", SkillVisibility.PUBLIC);
|
||||
setField(skill, "id", 10L);
|
||||
skill.setLatestVersionId(101L);
|
||||
|
||||
SkillVersion latest = publishedVersion(10L, 101L, "1.0.0");
|
||||
latest.setYankedAt(Instant.parse("2026-06-12T00:00:00Z"));
|
||||
SkillVersion oldInstallable = publishedVersion(10L, 100L, "0.9.0");
|
||||
|
||||
Namespace namespace = new Namespace("global", "Global", "owner-1");
|
||||
setField(namespace, "id", 1L);
|
||||
namespace.setStatus(NamespaceStatus.ACTIVE);
|
||||
|
||||
when(searchQueryService.search(any()))
|
||||
.thenReturn(new SearchResult(List.of(10L), 1, 0, 20));
|
||||
when(skillRepository.findByIdIn(List.of(10L))).thenReturn(List.of(skill));
|
||||
when(namespaceRepository.findByIdIn(List.of(1L))).thenReturn(List.of(namespace));
|
||||
when(skillVersionRepository.findByIdIn(List.of(101L))).thenReturn(List.of(latest));
|
||||
org.mockito.Mockito.lenient()
|
||||
.when(skillVersionRepository.findBySkillIdInAndStatus(List.of(10L), SkillVersionStatus.PUBLISHED))
|
||||
.thenReturn(List.of(oldInstallable));
|
||||
|
||||
SkillSearchAppService.SearchResponse response = service.search(null, null, "newest", 0, 20, null, null);
|
||||
|
||||
assertEquals(1, response.items().size());
|
||||
assertEquals("yanked-latest", response.items().getFirst().slug());
|
||||
assertNull(response.items().getFirst().publishedVersion());
|
||||
verify(skillVersionRepository, times(0))
|
||||
.findBySkillIdInAndStatus(List.of(10L), SkillVersionStatus.PUBLISHED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void search_shouldNotFallbackToOlderPublishedVersionWhenLatestDownloadUnavailable() {
|
||||
Skill skill = new Skill(1L, "not-ready", "owner-1", SkillVisibility.PUBLIC);
|
||||
setField(skill, "id", 10L);
|
||||
skill.setLatestVersionId(101L);
|
||||
|
|
@ -196,6 +257,7 @@ class SkillSearchAppServiceTest {
|
|||
setField(version, "id", 101L);
|
||||
version.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
version.setDownloadReady(false);
|
||||
SkillVersion oldInstallable = publishedVersion(10L, 100L, "0.9.0");
|
||||
|
||||
Namespace namespace = new Namespace("global", "Global", "owner-1");
|
||||
setField(namespace, "id", 1L);
|
||||
|
|
@ -206,14 +268,17 @@ class SkillSearchAppServiceTest {
|
|||
when(skillRepository.findByIdIn(List.of(10L))).thenReturn(List.of(skill));
|
||||
when(namespaceRepository.findByIdIn(List.of(1L))).thenReturn(List.of(namespace));
|
||||
when(skillVersionRepository.findByIdIn(List.of(101L))).thenReturn(List.of(version));
|
||||
when(skillVersionRepository.findBySkillIdInAndStatus(List.of(10L), SkillVersionStatus.PUBLISHED))
|
||||
.thenReturn(List.of(version));
|
||||
org.mockito.Mockito.lenient()
|
||||
.when(skillVersionRepository.findBySkillIdInAndStatus(List.of(10L), SkillVersionStatus.PUBLISHED))
|
||||
.thenReturn(List.of(oldInstallable));
|
||||
|
||||
SkillSearchAppService.SearchResponse response = service.search(null, null, "newest", 0, 20, null, null);
|
||||
|
||||
assertEquals(1, response.items().size());
|
||||
assertEquals("not-ready", response.items().getFirst().slug());
|
||||
assertEquals(null, response.items().getFirst().publishedVersion());
|
||||
assertNull(response.items().getFirst().publishedVersion());
|
||||
verify(skillVersionRepository, times(0))
|
||||
.findBySkillIdInAndStatus(List.of(10L), SkillVersionStatus.PUBLISHED);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -272,4 +337,12 @@ class SkillSearchAppServiceTest {
|
|||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private SkillVersion publishedVersion(Long skillId, Long versionId, String versionNumber) {
|
||||
SkillVersion version = new SkillVersion(skillId, versionNumber, "owner-1");
|
||||
setField(version, "id", versionId);
|
||||
version.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
version.setDownloadReady(true);
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.namespace.NamespaceStatus;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
|
|
@ -284,12 +285,20 @@ public class SkillDownloadService {
|
|||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skill.getSlug());
|
||||
}
|
||||
if (namespace.getStatus() == NamespaceStatus.ARCHIVED
|
||||
&& !isNamespaceMember(namespace.getId(), currentUserId, userNsRoles)) {
|
||||
throw new DomainForbiddenException("error.namespace.archived", namespace.getSlug());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAnonymousDownloadAllowed(Skill skill) {
|
||||
return skill.getVisibility() == SkillVisibility.PUBLIC;
|
||||
}
|
||||
|
||||
private boolean isNamespaceMember(Long namespaceId, String currentUserId, Map<Long, NamespaceRole> userNsRoles) {
|
||||
return currentUserId != null && userNsRoles != null && userNsRoles.containsKey(namespaceId);
|
||||
}
|
||||
|
||||
private Skill resolveVisibleSkill(Long namespaceId, String slug, String currentUserId) {
|
||||
return skillSlugResolutionService.resolve(
|
||||
namespaceId,
|
||||
|
|
|
|||
|
|
@ -89,21 +89,10 @@ public class SkillLifecycleProjectionService {
|
|||
.collect(Collectors.toMap(SkillVersion::getId, Function.identity()));
|
||||
|
||||
Map<Long, SkillVersion> publishedBySkillId = new java.util.HashMap<>();
|
||||
List<Long> unresolvedSkillIds = new java.util.ArrayList<>();
|
||||
for (Skill skill : skills) {
|
||||
SkillVersion latestVersion = latestVersionsById.get(skill.getLatestVersionId());
|
||||
if (SkillInstallability.isInstallableVersion(latestVersion)) {
|
||||
publishedBySkillId.put(skill.getId(), latestVersion);
|
||||
} else {
|
||||
unresolvedSkillIds.add(skill.getId());
|
||||
}
|
||||
}
|
||||
|
||||
if (!unresolvedSkillIds.isEmpty()) {
|
||||
for (SkillVersion version : skillVersionRepository.findBySkillIdInAndStatus(unresolvedSkillIds, SkillVersionStatus.PUBLISHED)) {
|
||||
if (SkillInstallability.isInstallableVersion(version)) {
|
||||
publishedBySkillId.merge(version.getSkillId(), version, this::newerVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -160,10 +149,6 @@ public class SkillLifecycleProjectionService {
|
|||
.thenComparing(SkillVersion::getId, Comparator.nullsLast(Comparator.naturalOrder()));
|
||||
}
|
||||
|
||||
private SkillVersion newerVersion(SkillVersion left, SkillVersion right) {
|
||||
return versionComparator().compare(left, right) >= 0 ? left : right;
|
||||
}
|
||||
|
||||
private VersionProjection toProjection(SkillVersion version) {
|
||||
if (version == null) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -119,6 +119,137 @@ class SkillDownloadServiceTest {
|
|||
verify(eventPublisher).publishEvent(any(SkillDownloadedEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadLatest_ShouldRejectSkillWithoutLatest() throws Exception {
|
||||
String namespaceSlug = "global";
|
||||
String skillSlug = "missing-latest";
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Global", "owner-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, "owner-1", 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));
|
||||
|
||||
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class, () ->
|
||||
service.downloadLatest(namespaceSlug, skillSlug, null, Map.of()));
|
||||
|
||||
assertEquals("error.skill.notFound", ex.messageCode());
|
||||
assertArrayEquals(new Object[]{skillSlug}, ex.messageArgs());
|
||||
verify(skillRepository, never()).incrementDownloadCount(anyLong());
|
||||
verify(skillVersionStatsRepository, never()).incrementDownloadCount(anyLong(), anyLong());
|
||||
verify(eventPublisher, never()).publishEvent(any(SkillDownloadedEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadLatest_ShouldRejectYankedLatestVersion() throws Exception {
|
||||
String namespaceSlug = "global";
|
||||
String skillSlug = "yanked-latest";
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Global", "owner-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, "owner-1", SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
skill.setLatestVersionId(10L);
|
||||
|
||||
SkillVersion version = new SkillVersion(1L, "1.0.0", "owner-1");
|
||||
setId(version, 10L);
|
||||
version.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
version.setDownloadReady(true);
|
||||
version.setYankedAt(Instant.parse("2026-06-12T00:00:00Z"));
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, null, Map.of())).thenReturn(true);
|
||||
when(skillVersionRepository.findById(10L)).thenReturn(Optional.of(version));
|
||||
|
||||
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class, () ->
|
||||
service.downloadLatest(namespaceSlug, skillSlug, null, Map.of()));
|
||||
|
||||
assertEquals("error.skill.version.notDownloadable", ex.messageCode());
|
||||
assertArrayEquals(new Object[]{"1.0.0"}, ex.messageArgs());
|
||||
verify(skillRepository, never()).incrementDownloadCount(anyLong());
|
||||
verify(skillVersionStatsRepository, never()).incrementDownloadCount(anyLong(), anyLong());
|
||||
verify(eventPublisher, never()).publishEvent(any(SkillDownloadedEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadLatest_ShouldRejectAnonymousArchivedNamespaceSkill() throws Exception {
|
||||
String namespaceSlug = "archived";
|
||||
String skillSlug = "archived-skill";
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Archived", "owner-1");
|
||||
setId(namespace, 1L);
|
||||
namespace.setStatus(com.iflytek.skillhub.domain.namespace.NamespaceStatus.ARCHIVED);
|
||||
Skill skill = new Skill(1L, skillSlug, "owner-1", SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
skill.setLatestVersionId(10L);
|
||||
|
||||
SkillVersion version = new SkillVersion(1L, "1.0.0", "owner-1");
|
||||
setId(version, 10L);
|
||||
version.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
version.setDownloadReady(true);
|
||||
ObjectMetadata metadata = new ObjectMetadata(1000L, "application/zip", Instant.now());
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, null, Map.of())).thenReturn(true);
|
||||
org.mockito.Mockito.lenient().when(skillVersionRepository.findById(10L)).thenReturn(Optional.of(version));
|
||||
org.mockito.Mockito.lenient().when(objectStorageService.exists("packages/1/10/bundle.zip")).thenReturn(true);
|
||||
org.mockito.Mockito.lenient().when(objectStorageService.getMetadata("packages/1/10/bundle.zip")).thenReturn(metadata);
|
||||
org.mockito.Mockito.lenient().when(objectStorageService.getObject("packages/1/10/bundle.zip"))
|
||||
.thenReturn(new ByteArrayInputStream("test".getBytes()));
|
||||
org.mockito.Mockito.lenient()
|
||||
.when(objectStorageService.generatePresignedUrl(eq("packages/1/10/bundle.zip"), any(), eq("archived-skill-1.0.0.zip")))
|
||||
.thenReturn(null);
|
||||
|
||||
assertThrows(com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException.class, () ->
|
||||
service.downloadLatest(namespaceSlug, skillSlug, null, Map.of()));
|
||||
verify(skillRepository, never()).incrementDownloadCount(anyLong());
|
||||
verify(skillVersionStatsRepository, never()).incrementDownloadCount(anyLong(), anyLong());
|
||||
verify(eventPublisher, never()).publishEvent(any(SkillDownloadedEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadLatest_ShouldRejectAnonymousHiddenPrivateAndUnpublishedSkills() throws Exception {
|
||||
Namespace namespace = new Namespace("global", "Global", "owner-1");
|
||||
setId(namespace, 1L);
|
||||
|
||||
Skill hiddenSkill = new Skill(1L, "hidden", "owner-1", SkillVisibility.PUBLIC);
|
||||
setId(hiddenSkill, 11L);
|
||||
hiddenSkill.setStatus(SkillStatus.ACTIVE);
|
||||
hiddenSkill.setLatestVersionId(101L);
|
||||
hiddenSkill.setHidden(true);
|
||||
|
||||
Skill privateSkill = new Skill(1L, "private", "owner-1", SkillVisibility.PRIVATE);
|
||||
setId(privateSkill, 12L);
|
||||
privateSkill.setStatus(SkillStatus.ACTIVE);
|
||||
privateSkill.setLatestVersionId(102L);
|
||||
|
||||
Skill unpublishedSkill = new Skill(1L, "unpublished", "owner-1", SkillVisibility.PUBLIC);
|
||||
setId(unpublishedSkill, 13L);
|
||||
unpublishedSkill.setStatus(SkillStatus.ACTIVE);
|
||||
|
||||
when(namespaceRepository.findBySlug("global")).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, "hidden")).thenReturn(List.of(hiddenSkill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, "private")).thenReturn(List.of(privateSkill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, "unpublished")).thenReturn(List.of(unpublishedSkill));
|
||||
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
service.downloadLatest("global", "hidden", null, Map.of()));
|
||||
assertThrows(com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException.class, () ->
|
||||
service.downloadLatest("global", "private", null, Map.of()));
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
service.downloadLatest("global", "unpublished", null, Map.of()));
|
||||
verify(skillRepository, never()).incrementDownloadCount(anyLong());
|
||||
verify(skillVersionStatsRepository, never()).incrementDownloadCount(anyLong(), anyLong());
|
||||
verify(eventPublisher, never()).publishEvent(any(SkillDownloadedEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadByTag_Success() throws Exception {
|
||||
// Arrange
|
||||
|
|
|
|||
|
|
@ -676,6 +676,160 @@ class SkillQueryServiceTest {
|
|||
assertArrayEquals(new Object[]{"1.0.0"}, ex.messageArgs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResolveVersion_ShouldRejectSkillWithoutLatest() throws Exception {
|
||||
String namespaceSlug = "global";
|
||||
String skillSlug = "missing-latest";
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Global", "owner-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, "owner-1", SkillVisibility.PUBLIC);
|
||||
setId(skill, 3L);
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
|
||||
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class, () ->
|
||||
service.resolveVersion(namespaceSlug, skillSlug, null, null, null, null, Map.of()));
|
||||
|
||||
assertEquals("error.skill.notFound", ex.messageCode());
|
||||
assertArrayEquals(new Object[]{skillSlug}, ex.messageArgs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResolveVersion_ShouldRejectYankedLatestVersion() throws Exception {
|
||||
String namespaceSlug = "global";
|
||||
String skillSlug = "yanked-latest";
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Global", "owner-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, "owner-1", SkillVisibility.PUBLIC);
|
||||
setId(skill, 3L);
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
skill.setLatestVersionId(11L);
|
||||
|
||||
SkillVersion version = new SkillVersion(3L, "1.0.0", "owner-1");
|
||||
setId(version, 11L);
|
||||
version.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
version.setDownloadReady(true);
|
||||
version.setYankedAt(Instant.parse("2026-06-12T00:00:00Z"));
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(skillVersionRepository.findBySkillIdAndStatus(3L, SkillVersionStatus.PUBLISHED)).thenReturn(List.of(version));
|
||||
when(skillVersionRepository.findById(11L)).thenReturn(Optional.of(version));
|
||||
|
||||
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class, () ->
|
||||
service.resolveVersion(namespaceSlug, skillSlug, null, null, null, null, Map.of()));
|
||||
|
||||
assertEquals("error.skill.version.notDownloadable", ex.messageCode());
|
||||
assertArrayEquals(new Object[]{"1.0.0"}, ex.messageArgs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResolveVersion_ShouldRejectDownloadUnavailableExplicitVersion() throws Exception {
|
||||
String namespaceSlug = "global";
|
||||
String skillSlug = "explicit-not-ready";
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Global", "owner-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, "owner-1", SkillVisibility.PUBLIC);
|
||||
setId(skill, 3L);
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
skill.setLatestVersionId(11L);
|
||||
|
||||
SkillVersion version = new SkillVersion(3L, "1.0.0", "owner-1");
|
||||
setId(version, 11L);
|
||||
version.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
version.setDownloadReady(false);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(3L, "1.0.0")).thenReturn(Optional.of(version));
|
||||
|
||||
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class, () ->
|
||||
service.resolveVersion(namespaceSlug, skillSlug, "1.0.0", null, null, null, Map.of()));
|
||||
|
||||
assertEquals("error.skill.version.notDownloadable", ex.messageCode());
|
||||
assertArrayEquals(new Object[]{"1.0.0"}, ex.messageArgs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResolveVersion_ShouldRejectDownloadUnavailableTaggedVersion() throws Exception {
|
||||
String namespaceSlug = "global";
|
||||
String skillSlug = "tag-not-ready";
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Global", "owner-1");
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, "owner-1", SkillVisibility.PUBLIC);
|
||||
setId(skill, 3L);
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
skill.setLatestVersionId(11L);
|
||||
|
||||
SkillVersion version = new SkillVersion(3L, "1.0.0", "owner-1");
|
||||
setId(version, 11L);
|
||||
version.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
version.setDownloadReady(false);
|
||||
SkillTag tag = new SkillTag(3L, "stable", 11L, "owner-1");
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(skillTagRepository.findBySkillIdAndTagName(3L, "stable")).thenReturn(Optional.of(tag));
|
||||
when(skillVersionRepository.findById(11L)).thenReturn(Optional.of(version));
|
||||
|
||||
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class, () ->
|
||||
service.resolveVersion(namespaceSlug, skillSlug, null, "stable", null, null, Map.of()));
|
||||
|
||||
assertEquals("error.skill.version.notDownloadable", ex.messageCode());
|
||||
assertArrayEquals(new Object[]{"1.0.0"}, ex.messageArgs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResolveVersion_ShouldRejectAnonymousHiddenPrivateArchivedAndUnpublishedSkills() throws Exception {
|
||||
Namespace activeNamespace = new Namespace("global", "Global", "owner-1");
|
||||
setId(activeNamespace, 1L);
|
||||
Namespace archivedNamespace = new Namespace("archived", "Archived", "owner-1");
|
||||
setId(archivedNamespace, 2L);
|
||||
archivedNamespace.setStatus(NamespaceStatus.ARCHIVED);
|
||||
|
||||
Skill hiddenSkill = new Skill(1L, "hidden", "owner-1", SkillVisibility.PUBLIC);
|
||||
setId(hiddenSkill, 10L);
|
||||
hiddenSkill.setStatus(SkillStatus.ACTIVE);
|
||||
hiddenSkill.setLatestVersionId(101L);
|
||||
hiddenSkill.setHidden(true);
|
||||
|
||||
Skill privateSkill = new Skill(1L, "private", "owner-1", SkillVisibility.PRIVATE);
|
||||
setId(privateSkill, 11L);
|
||||
privateSkill.setStatus(SkillStatus.ACTIVE);
|
||||
privateSkill.setLatestVersionId(102L);
|
||||
|
||||
Skill archivedSkill = new Skill(2L, "archived", "owner-1", SkillVisibility.PUBLIC);
|
||||
setId(archivedSkill, 12L);
|
||||
archivedSkill.setStatus(SkillStatus.ACTIVE);
|
||||
archivedSkill.setLatestVersionId(103L);
|
||||
|
||||
Skill unpublishedSkill = new Skill(1L, "unpublished", "owner-1", SkillVisibility.PUBLIC);
|
||||
setId(unpublishedSkill, 13L);
|
||||
unpublishedSkill.setStatus(SkillStatus.ACTIVE);
|
||||
|
||||
when(namespaceRepository.findBySlug("global")).thenReturn(Optional.of(activeNamespace));
|
||||
when(namespaceRepository.findBySlug("archived")).thenReturn(Optional.of(archivedNamespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, "hidden")).thenReturn(List.of(hiddenSkill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, "private")).thenReturn(List.of(privateSkill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(2L, "archived")).thenReturn(List.of(archivedSkill));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, "unpublished")).thenReturn(List.of(unpublishedSkill));
|
||||
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
service.resolveVersion("global", "hidden", null, null, null, null, Map.of()));
|
||||
assertThrows(DomainForbiddenException.class, () ->
|
||||
service.resolveVersion("global", "private", null, null, null, null, Map.of()));
|
||||
assertThrows(DomainForbiddenException.class, () ->
|
||||
service.resolveVersion("archived", "archived", null, null, null, null, Map.of()));
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
service.resolveVersion("global", "unpublished", null, null, null, null, Map.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetSkillDetail_ShouldFlagLifecyclePermissionForOwner() throws Exception {
|
||||
String namespaceSlug = "test-ns";
|
||||
|
|
|
|||
|
|
@ -363,6 +363,43 @@ class PostgresFullTextQueryServiceTest {
|
|||
.contains("ORDER BY s.updated_at DESC, d.skill_id DESC");
|
||||
}
|
||||
|
||||
@Test
|
||||
void anonymousSearchSqlShouldOnlyReadPublicActiveVisibleNonArchivedSkills() {
|
||||
EntityManager entityManager = mock(EntityManager.class);
|
||||
Query nativeQuery = mock(Query.class);
|
||||
Query countQuery = mock(Query.class);
|
||||
when(entityManager.createNativeQuery(anyString()))
|
||||
.thenReturn(nativeQuery)
|
||||
.thenReturn(countQuery);
|
||||
when(nativeQuery.setParameter(anyString(), org.mockito.ArgumentMatchers.any())).thenReturn(nativeQuery);
|
||||
when(countQuery.setParameter(anyString(), org.mockito.ArgumentMatchers.any())).thenReturn(countQuery);
|
||||
when(nativeQuery.getResultList()).thenReturn(List.of());
|
||||
when(countQuery.getSingleResult()).thenReturn(0L);
|
||||
|
||||
PostgresFullTextQueryService service = new PostgresFullTextQueryService(entityManager);
|
||||
|
||||
service.search(new SearchQuery(
|
||||
null,
|
||||
null,
|
||||
new SearchVisibilityScope(null, Set.of(), Set.of()),
|
||||
"newest",
|
||||
0,
|
||||
12
|
||||
));
|
||||
|
||||
ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(entityManager, org.mockito.Mockito.times(2)).createNativeQuery(sqlCaptor.capture());
|
||||
assertThat(sqlCaptor.getAllValues().getFirst())
|
||||
.contains("AND (d.visibility = 'PUBLIC' )")
|
||||
.contains("AND d.status = 'ACTIVE'")
|
||||
.contains("AND s.status = 'ACTIVE'")
|
||||
.contains("AND s.hidden = FALSE")
|
||||
.contains("AND (n.status <> 'ARCHIVED' )")
|
||||
.doesNotContain("memberNamespaceIds");
|
||||
verify(nativeQuery, never()).setParameter(org.mockito.ArgumentMatchers.eq("memberNamespaceIds"), org.mockito.ArgumentMatchers.any());
|
||||
verify(countQuery, never()).setParameter(org.mockito.ArgumentMatchers.eq("memberNamespaceIds"), org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void authenticatedQueriesShouldAllowArchivedNamespacesForMembers() {
|
||||
EntityManager entityManager = mock(EntityManager.class);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue