Files
Gnuboard7/tests/Feature/Auth/UserStatusAccessTest.php
2026-05-11 11:29:41 +09:00

473 lines
15 KiB
PHP

<?php
namespace Tests\Feature\Auth;
use App\Enums\ExtensionOwnerType;
use App\Enums\UserStatus;
use App\Extension\HookManager;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\PersonalAccessToken;
use Tests\TestCase;
/**
* 사용자 상태(UserStatus) 기반 접근 제한 테스트
*
* - 로그인 시 상태 체크
* - CheckUserStatus 미들웨어
* - 일괄 상태 변경 시 토큰/타임스탬프
* - 개별 상태 변경 시 토큰/타임스탬프
*/
class UserStatusAccessTest extends TestCase
{
use RefreshDatabase;
private User $admin;
private string $adminToken;
protected function setUp(): void
{
parent::setUp();
$this->admin = $this->createAdminUser();
$this->adminToken = $this->admin->createToken('test-token')->plainTextToken;
}
protected function tearDown(): void
{
HookManager::clearAction('sirsoft-core.user.before_bulk_update');
HookManager::clearAction('sirsoft-core.user.after_bulk_update');
parent::tearDown();
}
/**
* 관리자 사용자 생성 헬퍼
*
* @return User 관리자 사용자
*/
private function createAdminUser(): User
{
$user = User::factory()->create();
$permission = Permission::firstOrCreate(
['identifier' => 'core.users.update'],
[
'name' => json_encode(['ko' => '사용자 수정', 'en' => 'Update Users']),
'description' => json_encode(['ko' => '사용자 정보 수정 권한', 'en' => 'Permission to update users']),
'extension_type' => ExtensionOwnerType::Core,
'extension_identifier' => 'core',
'type' => 'admin',
]
);
$adminRole = Role::firstOrCreate(
['identifier' => 'admin'],
[
'name' => json_encode(['ko' => '관리자', 'en' => 'Administrator']),
'description' => json_encode(['ko' => '시스템 관리자', 'en' => 'System Administrator']),
'extension_type' => ExtensionOwnerType::Core,
'extension_identifier' => 'core',
'type' => 'admin',
'is_active' => true,
]
);
if (! $adminRole->permissions()->where('permissions.id', $permission->id)->exists()) {
$adminRole->permissions()->attach($permission->id);
}
$user->roles()->attach($adminRole->id, [
'assigned_at' => now(),
'assigned_by' => null,
]);
return $user->fresh();
}
/**
* 관리자 인증 요청 헬퍼
*
* @return static
*/
private function adminRequest(): static
{
return $this->withHeaders([
'Authorization' => 'Bearer '.$this->adminToken,
'Accept' => 'application/json',
]);
}
/**
* JSON 요청 헬퍼
*
* @return static
*/
private function jsonRequest(): static
{
return $this->withHeaders([
'Accept' => 'application/json',
]);
}
// ========================================================================
// A. 로그인 상태 체크 (4개)
// ========================================================================
/**
* Active 사용자는 로그인할 수 있다.
*/
public function test_active_user_can_login(): void
{
User::factory()->create([
'email' => 'active@example.com',
'password' => bcrypt('password123'),
'status' => UserStatus::Active->value,
]);
$response = $this->jsonRequest()->postJson('/api/auth/login', [
'email' => 'active@example.com',
'password' => 'password123',
]);
$response->assertStatus(200)
->assertJson(['success' => true])
->assertJsonStructure(['data' => ['token']]);
}
/**
* Inactive 사용자는 로그인할 수 없다.
*
* AuthService에서 ValidationException을 던지지만,
* 컨트롤러에서 catch하여 401 unauthorized로 반환 (보안: 상태 미노출)
*/
public function test_inactive_user_cannot_login(): void
{
User::factory()->create([
'email' => 'inactive@example.com',
'password' => bcrypt('password123'),
'status' => UserStatus::Inactive->value,
]);
$response = $this->jsonRequest()->postJson('/api/auth/login', [
'email' => 'inactive@example.com',
'password' => 'password123',
]);
$response->assertStatus(401)
->assertJson(['success' => false]);
}
/**
* Blocked 사용자는 로그인할 수 없다.
*/
public function test_blocked_user_cannot_login(): void
{
User::factory()->create([
'email' => 'blocked@example.com',
'password' => bcrypt('password123'),
'status' => UserStatus::Blocked->value,
]);
$response = $this->jsonRequest()->postJson('/api/auth/login', [
'email' => 'blocked@example.com',
'password' => 'password123',
]);
$response->assertStatus(401)
->assertJson(['success' => false]);
}
/**
* Withdrawn 사용자는 로그인할 수 없다.
*/
public function test_withdrawn_user_cannot_login(): void
{
User::factory()->create([
'email' => 'withdrawn@example.com',
'password' => bcrypt('password123'),
'status' => UserStatus::Withdrawn->value,
]);
$response = $this->jsonRequest()->postJson('/api/auth/login', [
'email' => 'withdrawn@example.com',
'password' => 'password123',
]);
$response->assertStatus(401)
->assertJson(['success' => false]);
}
// ========================================================================
// B. CheckUserStatus 미들웨어 (4개)
// ========================================================================
/**
* Active 사용자는 API에 정상 접근할 수 있다.
*/
public function test_active_user_can_access_api(): void
{
$user = User::factory()->create([
'status' => UserStatus::Active->value,
]);
$token = $user->createToken('test-token')->plainTextToken;
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,
'Accept' => 'application/json',
])->getJson('/api/me');
$response->assertStatus(200);
}
/**
* Inactive 사용자는 API 접근 시 403 응답을 받는다.
*/
public function test_inactive_user_gets_403(): void
{
$user = User::factory()->create([
'status' => UserStatus::Active->value,
]);
$token = $user->createToken('test-token')->plainTextToken;
// 토큰 생성 후 상태 변경 (미들웨어 테스트용)
$user->update(['status' => UserStatus::Inactive->value]);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,
'Accept' => 'application/json',
])->getJson('/api/me');
$response->assertStatus(403);
}
/**
* Blocked 사용자는 API 접근 시 403 응답을 받는다.
*/
public function test_blocked_user_gets_403(): void
{
$user = User::factory()->create([
'status' => UserStatus::Active->value,
]);
$token = $user->createToken('test-token')->plainTextToken;
// 토큰 생성 후 상태 변경 (미들웨어 테스트용)
$user->update(['status' => UserStatus::Blocked->value]);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,
'Accept' => 'application/json',
])->getJson('/api/me');
$response->assertStatus(403);
}
/**
* Guest(미인증)는 optional.sanctum + check.user_status 를 통과하되,
* permission 이 필요한 엔드포인트에서는 401 을 받는다.
*
* /api/user/notifications 는 permission:user,core.user-notifications.read 필요 → guest 401.
* CheckUserStatus 자체는 guest 를 차단하지 않음을 확인하기 위해
* permission 없는 엔드포인트(/api/user/auth/validate-reset-token 등)로 대체하거나
* 단순히 CheckUserStatus 에서 발생하는 403 이 아님을 검증한다.
*/
public function test_guest_passes_middleware(): void
{
$response = $this->jsonRequest()->getJson('/api/user/notifications');
// CheckUserStatus 가 guest 를 403 으로 차단하지 않음 (permission 401 은 별개)
$this->assertNotEquals(403, $response->getStatusCode());
}
// ========================================================================
// C. bulkUpdateStatus 토큰/타임스탬프 (3개)
// ========================================================================
/**
* 일괄 차단 시 토큰이 삭제되고 blocked_at이 설정된다.
*/
public function test_bulk_block_deletes_tokens_sets_timestamp(): void
{
$users = User::factory()->count(2)->create([
'status' => UserStatus::Active->value,
]);
// 각 사용자에 토큰 생성
foreach ($users as $user) {
$user->createToken('user-token');
}
$uuids = $users->pluck('uuid')->toArray();
$intIds = $users->pluck('id')->toArray();
// 토큰이 존재하는지 확인
$tokenCount = PersonalAccessToken::where('tokenable_type', User::class)
->whereIn('tokenable_id', $intIds)
->count();
$this->assertEquals(2, $tokenCount);
// 일괄 차단
$response = $this->adminRequest()->patchJson('/api/admin/users/bulk-status', [
'ids' => $uuids,
'status' => UserStatus::Blocked->value,
]);
$response->assertOk();
// 토큰 삭제 확인
$tokenCount = PersonalAccessToken::where('tokenable_type', User::class)
->whereIn('tokenable_id', $intIds)
->count();
$this->assertEquals(0, $tokenCount);
// blocked_at 설정 확인
foreach ($users as $user) {
$user->refresh();
$this->assertEquals(UserStatus::Blocked->value, $user->status);
$this->assertNotNull($user->blocked_at);
}
}
/**
* 일괄 활성화 시 blocked_at/withdrawn_at이 초기화된다.
*/
public function test_bulk_activate_clears_timestamps(): void
{
$users = User::factory()->count(2)->create([
'status' => UserStatus::Blocked->value,
'blocked_at' => now(),
]);
$ids = $users->pluck('uuid')->toArray();
$response = $this->adminRequest()->patchJson('/api/admin/users/bulk-status', [
'ids' => $ids,
'status' => UserStatus::Active->value,
]);
$response->assertOk();
// 타임스탬프 초기화 확인
foreach ($users as $user) {
$user->refresh();
$this->assertEquals(UserStatus::Active->value, $user->status);
$this->assertNull($user->blocked_at);
$this->assertNull($user->withdrawn_at);
}
}
/**
* 일괄 비활성화 시 기존 타임스탬프가 유지된다.
*/
public function test_bulk_inactive_preserves_timestamps(): void
{
$blockedAt = now()->subDays(5);
$users = User::factory()->count(2)->create([
'status' => UserStatus::Active->value,
'blocked_at' => $blockedAt,
]);
$ids = $users->pluck('uuid')->toArray();
$response = $this->adminRequest()->patchJson('/api/admin/users/bulk-status', [
'ids' => $ids,
'status' => UserStatus::Inactive->value,
]);
$response->assertOk();
// Inactive 전환 시 타임스탬프 변경 없음 확인
foreach ($users as $user) {
$user->refresh();
$this->assertEquals(UserStatus::Inactive->value, $user->status);
// blocked_at은 기존 값 유지 (Inactive로 전환 시 건드리지 않음)
$this->assertNotNull($user->blocked_at);
}
}
// ========================================================================
// D. updateUser 토큰/타임스탬프 (2개)
// ========================================================================
/**
* 개별 사용자 상태를 Blocked로 변경하면 토큰이 삭제되고 blocked_at이 설정된다.
*/
public function test_update_status_to_blocked_deletes_tokens(): void
{
$user = User::factory()->create([
'status' => UserStatus::Active->value,
]);
// 사용자에게 역할 할당 (UpdateUserRequest 검증용)
$userRole = Role::firstOrCreate(
['identifier' => 'user_default'],
[
'name' => json_encode(['ko' => '일반 사용자', 'en' => 'Default User']),
'is_active' => true,
]
);
$user->roles()->attach($userRole->id, ['assigned_at' => now()]);
// 사용자 토큰 생성
$user->createToken('user-token');
$this->assertEquals(1, $user->tokens()->count());
// 관리자가 사용자 상태를 Blocked로 변경
$response = $this->adminRequest()->putJson("/api/admin/users/{$user->uuid}", [
'name' => $user->name,
'email' => $user->email,
'nickname' => $user->nickname,
'status' => UserStatus::Blocked->value,
'role_ids' => [$userRole->id],
]);
$response->assertOk();
// 토큰 삭제 확인
$user->refresh();
$this->assertEquals(0, $user->tokens()->count());
$this->assertNotNull($user->blocked_at);
}
/**
* 개별 사용자 상태를 Active로 변경하면 타임스탬프가 초기화된다.
*/
public function test_update_status_to_active_clears_timestamps(): void
{
$user = User::factory()->create([
'status' => UserStatus::Blocked->value,
'blocked_at' => now(),
]);
// 사용자에게 역할 할당 (UpdateUserRequest 검증용)
$userRole = Role::firstOrCreate(
['identifier' => 'user_default'],
[
'name' => json_encode(['ko' => '일반 사용자', 'en' => 'Default User']),
'is_active' => true,
]
);
$user->roles()->attach($userRole->id, ['assigned_at' => now()]);
// 관리자가 사용자 상태를 Active로 변경
$response = $this->adminRequest()->putJson("/api/admin/users/{$user->uuid}", [
'name' => $user->name,
'email' => $user->email,
'nickname' => $user->nickname,
'status' => UserStatus::Active->value,
'role_ids' => [$userRole->id],
]);
$response->assertOk();
// 타임스탬프 초기화 확인
$user->refresh();
$this->assertEquals(UserStatus::Active->value, $user->status);
$this->assertNull($user->blocked_at);
$this->assertNull($user->withdrawn_at);
}
}