Files
Gnuboard7/tests/Feature/Api/Auth/UserAuthControllerTest.php
chym1217 cd11fc60ce feat(auth): 회원가입 시 휴대폰·전화번호 선택 입력 추가
마이페이지·관리자 회원 폼에는 이미 연락처 필드가 있으나 회원가입 폼에만
빠져 두 화면이 불일치했다. DB·모델·Resource는 기존 존재하므로 회원가입
경로에만 검증·저장·입력 UI를 연결한다. 연락처는 선택 항목이다.
2026-07-28 12:40:12 +09:00

1441 lines
45 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Tests\Feature\Api\Auth;
use App\Listeners\NotificationHookListener;
use App\Models\PasswordResetToken;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use App\Models\UserConsent;
use App\Notifications\GenericNotification;
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;
/**
* 테스트 환경 준비
*
* 이 클래스는 가입 환영/비밀번호 재설정 등 메일 발송을 단언하므로 mail 채널을
* 준비 완료 상태로 만든다. 기본 테스트 설정은 from_address 가 플레이스홀더라
* ChannelReadinessService 가 not-ready 로 판정해 mail 채널이 제외된다.
*/
protected function setUp(): void
{
parent::setUp();
$this->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']);
}
}