Files
Gnuboard7/tests/Feature/Api/Auth/UserAuthControllerTest.php
T
2026-04-01 10:30:52 +09:00

1335 lines
42 KiB
PHP

<?php
namespace Tests\Feature\Api\Auth;
use App\Models\PasswordResetToken;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use App\Models\UserConsent;
use App\Notifications\Auth\ResetPasswordNotification;
use App\Notifications\Auth\WelcomeNotification;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;
/**
* UserAuthController 테스트
*
* 사용자 인증 API 엔드포인트를 테스트합니다.
*/
class UserAuthControllerTest extends TestCase
{
use RefreshDatabase;
/**
* 권한이 부여된 사용자 생성 헬퍼
*/
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' => [
'id',
'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);
}
/**
* 회원가입 시 '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);
}
/**
* 회원가입 시 환영 알림이 발송되는지 확인
*/
public function test_register_sends_welcome_notification(): void
{
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, WelcomeNotification::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 = \App\Models\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
{
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, ResetPasswordNotification::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
{
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);
// 알림이 2번 발송됨 확인
Notification::assertSentToTimes($user, ResetPasswordNotification::class, 2);
}
/**
* 비밀번호 찾기 요청 시 redirect_prefix=admin 전달 성공
*/
public function test_forgot_password_accepts_redirect_prefix_admin(): void
{
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, ResetPasswordNotification::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
{
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, ResetPasswordNotification::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']);
}
}