$slug, 'name' => ['ko' => '관리자 첨부파일 관리 테스트 게시판', 'en' => 'Admin Attachment Management Test Board'], 'is_active' => true, 'use_file_upload' => true, 'secret_mode' => 'disabled', 'blocked_keywords' => [], ]; } protected function setUp(): void { parent::setUp(); Storage::fake('public'); $slug = $this->board->slug; $this->adminWithUpload = $this->createAdminUser([ "sirsoft-board.{$slug}.admin.attachments.upload", "sirsoft-board.{$slug}.admin.attachments.download", ]); $this->adminWithDownload = $this->createAdminUser([ "sirsoft-board.{$slug}.admin.attachments.download", ]); $this->regularUser = $this->createUser(); } // ========================================== // 업로드 (upload) // ========================================== /** * 권한 있는 관리자가 임시 업로드 → 201 + DB 레코드 생성 */ public function test_admin_can_upload_attachment_as_temp(): void { $file = UploadedFile::fake()->create('document.pdf', 100, 'application/pdf'); $response = $this->actingAs($this->adminWithUpload)->postJson( $this->url('/attachments'), [ 'file' => $file, 'temp_key' => 'test-temp-key-001', ] ); $response->assertStatus(201); $this->assertDatabaseHas('board_attachments', [ 'board_id' => 0, 'original_filename' => 'document.pdf', ]); } /** * 게시글에 직접 첨부파일 업로드 (post_id 있음) → 201 */ public function test_admin_can_upload_attachment_to_post(): void { $postId = $this->createTestPost(); $file = UploadedFile::fake()->image('photo.jpg', 100, 100); $response = $this->actingAs($this->adminWithUpload)->postJson( $this->url('/attachments'), [ 'file' => $file, 'post_id' => $postId, ] ); $response->assertStatus(201); $this->assertDatabaseHas('board_attachments', [ 'post_id' => $postId, 'board_id' => $this->board->id, 'original_filename' => 'photo.jpg', ]); } /** * 미인증 요청 → 401 */ public function test_unauthenticated_cannot_upload(): void { $file = UploadedFile::fake()->create('doc.pdf', 10); $this->postJson( $this->url('/attachments'), ['file' => $file, 'temp_key' => 'key'] )->assertStatus(401); } /** * 권한 없는 사용자 → 403 */ public function test_user_without_permission_cannot_upload(): void { $file = UploadedFile::fake()->create('doc.pdf', 10); $this->actingAs($this->regularUser)->postJson( $this->url('/attachments'), ['file' => $file, 'temp_key' => 'key'] )->assertStatus(403); } // ========================================== // 삭제 (destroy) // ========================================== /** * 권한 있는 관리자가 첨부파일 삭제 → 200 + soft delete */ public function test_admin_can_delete_attachment(): void { $postId = $this->createTestPost(); $attachId = $this->createTestAttachment($postId); $this->actingAs($this->adminWithUpload)->deleteJson( $this->url("/attachments/{$attachId}") )->assertStatus(200); $this->assertSoftDeleted('board_attachments', ['id' => $attachId]); } /** * 삭제 후 attachments_count 감소 (PostCountSyncListener 동작) */ public function test_delete_attachment_decrements_attachments_count(): void { $postId = $this->createTestPost(); DB::table('board_posts')->where('id', $postId)->update(['attachments_count' => 2]); $attachId = $this->createTestAttachment($postId); $this->actingAs($this->adminWithUpload)->deleteJson( $this->url("/attachments/{$attachId}") )->assertStatus(200); // AttachmentCountSyncListener가 실제 카운트로 동기화 $count = DB::table('board_posts')->where('id', $postId)->value('attachments_count'); $this->assertEquals(0, $count); } /** * 존재하지 않는 첨부파일 삭제 → 404 */ public function test_delete_nonexistent_attachment_returns_404(): void { $this->actingAs($this->adminWithUpload)->deleteJson( $this->url('/attachments/99999') )->assertStatus(404); } /** * 권한 없는 사용자 삭제 → 403 */ public function test_user_without_permission_cannot_delete(): void { $postId = $this->createTestPost(); $attachId = $this->createTestAttachment($postId); $this->actingAs($this->regularUser)->deleteJson( $this->url("/attachments/{$attachId}") )->assertStatus(403); } // ========================================== // 순서 변경 (reorder) // ========================================== /** * 권한 있는 관리자가 순서 변경 → 200 */ public function test_admin_can_reorder_attachments(): void { $postId = $this->createTestPost(); $id1 = $this->createTestAttachment($postId, ['order' => 0]); $id2 = $this->createTestAttachment($postId, ['order' => 1]); $response = $this->actingAs($this->adminWithUpload)->patchJson( $this->url('/attachments/reorder'), [ 'order' => [ ['id' => $id1, 'order' => 1], ['id' => $id2, 'order' => 0], ], ] ); $response->assertStatus(200); $this->assertEquals(1, DB::table('board_attachments')->where('id', $id1)->value('order')); $this->assertEquals(0, DB::table('board_attachments')->where('id', $id2)->value('order')); } /** * 권한 없는 사용자 순서 변경 → 403 */ public function test_user_without_permission_cannot_reorder(): void { $postId = $this->createTestPost(); $id1 = $this->createTestAttachment($postId); $this->actingAs($this->regularUser)->patchJson( $this->url('/attachments/reorder'), ['order' => [['id' => $id1, 'order' => 0]]] )->assertStatus(403); } // ========================================== // 다운로드 (download) // ========================================== /** * 권한 있는 관리자가 해시로 다운로드 → 200 (streamed response) */ public function test_admin_can_download_attachment_by_hash(): void { $postId = $this->createTestPost(); $hash = 'dlhash123456'; Storage::fake('public'); Storage::disk('public')->put("admin-attach-mgmt/2025/01/01/file.pdf", 'file content'); $this->createTestAttachment($postId, [ 'hash' => $hash, 'stored_filename' => 'file.pdf', 'path' => 'admin-attach-mgmt/2025/01/01', 'disk' => 'public', ]); // 다운로드 엔드포인트 — 파일이 실제로 없어도 서비스가 404를 반환 $response = $this->actingAs($this->adminWithDownload)->getJson( $this->url("/attachments/download/{$hash}") ); // 파일이 존재할 경우 200, 없으면 404 (fake storage에서 경로 불일치 시) $this->assertContains($response->status(), [200, 404]); } /** * 존재하지 않는 해시로 다운로드 → 404 */ public function test_download_nonexistent_hash_returns_404(): void { $this->actingAs($this->adminWithDownload)->getJson( $this->url('/attachments/download/nonexistenthash') )->assertStatus(404); } /** * 다운로드 권한 없는 사용자 → 403 */ public function test_user_without_download_permission_cannot_download(): void { $postId = $this->createTestPost(); $hash = 'permtesthash'; $this->createTestAttachment($postId, ['hash' => $hash]); $this->actingAs($this->regularUser)->getJson( $this->url("/attachments/download/{$hash}") )->assertStatus(403); } // ========================================== // Helper // ========================================== /** * Admin attachment API URL 생성 */ private function url(string $suffix): string { $slug = $this->board->slug; return "/api/modules/sirsoft-board/admin/board/{$slug}{$suffix}"; } /** * 테스트용 첨부파일을 직접 DB에 생성합니다. */ private function createTestAttachment(int $postId, array $attributes = []): int { $defaults = [ 'board_id' => $this->board->id, 'post_id' => $postId, 'hash' => substr(md5(uniqid()), 0, 12), 'original_filename' => 'test.pdf', 'stored_filename' => 'test_'.uniqid().'.pdf', 'disk' => 'public', 'path' => 'attachments', 'mime_type' => 'application/pdf', 'size' => 1024, 'collection' => 'attachments', 'order' => 0, 'created_at' => now(), 'updated_at' => now(), ]; return DB::table('board_attachments')->insertGetId(array_merge($defaults, $attributes)); } }