Files
2026-04-01 10:30:52 +09:00

667 lines
22 KiB
PHP

<?php
namespace Tests\Feature;
use App\Enums\ExtensionOwnerType;
use App\Enums\PermissionType;
use App\Extension\HookManager;
use App\Models\Attachment;
use App\Models\Permission;
use App\Models\PermissionHook;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
/**
* 첨부파일 기능 테스트
*/
class AttachmentTest extends TestCase
{
use RefreshDatabase;
private Role $adminRole;
protected function setUp(): void
{
parent::setUp();
Storage::fake('local');
HookManager::resetAll();
// admin 역할 직접 생성 (시더 대신)
$this->adminRole = Role::firstOrCreate(
['identifier' => 'admin'],
[
'name' => ['ko' => '관리자', 'en' => 'Administrator'],
'description' => ['ko' => '시스템 관리자', 'en' => 'System administrator'],
'extension_type' => ExtensionOwnerType::Core,
'extension_identifier' => 'core',
'is_active' => true,
]
);
}
/**
* 사용자에게 admin 역할을 부여하는 헬퍼 메서드
*/
private function assignAdminRole(User $user): void
{
$user->roles()->attach($this->adminRole->id, [
'assigned_at' => now(),
'assigned_by' => null,
]);
}
/**
* admin 역할에 첨부파일 관련 권한을 부여하는 헬퍼 메서드
*/
private function grantAttachmentPermissionsToAdmin(): void
{
$permissionIdentifiers = [
'core.attachments.create',
'core.attachments.update',
'core.attachments.delete',
];
foreach ($permissionIdentifiers as $identifier) {
$permission = Permission::firstOrCreate(
['identifier' => $identifier],
[
'name' => json_encode(['ko' => $identifier, 'en' => $identifier]),
'description' => json_encode(['ko' => $identifier . ' 권한', 'en' => $identifier . ' Permission']),
'extension_type' => ExtensionOwnerType::Core,
'extension_identifier' => 'core',
'type' => 'admin',
'resource_route_key' => in_array($identifier, ['core.attachments.update', 'core.attachments.delete']) ? 'attachment' : null,
'owner_key' => in_array($identifier, ['core.attachments.update', 'core.attachments.delete']) ? 'created_by' : null,
]
);
// admin 역할에 권한 부여 (scope_type=null → 전체 접근)
if (! $this->adminRole->permissions()->where('identifier', $identifier)->exists()) {
$this->adminRole->permissions()->attach($permission->id, ['granted_at' => now()]);
}
}
}
/**
* 특정 권한을 가진 관리자 사용자를 생성하는 헬퍼 메서드
*
* @param array $permissions 권한 식별자 배열
* @param string|null $scopeType 스코프 타입 (null: 전체, 'self': 본인만)
* @return User 생성된 사용자
*/
private function createAdminWithPermissions(array $permissions, ?string $scopeType = null): User
{
static $roleCounter = 0;
$roleCounter++;
$user = User::factory()->create();
// 테스트용 역할 생성 (admin 역할과 별도)
$testRole = Role::create([
'identifier' => 'attachment_test_' . $roleCounter . '_' . uniqid(),
'name' => json_encode(['ko' => '첨부파일 테스트', 'en' => 'Attachment Test']),
'description' => json_encode(['ko' => '테스트용', 'en' => 'For testing']),
'extension_type' => ExtensionOwnerType::Core,
'extension_identifier' => 'core',
'is_active' => true,
]);
// 권한 생성 및 역할에 연결
foreach ($permissions as $identifier) {
$permission = Permission::firstOrCreate(
['identifier' => $identifier],
[
'name' => json_encode(['ko' => $identifier, 'en' => $identifier]),
'description' => json_encode(['ko' => $identifier . ' 권한', 'en' => $identifier . ' Permission']),
'extension_type' => ExtensionOwnerType::Core,
'extension_identifier' => 'core',
'type' => 'admin',
'resource_route_key' => in_array($identifier, ['core.attachments.update', 'core.attachments.delete']) ? 'attachment' : null,
'owner_key' => in_array($identifier, ['core.attachments.update', 'core.attachments.delete']) ? 'created_by' : null,
]
);
// scope_type이 있는 경우 피벗에 설정
$pivotData = ['granted_at' => now()];
if ($scopeType !== null) {
$pivotData['scope_type'] = $scopeType;
}
$testRole->permissions()->attach($permission->id, $pivotData);
}
// 사용자에게 테스트 역할 부여
$user->roles()->attach($testRole->id, [
'assigned_at' => now(),
'assigned_by' => null,
]);
return $user->fresh();
}
// =============================================
// 업로드 테스트
// =============================================
/**
* 파일 업로드 성공 테스트
*/
public function test_파일_업로드_성공(): void
{
$this->grantAttachmentPermissionsToAdmin();
$user = User::factory()->create();
$this->assignAdminRole($user);
$file = UploadedFile::fake()->image('test.jpg', 800, 600);
$response = $this->actingAs($user)
->postJson('/api/admin/attachments', [
'file' => $file,
'collection' => 'images',
]);
$response->assertStatus(201)
->assertJsonStructure([
'data' => [
'id',
'hash',
'original_filename',
'mime_type',
'size',
'download_url',
],
]);
$this->assertDatabaseHas('attachments', [
'original_filename' => 'test.jpg',
'collection' => 'images',
]);
}
/**
* 일괄 업로드 성공 테스트
*/
public function test_일괄_업로드_성공(): void
{
$this->grantAttachmentPermissionsToAdmin();
$user = User::factory()->create();
$this->assignAdminRole($user);
$files = [
UploadedFile::fake()->image('test1.jpg'),
UploadedFile::fake()->image('test2.jpg'),
];
$response = $this->actingAs($user)
->postJson('/api/admin/attachments/batch', [
'files' => $files,
'collection' => 'gallery',
]);
$response->assertStatus(201);
$this->assertDatabaseCount('attachments', 2);
}
/**
* 관리자 아닌 사용자의 업로드 시도 403 테스트
*/
public function test_관리자_아닌_사용자_업로드_403(): void
{
$user = User::factory()->create();
// 관리자 역할 없음
$file = UploadedFile::fake()->image('test.jpg');
$response = $this->actingAs($user)
->postJson('/api/admin/attachments', [
'file' => $file,
]);
$response->assertStatus(403);
}
// =============================================
// 다운로드 테스트 (공개 API - HookManager만 체크)
// =============================================
/**
* 공개 파일 다운로드 성공 테스트 (비로그인)
*/
public function test_공개_파일_다운로드_비로그인_성공(): void
{
$owner = User::factory()->create();
$attachment = Attachment::factory()->uploadedBy($owner)->create();
Storage::disk($attachment->disk)->put($attachment->path, 'test content');
// 비로그인 사용자도 다운로드 가능 (HookManager 매핑 없음)
$response = $this->getJson("/api/attachment/{$attachment->hash}");
$response->assertOk();
}
/**
* 파일 다운로드 업로더 본인 테스트
*/
public function test_파일_다운로드_업로더_본인(): void
{
$user = User::factory()->create();
$attachment = Attachment::factory()->uploadedBy($user)->create();
Storage::disk($attachment->disk)->put($attachment->path, 'test content');
$response = $this->actingAs($user)
->get("/api/attachment/{$attachment->hash}");
$response->assertOk();
}
/**
* 파일 다운로드 관리자 테스트
*/
public function test_파일_다운로드_관리자(): void
{
$owner = User::factory()->create();
$admin = User::factory()->create();
$this->assignAdminRole($admin);
$attachment = Attachment::factory()->uploadedBy($owner)->create();
Storage::disk($attachment->disk)->put($attachment->path, 'test content');
$response = $this->actingAs($admin)
->get("/api/attachment/{$attachment->hash}");
$response->assertOk();
}
/**
* 존재하지 않는 파일 다운로드 404 테스트
*/
public function test_존재하지_않는_파일_다운로드_404(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->get('/api/attachment/nonexistent1');
$response->assertStatus(404);
}
// =============================================
// HookManager 권한 테스트 (공개 다운로드 기능 레벨)
// =============================================
/**
* 훅 권한 미매핑 시 모든 사용자 허용 테스트
*/
public function test_훅_권한_미매핑_시_모든_사용자_허용(): void
{
$owner = User::factory()->create();
$attachment = Attachment::factory()->uploadedBy($owner)->create();
Storage::disk($attachment->disk)->put($attachment->path, 'test content');
// permission_hooks에 매핑 없음 → 모든 사용자 허용
$response = $this->getJson("/api/attachment/{$attachment->hash}");
$response->assertOk();
}
/**
* 훅 권한 매핑 시 권한 있는 사용자 허용 테스트
*/
public function test_훅_권한_매핑_시_권한_있는_사용자_허용(): void
{
// 권한 생성
$permission = Permission::create([
'identifier' => 'attachment.download',
'name' => ['ko' => '첨부파일 다운로드', 'en' => 'Download Attachment'],
'type' => PermissionType::Admin,
]);
// 훅에 권한 매핑
PermissionHook::create([
'permission_id' => $permission->id,
'hook_name' => 'core.attachment.download',
]);
// 역할에 권한 부여
$role = Role::factory()->create();
$role->permissions()->attach($permission->id);
// 사용자에게 역할 부여
$user = User::factory()->create();
$user->roles()->attach($role->id);
$owner = User::factory()->create();
$attachment = Attachment::factory()->uploadedBy($owner)->create();
Storage::disk($attachment->disk)->put($attachment->path, 'test content');
$response = $this->actingAs($user)
->get("/api/attachment/{$attachment->hash}");
$response->assertOk();
}
/**
* 훅 권한 매핑 시 권한 없는 사용자 차단 테스트
*/
public function test_훅_권한_매핑_시_권한_없는_사용자_차단(): void
{
// 권한 생성
$permission = Permission::create([
'identifier' => 'attachment.download',
'name' => ['ko' => '첨부파일 다운로드', 'en' => 'Download Attachment'],
'type' => PermissionType::Admin,
]);
// 훅에 권한 매핑
PermissionHook::create([
'permission_id' => $permission->id,
'hook_name' => 'core.attachment.download',
]);
// 사용자에게 권한 미부여
$user = User::factory()->create();
$owner = User::factory()->create();
$attachment = Attachment::factory()->uploadedBy($owner)->create();
Storage::disk($attachment->disk)->put($attachment->path, 'test content');
$response = $this->actingAs($user)
->get("/api/attachment/{$attachment->hash}");
$response->assertStatus(403);
}
/**
* 훅 권한 매핑 시 비로그인 사용자 차단 테스트
*/
public function test_훅_권한_매핑_시_비로그인_사용자_차단(): void
{
// 권한 생성
$permission = Permission::create([
'identifier' => 'attachment.download',
'name' => ['ko' => '첨부파일 다운로드', 'en' => 'Download Attachment'],
'type' => PermissionType::Admin,
]);
// 훅에 권한 매핑
PermissionHook::create([
'permission_id' => $permission->id,
'hook_name' => 'core.attachment.download',
]);
$owner = User::factory()->create();
$attachment = Attachment::factory()->uploadedBy($owner)->create();
Storage::disk($attachment->disk)->put($attachment->path, 'test content');
// 비로그인 상태
$response = $this->getJson("/api/attachment/{$attachment->hash}");
$response->assertStatus(403);
}
/**
* 훅 권한 매핑 시 관리자 허용 테스트
*/
public function test_훅_권한_매핑_시_관리자_허용(): void
{
// admin 역할에 admin 타입 권한 부여 (isAdmin() = true 조건)
$this->grantAttachmentPermissionsToAdmin();
// 다운로드 훅에 별도 권한 매핑
$permission = Permission::create([
'identifier' => 'attachment.download',
'name' => ['ko' => '첨부파일 다운로드', 'en' => 'Download Attachment'],
'type' => PermissionType::Admin,
]);
// 훅에 권한 매핑
PermissionHook::create([
'permission_id' => $permission->id,
'hook_name' => 'core.attachment.download',
]);
// 관리자 (isAdmin()=true이므로 훅 권한 체크 bypass)
$admin = User::factory()->create();
$this->assignAdminRole($admin);
$owner = User::factory()->create();
$attachment = Attachment::factory()->uploadedBy($owner)->create();
Storage::disk($attachment->disk)->put($attachment->path, 'test content');
$response = $this->actingAs($admin)
->get("/api/attachment/{$attachment->hash}");
$response->assertOk();
}
// =============================================
// 관리자 CRUD 권한 테스트 (PermissionMiddleware)
// =============================================
/**
* 권한 있는 관리자 파일 삭제 성공 테스트
*/
public function test_권한_있는_관리자_파일_삭제_성공(): void
{
$user = $this->createAdminWithPermissions(['core.attachments.delete']);
$attachment = Attachment::factory()->create();
Storage::disk($attachment->disk)->put($attachment->path, 'test content');
$response = $this->actingAs($user)
->deleteJson("/api/admin/attachments/{$attachment->id}");
$response->assertOk();
$this->assertDatabaseMissing('attachments', ['id' => $attachment->id]);
}
/**
* scope_type=self 본인 파일 삭제 성공 테스트
*/
public function test_scope_self_본인_파일_삭제_성공(): void
{
$user = $this->createAdminWithPermissions(['core.attachments.delete'], 'self');
// 본인이 업로드한 파일
$attachment = Attachment::factory()->create(['created_by' => $user->id]);
Storage::disk($attachment->disk)->put($attachment->path, 'test content');
$response = $this->actingAs($user)
->deleteJson("/api/admin/attachments/{$attachment->id}");
$response->assertOk();
$this->assertDatabaseMissing('attachments', ['id' => $attachment->id]);
}
/**
* scope_type=self 타인 파일 삭제 403 테스트
*/
public function test_scope_self_타인_파일_삭제_403(): void
{
$user = $this->createAdminWithPermissions(['core.attachments.delete'], 'self');
// 타인이 업로드한 파일
$otherUser = User::factory()->create();
$attachment = Attachment::factory()->create(['created_by' => $otherUser->id]);
Storage::disk($attachment->disk)->put($attachment->path, 'test content');
$response = $this->actingAs($user)
->deleteJson("/api/admin/attachments/{$attachment->id}");
$response->assertStatus(403);
}
// =============================================
// 순서 변경 테스트
// =============================================
/**
* 파일 순서 변경 성공 테스트
*/
public function test_파일_순서_변경_성공(): void
{
$this->grantAttachmentPermissionsToAdmin();
$user = User::factory()->create();
$this->assignAdminRole($user);
$attachments = Attachment::factory()->count(3)->create();
$orderData = [
['id' => $attachments[2]->id, 'order' => 1],
['id' => $attachments[0]->id, 'order' => 2],
['id' => $attachments[1]->id, 'order' => 3],
];
$response = $this->actingAs($user)
->patchJson('/api/admin/attachments/reorder', [
'order' => $orderData,
]);
$response->assertOk();
$this->assertDatabaseHas('attachments', [
'id' => $attachments[2]->id,
'order' => 1,
]);
}
/**
* 파일 삭제 성공 테스트 (admin 역할)
*/
public function test_파일_삭제_성공(): void
{
$this->grantAttachmentPermissionsToAdmin();
$user = User::factory()->create();
$this->assignAdminRole($user);
$attachment = Attachment::factory()->create();
Storage::disk($attachment->disk)->put($attachment->path, 'test content');
$response = $this->actingAs($user)
->deleteJson("/api/admin/attachments/{$attachment->id}");
$response->assertOk();
// 첨부파일은 forceDelete로 영구 삭제됨
$this->assertDatabaseMissing('attachments', ['id' => $attachment->id]);
}
// =============================================
// 모델 속성 테스트
// =============================================
/**
* is_image 속성 테스트
*/
public function test_is_image_속성(): void
{
$imageAttachment = Attachment::factory()->image()->create();
$pdfAttachment = Attachment::factory()->pdf()->create();
$this->assertTrue($imageAttachment->is_image);
$this->assertFalse($pdfAttachment->is_image);
}
/**
* size_formatted 속성 테스트
*/
public function test_size_formatted_속성(): void
{
$attachment = Attachment::factory()->create(['size' => 1536]);
$this->assertEquals('1.5 KB', $attachment->size_formatted);
}
/**
* download_url 속성 테스트
*/
public function test_download_url_속성(): void
{
$attachment = Attachment::factory()->create(['hash' => 'abc123def456']);
$this->assertEquals('/api/attachment/abc123def456', $attachment->download_url);
}
// =============================================
// 이미지 캐싱 테스트
// =============================================
/**
* 이미지 다운로드 시 캐싱 헤더 포함 테스트
*/
public function test_이미지_다운로드_캐싱_헤더_포함(): void
{
$user = User::factory()->create();
$attachment = Attachment::factory()->image()->uploadedBy($user)->create();
Storage::disk($attachment->disk)->put($attachment->path, 'fake image content');
$response = $this->actingAs($user)
->get("/api/attachment/{$attachment->hash}");
$response->assertOk();
// 캐싱 헤더 확인
$response->assertHeader('ETag');
// Cache-Control 헤더 존재 확인 (개발 환경에서는 no-cache일 수 있음)
$this->assertTrue($response->headers->has('Cache-Control'));
}
/**
* 비이미지 파일 다운로드 시 기존 방식 유지 테스트
*/
public function test_비이미지_파일_다운로드_기존_방식(): void
{
$user = User::factory()->create();
$attachment = Attachment::factory()->pdf()->uploadedBy($user)->create();
Storage::disk($attachment->disk)->put($attachment->path, 'fake pdf content');
$response = $this->actingAs($user)
->get("/api/attachment/{$attachment->hash}");
$response->assertOk();
// Content-Disposition이 attachment (다운로드)인지 확인
$contentDisposition = $response->headers->get('Content-Disposition');
$this->assertStringContainsString('attachment', $contentDisposition);
}
/**
* 이미지 304 Not Modified 응답 테스트
*/
public function test_이미지_304_not_modified_응답(): void
{
$user = User::factory()->create();
$attachment = Attachment::factory()->image()->uploadedBy($user)->create();
Storage::disk($attachment->disk)->put($attachment->path, 'fake image content');
// 첫 번째 요청으로 ETag 획득
$firstResponse = $this->actingAs($user)
->get("/api/attachment/{$attachment->hash}");
$firstResponse->assertOk();
$etag = $firstResponse->headers->get('ETag');
// ETag와 함께 두 번째 요청
$secondResponse = $this->actingAs($user)
->withHeader('If-None-Match', $etag)
->get("/api/attachment/{$attachment->hash}");
// 304 Not Modified 응답 확인
$secondResponse->assertStatus(304);
}
}