clearPermissionCache(); $this->admin = $this->createAdminUser(); $this->token = $this->admin->createToken('test-token')->plainTextToken; } protected function tearDown(): void { $this->clearPermissionCache(); parent::tearDown(); } /** * PermissionHelper 정적 캐시 초기화 * * @return void */ private function clearPermissionCache(): void { $reflection = new \ReflectionClass(PermissionHelper::class); $prop = $reflection->getProperty('permissionCache'); $prop->setAccessible(true); $prop->setValue(null, []); } /** * 관리자 역할 생성 및 할당 (스코프 지정 가능) * * @param array $permissions 사용자에게 부여할 권한 식별자 목록 * @param string|null $scopeType 스코프 타입 (null: 전체, 'self': 본인, 'role': 소유역할) * @return User */ private function createAdminUser(array $permissions = ['core.activities.read'], ?string $scopeType = null): User { $user = User::factory()->create([ 'password' => Hash::make('password123'), ]); // 권한 생성 $permissionIds = []; foreach ($permissions as $permIdentifier) { $attrs = [ 'name' => json_encode(['ko' => $permIdentifier, 'en' => $permIdentifier]), 'description' => json_encode(['ko' => $permIdentifier.' 권한', 'en' => $permIdentifier.' Permission']), 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', 'type' => 'admin', ]; // 스코프 지원 권한에 resource_route_key, owner_key 설정 if ($permIdentifier === 'core.activities.read') { $attrs['resource_route_key'] = 'activityLog'; $attrs['owner_key'] = 'user_id'; } $permission = Permission::firstOrCreate( ['identifier' => $permIdentifier], $attrs ); // firstOrCreate로 이미 존재하는 경우 owner_key가 없을 수 있으므로 업데이트 if ($permIdentifier === 'core.activities.read' && $permission->owner_key === null) { $permission->update([ 'resource_route_key' => 'activityLog', 'owner_key' => 'user_id', ]); } $permissionIds[] = $permission->id; } // 고유한 식별자로 역할 생성 $roleIdentifier = 'admin_test_'.uniqid(); $adminRole = Role::create([ 'identifier' => $roleIdentifier, 'name' => json_encode(['ko' => '테스트 관리자', 'en' => 'Test Administrator']), 'description' => json_encode(['ko' => '테스트 관리자', 'en' => 'Test Administrator']), 'is_active' => true, ]); // admin 역할 (admin 미들웨어 통과용) $adminBaseRole = Role::firstOrCreate( ['identifier' => 'admin'], [ 'name' => json_encode(['ko' => '관리자', 'en' => 'Administrator']), 'description' => json_encode(['ko' => '시스템 관리자', 'en' => 'System Administrator']), 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', 'is_active' => true, ] ); // 테스트용 역할에 권한 할당 (스코프 타입 포함) $syncData = []; foreach ($permissionIds as $permId) { $syncData[$permId] = ['scope_type' => $scopeType]; } $adminRole->permissions()->sync($syncData); // 사용자에게 admin 역할과 테스트용 역할 모두 할당 $user->roles()->attach($adminBaseRole->id, [ 'assigned_at' => now(), 'assigned_by' => null, ]); $user->roles()->attach($adminRole->id, [ 'assigned_at' => now(), 'assigned_by' => null, ]); return $user->fresh(); } /** * 인증 헤더와 함께 요청 * * @return $this */ private function authRequest(): static { return $this->withHeaders([ 'Authorization' => 'Bearer '.$this->token, 'Accept' => 'application/json', ]); } /** * 테스트용 활동 로그 생성 * * @param array $overrides 오버라이드할 속성 * @return ActivityLog */ private function createActivityLog(array $overrides = []): ActivityLog { return ActivityLog::create(array_merge([ 'log_type' => ActivityLogType::Admin, 'action' => 'user.update', 'description_key' => 'activity_log.description.user_update', 'description_params' => ['user_id' => 'abc-123'], 'loggable_type' => 'App\\Models\\User', 'loggable_id' => 1, 'user_id' => $this->admin->id, 'ip_address' => '127.0.0.1', ], $overrides)); } // ======================================================================== // 인증/권한 테스트 // ======================================================================== public function test_index_returns_401_without_authentication(): void { $response = $this->getJson('/api/admin/activity-logs'); $response->assertStatus(401); } public function test_index_returns_403_without_permission(): void { $user = $this->createAdminUser([]); $token = $user->createToken('test-token')->plainTextToken; $response = $this->withHeaders([ 'Authorization' => 'Bearer '.$token, 'Accept' => 'application/json', ])->getJson('/api/admin/activity-logs'); $response->assertStatus(403); } // ======================================================================== // index 엔드포인트 - 응답 구조 // ======================================================================== public function test_index_returns_data_and_pagination_structure(): void { $this->createActivityLog(); $this->createActivityLog(['action' => 'user.create']); $this->createActivityLog(['action' => 'user.delete']); $response = $this->authRequest()->getJson('/api/admin/activity-logs'); $response->assertStatus(200) ->assertJsonStructure([ 'success', 'message', 'data' => [ 'data', 'pagination' => [ 'current_page', 'last_page', 'per_page', 'total', 'from', 'to', 'has_more_pages', // 활동 로그는 상한을 건 집계라 총 건수가 잘릴 수 있다. 정확도 필드가 // 빠지면 잘린 값이 화면에서 정확한 건수로 읽힌다 (실측: 실제 88,792 건이 // "10000건" 으로 표기). 메타를 손으로 조립하면 이 필드들이 사라지므로 // 표준 메타(BaseApiCollection::paginationMeta)를 쓰는지 여기서 고정한다. 'total_relation', 'total_is_exact', 'result_cap', ], ], ]); $this->assertTrue( $response->json('data.pagination.total_is_exact'), '작은 표본에서는 총 건수가 정확해야 합니다.' ); } public function test_index_data_items_have_correct_fields(): void { $this->createActivityLog(); $response = $this->authRequest()->getJson('/api/admin/activity-logs'); $response->assertStatus(200); $data = $response->json('data.data'); $this->assertCount(1, $data); $item = $data[0]; $this->assertArrayHasKey('id', $item); $this->assertArrayHasKey('log_type', $item); $this->assertArrayHasKey('log_type_label', $item); $this->assertArrayHasKey('loggable_type', $item); $this->assertArrayHasKey('loggable_type_display', $item); $this->assertArrayHasKey('loggable_id', $item); $this->assertArrayHasKey('action', $item); $this->assertArrayHasKey('action_label', $item); $this->assertArrayHasKey('localized_description', $item); $this->assertArrayHasKey('actor_name', $item); $this->assertArrayHasKey('user', $item); $this->assertArrayHasKey('ip_address', $item); $this->assertArrayHasKey('created_at', $item); $this->assertArrayHasKey('changes', $item); $this->assertArrayHasKey('has_changes', $item); $this->assertArrayHasKey('bulk_changes', $item); $this->assertArrayHasKey('number', $item); } public function test_index_includes_loggable_type_display(): void { $this->createActivityLog([ 'loggable_type' => 'Modules\\Sirsoft\\Ecommerce\\Models\\Product', 'loggable_id' => 42, ]); $response = $this->authRequest()->getJson('/api/admin/activity-logs'); $response->assertStatus(200); $data = $response->json('data.data'); $this->assertEquals('Product', $data[0]['loggable_type_display']); } public function test_index_returns_empty_data_when_no_logs(): void { $response = $this->authRequest()->getJson('/api/admin/activity-logs'); $response->assertStatus(200); $this->assertCount(0, $response->json('data.data')); $this->assertEquals(0, $response->json('data.pagination.total')); } // ======================================================================== // index 엔드포인트 - 필터 // ======================================================================== public function test_index_filters_by_log_type_array(): void { $this->createActivityLog(['log_type' => ActivityLogType::Admin]); $this->createActivityLog(['log_type' => ActivityLogType::User]); $this->createActivityLog(['log_type' => ActivityLogType::System]); $response = $this->authRequest()->getJson('/api/admin/activity-logs?log_type[]=admin&log_type[]=user'); $response->assertStatus(200); $this->assertCount(2, $response->json('data.data')); } public function test_index_filters_by_single_log_type(): void { $this->createActivityLog(['log_type' => ActivityLogType::Admin]); $this->createActivityLog(['log_type' => ActivityLogType::User]); $this->createActivityLog(['log_type' => ActivityLogType::System]); $response = $this->authRequest()->getJson('/api/admin/activity-logs?log_type[]=admin'); $response->assertStatus(200); $this->assertCount(1, $response->json('data.data')); } public function test_index_filters_by_search_keyword(): void { $this->createActivityLog(['action' => 'user.create', 'ip_address' => '10.0.0.1']); $this->createActivityLog(['action' => 'product.update', 'ip_address' => '192.168.1.1']); $response = $this->authRequest()->getJson('/api/admin/activity-logs?search=product'); $response->assertStatus(200); $this->assertCount(1, $response->json('data.data')); } public function test_index_search_matches_ip_address(): void { $this->createActivityLog(['ip_address' => '10.0.0.1']); $this->createActivityLog(['ip_address' => '192.168.1.1']); $response = $this->authRequest()->getJson('/api/admin/activity-logs?search=192.168'); $response->assertStatus(200); $this->assertCount(1, $response->json('data.data')); } public function test_index_filters_by_search_type_action(): void { $this->createActivityLog(['action' => 'user.create', 'ip_address' => '10.0.0.1']); $this->createActivityLog(['action' => 'product.update', 'ip_address' => '192.168.1.1']); // search_type=action → action 필드에서만 검색 $response = $this->authRequest()->getJson('/api/admin/activity-logs?search=product&search_type=action'); $response->assertStatus(200); $this->assertCount(1, $response->json('data.data')); $this->assertEquals('product.update', $response->json('data.data.0.action')); } public function test_index_filters_by_search_type_ip_address(): void { $this->createActivityLog(['action' => 'user.create', 'ip_address' => '10.0.0.1']); $this->createActivityLog(['action' => 'product.update', 'ip_address' => '192.168.1.1']); // search_type=ip_address → ip_address 필드에서만 검색 $response = $this->authRequest()->getJson('/api/admin/activity-logs?search=192.168&search_type=ip_address'); $response->assertStatus(200); $this->assertCount(1, $response->json('data.data')); } public function test_index_search_type_action_does_not_match_ip(): void { $this->createActivityLog(['action' => 'user.create', 'ip_address' => '192.168.1.1']); // search_type=action → IP 주소로 검색해도 결과 없음 $response = $this->authRequest()->getJson('/api/admin/activity-logs?search=192.168&search_type=action'); $response->assertStatus(200); $this->assertCount(0, $response->json('data.data')); } public function test_index_filters_by_created_by_uuid(): void { $user1 = User::factory()->create(['name' => '홍길동']); $user2 = User::factory()->create(['name' => '김철수']); $this->createActivityLog(['user_id' => $user1->id]); $this->createActivityLog(['user_id' => $user2->id]); $response = $this->authRequest()->getJson('/api/admin/activity-logs?created_by='.$user1->uuid); $response->assertStatus(200); $this->assertCount(1, $response->json('data.data')); $this->assertEquals('홍길동', $response->json('data.data.0.actor_name')); } public function test_index_bilingual_search_finds_by_action_translation(): void { // action 'user.create' → 번역 키 'activity_log.action.user.create' 또는 'activity_log.action.create' $this->createActivityLog(['action' => 'user.create']); $this->createActivityLog(['action' => 'product.update']); // 영어 원문으로 검색 $response = $this->authRequest()->getJson('/api/admin/activity-logs?search=user.create&search_type=action'); $response->assertStatus(200); $this->assertCount(1, $response->json('data.data')); $this->assertEquals('user.create', $response->json('data.data.0.action')); } public function test_index_bilingual_search_finds_by_description_key(): void { $this->createActivityLog([ 'description_key' => 'activity_log.description.user_update', ]); $this->createActivityLog([ 'description_key' => 'activity_log.description.product_create', ]); // description_key 원문으로 검색 $response = $this->authRequest()->getJson('/api/admin/activity-logs?search=user_update&search_type=description'); $response->assertStatus(200); $this->assertCount(1, $response->json('data.data')); } public function test_index_includes_has_changes_field(): void { // changes가 있는 로그 (오래된 것) $this->createActivityLog([ 'changes' => [ ['field' => 'name', 'old' => '기존', 'new' => '변경'], ], 'created_at' => Carbon::parse('2026-01-01 10:00:00'), ]); // changes가 없는 로그 (최신) $this->createActivityLog([ 'changes' => null, 'created_at' => Carbon::parse('2026-01-02 10:00:00'), ]); $response = $this->authRequest()->getJson('/api/admin/activity-logs'); $response->assertStatus(200); $data = $response->json('data.data'); $this->assertCount(2, $data); // 최신순 정렬이므로 changes=null이 먼저 $this->assertFalse($data[0]['has_changes']); $this->assertTrue($data[1]['has_changes']); } public function test_index_filters_by_date_range(): void { $this->createActivityLog(['created_at' => Carbon::parse('2026-01-15')]); $this->createActivityLog(['created_at' => Carbon::parse('2026-02-15')]); $this->createActivityLog(['created_at' => Carbon::parse('2026-03-15')]); $response = $this->authRequest()->getJson('/api/admin/activity-logs?date_from=2026-02-01&date_to=2026-02-28'); $response->assertStatus(200); $this->assertCount(1, $response->json('data.data')); } public function test_index_filters_by_action(): void { $this->createActivityLog(['action' => 'user.create']); $this->createActivityLog(['action' => 'user.update']); $this->createActivityLog(['action' => 'user.delete']); $response = $this->authRequest()->getJson('/api/admin/activity-logs?action=user.create'); $response->assertStatus(200); $this->assertCount(1, $response->json('data.data')); } // ======================================================================== // index 엔드포인트 - 페이지네이션 // ======================================================================== public function test_index_respects_per_page_parameter(): void { for ($i = 0; $i < 5; $i++) { $this->createActivityLog(['action' => "action_{$i}"]); } $response = $this->authRequest()->getJson('/api/admin/activity-logs?per_page=2'); $response->assertStatus(200); $this->assertCount(2, $response->json('data.data')); $this->assertEquals(5, $response->json('data.pagination.total')); $this->assertEquals(3, $response->json('data.pagination.last_page')); } public function test_index_returns_row_numbers(): void { for ($i = 0; $i < 3; $i++) { $this->createActivityLog(['action' => "action_{$i}"]); } $response = $this->authRequest()->getJson('/api/admin/activity-logs'); $response->assertStatus(200); $data = $response->json('data.data'); // 내림차순 기본 정렬 → 번호는 total부터 역순 $this->assertEquals(3, $data[0]['number']); $this->assertEquals(2, $data[1]['number']); $this->assertEquals(1, $data[2]['number']); } // ======================================================================== // index 엔드포인트 - 정렬 // ======================================================================== public function test_index_sorts_by_created_at_desc_by_default(): void { $this->createActivityLog(['action' => 'old', 'created_at' => Carbon::parse('2026-01-01')]); $this->createActivityLog(['action' => 'new', 'created_at' => Carbon::parse('2026-03-01')]); $response = $this->authRequest()->getJson('/api/admin/activity-logs'); $response->assertStatus(200); $data = $response->json('data.data'); $this->assertEquals('new', $data[0]['action']); $this->assertEquals('old', $data[1]['action']); } public function test_index_sorts_ascending_when_requested(): void { $this->createActivityLog(['action' => 'old', 'created_at' => Carbon::parse('2026-01-01')]); $this->createActivityLog(['action' => 'new', 'created_at' => Carbon::parse('2026-03-01')]); $response = $this->authRequest()->getJson('/api/admin/activity-logs?sort_order=asc'); $response->assertStatus(200); $data = $response->json('data.data'); $this->assertEquals('old', $data[0]['action']); $this->assertEquals('new', $data[1]['action']); } // ======================================================================== // 비활성화 모듈 로그 안전 처리 // ======================================================================== public function test_index_handles_deactivated_module_loggable_type_safely(): void { // 비활성화된 모듈의 FQCN — 클래스 미존재 $this->createActivityLog([ 'loggable_type' => 'Modules\\Nonexistent\\Models\\SomeModel', 'loggable_id' => 999, ]); $response = $this->authRequest()->getJson('/api/admin/activity-logs'); // loggable 관계를 로드하지 않으므로 오류 없이 정상 응답 $response->assertStatus(200); $data = $response->json('data.data'); $this->assertCount(1, $data); $this->assertEquals('SomeModel', $data[0]['loggable_type_display']); } // ======================================================================== // 유효성 검증 실패 // ======================================================================== public function test_index_rejects_invalid_log_type(): void { $response = $this->authRequest()->getJson('/api/admin/activity-logs?log_type[]=invalid'); $response->assertStatus(422); } public function test_index_rejects_invalid_date_range(): void { $response = $this->authRequest()->getJson('/api/admin/activity-logs?date_from=2026-03-28&date_to=2026-01-01'); $response->assertStatus(422); } public function test_index_rejects_per_page_exceeding_max(): void { $response = $this->authRequest()->getJson('/api/admin/activity-logs?per_page=101'); $response->assertStatus(422); } // ======================================================================== // index 엔드포인트 - abilities // ======================================================================== public function test_index_includes_abilities_with_delete_permission(): void { // 삭제 권한도 가진 관리자 $adminWithDelete = $this->createAdminUser(['core.activities.read', 'core.activities.delete']); $token = $adminWithDelete->createToken('test-token')->plainTextToken; $this->createActivityLog(['user_id' => $adminWithDelete->id]); $response = $this->withHeaders([ 'Authorization' => 'Bearer '.$token, 'Accept' => 'application/json', ])->getJson('/api/admin/activity-logs'); $response->assertStatus(200); $data = $response->json('data.data'); $this->assertTrue($data[0]['abilities']['can_delete']); } public function test_index_abilities_can_delete_false_without_permission(): void { $this->createActivityLog(); $response = $this->authRequest()->getJson('/api/admin/activity-logs'); $response->assertStatus(200); $data = $response->json('data.data'); $this->assertFalse($data[0]['abilities']['can_delete']); } // ======================================================================== // index 엔드포인트 - collection-level abilities // ======================================================================== public function test_index_includes_collection_level_abilities_with_delete_permission(): void { $adminWithDelete = $this->createAdminUser(['core.activities.read', 'core.activities.delete']); $token = $adminWithDelete->createToken('test-token')->plainTextToken; $this->createActivityLog(['user_id' => $adminWithDelete->id]); $response = $this->withHeaders([ 'Authorization' => 'Bearer '.$token, 'Accept' => 'application/json', ])->getJson('/api/admin/activity-logs'); $response->assertStatus(200); $this->assertTrue($response->json('data.abilities.can_delete')); } public function test_index_collection_level_abilities_can_delete_false_without_permission(): void { $this->createActivityLog(); $response = $this->authRequest()->getJson('/api/admin/activity-logs'); $response->assertStatus(200); $this->assertFalse($response->json('data.abilities.can_delete')); } // ======================================================================== // destroy 엔드포인트 // ======================================================================== public function test_destroy_returns_401_without_authentication(): void { $log = $this->createActivityLog(); $response = $this->deleteJson("/api/admin/activity-logs/{$log->id}"); $response->assertStatus(401); } public function test_destroy_returns_403_without_delete_permission(): void { $log = $this->createActivityLog(); $response = $this->authRequest()->deleteJson("/api/admin/activity-logs/{$log->id}"); $response->assertStatus(403); } public function test_destroy_deletes_activity_log_successfully(): void { $adminWithDelete = $this->createAdminUser(['core.activities.read', 'core.activities.delete']); $token = $adminWithDelete->createToken('test-token')->plainTextToken; $log = $this->createActivityLog(); $response = $this->withHeaders([ 'Authorization' => 'Bearer '.$token, 'Accept' => 'application/json', ])->deleteJson("/api/admin/activity-logs/{$log->id}"); $response->assertStatus(200) ->assertJsonPath('success', true); $this->assertDatabaseMissing('activity_logs', ['id' => $log->id]); } public function test_destroy_returns_404_for_nonexistent_log(): void { $adminWithDelete = $this->createAdminUser(['core.activities.read', 'core.activities.delete']); $token = $adminWithDelete->createToken('test-token')->plainTextToken; $response = $this->withHeaders([ 'Authorization' => 'Bearer '.$token, 'Accept' => 'application/json', ])->deleteJson('/api/admin/activity-logs/99999'); $response->assertStatus(404); } // ======================================================================== // bulkDestroy 엔드포인트 // ======================================================================== public function test_bulk_destroy_returns_401_without_authentication(): void { $response = $this->postJson('/api/admin/activity-logs/bulk-delete', ['ids' => [1]]); $response->assertStatus(401); } public function test_bulk_destroy_returns_403_without_delete_permission(): void { $log = $this->createActivityLog(); $response = $this->authRequest()->postJson('/api/admin/activity-logs/bulk-delete', ['ids' => [$log->id]]); $response->assertStatus(403); } public function test_bulk_destroy_deletes_multiple_logs_successfully(): void { $adminWithDelete = $this->createAdminUser(['core.activities.read', 'core.activities.delete']); $token = $adminWithDelete->createToken('test-token')->plainTextToken; $log1 = $this->createActivityLog(['action' => 'action1']); $log2 = $this->createActivityLog(['action' => 'action2']); $log3 = $this->createActivityLog(['action' => 'action3']); $response = $this->withHeaders([ 'Authorization' => 'Bearer '.$token, 'Accept' => 'application/json', ])->postJson('/api/admin/activity-logs/bulk-delete', [ 'ids' => [$log1->id, $log2->id], ]); $response->assertStatus(200) ->assertJsonPath('success', true) ->assertJsonPath('data.deleted_count', 2); $this->assertDatabaseMissing('activity_logs', ['id' => $log1->id]); $this->assertDatabaseMissing('activity_logs', ['id' => $log2->id]); $this->assertDatabaseHas('activity_logs', ['id' => $log3->id]); } public function test_bulk_destroy_validates_ids_required(): void { $adminWithDelete = $this->createAdminUser(['core.activities.read', 'core.activities.delete']); $token = $adminWithDelete->createToken('test-token')->plainTextToken; $response = $this->withHeaders([ 'Authorization' => 'Bearer '.$token, 'Accept' => 'application/json', ])->postJson('/api/admin/activity-logs/bulk-delete', []); $response->assertStatus(422); } public function test_bulk_destroy_validates_ids_exist(): void { $adminWithDelete = $this->createAdminUser(['core.activities.read', 'core.activities.delete']); $token = $adminWithDelete->createToken('test-token')->plainTextToken; $response = $this->withHeaders([ 'Authorization' => 'Bearer '.$token, 'Accept' => 'application/json', ])->postJson('/api/admin/activity-logs/bulk-delete', [ 'ids' => [99999], ]); $response->assertStatus(422); } // ======================================================================== // index 엔드포인트 - 스코프 기반 권한 // ======================================================================== public function test_index_scope_self_returns_only_own_logs(): void { $selfScopeUser = $this->createAdminUser(['core.activities.read'], 'self'); $token = $selfScopeUser->createToken('test-token')->plainTextToken; $otherUser = User::factory()->create(); // 본인 로그 $this->createActivityLog(['user_id' => $selfScopeUser->id, 'action' => 'own.action']); // 타인 로그 $this->createActivityLog(['user_id' => $otherUser->id, 'action' => 'other.action']); $response = $this->withHeaders([ 'Authorization' => 'Bearer '.$token, 'Accept' => 'application/json', ])->getJson('/api/admin/activity-logs'); $response->assertStatus(200); $data = $response->json('data.data'); $this->assertCount(1, $data); $this->assertEquals('own.action', $data[0]['action']); } public function test_index_scope_null_returns_all_logs(): void { // 기본 createAdminUser는 scopeType=null (전체 접근) $otherUser = User::factory()->create(); $this->createActivityLog(['user_id' => $this->admin->id, 'action' => 'own.action']); $this->createActivityLog(['user_id' => $otherUser->id, 'action' => 'other.action']); $response = $this->authRequest()->getJson('/api/admin/activity-logs'); $response->assertStatus(200); $this->assertCount(2, $response->json('data.data')); } public function test_index_scope_role_returns_shared_role_logs(): void { $roleScopeUser = $this->createAdminUser(['core.activities.read'], 'role'); $token = $roleScopeUser->createToken('test-token')->plainTextToken; // 동일 역할을 공유하는 사용자 생성 $sharedRoleUser = User::factory()->create(); $roleScopeUserRoles = $roleScopeUser->roles()->where('identifier', '!=', 'admin')->pluck('roles.id'); foreach ($roleScopeUserRoles as $roleId) { $sharedRoleUser->roles()->attach($roleId, [ 'assigned_at' => now(), 'assigned_by' => null, ]); } // 다른 역할만 가진 사용자 $unrelatedUser = User::factory()->create(); // 본인 로그 $this->createActivityLog(['user_id' => $roleScopeUser->id, 'action' => 'own.action']); // 동일 역할 사용자 로그 $this->createActivityLog(['user_id' => $sharedRoleUser->id, 'action' => 'shared.action']); // 관련 없는 사용자 로그 $this->createActivityLog(['user_id' => $unrelatedUser->id, 'action' => 'unrelated.action']); $response = $this->withHeaders([ 'Authorization' => 'Bearer '.$token, 'Accept' => 'application/json', ])->getJson('/api/admin/activity-logs'); $response->assertStatus(200); $data = $response->json('data.data'); $this->assertCount(2, $data); $actions = collect($data)->pluck('action')->toArray(); $this->assertContains('own.action', $actions); $this->assertContains('shared.action', $actions); $this->assertNotContains('unrelated.action', $actions); } }