where('identifier', 'like', 'sirsoft-board.test-board-%')->delete(); DB::table('boards')->where('slug', 'like', 'test-board-%')->delete(); DB::statement('SET FOREIGN_KEY_CHECKS=1'); // 이전 테스트의 훅 정리 (정적 속성은 테스트 간 유지됨) $this->clearHooks(); // 관리자 사용자 생성 (faker 이메일 사용: 하드코딩 시 DDL 커밋으로 다음 테스트와 충돌) $this->adminUser = User::factory()->create([ 'name' => 'Admin User', ]); $this->actingAs($this->adminUser); // BoardService 인스턴스 생성 $this->boardService = app(BoardService::class); } /** * 테스트 종료 후 훅 정리 */ protected function tearDown(): void { $this->clearHooks(); parent::tearDown(); } /** * HookManager의 정적 속성과 Laravel Event 리스너를 초기화합니다. */ private function clearHooks(): void { // 리플렉션으로 HookManager의 정적 속성 초기화 $reflection = new \ReflectionClass(HookManager::class); $hooksProperty = $reflection->getProperty('hooks'); $hooksProperty->setAccessible(true); $hooksProperty->setValue(null, []); $filtersProperty = $reflection->getProperty('filters'); $filtersProperty->setAccessible(true); $filtersProperty->setValue(null, []); // Laravel 이벤트 시스템에서도 훅 관련 리스너 제거 Event::forget('hook.sirsoft-board.board.before_create'); Event::forget('hook.sirsoft-board.board.filter_create_data'); Event::forget('hook.sirsoft-board.board.after_create'); Event::forget('hook.sirsoft-board.permissions.after_create'); Event::forget('hook.sirsoft-board.board.before_update'); Event::forget('hook.sirsoft-board.board.filter_update_data'); Event::forget('hook.sirsoft-board.board.after_update'); Event::forget('hook.sirsoft-board.board.before_delete'); Event::forget('hook.sirsoft-board.board.after_delete'); Event::forget('hook.sirsoft-board.permissions.after_delete'); Event::forget('hook.sirsoft-board.board.before_copy'); Event::forget('hook.sirsoft-board.board.filter_copy_data'); } /** * HookManager의 정적 배열에만 직접 훅을 등록합니다. * (Laravel Event 중복 실행 방지용) */ private function addHookDirectly(string $hookName, callable $callback, int $priority = 10): void { $reflection = new \ReflectionClass(HookManager::class); $hooksProperty = $reflection->getProperty('hooks'); $hooksProperty->setAccessible(true); $hooks = $hooksProperty->getValue(null) ?? []; if (!isset($hooks[$hookName])) { $hooks[$hookName] = []; } if (!isset($hooks[$hookName][$priority])) { $hooks[$hookName][$priority] = []; } $hooks[$hookName][$priority][] = $callback; $hooksProperty->setValue(null, $hooks); } /** * 게시판 생성 전 before_create 훅이 실행되는지 테스트 */ public function test_before_create_hook_is_called(): void { $hookCalled = false; HookManager::addAction('sirsoft-board.board.before_create', function ($data) use (&$hookCalled) { $hookCalled = true; $this->assertIsArray($data); $this->assertArrayHasKey('slug', $data); $this->assertArrayHasKey('name', $data); }); $board = $this->boardService->createBoard([ 'slug' => 'test-board-1', 'name' => ['ko' => '테스트 게시판 1', 'en' => 'Test Board 1'], 'type' => 'default', ]); $this->assertInstanceOf(Board::class, $board); $this->assertTrue($hookCalled, 'before_create 훅이 호출되지 않았습니다.'); } /** * 게시판 생성 시 filter_create_data 훅으로 데이터를 변형할 수 있는지 테스트 */ public function test_filter_create_data_hook_modifies_data(): void { // 필터 훅: 모든 게시판 이름에 [테스트] prefix 추가 HookManager::addFilter('sirsoft-board.board.filter_create_data', function ($data) { $data['name']['ko'] = '[테스트] '.$data['name']['ko']; $data['name']['en'] = '[Test] '.$data['name']['en']; return $data; }); $board = $this->boardService->createBoard([ 'slug' => 'test-board-2', 'name' => ['ko' => '원본 이름', 'en' => 'Original Name'], 'type' => 'default', ]); // 필터가 적용되었는지 확인 $this->assertNotNull($board); $this->assertEquals('[테스트] 원본 이름', $board->name['ko']); $this->assertEquals('[Test] Original Name', $board->name['en']); } /** * 게시판 생성 후 after_create 훅이 실행되는지 테스트 */ public function test_after_create_hook_is_called(): void { $hookCalled = false; $createdBoardId = null; HookManager::addAction('sirsoft-board.board.after_create', function ($board, $data) use (&$hookCalled, &$createdBoardId) { $hookCalled = true; $createdBoardId = $board->id; $this->assertInstanceOf(Board::class, $board); $this->assertIsArray($data); }); $board = $this->boardService->createBoard([ 'slug' => 'test-board-3', 'name' => ['ko' => '테스트 게시판 3', 'en' => 'Test Board 3'], 'type' => 'default', ]); $this->assertInstanceOf(Board::class, $board); $this->assertTrue($hookCalled, 'after_create 훅이 호출되지 않았습니다.'); $this->assertNotNull($createdBoardId); } /** * 권한 생성 후 permissions.after_create 훅이 실행되는지 테스트 */ public function test_permissions_after_create_hook_is_called(): void { $hookCalled = false; HookManager::addAction('sirsoft-board.permissions.after_create', function ($board) use (&$hookCalled) { $hookCalled = true; $this->assertInstanceOf(Board::class, $board); // 권한이 실제로 생성되었는지 확인 (board_permission_definitions 키 수 = 16개) $permissions = Permission::where('identifier', 'like', "sirsoft-board.{$board->slug}.%")->get(); $this->assertCount(16, $permissions); }); $board = $this->boardService->createBoard([ 'slug' => 'test-board-5', 'name' => ['ko' => '테스트 게시판 5', 'en' => 'Test Board 5'], 'type' => 'default', ]); $this->assertInstanceOf(Board::class, $board); $this->assertTrue($hookCalled, 'permissions.after_create 훅이 호출되지 않았습니다.'); } /** * 게시판 수정 시 before_update 훅이 실행되는지 테스트 */ public function test_before_update_hook_is_called(): void { $board = $this->createTestBoard('test-board-6'); $hookCalled = false; HookManager::addAction('sirsoft-board.board.before_update', function ($boardInstance, $data) use (&$hookCalled) { $hookCalled = true; $this->assertInstanceOf(Board::class, $boardInstance); $this->assertIsArray($data); }); $updatedBoard = $this->boardService->updateBoard($board->id, [ 'name' => ['ko' => '수정된 이름', 'en' => 'Modified Name'], ]); $this->assertInstanceOf(Board::class, $updatedBoard); $this->assertTrue($hookCalled, 'before_update 훅이 호출되지 않았습니다.'); } /** * 게시판 수정 시 filter_update_data 훅으로 데이터를 변형할 수 있는지 테스트 */ public function test_filter_update_data_hook_modifies_data(): void { $board = $this->createTestBoard('test-board-7'); // 필터 훅: 수정 시 자동으로 suffix 추가 HookManager::addFilter('sirsoft-board.board.filter_update_data', function ($data) { if (isset($data['name'])) { $data['name']['ko'] .= ' (수정됨)'; $data['name']['en'] .= ' (Modified)'; } return $data; }, 10); $updatedBoard = $this->boardService->updateBoard($board->id, [ 'name' => ['ko' => '새 이름', 'en' => 'New Name'], ]); $this->assertEquals('새 이름 (수정됨)', $updatedBoard->name['ko']); $this->assertEquals('New Name (Modified)', $updatedBoard->name['en']); } /** * 게시판 수정 후 after_update 훅이 실행되는지 테스트 */ public function test_after_update_hook_is_called(): void { $board = $this->createTestBoard('test-board-8'); $hookCalled = false; HookManager::addAction('sirsoft-board.board.after_update', function ($boardInstance, $data) use (&$hookCalled) { $hookCalled = true; $this->assertInstanceOf(Board::class, $boardInstance); $this->assertIsArray($data); }); $updatedBoard = $this->boardService->updateBoard($board->id, [ 'name' => ['ko' => '수정된 이름', 'en' => 'Modified Name'], ]); $this->assertInstanceOf(Board::class, $updatedBoard); $this->assertTrue($hookCalled, 'after_update 훅이 호출되지 않았습니다.'); } /** * 게시판 삭제 전 before_delete 훅이 실행되는지 테스트 */ public function test_before_delete_hook_is_called(): void { $board = $this->createTestBoard('test-board-9'); $hookCalled = false; HookManager::addAction('sirsoft-board.board.before_delete', function ($boardInstance) use (&$hookCalled) { $hookCalled = true; $this->assertInstanceOf(Board::class, $boardInstance); }); $this->boardService->deleteBoard($board->id); $this->assertTrue($hookCalled, 'before_delete 훅이 호출되지 않았습니다.'); } /** * 게시판 삭제 전 before_delete 훅에서 예외를 던져 삭제를 차단할 수 있는지 테스트 */ public function test_before_delete_hook_can_prevent_deletion(): void { $board = $this->createTestBoard('test-board-10'); // 삭제 차단 훅 HookManager::addAction('sirsoft-board.board.before_delete', function () { throw new \Exception('삭제가 차단되었습니다.'); }); $this->expectException(\Exception::class); $this->expectExceptionMessage('삭제가 차단되었습니다.'); $this->boardService->deleteBoard($board->id); } /** * 권한 삭제 후 permissions.after_delete 훅이 실행되는지 테스트 */ public function test_permissions_after_delete_hook_is_called(): void { $board = $this->createTestBoard('test-board-11'); $hookCalled = false; $deletedSlug = null; HookManager::addAction('sirsoft-board.permissions.after_delete', function ($slug) use (&$hookCalled, &$deletedSlug) { $hookCalled = true; $deletedSlug = $slug; $this->assertIsString($slug); // 권한이 실제로 삭제되었는지 확인 $permissions = Permission::where('identifier', 'like', "sirsoft-board.{$slug}.%")->get(); $this->assertCount(0, $permissions); }); $this->boardService->deleteBoard($board->id); $this->assertTrue($hookCalled, 'permissions.after_delete 훅이 호출되지 않았습니다.'); $this->assertEquals('test-board-11', $deletedSlug); } /** * 게시판 삭제 후 after_delete 훅이 실행되는지 테스트 */ public function test_after_delete_hook_is_called(): void { $board = $this->createTestBoard('test-board-13'); $hookCalled = false; HookManager::addAction('sirsoft-board.board.after_delete', function ($boardInstance) use (&$hookCalled) { $hookCalled = true; $this->assertInstanceOf(Board::class, $boardInstance); }); $this->boardService->deleteBoard($board->id); $this->assertTrue($hookCalled, 'after_delete 훅이 호출되지 않았습니다.'); } /** * 게시판 복사 전 before_copy 훅이 실행되는지 테스트 */ public function test_before_copy_hook_is_called(): void { $board = $this->createTestBoard('test-board-14'); $hookCalled = false; HookManager::addAction('sirsoft-board.board.before_copy', function ($originalBoard) use (&$hookCalled) { $hookCalled = true; $this->assertInstanceOf(Board::class, $originalBoard); }); $copyData = $this->boardService->copyBoard($board->id); $this->assertIsArray($copyData); $this->assertTrue($hookCalled, 'before_copy 훅이 호출되지 않았습니다.'); } /** * 게시판 복사 시 filter_copy_data 훅으로 데이터를 변형할 수 있는지 테스트 */ public function test_filter_copy_data_hook_modifies_data(): void { $board = $this->createTestBoard('test-board-15'); // 필터 훅: 복사 데이터에 메타 정보 추가 HookManager::addFilter('sirsoft-board.board.filter_copy_data', function ($copyData) { $copyData['description'] = ['ko' => '복사된 게시판입니다.', 'en' => 'This is a copied board.']; return $copyData; }, 10, 2); $copyData = $this->boardService->copyBoard($board->id); $this->assertArrayHasKey('description', $copyData); $this->assertEquals('복사된 게시판입니다.', $copyData['description']['ko']); $this->assertEquals('This is a copied board.', $copyData['description']['en']); } /** * 여러 훅이 우선순위에 따라 순차적으로 실행되는지 테스트 * * 참고: HookManager::addAction은 정적 배열과 Laravel Event 양쪽에 등록하여 * doAction에서 2번 실행되므로, 이 테스트에서는 정적 배열에만 직접 등록합니다. */ public function test_multiple_hooks_execute_in_priority_order(): void { $executionOrder = []; // 우선순위가 다른 3개의 훅을 정적 배열에만 직접 등록 (중복 실행 방지) $this->addHookDirectly('sirsoft-board.board.after_create', function () use (&$executionOrder) { $executionOrder[] = 'priority-20'; }, 20); $this->addHookDirectly('sirsoft-board.board.after_create', function () use (&$executionOrder) { $executionOrder[] = 'priority-5'; }, 5); $this->addHookDirectly('sirsoft-board.board.after_create', function () use (&$executionOrder) { $executionOrder[] = 'priority-10'; }, 10); $this->boardService->createBoard([ 'slug' => 'test-board-16', 'name' => ['ko' => '우선순위 테스트', 'en' => 'Priority Test'], 'type' => 'default', ]); // 우선순위가 낮은 순서대로 실행되어야 함 (5 -> 10 -> 20) $this->assertEquals(['priority-5', 'priority-10', 'priority-20'], $executionOrder); } /** * 테스트용 게시판 생성 헬퍼 메서드 */ private function createTestBoard(string $slug): Board { return $this->boardService->createBoard([ 'slug' => $slug, 'name' => ['ko' => "테스트 {$slug}", 'en' => "Test {$slug}"], 'type' => 'default', ]); } }