인증/권한 업무 규칙과 전환 불변식 추출 (codex-bais-final4-20260713-195657-BAIS-SPRING-ANA-AUTH-001)
This commit is contained in:
parent
7689c9b968
commit
817bce3478
1 changed files with 119 additions and 0 deletions
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue