diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/TokenControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/TokenControllerTest.java index 86c9e8d3..a93aea16 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/TokenControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/TokenControllerTest.java @@ -93,6 +93,28 @@ class TokenControllerTest { .andExpect(jsonPath("$.msg").value("Token 名称最多 64 个字符")); } + @Test + void create_rejectsDuplicateActiveNames() throws Exception { + PlatformPrincipal principal = new PlatformPrincipal( + "user-42", "tester", "tester@example.com", "", "github", Set.of("USER") + ); + var auth = new UsernamePasswordAuthenticationToken( + principal, null, List.of(new SimpleGrantedAuthority("ROLE_USER")) + ); + given(apiTokenService.createToken(anyString(), anyString(), anyString(), org.mockito.ArgumentMatchers.nullable(String.class))) + .willThrow(new DomainBadRequestException("error.token.name.duplicate")); + + mockMvc.perform(post("/api/v1/tokens") + .with(authentication(auth)) + .with(csrf()) + .contentType("application/json") + .content(""" + {"name":"cli"} + """)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.msg").value("你已经有同名 Token")); + } + @Test void create_passesExpirationToService() throws Exception { PlatformPrincipal principal = new PlatformPrincipal( diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenServiceTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenServiceTest.java index 6aa7c5ef..af137f20 100644 --- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenServiceTest.java +++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenServiceTest.java @@ -1,12 +1,16 @@ package com.iflytek.skillhub.auth.token; import com.iflytek.skillhub.auth.repository.ApiTokenRepository; +import com.iflytek.skillhub.auth.entity.ApiToken; import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.DataIntegrityViolationException; + +import java.time.LocalDateTime; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThat; @@ -83,4 +87,37 @@ class ApiTokenServiceTest { verify(tokenRepo, never()).save(any()); } + + @Test + void createToken_rejectsBlankNamesAfterTrimming() { + assertThatThrownBy(() -> service.createToken("user-1", " ", "[]")) + .isInstanceOf(DomainBadRequestException.class) + .hasMessageContaining("validation.token.name.notBlank"); + + verify(tokenRepo, never()).save(any()); + } + + @Test + void createToken_allowsReusingNameWhenPreviousTokenIsRevoked() { + when(tokenRepo.existsByUserIdAndRevokedAtIsNullAndNameIgnoreCase("user-1", "CLI")) + .thenReturn(false); + when(tokenRepo.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + var result = service.createToken("user-1", " CLI ", "[]"); + + assertThat(result.entity().getName()).isEqualTo("CLI"); + verify(tokenRepo).existsByUserIdAndRevokedAtIsNullAndNameIgnoreCase("user-1", "CLI"); + verify(tokenRepo).save(any(ApiToken.class)); + } + + @Test + void createToken_translatesDatabaseConstraintViolationToDuplicateError() { + when(tokenRepo.existsByUserIdAndRevokedAtIsNullAndNameIgnoreCase("user-1", "CLI")) + .thenReturn(false); + when(tokenRepo.save(any())).thenThrow(new DataIntegrityViolationException("duplicate key")); + + assertThatThrownBy(() -> service.createToken("user-1", "CLI", "[]")) + .isInstanceOf(DomainBadRequestException.class) + .hasMessageContaining("error.token.name.duplicate"); + } }