diff --git a/apps/user-service/src/main/java/site/icebang/domain/auth/controller/AuthController.java b/apps/user-service/src/main/java/site/icebang/domain/auth/controller/AuthController.java index d0a98142..2303cf74 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/auth/controller/AuthController.java +++ b/apps/user-service/src/main/java/site/icebang/domain/auth/controller/AuthController.java @@ -61,4 +61,18 @@ public ApiResponse checkSession(@AuthenticationPrincipal AuthCredential public ApiResponse getPermissions(@AuthenticationPrincipal AuthCredential user) { return ApiResponse.success(user); } + + @PostMapping("/logout") + public ApiResponse logout(HttpServletRequest request) { + // SecurityContext 정리 + SecurityContextHolder.clearContext(); + + // 세션 무효화 + HttpSession session = request.getSession(false); + if (session != null) { + session.invalidate(); + } + + return ApiResponse.success(null); + } } diff --git a/apps/user-service/src/main/resources/sql/01-schema-h2.sql b/apps/user-service/src/main/resources/sql/01-schema-h2.sql new file mode 100644 index 00000000..018ebb1d --- /dev/null +++ b/apps/user-service/src/main/resources/sql/01-schema-h2.sql @@ -0,0 +1,328 @@ +-- H2 데이터베이스 호환 스키마 (테스트용) +-- MySQL의 unsigned, AFTER 절 등을 H2 호환으로 변경 + +CREATE TABLE `permission` ( + `id` int NOT NULL AUTO_INCREMENT, + `resource` varchar(100) NULL, + `description` varchar(255) NULL, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `is_active` boolean DEFAULT TRUE, + `updated_by` bigint NULL, + `created_by` bigint NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE `organization` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(150) NULL, + `domain_name` varchar(100) NULL, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +); + +CREATE TABLE `role` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `organization_id` bigint NULL, + `name` varchar(100) NULL, + `description` varchar(500) NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE `user` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(50) NULL, + `email` varchar(100) NULL, + `password` varchar(255) NULL, + `status` varchar(20) NULL, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +); + +CREATE TABLE `department` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `organization_id` bigint NOT NULL, + `name` varchar(100) NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE `position` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `organization_id` bigint NOT NULL, + `title` varchar(100) NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE `user_organization` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL, + `organization_id` bigint NOT NULL, + `position_id` bigint NOT NULL, + `department_id` bigint NOT NULL, + `employee_number` varchar(50) NULL, + `status` varchar(20) NULL, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +); + +CREATE TABLE `role_permission` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `role_id` bigint NOT NULL, + `permission_id` int NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_role_permission` (`role_id`, `permission_id`) +); + +CREATE TABLE `user_role` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `role_id` bigint NOT NULL, + `user_organization_id` bigint NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_role` (`role_id`, `user_organization_id`) +); + +-- 성능 최적화를 위한 인덱스 +CREATE INDEX `idx_user_email` ON `user` (`email`); +CREATE INDEX `idx_user_status` ON `user` (`status`); +CREATE INDEX `idx_user_organization_user` ON `user_organization` (`user_id`); +CREATE INDEX `idx_user_organization_org` ON `user_organization` (`organization_id`); +CREATE INDEX `idx_user_organization_status` ON `user_organization` (`status`); +CREATE INDEX `idx_role_org` ON `role` (`organization_id`); +CREATE INDEX `idx_permission_resource` ON `permission` (`resource`); +CREATE INDEX `idx_permission_active` ON `permission` (`is_active`); + +CREATE TABLE `workflow` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL UNIQUE, + `description` text NULL, + `is_enabled` boolean DEFAULT TRUE, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `created_by` bigint NULL, + `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `updated_by` bigint NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE `schedule` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `workflow_id` bigint NOT NULL, + `cron_expression` varchar(50) NULL, + `parameters` json NULL, + `is_active` boolean DEFAULT TRUE, + `last_run_status` varchar(20) NULL, + `last_run_at` timestamp NULL, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `created_by` bigint NULL, + `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `updated_by` bigint NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE `job` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL UNIQUE, + `description` text NULL, + `is_enabled` boolean DEFAULT TRUE, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `created_by` bigint NULL, + `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `updated_by` bigint NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE `task` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL UNIQUE, + `type` varchar(50) NULL, + `parameters` json NULL, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +); + +CREATE TABLE `workflow_job` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `workflow_id` bigint NOT NULL, + `job_id` bigint NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_workflow_job` (`workflow_id`, `job_id`) +); + +CREATE TABLE `job_task` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `job_id` bigint NOT NULL, + `task_id` bigint NOT NULL, + `execution_order` int NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_job_task` (`job_id`, `task_id`) +); + +CREATE TABLE `execution_log` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `execution_type` varchar(20) NULL COMMENT 'task, schedule, job, workflow', + `source_id` bigint NULL COMMENT '모든 데이터에 대한 ID ex: job_id, schedule_id, task_id, ...', + `log_level` varchar(20) NULL, + `executed_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `log_message` text NULL, + `trace_id` char(36) NULL, + `config_snapshot` json NULL, + PRIMARY KEY (`id`), + INDEX `idx_source_id_type` (`source_id`, `execution_type`) +); + +CREATE TABLE `task_io_data` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `task_run_id` bigint NOT NULL, + `io_type` varchar(10) NOT NULL COMMENT 'INPUT, OUTPUT', + `name` varchar(100) NOT NULL COMMENT '파라미터/변수 이름', + `data_type` varchar(50) NOT NULL COMMENT 'string, number, json, file, etc', + `data_value` json NULL COMMENT '실제 데이터 값', + `data_size` bigint NULL COMMENT '데이터 크기 (bytes)', + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + INDEX `idx_task_io_task_run_id` (`task_run_id`), + INDEX `idx_task_io_type` (`io_type`), + INDEX `idx_task_io_name` (`name`) +); + +CREATE TABLE `config` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `target_type` varchar(50) NULL COMMENT 'user, job, workflow', + `target_id` bigint NULL, + `version` int NULL, + `json` json NULL, + `is_active` boolean DEFAULT TRUE, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `created_by` bigint NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_config_target` (`target_type`, `target_id`) +); + +CREATE TABLE `category` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) NULL, + `description` text NULL, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +); + +CREATE TABLE `user_config` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL, + `type` varchar(50) NULL, + `name` varchar(100) NULL, + `json` json NULL, + `is_active` boolean DEFAULT TRUE, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +); + +-- 인덱스 추가 (성능 최적화) +CREATE INDEX `idx_schedule_workflow` ON `schedule` (`workflow_id`); +CREATE INDEX `idx_job_enabled` ON `job` (`is_enabled`); +CREATE INDEX `idx_task_type` ON `task` (`type`); +CREATE INDEX `idx_workflow_enabled` ON `workflow` (`is_enabled`); +CREATE UNIQUE INDEX `uk_schedule_workflow` ON `schedule` (`workflow_id`); +CREATE UNIQUE INDEX `uk_job_name` ON `job` (`name`); +CREATE UNIQUE INDEX `uk_task_name` ON `task` (`name`); +CREATE UNIQUE INDEX `uk_workflow_name` ON `workflow` (`name`); +CREATE INDEX `idx_user_config_user` ON `user_config` (`user_id`); + +-- 워크플로우 실행 테이블 +CREATE TABLE `workflow_run` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `workflow_id` bigint NOT NULL, + `trace_id` char(36) NOT NULL, + `run_number` varchar(20) NULL, + `status` varchar(20) NULL COMMENT 'pending, running, success, failed, cancelled', + `trigger_type` varchar(20) NULL COMMENT 'manual, schedule, push, pull_request', + `started_at` timestamp NULL, + `finished_at` timestamp NULL, + `created_by` bigint NULL, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_workflow_run_trace` (`trace_id`), + INDEX `idx_workflow_run_status` (`status`), + INDEX `idx_workflow_run_workflow_id` (`workflow_id`), + INDEX `idx_workflow_run_created_at` (`created_at`) +); + +-- Job 실행 테이블 +CREATE TABLE `job_run` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `workflow_run_id` bigint NOT NULL, + `job_id` bigint NOT NULL, + `status` varchar(20) NULL COMMENT 'pending, running, success, failed, cancelled, skipped', + `started_at` timestamp NULL, + `finished_at` timestamp NULL, + `execution_order` int NULL, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + INDEX `idx_job_run_workflow_run_id` (`workflow_run_id`), + INDEX `idx_job_run_status` (`status`), + INDEX `idx_job_run_job_id` (`job_id`) +); + +-- Task 실행 테이블 +CREATE TABLE `task_run` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `job_run_id` bigint NOT NULL, + `task_id` bigint NOT NULL, + `status` varchar(20) NULL COMMENT 'pending, running, success, failed, cancelled, skipped', + `started_at` timestamp NULL, + `finished_at` timestamp NULL, + `execution_order` int NULL, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + INDEX `idx_task_run_job_run_id` (`job_run_id`), + INDEX `idx_task_run_status` (`status`), + INDEX `idx_task_run_task_id` (`task_id`) +); + +-- v0.0.3 - H2 호환 버전 +DROP TABLE IF EXISTS `config`; + +-- H2에서는 한 번에 하나씩 컬럼 추가 +ALTER TABLE `workflow_job` ADD COLUMN `execution_order` INT NULL; + +ALTER TABLE `schedule` ADD COLUMN `schedule_text` varchar(20) NULL; + +ALTER TABLE `workflow` ADD COLUMN `default_config` json NULL; + +ALTER TABLE `user` ADD COLUMN `joined_at` timestamp NULL; + +ALTER TABLE `department` ADD COLUMN `description` varchar(100) NULL; + +-- v0.4 - H2 호환 버전 (AFTER 절 제거, unsigned 제거, 개별 ALTER 구문으로 분리) +-- execution_log 테이블 컬럼 추가 (H2 호환) +ALTER TABLE `execution_log` ADD COLUMN `run_id` bigint NULL; +ALTER TABLE `execution_log` ADD COLUMN `status` varchar(20) NULL; +ALTER TABLE `execution_log` ADD COLUMN `duration_ms` int NULL; +ALTER TABLE `execution_log` ADD COLUMN `error_code` varchar(50) NULL; +ALTER TABLE `execution_log` ADD COLUMN `reserved1` varchar(100) NULL; +ALTER TABLE `execution_log` ADD COLUMN `reserved2` varchar(100) NULL; +ALTER TABLE `execution_log` ADD COLUMN `reserved3` int NULL; +ALTER TABLE `execution_log` ADD COLUMN `reserved4` json NULL; +ALTER TABLE `execution_log` ADD COLUMN `reserved5` timestamp NULL; + +-- 기존 컬럼 수정 (H2 호환) +ALTER TABLE `execution_log` ALTER COLUMN `log_message` varchar(500) NOT NULL; +ALTER TABLE `execution_log` ALTER COLUMN `executed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- 기존 불필요한 컬럼 제거 +ALTER TABLE `execution_log` DROP COLUMN IF EXISTS `config_snapshot`; + +-- 새로운 인덱스 추가 +CREATE INDEX `idx_run_id` ON `execution_log` (`run_id`); +CREATE INDEX `idx_log_level_status` ON `execution_log` (`log_level`, `status`); +CREATE INDEX `idx_error_code` ON `execution_log` (`error_code`); +CREATE INDEX `idx_duration` ON `execution_log` (`duration_ms`); + +-- 기존 인덱스 수정 +DROP INDEX IF EXISTS `idx_source_id_type`; +CREATE INDEX `idx_execution_type_source` ON `execution_log` (`execution_type`, `source_id`); diff --git a/apps/user-service/src/test/java/site/icebang/e2e/scenario/UserLogoutFlowE2eTest.java b/apps/user-service/src/test/java/site/icebang/e2e/scenario/UserLogoutFlowE2eTest.java new file mode 100644 index 00000000..8fea2764 --- /dev/null +++ b/apps/user-service/src/test/java/site/icebang/e2e/scenario/UserLogoutFlowE2eTest.java @@ -0,0 +1,159 @@ +package site.icebang.e2e.scenario; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.*; +import org.springframework.test.context.jdbc.Sql; + +import site.icebang.e2e.setup.annotation.E2eTest; +import site.icebang.e2e.setup.support.E2eTestSupport; + +@Sql( + value = "classpath:sql/01-insert-internal-users.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +@DisplayName("사용자 로그아웃 플로우 E2E 테스트") +@E2eTest +class UserLogoutFlowE2eTest extends E2eTestSupport { + + @SuppressWarnings("unchecked") + @Test + @DisplayName("정상 로그아웃 전체 플로우 - TDD REd 단계") + void completeUserRegistrationFlow_shouldFailBecauseApiNotImplemented() throws Exception { + logStep(1, "관리자 로그인 (최우선)"); + + // 1. 관리자 로그인으로 인증 상태 확립 + Map loginRequest = new HashMap<>(); + loginRequest.put("email", "admin@icebang.site"); + loginRequest.put("password", "qwer1234!A"); + + HttpHeaders loginHeaders = new HttpHeaders(); + loginHeaders.setContentType(MediaType.APPLICATION_JSON); + loginHeaders.set("Origin", "https://admin.icebang.site"); + loginHeaders.set("Referer", "https://admin.icebang.site/"); + + HttpEntity> loginEntity = new HttpEntity<>(loginRequest, loginHeaders); + + ResponseEntity loginResponse = + restTemplate.postForEntity(getV0ApiUrl("/auth/login"), loginEntity, Map.class); + + assertThat(loginResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat((Boolean) loginResponse.getBody().get("success")).isTrue(); + + logSuccess("관리자 로그인 성공 - 인증 상태 확립 완료"); + + logStep(2, "로그인 상태에서 보호된 리소스 접근 확인"); + + // 로그인 응답에서 세션 쿠키 추출 + String sessionCookie = null; + java.util.List 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 authenticatedEntity = new HttpEntity<>(authenticatedHeaders); + ResponseEntity beforeLogoutResponse = + restTemplate.exchange( + getV0ApiUrl("/users/me"), HttpMethod.GET, authenticatedEntity, Map.class); + + assertThat(beforeLogoutResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat((Boolean) beforeLogoutResponse.getBody().get("success")).isTrue(); + assertThat(beforeLogoutResponse.getBody().get("data")).isNotNull(); + + logSuccess("인증된 상태에서 본인 프로필 조회 성공"); + + // 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> logoutEntity = new HttpEntity<>(new HashMap<>(), logoutHeaders); + + try { + ResponseEntity logoutResponse = + restTemplate.postForEntity(getV0ApiUrl("/auth/logout"), logoutEntity, Map.class); + logStep(4, "로그아웃 응답 검증 (API구현 돼있으면)"); + + logSuccess("로그아웃 API 호출 성공"); + + logStep(5, "로그아웃 후 인증 무효화 확인"); + + // 5. 로그아웃 후 동일한 프로필 API 접근 시 인증 실패 확인 + HttpEntity afterLogoutEntity = new HttpEntity<>(authenticatedHeaders); + ResponseEntity afterLogoutResponse = + restTemplate.exchange( + getV0ApiUrl("/users/me"), HttpMethod.GET, afterLogoutEntity, Map.class); + + // 핵심 검증: 로그아웃 후에는 인증 실패로 401 또는 403 응답이어야 함 + assertThat(afterLogoutResponse.getStatusCode()) + .withFailMessage( + "로그아웃 후 프로필 접근이 차단되어야 합니다. 현재 상태코드: %s", afterLogoutResponse.getStatusCode()) + .isIn(HttpStatus.UNAUTHORIZED, HttpStatus.FORBIDDEN); + logSuccess("로그아웃 후 프로필 접근 차단 확인 - 인증 무효화 성공"); + + logCompletion("일반 사용자 로그아웃 플로우"); + + } catch (org.springframework.web.client.HttpClientErrorException.NotFound ex) { + logError("예상된 실패: 로그아웃 API가 구현되지 않음 (404 Not Found"); + logError("에러 메시지 : " + ex.getMessage()); + + fail( + "로그아웃 API (/v0/auth/logout)가 구현되지 않았습니다. " + + "다음 단계에서 API를 구현해야 합니다. 에러: " + + ex.getMessage()); + } catch (Exception ex) { + logError("예상치 못한 오류 발생: " + ex.getClass().getSimpleName()); + logError("에러 메시지: " + ex.getMessage()); + + // 기타 예상치 못한 에러도 기록 + fail("로그아웃 API 호출 중 예상치 못한 오류 발생: " + ex.getMessage()); + } + } + + /** 일반 사용자 로그인을 수행하는 헬퍼 메서드 관리자가 아닌 콘텐츠팀장으로 로그인 */ + private void performRegularUserLogin() { + Map loginRequest = new HashMap<>(); + loginRequest.put("email", "viral.jung@icebang.site"); + 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> entity = new HttpEntity<>(loginRequest, headers); + + ResponseEntity response = + restTemplate.postForEntity(getV0ApiUrl("/auth/login"), entity, Map.class); + + if (response.getStatusCode() != HttpStatus.OK) { + logError("일반 사용자 로그인 실패: " + response.getStatusCode()); + throw new RuntimeException("Regular user login failed for logout test"); + } + + logSuccess("일반 사용자 로그인 완료 (로그아웃 테스트용)"); + } +} diff --git a/apps/user-service/src/test/java/site/icebang/integration/tests/auth/AuthApiIntegrationTest.java b/apps/user-service/src/test/java/site/icebang/integration/tests/auth/AuthApiIntegrationTest.java index 5c538105..4fe3b00d 100644 --- a/apps/user-service/src/test/java/site/icebang/integration/tests/auth/AuthApiIntegrationTest.java +++ b/apps/user-service/src/test/java/site/icebang/integration/tests/auth/AuthApiIntegrationTest.java @@ -13,6 +13,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.*; +import org.springframework.mock.web.MockHttpSession; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.context.jdbc.Sql; import org.springframework.transaction.annotation.Transactional; @@ -79,4 +80,62 @@ void login_success() throws Exception { .description("HTTP 상태")) .build()))); } + + @Test + @DisplayName("사용자 로그아웃 성공") + void logout_success() throws Exception { + // given - 먼저 로그인 + Map loginRequest = new HashMap<>(); + loginRequest.put("email", "admin@icebang.site"); + loginRequest.put("password", "qwer1234!A"); + + MockHttpSession session = new MockHttpSession(); + + // 로그인 먼저 수행 + mockMvc + .perform( + post(getApiUrlForDocs("/v0/auth/login")) + .contentType(MediaType.APPLICATION_JSON) + .session(session) + .content(objectMapper.writeValueAsString(loginRequest))) + .andExpect(status().isOk()); + + // when & then - 로그아웃 수행 + mockMvc + .perform( + post(getApiUrlForDocs("/v0/auth/logout")) + .contentType(MediaType.APPLICATION_JSON) + .session(session) + .header("Origin", "https://admin.icebang.site") + .header("Referer", "https://admin.icebang.site/")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value("OK")) + .andExpect(jsonPath("$.message").value("OK")) + .andExpect(jsonPath("$.data").isEmpty()) + .andDo( + document( + "auth-logout", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Authentication") + .summary("사용자 로그아웃") + .description("현재 인증된 사용자의 세션을 무효화합니다") + .responseFields( + fieldWithPath("success") + .type(JsonFieldType.BOOLEAN) + .description("요청 성공 여부"), + fieldWithPath("data") + .type(JsonFieldType.NULL) + .description("응답 데이터 (로그아웃 성공 시 null)"), + fieldWithPath("message") + .type(JsonFieldType.STRING) + .description("응답 메시지"), + fieldWithPath("status") + .type(JsonFieldType.STRING) + .description("HTTP 상태")) + .build()))); + } }