인증/권한 업무 규칙과 전환 불변식 추출 #2

Open
forge-bot wants to merge 5 commits from forge/codex-bais-final4-20260713-195657-BAIS-SPRING-ANA-AUTH-001-attempt-1-run-763caa509a46 into main
Showing only changes of commit 817bce3478 - Show all commits

View file

@ -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());
}
}