test(auth): cover token replay and private search (#605)

Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
dongmucat 2026-07-28 13:52:26 +08:00
parent 5805e0f1d3
commit 726eeac8b2
2 changed files with 91 additions and 0 deletions

View file

@ -11,6 +11,8 @@ import com.iflytek.skillhub.domain.skill.SkillVersionStatus;
import com.iflytek.skillhub.domain.skill.SkillVisibility;
import com.iflytek.skillhub.domain.user.UserAccount;
import com.iflytek.skillhub.domain.user.UserAccountRepository;
import com.iflytek.skillhub.infra.jpa.SkillSearchDocumentEntity;
import com.iflytek.skillhub.infra.jpa.SkillSearchDocumentJpaRepository;
import java.time.Instant;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
@ -23,6 +25,9 @@ import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import static org.hamcrest.Matchers.aMapWithSize;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.not;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -38,6 +43,7 @@ class CliRestrictedReadAuthorizationIntegrationTest {
@Autowired NamespaceRepository namespaceRepository;
@Autowired SkillRepository skillRepository;
@Autowired SkillVersionRepository skillVersionRepository;
@Autowired SkillSearchDocumentJpaRepository skillSearchDocumentRepository;
private String namespaceSlug;
private String skillSlug;
@ -76,6 +82,29 @@ class CliRestrictedReadAuthorizationIntegrationTest {
skillRepository.save(skill);
skillRepository.flush();
skillVersionRepository.flush();
skillSearchDocumentRepository.saveAndFlush(new SkillSearchDocumentEntity(
skill.getId(),
namespace.getId(),
namespaceSlug,
ownerId,
skillSlug,
"Private skill search fixture",
"private",
skillSlug,
"",
SkillVisibility.PRIVATE.name(),
skill.getStatus().name()));
}
@Test
void outsiderSearchOmitsPersistedPrivateSkill() throws Exception {
mockMvc.perform(withBearer(
get("/api/cli/v1/skills/search").param("limit", "20"),
outsiderToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$", aMapWithSize(5)))
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.items[*].slug", not(hasItem(skillSlug))));
}
@Test

View file

@ -29,8 +29,11 @@ import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import static org.hamcrest.Matchers.aMapWithSize;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
@ -205,6 +208,65 @@ class CliTokenLifecycleSecurityIntegrationTest {
verifyNoInteractions(cliSkillAppService);
}
@Test
void sameRawTokenIsRejectedByAllEndpointsAfterValidUseAndRevocation() throws Exception {
ApiTokenService.TokenCreateResult token = createToken();
String rawToken = token.rawToken();
assertSuccessEnvelope(withBearer(get("/api/cli/v1/auth/whoami"), rawToken))
.andExpect(jsonPath("$.data.handle").value(userId));
assertSuccessEnvelope(withBearer(
get("/api/cli/v1/skills/search").param("q", "demo").param("limit", "20"),
rawToken));
assertSuccessEnvelope(withBearer(
get("/api/cli/v1/skills/global/demo/resolve"), rawToken));
mockMvc.perform(withBearer(
get("/api/cli/v1/skills/global/demo/download"), rawToken))
.andExpect(status().isOk())
.andExpect(content().contentType("application/zip"));
mockMvc.perform(withBearer(
get("/api/cli/v1/skills/global/demo/versions/1.0.0/download"), rawToken))
.andExpect(status().isOk())
.andExpect(content().contentType("application/zip"));
apiTokenService.revokeToken(token.entity().getId(), userId);
clearInvocations(cliSkillAppService);
assertUnauthorizedEnvelope(withBearer(get("/api/cli/v1/auth/whoami"), rawToken));
assertUnauthorizedEnvelope(withBearer(
get("/api/cli/v1/skills/search").param("q", "demo").param("limit", "20"),
rawToken));
assertUnauthorizedEnvelope(withBearer(
get("/api/cli/v1/skills/global/demo/resolve"), rawToken));
assertUnauthorizedEnvelope(withBearer(
get("/api/cli/v1/skills/global/demo/download"), rawToken));
assertUnauthorizedEnvelope(withBearer(
get("/api/cli/v1/skills/global/demo/versions/1.0.0/download"), rawToken));
verifyNoInteractions(cliSkillAppService);
}
private ResultActions assertSuccessEnvelope(MockHttpServletRequestBuilder request) throws Exception {
return mockMvc.perform(request)
.andExpect(status().isOk())
.andExpect(jsonPath("$", aMapWithSize(5)))
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.msg").isString())
.andExpect(jsonPath("$.data").exists())
.andExpect(jsonPath("$.timestamp").isString())
.andExpect(jsonPath("$.requestId").isString());
}
private void assertUnauthorizedEnvelope(MockHttpServletRequestBuilder request) throws Exception {
mockMvc.perform(request)
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$", aMapWithSize(5)))
.andExpect(jsonPath("$.code").value(401))
.andExpect(jsonPath("$.msg").isString())
.andExpect(jsonPath("$.data").value(nullValue()))
.andExpect(jsonPath("$.timestamp").isString())
.andExpect(jsonPath("$.requestId").isString());
}
private MockHttpServletRequestBuilder withInvalidBearer(
MockHttpServletRequestBuilder request,
InvalidCredentialState state) {