Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ class UserLogoutFlowE2eTest extends E2eTestSupport {

@SuppressWarnings("unchecked")
@Disabled
@DisplayName("정상 로그아웃 전체 플로우 - TDD REd 단계")
void completeUserRegistrationFlow_shouldFailBecauseApiNotImplemented() throws Exception {
@DisplayName("정상 로그아웃 전체 플로우 - TDD Red 단계")
void completeUserLogoutFlow_shouldFailBecauseApiNotImplemented() throws Exception {
logStep(1, "관리자 로그인 (최우선)");

// 1. 관리자 로그인으로 인증 상태 확립
Expand All @@ -45,85 +45,81 @@ void completeUserRegistrationFlow_shouldFailBecauseApiNotImplemented() throws Ex
assertThat(loginResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat((Boolean) loginResponse.getBody().get("success")).isTrue();

logSuccess("관리자 로그인 성공 - 인증 상태 확립 완료");
logSuccess("관리자 로그인 성공 - 세션 쿠키 자동 저장됨");
logDebug("현재 세션 쿠키: " + getSessionCookies());

logStep(2, "로그인 상태에서 보호된 리소스 접근 확인");

// 로그인 응답에서 세션 쿠키 추출
String sessionCookie = null;
java.util.List<String> cookies = loginResponse.getHeaders().get("Set-Cookie");
if (cookies != null) {
for (String cookie : cookies) {
if (cookie.startsWith("JSESSIONID")) {
sessionCookie = cookie.split(";")[0]; // JSESSIONID=XXX 부분만 추출
break;
}
}
}

// 2. 로그인된 상태에서 본인 프로필 조회로 인증 상태 확인
// /v0/users/me는 인증된 사용자만 접근 가능한 일반적인 API
HttpHeaders authenticatedHeaders = new HttpHeaders();
if (sessionCookie != null) {
authenticatedHeaders.set("Cookie", sessionCookie);
}

HttpEntity<Void> authenticatedEntity = new HttpEntity<>(authenticatedHeaders);
// 쿠키는 인터셉터에 의해 자동으로 전송됨
ResponseEntity<Map> beforeLogoutResponse =
restTemplate.exchange(
getV0ApiUrl("/users/me"), HttpMethod.GET, authenticatedEntity, Map.class);
restTemplate.getForEntity(getV0ApiUrl("/users/me"), Map.class);

assertThat(beforeLogoutResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat((Boolean) beforeLogoutResponse.getBody().get("success")).isTrue();
assertThat(beforeLogoutResponse.getBody().get("data")).isNotNull();

logSuccess("인증된 상태에서 본인 프로필 조회 성공");

// 3. 로그아웃 API 호출
logStep(3, "로그아웃 API 호출");

// 3. 로그아웃 API 호출 (세션 쿠키는 인터셉터가 자동 처리)
HttpHeaders logoutHeaders = new HttpHeaders();
logoutHeaders.setContentType(MediaType.APPLICATION_JSON);
logoutHeaders.set("Origin", "https://admin.icebang.site");
logoutHeaders.set("Referer", "https://admin.icebang.site/");

// 로그아웃 요청에도 세션 쿠키 포함
if (sessionCookie != null) {
logoutHeaders.set("Cookie", sessionCookie);
}

HttpEntity<Map<String, Object>> logoutEntity = new HttpEntity<>(new HashMap<>(), logoutHeaders);

try {
ResponseEntity<Map> logoutResponse =
restTemplate.postForEntity(getV0ApiUrl("/auth/logout"), logoutEntity, Map.class);
logStep(4, "로그아웃 응답 검증 (API구현 돼있으면)");

logStep(4, "로그아웃 응답 검증");
assertThat(logoutResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat((Boolean) logoutResponse.getBody().get("success")).isTrue();

logSuccess("로그아웃 API 호출 성공");

logStep(5, "로그아웃 후 인증 무효화 확인");

// 로그아웃 후 세션 쿠키 상태 확인
logDebug("로그아웃 후 세션 쿠키: " + getSessionCookies());

// 5. 로그아웃 후 동일한 프로필 API 접근 시 인증 실패 확인
HttpEntity<Void> afterLogoutEntity = new HttpEntity<>(authenticatedHeaders);
ResponseEntity<Map> afterLogoutResponse =
restTemplate.exchange(
getV0ApiUrl("/users/me"), HttpMethod.GET, afterLogoutEntity, Map.class);
restTemplate.getForEntity(getV0ApiUrl("/users/me"), Map.class);

// 핵심 검증: 로그아웃 후에는 인증 실패로 401 또는 403 응답이어야 함
assertThat(afterLogoutResponse.getStatusCode())
.withFailMessage(
"로그아웃 후 프로필 접근이 차단되어야 합니다. 현재 상태코드: %s", afterLogoutResponse.getStatusCode())
.isIn(HttpStatus.UNAUTHORIZED, HttpStatus.FORBIDDEN);
logSuccess("로그아웃 후 프로필 접근 차단 확인 - 인증 무효화 성공");

logCompletion("일반 사용자 로그아웃 플로우");
logSuccess("로그아웃 후 프로필 접근 차단 확인 - 인증 무효화 성공");
logCompletion("관리자 로그아웃 플로우");

} catch (org.springframework.web.client.HttpClientErrorException.NotFound ex) {
logError("예상된 실패: 로그아웃 API가 구현되지 않음 (404 Not Found");
logError("에러 메시지 : " + ex.getMessage());
logError("예상된 실패: 로그아웃 API가 구현되지 않음 (404 Not Found)");
logError("에러 메시지: " + ex.getMessage());
logError("TDD Red 단계 - API 구현 필요");

fail(
"로그아웃 API (/v0/auth/logout)가 구현되지 않았습니다. "
+ "다음 단계에서 API를 구현해야 합니다. 에러: "
+ ex.getMessage());

} catch (org.springframework.web.client.HttpClientErrorException ex) {
logError("HTTP 클라이언트 에러: " + ex.getStatusCode() + " - " + ex.getMessage());

if (ex.getStatusCode() == HttpStatus.METHOD_NOT_ALLOWED) {
logError("로그아웃 엔드포인트는 존재하지만 POST 메서드를 지원하지 않습니다.");
fail("로그아웃 API가 POST 메서드를 지원하지 않습니다. 구현을 확인해주세요.");
} else {
fail("로그아웃 API 호출 중 HTTP 에러 발생: " + ex.getStatusCode() + " - " + ex.getMessage());
}

} catch (Exception ex) {
logError("예상치 못한 오류 발생: " + ex.getClass().getSimpleName());
logError("에러 메시지: " + ex.getMessage());
Expand All @@ -133,7 +129,63 @@ void completeUserRegistrationFlow_shouldFailBecauseApiNotImplemented() throws Ex
}
}

/** 일반 사용자 로그인을 수행하는 헬퍼 메서드 관리자가 아닌 콘텐츠팀장으로 로그인 */
@SuppressWarnings("unchecked")
@DisplayName("일반 사용자 로그아웃 플로우 테스트")
void regularUserLogoutFlow() throws Exception {
logStep(1, "일반 사용자 로그인");

// 세션 쿠키 초기화
clearSessionCookies();

// 일반 사용자 로그인 수행
performRegularUserLogin();

logStep(2, "일반 사용자 권한으로 프로필 조회");

// 로그인된 상태에서 프로필 조회
ResponseEntity<Map> beforeLogoutResponse =
restTemplate.getForEntity(getV0ApiUrl("/users/me"), Map.class);

assertThat(beforeLogoutResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat((Boolean) beforeLogoutResponse.getBody().get("success")).isTrue();

logSuccess("일반 사용자 프로필 조회 성공");

logStep(3, "일반 사용자 로그아웃 시도");

try {
HttpHeaders logoutHeaders = new HttpHeaders();
logoutHeaders.setContentType(MediaType.APPLICATION_JSON);
logoutHeaders.set("Origin", "https://admin.icebang.site");
logoutHeaders.set("Referer", "https://admin.icebang.site/");

HttpEntity<Map<String, Object>> logoutEntity =
new HttpEntity<>(new HashMap<>(), logoutHeaders);

ResponseEntity<Map> logoutResponse =
restTemplate.postForEntity(getV0ApiUrl("/auth/logout"), logoutEntity, Map.class);

assertThat(logoutResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
logSuccess("일반 사용자 로그아웃 성공");

logStep(4, "로그아웃 후 접근 권한 무효화 확인");

ResponseEntity<Map> afterLogoutResponse =
restTemplate.getForEntity(getV0ApiUrl("/users/me"), Map.class);

assertThat(afterLogoutResponse.getStatusCode())
.isIn(HttpStatus.UNAUTHORIZED, HttpStatus.FORBIDDEN);

logSuccess("일반 사용자 로그아웃 후 접근 차단 확인");
logCompletion("일반 사용자 로그아웃 플로우");

} catch (org.springframework.web.client.HttpClientErrorException.NotFound ex) {
logError("예상된 실패: 로그아웃 API 미구현");
fail("로그아웃 API가 구현되지 않았습니다: " + ex.getMessage());
}
}

/** 일반 사용자 로그인을 수행하는 헬퍼 메서드 - 관리자가 아닌 콘텐츠팀장으로 로그인 */
private void performRegularUserLogin() {
Map<String, String> loginRequest = new HashMap<>();
loginRequest.put("email", "[email protected]");
Expand All @@ -154,6 +206,34 @@ private void performRegularUserLogin() {
throw new RuntimeException("Regular user login failed for logout test");
}

logSuccess("일반 사용자 로그인 완료 (로그아웃 테스트용)");
logSuccess("일반 사용자 로그인 완료 - 세션 쿠키 저장됨");
logDebug("일반 사용자 세션 쿠키: " + getSessionCookies());
}

/** 관리자 로그인을 수행하는 헬퍼 메서드 */
private void performAdminLogin() {
clearSessionCookies(); // 기존 세션 정리

Map<String, String> loginRequest = new HashMap<>();
loginRequest.put("email", "[email protected]");
loginRequest.put("password", "qwer1234!A");

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Origin", "https://admin.icebang.site");
headers.set("Referer", "https://admin.icebang.site/");

HttpEntity<Map<String, String>> entity = new HttpEntity<>(loginRequest, headers);

ResponseEntity<Map> response =
restTemplate.postForEntity(getV0ApiUrl("/auth/login"), entity, Map.class);

if (response.getStatusCode() != HttpStatus.OK) {
logError("관리자 로그인 실패: " + response.getStatusCode());
throw new RuntimeException("Admin login failed");
}

logSuccess("관리자 로그인 완료 - 세션 쿠키 저장됨");
logDebug("관리자 세션 쿠키: " + getSessionCookies());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,20 @@ void completeUserRegistrationFlow() throws Exception {
assertThat(loginResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat((Boolean) loginResponse.getBody().get("success")).isTrue();

logSuccess("관리자 로그인 성공 - 이제 모든 리소스 접근 가능");
logSuccess("관리자 로그인 성공 - 세션 쿠키 자동 저장됨");
logDebug("현재 세션 쿠키: " + getSessionCookies());

logStep(2, "조직 목록 조회 (인증된 상태)");

// 2. 조직 목록 조회 (로그인 후 가능)
// 2. 조직 목록 조회 (로그인 후 가능, 쿠키 자동 전송)
ResponseEntity<Map> organizationsResponse =
restTemplate.getForEntity(getV0ApiUrl("/organizations"), Map.class);

assertThat(organizationsResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat((Boolean) organizationsResponse.getBody().get("success")).isTrue();
assertThat(organizationsResponse.getBody().get("data")).isNotNull();

logSuccess("조직 목록 조회 성공");
logSuccess("조직 목록 조회 성공 (인증된 요청)");

logStep(3, "부서 및 각종 데이터 조회 (특정 조직 옵션)");

Expand Down Expand Up @@ -229,7 +230,8 @@ private void performAdminLogin() {
throw new RuntimeException("Admin login failed");
}

logSuccess("관리자 로그인 완료");
logSuccess("관리자 로그인 완료 - 세션 쿠키 저장됨");
logDebug("세션 쿠키: " + getSessionCookies());
}

/** 사용자 등록을 수행하는 헬퍼 메서드 */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
package site.icebang.e2e.setup.support;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;
import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.annotation.PostConstruct;

import site.icebang.e2e.setup.annotation.E2eTest;
import site.icebang.e2e.setup.config.E2eTestConfiguration;

Expand All @@ -26,6 +33,53 @@ public abstract class E2eTestSupport {

protected MockMvc mockMvc;

private List<String> sessionCookies = new ArrayList<>();

@PostConstruct
void setupCookieManagement() {
// RestTemplate에 쿠키 인터셉터 추가
restTemplate.getRestTemplate().getInterceptors().add(createCookieInterceptor());
logDebug("쿠키 관리 인터셉터 설정 완료");
}

private ClientHttpRequestInterceptor createCookieInterceptor() {
return (request, body, execution) -> {
// 요청에 저장된 쿠키 추가
if (!sessionCookies.isEmpty()) {
request.getHeaders().put("Cookie", sessionCookies);
logDebug("쿠키 전송: " + String.join("; ", sessionCookies));
}

// 요청 실행
ClientHttpResponse response = execution.execute(request, body);

// 응답에서 Set-Cookie 헤더 추출하여 저장
List<String> setCookieHeaders = response.getHeaders().get("Set-Cookie");
if (setCookieHeaders != null && !setCookieHeaders.isEmpty()) {
updateSessionCookies(setCookieHeaders);
logDebug("세션 쿠키 업데이트: " + String.join("; ", sessionCookies));
}

return response;
};
}

private void updateSessionCookies(List<String> setCookieHeaders) {
for (String setCookie : setCookieHeaders) {
// 쿠키 이름 추출
String cookieName = setCookie.split("=")[0];
String cookieValue = setCookie.split(";")[0]; // 쿠키 값만 추출 (속성 제외)

// 같은 이름의 쿠키가 있으면 제거
sessionCookies.removeIf(cookie -> cookie.startsWith(cookieName + "="));

// 새 쿠키 추가 (빈 값이 아닌 경우만)
if (!cookieValue.endsWith("=")) {
sessionCookies.add(cookieValue);
}
}
}

protected String getBaseUrl() {
return "http://localhost:" + port;
}
Expand All @@ -38,6 +92,20 @@ protected String getV0ApiUrl(String path) {
return getBaseUrl() + "/v0" + path;
}

/** 세션 쿠키 관리 메서드들 */
protected void clearSessionCookies() {
sessionCookies.clear();
logDebug("세션 쿠키 초기화됨");
}

protected List<String> getSessionCookies() {
return new ArrayList<>(sessionCookies);
}

protected boolean hasSessionCookie(String cookieName) {
return sessionCookies.stream().anyMatch(cookie -> cookie.startsWith(cookieName + "="));
}

/** 테스트 시나리오 단계별 로깅을 위한 유틸리티 메서드 */
protected void logStep(int stepNumber, String description) {
System.out.println(String.format("📋 Step %d: %s", stepNumber, description));
Expand All @@ -57,4 +125,16 @@ protected void logError(String message) {
protected void logCompletion(String scenario) {
System.out.println(String.format("🎉 %s 시나리오 완료!", scenario));
}

/** 디버그 로깅을 위한 유틸리티 메서드 */
protected void logDebug(String message) {
if (isDebugEnabled()) {
System.out.println("🐛 DEBUG: " + message);
}
}

private boolean isDebugEnabled() {
return System.getProperty("test.debug", "false").equals("true")
|| System.getProperty("e2e.debug", "false").equals("true");
}
}
Loading