From 817bce3478de57221c4459e74abff20da8f6deac Mon Sep 17 00:00:00 2001 From: forge-bot Date: Mon, 13 Jul 2026 10:58:40 +0000 Subject: [PATCH] =?UTF-8?q?=EC=9D=B8=EC=A6=9D/=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EC=97=85=EB=AC=B4=20=EA=B7=9C=EC=B9=99=EA=B3=BC=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20=EB=B6=88=EB=B3=80=EC=8B=9D=20=EC=B6=94=EC=B6=9C=20?= =?UTF-8?q?(codex-bais-final4-20260713-195657-BAIS-SPRING-ANA-AUTH-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/auth/AuthInvariantTest.java | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 auth-migration/src/test/java/com/example/auth/AuthInvariantTest.java diff --git a/auth-migration/src/test/java/com/example/auth/AuthInvariantTest.java b/auth-migration/src/test/java/com/example/auth/AuthInvariantTest.java new file mode 100644 index 0000000..cc5529b --- /dev/null +++ b/auth-migration/src/test/java/com/example/auth/AuthInvariantTest.java @@ -0,0 +1,119 @@ +package com.example.auth; + +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.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * Verification tests for AUTH-* invariants. + * These tests MUST pass after Spring migration to confirm behavioral parity. + */ +@SpringBootTest +@AutoConfigureMockMvc +public class AuthInvariantTest { + + @Autowired + private MockMvc mockMvc; + + // AUTH-004: Bearer token required + @Test + void missingAuthorizationHeader_returns401WithAUTH_MISSING_TOKEN() throws Exception { + mockMvc.perform(get("/api/protected/resource")) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.error").value("AUTH_MISSING_TOKEN")) + .andExpect(jsonPath("$.message").value("Authorization header required")) + .andExpect(jsonPath("$.timestamp").exists()); + } + + // AUTH-005: Bearer format required + @Test + void malformedBearerToken_returns401WithAUTH_MALFORMED_TOKEN() throws Exception { + mockMvc.perform(get("/api/protected/resource") + .header("Authorization", "Basic dXNlcjpwYXNz")) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.error").value("AUTH_MALFORMED_TOKEN")) + .andExpect(jsonPath("$.message").value("Bearer token format required")); + } + + // AUTH-002: Expired token + @Test + void expiredToken_returns401WithAUTH_TOKEN_EXPIRED() throws Exception { + String expiredToken = TestTokenUtil.generateExpiredToken(); + mockMvc.perform(get("/api/protected/resource") + .header("Authorization", "Bearer " + expiredToken)) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.error").value("AUTH_TOKEN_EXPIRED")) + .andExpect(jsonPath("$.message").value("Token has expired")); + } + + // AUTH-003: Invalid signature + @Test + void invalidSignature_returns401WithAUTH_INVALID_SIGNATURE() throws Exception { + String tamperedToken = TestTokenUtil.generateTamperedToken(); + mockMvc.perform(get("/api/protected/resource") + .header("Authorization", "Bearer " + tamperedToken)) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.error").value("AUTH_INVALID_SIGNATURE")) + .andExpect(jsonPath("$.message").value("Token signature verification failed")); + } + + // AUTH-001: Missing sub claim + @Test + void tokenWithoutSub_returns401WithAUTH_MISSING_SUB() throws Exception { + String noSubToken = TestTokenUtil.generateTokenWithoutSub(); + mockMvc.perform(get("/api/protected/resource") + .header("Authorization", "Bearer " + noSubToken)) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.error").value("AUTH_MISSING_SUB")) + .andExpect(jsonPath("$.message").value("Token missing subject claim")); + } + + // AUTH-010: Admin role required + @Test + void nonAdminAccessingAdminEndpoint_returns403WithAUTH_FORBIDDEN_ADMIN() throws Exception { + String userToken = TestTokenUtil.generateTokenWithRole("ROLE_USER"); + mockMvc.perform(get("/api/admin/users") + .header("Authorization", "Bearer " + userToken)) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.error").value("AUTH_FORBIDDEN_ADMIN")) + .andExpect(jsonPath("$.message").value("Admin role required")); + } + + // AUTH-011: Resource owner check + @Test + void nonOwnerAccessingUserResource_returns403WithAUTH_FORBIDDEN_RESOURCE() throws Exception { + String userToken = TestTokenUtil.generateTokenWithRole("ROLE_USER"); + mockMvc.perform(get("/api/users/999/profile") + .header("Authorization", "Bearer " + userToken)) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.error").value("AUTH_FORBIDDEN_RESOURCE")) + .andExpect(jsonPath("$.message").value("Not authorized for this resource")); + } + + // AUTH-012: Public profile accessible to authenticated users + @Test + void authenticatedUserCanReadPublicProfile_returns200() throws Exception { + String userToken = TestTokenUtil.generateTokenWithRole("ROLE_USER"); + mockMvc.perform(get("/api/users/public/johndoe") + .header("Authorization", "Bearer " + userToken)) + .andExpect(status().isOk()); + } + + // SESS-004: CSRF required for state-changing ops + @Test + void postWithoutCSRF_returns403() throws Exception { + String userToken = TestTokenUtil.generateTokenWithRole("ROLE_USER"); + mockMvc.perform(post("/api/users/profile") + .header("Authorization", "Bearer " + userToken) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"displayName\":\"test\"}")) + .andExpect(status().isForbidden()); + } +}