admin()->create(); } private function makeUser(): User { return User::factory()->create(); } private function makeProject(User $owner, array $attrs = []): TranscriptionProject { return TranscriptionProject::factory()->create(array_merge([ 'owner_user_id' => $owner->id, ], $attrs)); } // ------------------------------------------------------- // Dashboard // ------------------------------------------------------- public function test_admin_dashboard_loads_with_full_stats(): void { $admin = $this->makeAdmin(); $this->actingAs($admin) ->get(route('admin.dashboard')) ->assertOk() ->assertViewIs('admin.dashboard') ->assertViewHas('stats') ->assertViewHas('topUsers') ->assertViewHas('trendRaw') ->assertViewHas('deptStats'); } public function test_user_cannot_access_admin_dashboard(): void { $user = $this->makeUser(); $this->actingAs($user) ->get(route('admin.dashboard')) ->assertForbidden(); } public function test_dashboard_shows_storage_stats(): void { $admin = $this->makeAdmin(); $owner = $this->makeUser(); TranscriptionProject::factory()->create([ 'owner_user_id' => $owner->id, 'file_size' => 10485760, // 10 MB ]); $response = $this->actingAs($admin) ->get(route('admin.dashboard')) ->assertOk(); $stats = $response->viewData('stats'); $this->assertGreaterThanOrEqual(10485760, $stats['storage_bytes']); } // ------------------------------------------------------- // Transfer Ownership // ------------------------------------------------------- public function test_admin_can_transfer_project_ownership(): void { $admin = $this->makeAdmin(); $owner = $this->makeUser(); $newOwner = $this->makeUser(); $project = $this->makeProject($owner); $this->actingAs($admin) ->post(route('admin.projects.transfer-owner', $project), [ 'new_owner_id' => $newOwner->id, 'justification' => 'Pekerja telah berpindah jabatan dan projek perlu dipindahkan.', ])->assertRedirect(route('admin.projects.index')); $project->refresh(); $this->assertEquals($newOwner->id, $project->owner_user_id); } public function test_transfer_ownership_creates_audit_log(): void { $admin = $this->makeAdmin(); $owner = $this->makeUser(); $newOwner = $this->makeUser(); $project = $this->makeProject($owner); $this->actingAs($admin) ->post(route('admin.projects.transfer-owner', $project), [ 'new_owner_id' => $newOwner->id, 'justification' => 'Sebab pemindahan yang sah dan lengkap.', ]); $this->assertDatabaseHas('audit_logs', [ 'action' => 'project_ownership_transferred', 'project_id' => $project->id, 'target_user_id' => $newOwner->id, ]); $log = AuditLog::where('action', 'project_ownership_transferred')->first(); $this->assertStringContainsString('Sebab pemindahan', $log->justification); } public function test_user_cannot_transfer_ownership(): void { $user = $this->makeUser(); $owner = $this->makeUser(); $newOwner = $this->makeUser(); $project = $this->makeProject($owner); $this->actingAs($user) ->post(route('admin.projects.transfer-owner', $project), [ 'new_owner_id' => $newOwner->id, 'justification' => 'Cuba pindah oleh pengguna biasa.', ])->assertForbidden(); $project->refresh(); $this->assertEquals($owner->id, $project->owner_user_id); } public function test_transfer_requires_justification(): void { $admin = $this->makeAdmin(); $owner = $this->makeUser(); $newOwner = $this->makeUser(); $project = $this->makeProject($owner); $this->actingAs($admin) ->post(route('admin.projects.transfer-owner', $project), [ 'new_owner_id' => $newOwner->id, 'justification' => 'Singkat', // less than 10 chars ])->assertSessionHasErrors('justification'); $project->refresh(); $this->assertEquals($owner->id, $project->owner_user_id); } public function test_transfer_requires_valid_new_owner(): void { $admin = $this->makeAdmin(); $owner = $this->makeUser(); $project = $this->makeProject($owner); $this->actingAs($admin) ->post(route('admin.projects.transfer-owner', $project), [ 'new_owner_id' => 99999, 'justification' => 'Justifikasi yang sah untuk pemindahan projek ini.', ])->assertSessionHasErrors('new_owner_id'); } public function test_cannot_transfer_to_admin_user(): void { $admin = $this->makeAdmin(); $admin2 = $this->makeAdmin(); $owner = $this->makeUser(); $project = $this->makeProject($owner); $this->actingAs($admin) ->post(route('admin.projects.transfer-owner', $project), [ 'new_owner_id' => $admin2->id, 'justification' => 'Justifikasi yang sah untuk pemindahan projek ini.', ])->assertForbidden(); } public function test_cannot_transfer_to_same_owner(): void { $admin = $this->makeAdmin(); $owner = $this->makeUser(); $project = $this->makeProject($owner); $this->actingAs($admin) ->post(route('admin.projects.transfer-owner', $project), [ 'new_owner_id' => $owner->id, 'justification' => 'Justifikasi yang sah untuk pemindahan projek ini.', ])->assertStatus(422); } // ------------------------------------------------------- // Audit Log Export // ------------------------------------------------------- public function test_admin_can_export_audit_log_csv(): void { $admin = $this->makeAdmin(); AuditLog::create([ 'actor_user_id' => $admin->id, 'actor_role' => 'admin', 'action' => 'user_created', 'ip_address' => '127.0.0.1', 'created_at' => now(), ]); $response = $this->actingAs($admin) ->get(route('admin.audit-logs.export')); $response->assertOk(); $this->assertStringContainsString('text/csv', $response->headers->get('Content-Type')); $this->assertStringContainsString('audit-log-', $response->headers->get('Content-Disposition')); } public function test_user_cannot_export_audit_log(): void { $user = $this->makeUser(); $this->actingAs($user) ->get(route('admin.audit-logs.export')) ->assertForbidden(); } public function test_audit_log_export_respects_filters(): void { $admin = $this->makeAdmin(); AuditLog::create([ 'actor_user_id' => $admin->id, 'actor_role' => 'admin', 'action' => 'user_activated', 'ip_address' => '127.0.0.1', 'created_at' => now(), ]); AuditLog::create([ 'actor_user_id' => $admin->id, 'actor_role' => 'admin', 'action' => 'user_deactivated', 'ip_address' => '127.0.0.1', 'created_at' => now(), ]); $response = $this->actingAs($admin) ->get(route('admin.audit-logs.export', ['action' => 'user_activated'])); $response->assertOk(); $content = $response->streamedContent(); $this->assertStringContainsString('user_activated', $content); $this->assertStringNotContainsString('user_deactivated', $content); } }