From 556d556724706d8556599cd5419e659492f32d37 Mon Sep 17 00:00:00 2001 From: yun-zhi-ztl <15071461069@163.com> Date: Fri, 13 Mar 2026 11:49:57 +0800 Subject: [PATCH] fix(token): align revoke endpoint with 204 contract Change DELETE /api/v1/tokens/{id} to return HTTP 204 No Content so the backend matches the existing OpenAPI contract and the frontend delete flow no longer rejects successful revocations. Add a controller regression test that verifies the endpoint returns 204 with an empty body and still delegates the revoke call to ApiTokenService. Verified with the targeted TokenControllerTest plus full server mvn test. --- .../skillhub/controller/TokenController.java | 6 +- .../controller/TokenControllerTest.java | 64 +++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/TokenControllerTest.java diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/TokenController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/TokenController.java index d1b8420b..cb2af098 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/TokenController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/TokenController.java @@ -4,11 +4,11 @@ import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; import com.iflytek.skillhub.auth.token.ApiTokenService; import com.iflytek.skillhub.dto.ApiResponse; import com.iflytek.skillhub.dto.ApiResponseFactory; -import com.iflytek.skillhub.dto.MessageResponse; import com.iflytek.skillhub.dto.TokenCreateRequest; import com.iflytek.skillhub.dto.TokenCreateResponse; import com.iflytek.skillhub.dto.TokenSummaryResponse; import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -59,10 +59,10 @@ public class TokenController extends BaseApiController { } @DeleteMapping("/{id}") - public ApiResponse revoke( + public ResponseEntity revoke( @AuthenticationPrincipal PlatformPrincipal principal, @PathVariable Long id) { apiTokenService.revokeToken(id, principal.userId()); - return ok("response.success.revoked", new MessageResponse("Token revoked")); + return ResponseEntity.noContent().build(); } } 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 new file mode 100644 index 00000000..52b98554 --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/TokenControllerTest.java @@ -0,0 +1,64 @@ +package com.iflytek.skillhub.controller; + +import com.iflytek.skillhub.TestRedisConfig; +import com.iflytek.skillhub.auth.device.DeviceAuthService; +import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; +import com.iflytek.skillhub.auth.token.ApiTokenService; +import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository; +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.context.annotation.Import; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; +import java.util.Set; + +import static org.mockito.Mockito.verify; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@Import(TestRedisConfig.class) +class TokenControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private NamespaceMemberRepository namespaceMemberRepository; + + @MockBean + private DeviceAuthService deviceAuthService; + + @MockBean + private ApiTokenService apiTokenService; + + @Test + void revoke_returns204NoContent() 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")) + ); + + mockMvc.perform(delete("/api/v1/tokens/7") + .with(authentication(auth)) + .with(csrf())) + .andExpect(status().isNoContent()) + .andExpect(content().string("")); + + verify(apiTokenService).revokeToken(7L, "user-42"); + } +}