enableMailChannelReadiness(); } /** * 알림 시스템은 notification_definitions 시드 + 동적 훅 재등록 필요 * * 부팅 시점에 DB 가 비어있어 registerDynamicHooks 가 실행되었지만 아무것도 등록하지 못했으므로 * 테스트 시 시드 후 수동 재등록이 필요 (이 시점엔 중복 등록 없음). */ private function seedNotificationDefinitions(): void { $this->artisan('db:seed', ['--class' => 'NotificationDefinitionSeeder']); app(NotificationHookListener::class)->registerDynamicHooks(); } /** * 권한이 부여된 사용자 생성 헬퍼 */ private function createUserWithPermissions(array $attributes = [], array $permissions = []): User { $user = User::factory()->create($attributes); if (! empty($permissions)) { $permissionIds = []; foreach ($permissions as $permIdentifier) { $permission = Permission::firstOrCreate( ['identifier' => $permIdentifier], [ 'name' => json_encode(['ko' => $permIdentifier, 'en' => $permIdentifier]), 'description' => json_encode(['ko' => $permIdentifier, 'en' => $permIdentifier]), 'extension_type' => 'core', 'extension_identifier' => 'core', 'type' => 'user', ] ); $permissionIds[] = $permission->id; } $testRole = Role::create([ 'identifier' => 'user_test_'.uniqid(), 'name' => json_encode(['ko' => '테스트', 'en' => 'Test']), 'is_active' => true, ]); $testRole->permissions()->sync($permissionIds); $user->roles()->attach($testRole->id, ['assigned_at' => now()]); $user = $user->fresh(); } return $user; } /** * JSON 요청 헬퍼 메서드 */ private function jsonRequest(): static { return $this->withHeaders([ 'Accept' => 'application/json', ]); } /** * 인증된 요청 헬퍼 메서드 */ private function authRequest(User $user): static { $token = $user->createToken('test-token')->plainTextToken; return $this->withHeaders([ 'Authorization' => 'Bearer '.$token, 'Accept' => 'application/json', ]); } // ======================================================================== // 로그인 테스트 (login) // ======================================================================== /** * 유효한 자격 증명으로 로그인 성공 */ public function test_login_succeeds_with_valid_credentials(): void { $user = User::factory()->create([ 'email' => 'test@example.com', 'password' => bcrypt('password123'), ]); $response = $this->jsonRequest()->postJson('/api/auth/login', [ 'email' => 'test@example.com', 'password' => 'password123', ]); $response->assertStatus(200) ->assertJson(['success' => true]); } /** * 로그인 성공 시 토큰과 사용자 정보 반환 */ public function test_login_returns_token_and_user(): void { $user = User::factory()->create([ 'email' => 'test@example.com', 'password' => bcrypt('password123'), ]); $response = $this->jsonRequest()->postJson('/api/auth/login', [ 'email' => 'test@example.com', 'password' => 'password123', ]); $response->assertStatus(200) ->assertJsonStructure([ 'success', 'message', 'data' => [ 'token', 'user' => [ 'uuid', 'name', 'email', ], ], ]); } /** * 잘못된 이메일로 로그인 실패 */ public function test_login_fails_with_invalid_email(): void { User::factory()->create([ 'email' => 'test@example.com', 'password' => bcrypt('password123'), ]); $response = $this->jsonRequest()->postJson('/api/auth/login', [ 'email' => 'wrong@example.com', 'password' => 'password123', ]); $response->assertStatus(401); } /** * 잘못된 비밀번호로 로그인 실패 */ public function test_login_fails_with_invalid_password(): void { User::factory()->create([ 'email' => 'test@example.com', 'password' => bcrypt('password123'), ]); $response = $this->jsonRequest()->postJson('/api/auth/login', [ 'email' => 'test@example.com', 'password' => 'wrongpassword', ]); $response->assertStatus(401); } /** * 이메일 필수 검증 */ public function test_login_validates_email_required(): void { $response = $this->jsonRequest()->postJson('/api/auth/login', [ 'password' => 'password123', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['email']); } /** * 비밀번호 필수 검증 */ public function test_login_validates_password_required(): void { $response = $this->jsonRequest()->postJson('/api/auth/login', [ 'email' => 'test@example.com', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['password']); } /** * 이메일 형식 검증 */ public function test_login_validates_email_format(): void { $response = $this->jsonRequest()->postJson('/api/auth/login', [ 'email' => 'invalid-email', 'password' => 'password123', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['email']); } // ======================================================================== // 회원가입 테스트 (register) // ======================================================================== /** * 회원가입 성공 */ public function test_register_creates_user_successfully(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'newuser@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(201) ->assertJson(['success' => true]); $this->assertDatabaseHas('users', [ 'email' => 'newuser@example.com', ]); } /** * 회원가입 시 user_consents 테이블에 동의 이력이 기록되는지 확인 */ public function test_register_stores_consent_records(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'agreement@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'agree_terms' => true, 'agree_privacy' => true, 'terms_version' => 3, 'privacy_version' => 2, ]); $response->assertStatus(201); $user = User::where('email', 'agreement@example.com')->first(); $this->assertNotNull($user); $this->assertDatabaseHas('user_consents', [ 'user_id' => $user->id, 'consent_type' => 'terms', ]); $this->assertDatabaseHas('user_consents', [ 'user_id' => $user->id, 'consent_type' => 'privacy', ]); // users 테이블에 agreed_terms_at 컬럼 없음 확인 $this->assertArrayNotHasKey('agreed_terms_at', $user->toArray()); $this->assertArrayNotHasKey('agreed_privacy_at', $user->toArray()); } /** * 버전 미전달 시 version=0으로 동의 이력이 기록되는지 확인 */ public function test_register_stores_consent_without_version(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'noversion@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(201); $user = User::where('email', 'noversion@example.com')->first(); $this->assertNotNull($user); // 버전 파라미터 없이도 동의 이력이 정상 기록되는지 확인 $this->assertDatabaseHas('user_consents', [ 'user_id' => $user->id, 'consent_type' => 'terms', ]); $this->assertDatabaseHas('user_consents', [ 'user_id' => $user->id, 'consent_type' => 'privacy', ]); } /** * 회원가입 시 동의 이력에 IP 주소가 기록되는지 확인 */ public function test_register_stores_ip_address_in_consent(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'consentip@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(201); $user = User::where('email', 'consentip@example.com')->first(); $this->assertNotNull($user); $consent = UserConsent::where('user_id', $user->id) ->where('consent_type', 'terms') ->first(); $this->assertNotNull($consent); $this->assertNotNull($consent->ip_address); $this->assertNotNull($consent->agreed_at); } /** * 회원가입 시 닉네임이 선택 사항인지 확인 */ public function test_register_nickname_is_optional(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'nickname' => '테스트닉네임', 'email' => 'withnickname@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(201); $user = User::where('email', 'withnickname@example.com')->first(); $this->assertNotNull($user); $this->assertEquals('테스트닉네임', $user->nickname); } /** * 회원가입 시 언어 설정이 저장되는지 확인 */ public function test_register_stores_language_preference(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'english@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'language' => 'en', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(201); $user = User::where('email', 'english@example.com')->first(); $this->assertNotNull($user); $this->assertEquals('en', $user->language); } /** * 회원가입 시 휴대폰/전화번호가 저장되는지 확인 */ public function test_register_stores_mobile_and_phone(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'contact@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'mobile' => '010-1234-5678', 'phone' => '02-123-4567', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(201); $this->assertDatabaseHas('users', [ 'email' => 'contact@example.com', 'mobile' => '010-1234-5678', 'phone' => '02-123-4567', ]); } /** * 휴대폰/전화번호는 선택 항목이므로 미입력 시에도 가입에 성공한다 */ public function test_register_mobile_and_phone_are_optional(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'nocontact@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(201); $user = User::where('email', 'nocontact@example.com')->first(); $this->assertNotNull($user); $this->assertNull($user->mobile); $this->assertNull($user->phone); } /** * 휴대폰번호 형식 위반 시 검증 실패(422) */ public function test_register_validates_mobile_format(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'badmobile@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'mobile' => '휴대폰없음', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(422) ->assertJsonValidationErrors(['mobile']); $this->assertDatabaseMissing('users', [ 'email' => 'badmobile@example.com', ]); } /** * 회원가입 시 'user' 역할 자동 할당 */ public function test_register_assigns_user_role_automatically(): void { // 'user' 역할이 존재해야 함 (RolePermissionSeeder에서 생성) $this->artisan('db:seed', ['--class' => 'RolePermissionSeeder']); $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'roletest@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(201); $user = User::where('email', 'roletest@example.com')->first(); $this->assertNotNull($user); $this->assertTrue($user->roles()->where('identifier', 'user')->exists()); } /** * 이름 필수 검증 */ public function test_register_validates_name_required(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'email' => 'newuser@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(422) ->assertJsonValidationErrors(['name']); } /** * 이용약관 동의 필수 검증 */ public function test_register_validates_agree_terms_required(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'newuser@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'agree_privacy' => true, ]); $response->assertStatus(422) ->assertJsonValidationErrors(['agree_terms']); } /** * 개인정보처리방침 동의 필수 검증 */ public function test_register_validates_agree_privacy_required(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'newuser@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'agree_terms' => true, ]); $response->assertStatus(422) ->assertJsonValidationErrors(['agree_privacy']); } /** * 이메일 고유성 검증 */ public function test_register_validates_email_unique(): void { User::factory()->create([ 'email' => 'existing@example.com', ]); $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'existing@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(422) ->assertJsonValidationErrors(['email']); } /** * 비밀번호 확인 검증 */ public function test_register_validates_password_confirmed(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'newuser@example.com', 'password' => 'password123', 'password_confirmation' => 'differentpassword', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(422) ->assertJsonValidationErrors(['password']); } /** * 비밀번호 최소 길이 검증 (8자 미만 실패) */ public function test_register_validates_password_min_length(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'newuser@example.com', 'password' => 'short', 'password_confirmation' => 'short', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(422) ->assertJsonValidationErrors(['password']); } /** * 비밀번호 7자는 실패 (최소 8자 경계값 테스트) */ public function test_register_validates_password_min_8_characters(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'newuser@example.com', 'password' => 'pass123', // 7자 'password_confirmation' => 'pass123', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(422) ->assertJsonValidationErrors(['password']); } /** * 비밀번호 8자는 성공 (최소 8자 경계값 테스트) */ public function test_register_accepts_password_with_exactly_8_characters(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'newuser@example.com', 'password' => 'pass1234', // 8자 'password_confirmation' => 'pass1234', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(201); } /** * 회원가입 시 country 필드가 Accept-Language 헤더로부터 자동 저장되는지 확인 */ public function test_register_stores_country_from_accept_language(): void { $response = $this->withHeaders([ 'Accept' => 'application/json', 'Accept-Language' => 'ko-KR,ko;q=0.9,en-US;q=0.8', ])->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'country@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(201); $user = User::where('email', 'country@example.com')->first(); $this->assertNotNull($user); $this->assertEquals('KR', $user->country); } /** * 회원가입 시 ip_address가 자동 저장되는지 확인 */ public function test_register_stores_ip_address(): void { $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'iptest@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(201); $user = User::where('email', 'iptest@example.com')->first(); $this->assertNotNull($user); $this->assertNotNull($user->ip_address); } /** * 회원가입 시 환영 알림이 발송되는지 확인 * * 알림 시스템은 DB 기반 동적 훅 구독(NotificationHookListener::registerDynamicHooks) * 이므로 notification_definitions + notification_templates 시딩 필수. */ public function test_register_sends_welcome_notification(): void { $this->seedNotificationDefinitions(); Notification::fake(); $response = $this->jsonRequest()->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'welcome@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(201); $user = User::where('email', 'welcome@example.com')->first(); $this->assertNotNull($user); // 환영 알림 발송 확인 Notification::assertSentTo($user, GenericNotification::class); } /** * Accept-Language 헤더가 빈 문자열일 때 country가 null인지 확인 */ public function test_register_stores_null_country_with_empty_accept_language(): void { $response = $this->withHeaders([ 'Accept' => 'application/json', 'Accept-Language' => '', ])->postJson('/api/auth/register', [ 'name' => '테스트 사용자', 'email' => 'nocountry@example.com', 'password' => 'password123', 'password_confirmation' => 'password123', 'agree_terms' => true, 'agree_privacy' => true, ]); $response->assertStatus(201); $user = User::where('email', 'nocountry@example.com')->first(); $this->assertNotNull($user); $this->assertNull($user->country); } // ======================================================================== // 로그아웃 테스트 (logout) // ======================================================================== /** * 현재 토큰만 무효화하여 로그아웃 */ public function test_logout_invalidates_current_token(): void { $user = $this->createUserWithPermissions([], ['core.auth.logout']); $response = $this->authRequest($user)->postJson('/api/user/auth/logout'); $response->assertStatus(200) ->assertJson(['success' => true]); } /** * 인증 없이 로그아웃 시 401 반환 */ public function test_logout_returns_401_without_authentication(): void { $response = $this->jsonRequest()->postJson('/api/user/auth/logout'); $response->assertStatus(401); } // ======================================================================== // 전체 디바이스 로그아웃 테스트 (logoutFromAllDevices) // ======================================================================== /** * 모든 디바이스에서 로그아웃 */ public function test_logout_all_devices_invalidates_all_tokens(): void { $user = $this->createUserWithPermissions([], ['core.auth.logout']); // 여러 토큰 생성 $user->createToken('device-1'); $user->createToken('device-2'); $token = $user->createToken('current-device')->plainTextToken; $response = $this->withHeaders([ 'Authorization' => 'Bearer '.$token, 'Accept' => 'application/json', ])->postJson('/api/user/auth/logout-all-devices'); $response->assertStatus(200) ->assertJson(['success' => true]); // 모든 토큰이 삭제되었는지 확인 $user->refresh(); $this->assertCount(0, $user->tokens); } // ======================================================================== // 토큰 갱신 테스트 (refresh) // ======================================================================== /** * 토큰 갱신 시 새 토큰 반환 */ public function test_refresh_returns_new_token(): void { $user = $this->createUserWithPermissions([], ['core.auth.refresh']); $response = $this->authRequest($user)->postJson('/api/user/auth/refresh'); $response->assertStatus(200) ->assertJson(['success' => true]) ->assertJsonStructure([ 'success', 'message', 'data' => [ 'token', ], ]); } /** * 인증 없이 토큰 갱신 시 401 반환 */ public function test_refresh_returns_401_without_authentication(): void { $response = $this->jsonRequest()->postJson('/api/user/auth/refresh'); $response->assertStatus(401); } // ======================================================================== // 현재 사용자 정보 테스트 (user) // ======================================================================== /** * 인증된 사용자 정보 반환 */ public function test_user_returns_authenticated_user_info(): void { $user = $this->createUserWithPermissions([ 'name' => '테스트 사용자', 'email' => 'test@example.com', ], ['core.auth.user']); $response = $this->authRequest($user)->getJson('/api/user/auth/user'); $response->assertStatus(200) ->assertJson(['success' => true]) ->assertJsonPath('data.email', 'test@example.com'); } /** * 인증 없이 사용자 정보 조회 시 401 반환 */ public function test_user_returns_401_without_authentication(): void { $response = $this->jsonRequest()->getJson('/api/user/auth/user'); $response->assertStatus(401); } // ======================================================================== // 공용 인증 라우트 테스트 (/api/auth/*) // ======================================================================== /** * /api/auth/user 공용 라우트로 인증된 사용자 정보 반환 */ public function test_auth_user_returns_authenticated_user_info(): void { $user = User::factory()->create([ 'name' => '테스트 사용자', 'email' => 'test@example.com', ]); $response = $this->authRequest($user)->getJson('/api/auth/user'); $response->assertStatus(200) ->assertJson(['success' => true]) ->assertJsonPath('data.email', 'test@example.com'); } /** * /api/auth/user 공용 라우트 - 인증 없이 접근 시 401 반환 */ public function test_auth_user_returns_401_without_authentication(): void { $response = $this->jsonRequest()->getJson('/api/auth/user'); $response->assertStatus(401); } /** * /api/auth/logout 공용 라우트로 로그아웃 성공 */ public function test_auth_logout_invalidates_token(): void { $user = User::factory()->create(); $response = $this->authRequest($user)->postJson('/api/auth/logout'); $response->assertStatus(200) ->assertJson(['success' => true]); } /** * /api/auth/logout 공용 라우트 - 인증 없이 접근 시 401 반환 */ public function test_auth_logout_returns_401_without_authentication(): void { $response = $this->jsonRequest()->postJson('/api/auth/logout'); $response->assertStatus(401); } /** * /api/auth/user 응답에 is_admin 필드 포함 확인 (일반 사용자) */ public function test_auth_user_returns_is_admin_false_for_regular_user(): void { $user = User::factory()->create(); $response = $this->authRequest($user)->getJson('/api/auth/user'); $response->assertStatus(200) ->assertJsonPath('data.is_admin', false); } /** * /api/auth/user 응답에 is_admin 필드 포함 확인 (관리자) */ public function test_auth_user_returns_is_admin_true_for_admin_user(): void { // 권한 시더 실행 $this->artisan('db:seed', ['--class' => 'RolePermissionSeeder']); $user = User::factory()->create(); // admin 역할 할당 $adminRole = Role::where('identifier', 'admin')->first(); if ($adminRole) { $user->roles()->sync([$adminRole->id]); } $response = $this->authRequest($user)->getJson('/api/auth/user'); $response->assertStatus(200) ->assertJsonPath('data.is_admin', true); } // ======================================================================== // 비밀번호 찾기 테스트 (forgotPassword) // ======================================================================== /** * 비밀번호 찾기 요청 시 이메일 발송 성공 */ public function test_forgot_password_sends_email(): void { $this->seedNotificationDefinitions(); Notification::fake(); $user = User::factory()->create([ 'email' => 'test@example.com', ]); $response = $this->jsonRequest()->postJson('/api/auth/forgot-password', [ 'email' => 'test@example.com', ]); $response->assertStatus(200) ->assertJson(['success' => true]); // 토큰이 DB에 저장되었는지 확인 $this->assertDatabaseHas('password_reset_tokens', [ 'email' => 'test@example.com', ]); // 알림 발송 확인 Notification::assertSentTo($user, GenericNotification::class); } /** * 미등록 이메일로 비밀번호 찾기 요청 시 실패 */ public function test_forgot_password_fails_with_unregistered_email(): void { $response = $this->jsonRequest()->postJson('/api/auth/forgot-password', [ 'email' => 'nonexistent@example.com', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['email']); } /** * 비밀번호 찾기 요청 시 이메일 형식 검증 */ public function test_forgot_password_validates_email_format(): void { $response = $this->jsonRequest()->postJson('/api/auth/forgot-password', [ 'email' => 'invalid-email', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['email']); } /** * 중복 비밀번호 찾기 요청 시 토큰 갱신 */ public function test_forgot_password_updates_existing_token(): void { $this->seedNotificationDefinitions(); Notification::fake(); $user = User::factory()->create([ 'email' => 'test@example.com', ]); // 첫 번째 요청 $this->jsonRequest()->postJson('/api/auth/forgot-password', [ 'email' => 'test@example.com', ]); $firstToken = PasswordResetToken::where('email', 'test@example.com')->first(); $firstCreatedAt = $firstToken->created_at; // 시간을 약간 앞으로 $this->travel(1)->minutes(); // 두 번째 요청 $this->jsonRequest()->postJson('/api/auth/forgot-password', [ 'email' => 'test@example.com', ]); $secondToken = PasswordResetToken::where('email', 'test@example.com')->first(); // 토큰이 갱신되었는지 확인 (created_at이 다름) $this->assertNotEquals($firstCreatedAt->timestamp, $secondToken->created_at->timestamp); // reset_password 정의는 ['mail', 'database'] 2채널 × 2회 요청 = 4회 발송 // (NotificationHookListener::dispatch 참조) Notification::assertSentToTimes($user, GenericNotification::class, 4); } /** * 비밀번호 찾기 요청 시 redirect_prefix=admin 전달 성공 */ public function test_forgot_password_accepts_redirect_prefix_admin(): void { $this->seedNotificationDefinitions(); Notification::fake(); $user = User::factory()->create([ 'email' => 'admin@example.com', ]); $response = $this->jsonRequest()->postJson('/api/auth/forgot-password', [ 'email' => 'admin@example.com', 'redirect_prefix' => 'admin', ]); $response->assertStatus(200) ->assertJson(['success' => true]); Notification::assertSentTo($user, GenericNotification::class); } /** * 비밀번호 찾기 요청 시 잘못된 redirect_prefix 거부 */ public function test_forgot_password_rejects_invalid_redirect_prefix(): void { User::factory()->create([ 'email' => 'test@example.com', ]); $response = $this->jsonRequest()->postJson('/api/auth/forgot-password', [ 'email' => 'test@example.com', 'redirect_prefix' => 'invalid', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['redirect_prefix']); } /** * 비밀번호 찾기 요청 시 redirect_prefix 없이도 정상 동작 (하위 호환성) */ public function test_forgot_password_works_without_redirect_prefix(): void { $this->seedNotificationDefinitions(); Notification::fake(); $user = User::factory()->create([ 'email' => 'test@example.com', ]); $response = $this->jsonRequest()->postJson('/api/auth/forgot-password', [ 'email' => 'test@example.com', ]); $response->assertStatus(200) ->assertJson(['success' => true]); Notification::assertSentTo($user, GenericNotification::class); } // ======================================================================== // 비밀번호 재설정 테스트 (resetPassword) // ======================================================================== /** * 비밀번호 재설정 성공 */ public function test_reset_password_succeeds(): void { $user = User::factory()->create([ 'email' => 'test@example.com', ]); $token = 'valid-reset-token-12345678901234567890123456789012345678901234'; // 토큰 저장 (해시로) PasswordResetToken::create([ 'email' => 'test@example.com', 'token' => Hash::make($token), 'created_at' => now(), ]); $response = $this->jsonRequest()->postJson('/api/auth/reset-password', [ 'token' => $token, 'email' => 'test@example.com', 'password' => 'newpassword123', 'password_confirmation' => 'newpassword123', ]); $response->assertStatus(200) ->assertJson(['success' => true]); // 비밀번호가 변경되었는지 확인 $user->refresh(); $this->assertTrue(Hash::check('newpassword123', $user->password)); // 토큰이 삭제되었는지 확인 $this->assertDatabaseMissing('password_reset_tokens', [ 'email' => 'test@example.com', ]); } /** * 잘못된 토큰으로 비밀번호 재설정 실패 */ public function test_reset_password_fails_with_invalid_token(): void { $user = User::factory()->create([ 'email' => 'test@example.com', ]); $token = 'valid-reset-token-12345678901234567890123456789012345678901234'; PasswordResetToken::create([ 'email' => 'test@example.com', 'token' => Hash::make($token), 'created_at' => now(), ]); $response = $this->jsonRequest()->postJson('/api/auth/reset-password', [ 'token' => 'wrong-token', 'email' => 'test@example.com', 'password' => 'newpassword123', 'password_confirmation' => 'newpassword123', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['token']); } /** * 만료된 토큰으로 비밀번호 재설정 실패 */ public function test_reset_password_fails_with_expired_token(): void { $user = User::factory()->create([ 'email' => 'test@example.com', ]); $token = 'valid-reset-token-12345678901234567890123456789012345678901234'; // 61분 전에 생성된 토큰 (만료됨) PasswordResetToken::create([ 'email' => 'test@example.com', 'token' => Hash::make($token), 'created_at' => now()->subMinutes(61), ]); $response = $this->jsonRequest()->postJson('/api/auth/reset-password', [ 'token' => $token, 'email' => 'test@example.com', 'password' => 'newpassword123', 'password_confirmation' => 'newpassword123', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['token']); // 만료된 토큰이 삭제되었는지 확인 $this->assertDatabaseMissing('password_reset_tokens', [ 'email' => 'test@example.com', ]); } /** * 미등록 이메일로 비밀번호 재설정 실패 */ public function test_reset_password_fails_with_unregistered_email(): void { $response = $this->jsonRequest()->postJson('/api/auth/reset-password', [ 'token' => 'some-token', 'email' => 'nonexistent@example.com', 'password' => 'newpassword123', 'password_confirmation' => 'newpassword123', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['email']); } /** * 비밀번호 확인 불일치 시 실패 */ public function test_reset_password_fails_with_password_mismatch(): void { $user = User::factory()->create([ 'email' => 'test@example.com', ]); $response = $this->jsonRequest()->postJson('/api/auth/reset-password', [ 'token' => 'some-token', 'email' => 'test@example.com', 'password' => 'newpassword123', 'password_confirmation' => 'differentpassword', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['password']); } /** * 토큰이 존재하지 않을 때 비밀번호 재설정 실패 */ public function test_reset_password_fails_when_no_token_exists(): void { $user = User::factory()->create([ 'email' => 'test@example.com', ]); // 토큰을 생성하지 않음 $response = $this->jsonRequest()->postJson('/api/auth/reset-password', [ 'token' => 'some-token', 'email' => 'test@example.com', 'password' => 'newpassword123', 'password_confirmation' => 'newpassword123', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['token']); } // ======================================================================== // 비밀번호 재설정 토큰 검증 테스트 (validateResetToken) // ======================================================================== /** * 유효한 토큰으로 검증 성공 */ public function test_validate_reset_token_succeeds_with_valid_token(): void { $user = User::factory()->create([ 'email' => 'test@example.com', ]); $token = 'valid-reset-token-12345678901234567890123456789012345678901234'; PasswordResetToken::create([ 'email' => 'test@example.com', 'token' => Hash::make($token), 'created_at' => now(), ]); $response = $this->jsonRequest()->postJson('/api/auth/validate-reset-token', [ 'token' => $token, 'email' => 'test@example.com', ]); $response->assertStatus(200) ->assertJson([ 'success' => true, 'data' => [ 'valid' => true, ], ]); } /** * 잘못된 토큰으로 검증 실패 */ public function test_validate_reset_token_fails_with_invalid_token(): void { $user = User::factory()->create([ 'email' => 'test@example.com', ]); $token = 'valid-reset-token-12345678901234567890123456789012345678901234'; PasswordResetToken::create([ 'email' => 'test@example.com', 'token' => Hash::make($token), 'created_at' => now(), ]); $response = $this->jsonRequest()->postJson('/api/auth/validate-reset-token', [ 'token' => 'wrong-token', 'email' => 'test@example.com', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['token']); } /** * 만료된 토큰으로 검증 실패 */ public function test_validate_reset_token_fails_with_expired_token(): void { $user = User::factory()->create([ 'email' => 'test@example.com', ]); $token = 'valid-reset-token-12345678901234567890123456789012345678901234'; // 61분 전에 생성된 토큰 (만료됨) PasswordResetToken::create([ 'email' => 'test@example.com', 'token' => Hash::make($token), 'created_at' => now()->subMinutes(61), ]); $response = $this->jsonRequest()->postJson('/api/auth/validate-reset-token', [ 'token' => $token, 'email' => 'test@example.com', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['token']); // 만료된 토큰이 삭제되었는지 확인 $this->assertDatabaseMissing('password_reset_tokens', [ 'email' => 'test@example.com', ]); } /** * 미등록 이메일로 토큰 검증 실패 */ public function test_validate_reset_token_fails_with_unregistered_email(): void { $response = $this->jsonRequest()->postJson('/api/auth/validate-reset-token', [ 'token' => 'some-token', 'email' => 'nonexistent@example.com', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['token']); } /** * 토큰이 존재하지 않을 때 검증 실패 */ public function test_validate_reset_token_fails_when_no_token_exists(): void { $user = User::factory()->create([ 'email' => 'test@example.com', ]); // 토큰을 생성하지 않음 $response = $this->jsonRequest()->postJson('/api/auth/validate-reset-token', [ 'token' => 'some-token', 'email' => 'test@example.com', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['token']); } /** * 토큰 필수 검증 */ public function test_validate_reset_token_validates_token_required(): void { $response = $this->jsonRequest()->postJson('/api/auth/validate-reset-token', [ 'email' => 'test@example.com', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['token']); } /** * 이메일 필수 검증 */ public function test_validate_reset_token_validates_email_required(): void { $response = $this->jsonRequest()->postJson('/api/auth/validate-reset-token', [ 'token' => 'some-token', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['email']); } /** * 이메일 형식 검증 */ public function test_validate_reset_token_validates_email_format(): void { $response = $this->jsonRequest()->postJson('/api/auth/validate-reset-token', [ 'token' => 'some-token', 'email' => 'invalid-email', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['email']); } }