mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-28 11:25:00 +00:00
feat(compat): add clawhub registry adapter
This commit is contained in:
parent
3682a4646b
commit
f96fa74413
11 changed files with 598 additions and 0 deletions
|
|
@ -0,0 +1,54 @@
|
|||
package com.iflytek.skillhub.compat;
|
||||
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubRegistrySearchResponse;
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubRegistrySkillResponse;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import java.util.Map;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1")
|
||||
public class ClawHubRegistryController {
|
||||
|
||||
private final ClawHubRegistryFacade facade;
|
||||
|
||||
public ClawHubRegistryController(ClawHubRegistryFacade facade) {
|
||||
this.facade = facade;
|
||||
}
|
||||
|
||||
@GetMapping("/search")
|
||||
public ClawHubRegistrySearchResponse search(
|
||||
@RequestParam String q,
|
||||
@RequestParam(defaultValue = "20") int limit,
|
||||
@RequestAttribute(value = "userId", required = false) String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
|
||||
return facade.search(q, limit, userId, userNsRoles);
|
||||
}
|
||||
|
||||
@GetMapping("/skills/{slug}")
|
||||
public ClawHubRegistrySkillResponse getSkill(
|
||||
@org.springframework.web.bind.annotation.PathVariable String slug,
|
||||
@RequestAttribute(value = "userId", required = false) String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
|
||||
return facade.getSkill(slug, userId, userNsRoles);
|
||||
}
|
||||
|
||||
@GetMapping("/download")
|
||||
public ResponseEntity<Void> download(
|
||||
@RequestParam String slug,
|
||||
@RequestParam(required = false) String version,
|
||||
@RequestAttribute(value = "userId", required = false) String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
|
||||
String location = facade.resolveDownloadUrl(slug, version, userId, userNsRoles);
|
||||
return ResponseEntity.status(HttpStatus.FOUND)
|
||||
.header(HttpHeaders.LOCATION, location)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
package com.iflytek.skillhub.compat;
|
||||
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubRegistryModeration;
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubRegistryOwner;
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubRegistrySearchItem;
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubRegistrySearchResponse;
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubRegistrySkill;
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubRegistrySkillResponse;
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubRegistrySkillVersion;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.skill.Skill;
|
||||
import com.iflytek.skillhub.domain.skill.SkillRepository;
|
||||
import com.iflytek.skillhub.domain.skill.SkillVersion;
|
||||
import com.iflytek.skillhub.domain.skill.SkillVersionRepository;
|
||||
import com.iflytek.skillhub.domain.skill.service.SkillQueryService;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.dto.SkillSummaryResponse;
|
||||
import com.iflytek.skillhub.service.SkillSearchAppService;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ClawHubRegistryFacade {
|
||||
|
||||
private static final int DEFAULT_LIMIT = 20;
|
||||
private static final int MAX_LIMIT = 100;
|
||||
|
||||
private final CanonicalSlugMapper canonicalSlugMapper;
|
||||
private final SkillSearchAppService skillSearchAppService;
|
||||
private final SkillQueryService skillQueryService;
|
||||
private final SkillRepository skillRepository;
|
||||
private final SkillVersionRepository skillVersionRepository;
|
||||
private final UserAccountRepository userAccountRepository;
|
||||
|
||||
public ClawHubRegistryFacade(
|
||||
CanonicalSlugMapper canonicalSlugMapper,
|
||||
SkillSearchAppService skillSearchAppService,
|
||||
SkillQueryService skillQueryService,
|
||||
SkillRepository skillRepository,
|
||||
SkillVersionRepository skillVersionRepository,
|
||||
UserAccountRepository userAccountRepository) {
|
||||
this.canonicalSlugMapper = canonicalSlugMapper;
|
||||
this.skillSearchAppService = skillSearchAppService;
|
||||
this.skillQueryService = skillQueryService;
|
||||
this.skillRepository = skillRepository;
|
||||
this.skillVersionRepository = skillVersionRepository;
|
||||
this.userAccountRepository = userAccountRepository;
|
||||
}
|
||||
|
||||
public ClawHubRegistrySearchResponse search(
|
||||
String keyword,
|
||||
int limit,
|
||||
String userId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
int boundedLimit = clampLimit(limit);
|
||||
List<SkillSummaryResponse> items = skillSearchAppService.search(
|
||||
keyword,
|
||||
null,
|
||||
"relevance",
|
||||
0,
|
||||
boundedLimit,
|
||||
userId,
|
||||
normalizeRoles(userNsRoles))
|
||||
.items();
|
||||
|
||||
List<ClawHubRegistrySearchItem> results = buildSearchResults(items);
|
||||
return new ClawHubRegistrySearchResponse(results);
|
||||
}
|
||||
|
||||
public ClawHubRegistrySkillResponse getSkill(
|
||||
String canonicalSlug,
|
||||
String userId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
SkillCoordinate coordinate = canonicalSlugMapper.fromCanonical(canonicalSlug);
|
||||
SkillQueryService.SkillDetailDTO detail = skillQueryService.getSkillDetail(
|
||||
coordinate.namespace(),
|
||||
coordinate.slug(),
|
||||
userId,
|
||||
normalizeRoles(userNsRoles));
|
||||
|
||||
Skill skill = skillRepository.findById(detail.id())
|
||||
.orElseThrow(() -> new IllegalStateException("Skill unexpectedly missing: " + canonicalSlug));
|
||||
|
||||
ClawHubRegistrySkill payload = new ClawHubRegistrySkill(
|
||||
canonicalSlugMapper.toCanonical(coordinate.namespace(), detail.slug()),
|
||||
normalizeDisplayName(detail.displayName(), canonicalSlug),
|
||||
detail.summary(),
|
||||
List.of(),
|
||||
Map.of(),
|
||||
toEpochMillis(skill.getCreatedAt()),
|
||||
toEpochMillis(skill.getUpdatedAt())
|
||||
);
|
||||
|
||||
ClawHubRegistrySkillVersion latestVersion = buildLatestVersion(skill, detail.latestVersion());
|
||||
ClawHubRegistryOwner owner = buildOwner(skill.getOwnerId());
|
||||
|
||||
return new ClawHubRegistrySkillResponse(
|
||||
payload,
|
||||
latestVersion,
|
||||
owner,
|
||||
ClawHubRegistryModeration.clean()
|
||||
);
|
||||
}
|
||||
|
||||
public String resolveDownloadUrl(
|
||||
String canonicalSlug,
|
||||
String version,
|
||||
String userId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
SkillCoordinate coordinate = canonicalSlugMapper.fromCanonical(canonicalSlug);
|
||||
String normalizedVersion = normalizeVersion(version);
|
||||
return skillQueryService.resolveVersion(
|
||||
coordinate.namespace(),
|
||||
coordinate.slug(),
|
||||
normalizedVersion,
|
||||
null,
|
||||
null,
|
||||
userId,
|
||||
normalizeRoles(userNsRoles))
|
||||
.downloadUrl();
|
||||
}
|
||||
|
||||
private List<ClawHubRegistrySearchItem> buildSearchResults(List<SkillSummaryResponse> items) {
|
||||
return java.util.stream.IntStream.range(0, items.size())
|
||||
.mapToObj(index -> toSearchItem(items.get(index), index))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private ClawHubRegistrySearchItem toSearchItem(SkillSummaryResponse item, int index) {
|
||||
String canonicalSlug = canonicalSlugMapper.toCanonical(item.namespace(), item.slug());
|
||||
return new ClawHubRegistrySearchItem(
|
||||
canonicalSlug,
|
||||
normalizeDisplayName(item.displayName(), canonicalSlug),
|
||||
item.summary(),
|
||||
item.latestVersion(),
|
||||
scoreFor(index),
|
||||
toEpochMillis(item.updatedAt())
|
||||
);
|
||||
}
|
||||
|
||||
private ClawHubRegistrySkillVersion buildLatestVersion(Skill skill, String version) {
|
||||
if (version == null || version.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Optional<SkillVersion> latestVersion = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), version);
|
||||
if (latestVersion.isEmpty()) {
|
||||
return new ClawHubRegistrySkillVersion(version, 0L, "", null);
|
||||
}
|
||||
|
||||
SkillVersion entity = latestVersion.get();
|
||||
LocalDateTime createdAt = entity.getPublishedAt() != null ? entity.getPublishedAt() : entity.getCreatedAt();
|
||||
return new ClawHubRegistrySkillVersion(
|
||||
entity.getVersion(),
|
||||
toEpochMillis(createdAt),
|
||||
entity.getChangelog() != null ? entity.getChangelog() : "",
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private ClawHubRegistryOwner buildOwner(String ownerId) {
|
||||
if (ownerId == null || ownerId.isBlank()) {
|
||||
return ClawHubRegistryOwner.empty();
|
||||
}
|
||||
|
||||
return userAccountRepository.findById(ownerId)
|
||||
.map(user -> new ClawHubRegistryOwner(
|
||||
null,
|
||||
user.getDisplayName(),
|
||||
user.getAvatarUrl()))
|
||||
.orElseGet(ClawHubRegistryOwner::empty);
|
||||
}
|
||||
|
||||
private Map<Long, NamespaceRole> normalizeRoles(Map<Long, NamespaceRole> userNsRoles) {
|
||||
return userNsRoles != null ? userNsRoles : Map.of();
|
||||
}
|
||||
|
||||
private int clampLimit(int limit) {
|
||||
if (limit <= 0) {
|
||||
return DEFAULT_LIMIT;
|
||||
}
|
||||
return Math.min(limit, MAX_LIMIT);
|
||||
}
|
||||
|
||||
private String normalizeDisplayName(String displayName, String fallback) {
|
||||
if (displayName == null || displayName.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
return displayName;
|
||||
}
|
||||
|
||||
private String normalizeVersion(String version) {
|
||||
if (version == null || version.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
private double scoreFor(int index) {
|
||||
return Math.max(0.001d, 1.0d - (index * 0.001d));
|
||||
}
|
||||
|
||||
private long toEpochMillis(LocalDateTime timestamp) {
|
||||
if (timestamp == null) {
|
||||
return 0L;
|
||||
}
|
||||
return timestamp.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.iflytek.skillhub.compat;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
public class ClawHubRegistrySecurityConfig {
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
public SecurityFilterChain clawHubRegistryFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.securityMatcher("/api/v1/search", "/api/v1/download", "/api/v1/skills/*")
|
||||
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
|
||||
.requestCache(cache -> cache.disable())
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.iflytek.skillhub.compat.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record ClawHubRegistryModeration(
|
||||
boolean isSuspicious,
|
||||
boolean isMalwareBlocked,
|
||||
String verdict,
|
||||
List<String> reasonCodes,
|
||||
Long updatedAt,
|
||||
String engineVersion,
|
||||
String summary
|
||||
) {
|
||||
public static ClawHubRegistryModeration clean() {
|
||||
return new ClawHubRegistryModeration(
|
||||
false,
|
||||
false,
|
||||
"clean",
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.iflytek.skillhub.compat.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.ALWAYS)
|
||||
public record ClawHubRegistryOwner(
|
||||
String handle,
|
||||
String displayName,
|
||||
String image
|
||||
) {
|
||||
public static ClawHubRegistryOwner empty() {
|
||||
return new ClawHubRegistryOwner(null, null, null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.iflytek.skillhub.compat.dto;
|
||||
|
||||
public record ClawHubRegistrySearchItem(
|
||||
String slug,
|
||||
String displayName,
|
||||
String summary,
|
||||
String version,
|
||||
double score,
|
||||
long updatedAt
|
||||
) {}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.iflytek.skillhub.compat.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record ClawHubRegistrySearchResponse(List<ClawHubRegistrySearchItem> results) {}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.iflytek.skillhub.compat.dto;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public record ClawHubRegistrySkill(
|
||||
String slug,
|
||||
String displayName,
|
||||
String summary,
|
||||
List<String> tags,
|
||||
Map<String, Object> stats,
|
||||
long createdAt,
|
||||
long updatedAt
|
||||
) {}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.iflytek.skillhub.compat.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.ALWAYS)
|
||||
public record ClawHubRegistrySkillResponse(
|
||||
ClawHubRegistrySkill skill,
|
||||
ClawHubRegistrySkillVersion latestVersion,
|
||||
ClawHubRegistryOwner owner,
|
||||
ClawHubRegistryModeration moderation
|
||||
) {}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.iflytek.skillhub.compat.dto;
|
||||
|
||||
public record ClawHubRegistrySkillVersion(
|
||||
String version,
|
||||
long createdAt,
|
||||
String changelog,
|
||||
Object license
|
||||
) {}
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
package com.iflytek.skillhub.compat;
|
||||
|
||||
import com.iflytek.skillhub.auth.device.DeviceAuthService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.skill.Skill;
|
||||
import com.iflytek.skillhub.domain.skill.SkillRepository;
|
||||
import com.iflytek.skillhub.domain.skill.SkillVersion;
|
||||
import com.iflytek.skillhub.domain.skill.SkillVersionRepository;
|
||||
import com.iflytek.skillhub.domain.skill.SkillVisibility;
|
||||
import com.iflytek.skillhub.domain.skill.service.SkillQueryService;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.dto.SkillSummaryResponse;
|
||||
import com.iflytek.skillhub.service.SkillSearchAppService;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class ClawHubRegistryControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockBean
|
||||
private NamespaceMemberRepository namespaceMemberRepository;
|
||||
|
||||
@MockBean
|
||||
private DeviceAuthService deviceAuthService;
|
||||
|
||||
@MockBean
|
||||
private SkillSearchAppService skillSearchAppService;
|
||||
|
||||
@MockBean
|
||||
private SkillQueryService skillQueryService;
|
||||
|
||||
@MockBean
|
||||
private SkillRepository skillRepository;
|
||||
|
||||
@MockBean
|
||||
private SkillVersionRepository skillVersionRepository;
|
||||
|
||||
@MockBean
|
||||
private UserAccountRepository userAccountRepository;
|
||||
|
||||
@Test
|
||||
void search_returns_clawhub_registry_schema() throws Exception {
|
||||
LocalDateTime updatedAt = LocalDateTime.of(2026, 3, 13, 10, 30);
|
||||
given(skillSearchAppService.search("test", null, "relevance", 0, 2, null, Map.of()))
|
||||
.willReturn(new SkillSearchAppService.SearchResponse(
|
||||
List.of(
|
||||
new SkillSummaryResponse(
|
||||
1L,
|
||||
"global-skill",
|
||||
"Global Skill",
|
||||
"global summary",
|
||||
10L,
|
||||
5,
|
||||
BigDecimal.ZERO,
|
||||
0,
|
||||
"1.2.0",
|
||||
"global",
|
||||
updatedAt
|
||||
),
|
||||
new SkillSummaryResponse(
|
||||
2L,
|
||||
"team-skill",
|
||||
"Team Skill",
|
||||
"team summary",
|
||||
20L,
|
||||
8,
|
||||
BigDecimal.ONE,
|
||||
2,
|
||||
"2.0.0",
|
||||
"team-ai",
|
||||
updatedAt.plusHours(1)
|
||||
)
|
||||
),
|
||||
2,
|
||||
0,
|
||||
2
|
||||
));
|
||||
|
||||
mockMvc.perform(get("/api/v1/search")
|
||||
.param("q", "test")
|
||||
.param("limit", "2"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.results").isArray())
|
||||
.andExpect(jsonPath("$.results[0].slug").value("global-skill"))
|
||||
.andExpect(jsonPath("$.results[0].displayName").value("Global Skill"))
|
||||
.andExpect(jsonPath("$.results[0].version").value("1.2.0"))
|
||||
.andExpect(jsonPath("$.results[0].score").value(1.0))
|
||||
.andExpect(jsonPath("$.results[0].updatedAt").value(toEpochMillis(updatedAt)))
|
||||
.andExpect(jsonPath("$.results[1].slug").value("team-ai--team-skill"))
|
||||
.andExpect(jsonPath("$.results[1].displayName").value("Team Skill"))
|
||||
.andExpect(jsonPath("$.results[1].version").value("2.0.0"))
|
||||
.andExpect(jsonPath("$.results[1].score").value(0.999))
|
||||
.andExpect(jsonPath("$.results[1].updatedAt").value(toEpochMillis(updatedAt.plusHours(1))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void get_skill_returns_clawhub_install_metadata() throws Exception {
|
||||
LocalDateTime skillCreatedAt = LocalDateTime.of(2026, 1, 1, 9, 0);
|
||||
LocalDateTime skillUpdatedAt = LocalDateTime.of(2026, 3, 10, 18, 30);
|
||||
LocalDateTime versionPublishedAt = LocalDateTime.of(2026, 3, 12, 12, 0);
|
||||
|
||||
given(skillQueryService.getSkillDetail(
|
||||
eq("global"),
|
||||
eq("global-skill"),
|
||||
isNull(),
|
||||
eq(Map.<Long, NamespaceRole>of())))
|
||||
.willReturn(new SkillQueryService.SkillDetailDTO(
|
||||
1L,
|
||||
"global-skill",
|
||||
"Global Skill",
|
||||
"global summary",
|
||||
"PUBLIC",
|
||||
"ACTIVE",
|
||||
10L,
|
||||
5,
|
||||
BigDecimal.ZERO,
|
||||
0,
|
||||
false,
|
||||
"1.2.0",
|
||||
1L
|
||||
));
|
||||
|
||||
Skill skill = new Skill(1L, "global-skill", "owner-1", SkillVisibility.PUBLIC);
|
||||
skill.setDisplayName("Global Skill");
|
||||
skill.setSummary("global summary");
|
||||
ReflectionTestUtils.setField(skill, "id", 1L);
|
||||
ReflectionTestUtils.setField(skill, "createdAt", skillCreatedAt);
|
||||
ReflectionTestUtils.setField(skill, "updatedAt", skillUpdatedAt);
|
||||
|
||||
SkillVersion version = new SkillVersion(1L, "1.2.0", "owner-1");
|
||||
version.setChangelog("Initial release");
|
||||
version.setPublishedAt(versionPublishedAt);
|
||||
ReflectionTestUtils.setField(version, "id", 11L);
|
||||
ReflectionTestUtils.setField(version, "createdAt", versionPublishedAt.minusHours(2));
|
||||
|
||||
given(skillRepository.findById(1L)).willReturn(java.util.Optional.of(skill));
|
||||
given(skillVersionRepository.findBySkillIdAndVersion(1L, "1.2.0")).willReturn(java.util.Optional.of(version));
|
||||
given(userAccountRepository.findById("owner-1"))
|
||||
.willReturn(java.util.Optional.of(new UserAccount(
|
||||
"owner-1",
|
||||
"Skill Owner",
|
||||
"owner@example.com",
|
||||
"https://example.com/avatar.png"
|
||||
)));
|
||||
|
||||
mockMvc.perform(get("/api/v1/skills/global-skill"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.skill.slug").value("global-skill"))
|
||||
.andExpect(jsonPath("$.skill.displayName").value("Global Skill"))
|
||||
.andExpect(jsonPath("$.skill.summary").value("global summary"))
|
||||
.andExpect(jsonPath("$.skill.createdAt").value(toEpochMillis(skillCreatedAt)))
|
||||
.andExpect(jsonPath("$.skill.updatedAt").value(toEpochMillis(skillUpdatedAt)))
|
||||
.andExpect(jsonPath("$.latestVersion.version").value("1.2.0"))
|
||||
.andExpect(jsonPath("$.latestVersion.createdAt").value(toEpochMillis(versionPublishedAt)))
|
||||
.andExpect(jsonPath("$.latestVersion.changelog").value("Initial release"))
|
||||
.andExpect(jsonPath("$.owner.displayName").value("Skill Owner"))
|
||||
.andExpect(jsonPath("$.owner.image").value("https://example.com/avatar.png"))
|
||||
.andExpect(jsonPath("$.moderation.isSuspicious").value(false))
|
||||
.andExpect(jsonPath("$.moderation.isMalwareBlocked").value(false))
|
||||
.andExpect(jsonPath("$.moderation.verdict").value("clean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void download_redirects_to_versioned_skill_url() throws Exception {
|
||||
given(skillQueryService.resolveVersion(
|
||||
eq("team-ai"),
|
||||
eq("team-skill"),
|
||||
eq("2.0.0"),
|
||||
isNull(),
|
||||
isNull(),
|
||||
isNull(),
|
||||
eq(Map.<Long, NamespaceRole>of())))
|
||||
.willReturn(new SkillQueryService.ResolvedVersionDTO(
|
||||
2L,
|
||||
"team-ai",
|
||||
"team-skill",
|
||||
"2.0.0",
|
||||
21L,
|
||||
"sha256:test",
|
||||
true,
|
||||
"/api/v1/skills/team-ai/team-skill/versions/2.0.0/download"
|
||||
));
|
||||
|
||||
mockMvc.perform(get("/api/v1/download")
|
||||
.param("slug", "team-ai--team-skill")
|
||||
.param("version", "2.0.0"))
|
||||
.andExpect(status().isFound())
|
||||
.andExpect(header().string("Location", "/api/v1/skills/team-ai/team-skill/versions/2.0.0/download"));
|
||||
}
|
||||
|
||||
private long toEpochMillis(LocalDateTime timestamp) {
|
||||
return timestamp.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue