artisan('db:seed', ['--class' => 'RoleSeeder']); } protected function createAdminUser(): User { $admin = User::factory()->create(); $admin->assignRole('admin'); return $admin; } public function test_admin_can_create_note_for_member(): void { $admin = $this->createAdminUser(); $member = Member::factory()->create(); $response = $this->actingAs($admin)->postJson( route('admin.members.notes.store', $member), ['content' => 'Test note content'] ); $response->assertStatus(201); $response->assertJsonStructure([ 'note' => ['id', 'content', 'created_at', 'author'], 'message', ]); $response->assertJson([ 'note' => [ 'content' => 'Test note content', 'author' => [ 'id' => $admin->id, 'name' => $admin->name, ], ], 'message' => '備忘錄已新增', ]); $this->assertDatabaseHas('notes', [ 'notable_type' => 'member', 'notable_id' => $member->id, 'content' => 'Test note content', 'author_user_id' => $admin->id, ]); } public function test_admin_can_retrieve_notes_for_member(): void { $admin = $this->createAdminUser(); $member = Member::factory()->create(); // Create 3 notes Note::factory()->forMember($member)->byAuthor($admin)->create(['content' => 'Note 1']); Note::factory()->forMember($member)->byAuthor($admin)->create(['content' => 'Note 2']); Note::factory()->forMember($member)->byAuthor($admin)->create(['content' => 'Note 3']); $response = $this->actingAs($admin)->getJson( route('admin.members.notes.index', $member) ); $response->assertStatus(200); $response->assertJsonStructure([ 'notes' => [ '*' => ['id', 'content', 'created_at', 'updated_at', 'author'], ], ]); $notes = $response->json('notes'); $this->assertCount(3, $notes); // Verify each note has required fields foreach ($notes as $note) { $this->assertArrayHasKey('content', $note); $this->assertArrayHasKey('created_at', $note); $this->assertArrayHasKey('author', $note); $this->assertArrayHasKey('name', $note['author']); } } public function test_note_creation_requires_content(): void { $admin = $this->createAdminUser(); $member = Member::factory()->create(); $response = $this->actingAs($admin)->postJson( route('admin.members.notes.store', $member), ['content' => ''] ); $response->assertStatus(422); $response->assertJsonValidationErrors('content'); } public function test_note_creation_logs_audit_trail(): void { $admin = $this->createAdminUser(); $member = Member::factory()->create(); $this->actingAs($admin)->postJson( route('admin.members.notes.store', $member), ['content' => 'Audit test note'] ); $this->assertDatabaseHas('audit_logs', [ 'action' => 'note.created', ]); // Verify the audit log has the correct metadata $auditLog = \App\Models\AuditLog::where('action', 'note.created')->first(); $this->assertNotNull($auditLog); $this->assertEquals($admin->id, $auditLog->user_id); $this->assertArrayHasKey('member_id', $auditLog->metadata); $this->assertEquals($member->id, $auditLog->metadata['member_id']); $this->assertEquals($member->full_name, $auditLog->metadata['member_name']); } public function test_non_admin_cannot_create_note(): void { $user = User::factory()->create(); // User has no admin role and no permissions $member = Member::factory()->create(); $response = $this->actingAs($user)->postJson( route('admin.members.notes.store', $member), ['content' => 'Should fail'] ); $response->assertStatus(403); } public function test_member_list_includes_notes_count(): void { $admin = $this->createAdminUser(); $member1 = Member::factory()->create(); $member2 = Member::factory()->create(); // Create 3 notes for member1, 0 notes for member2 Note::factory()->forMember($member1)->byAuthor($admin)->count(3)->create(); $response = $this->actingAs($admin)->get(route('admin.members.index')); $response->assertStatus(200); $response->assertViewHas('members'); $members = $response->viewData('members'); // Find the members in the paginated result $foundMember1 = $members->firstWhere('id', $member1->id); $foundMember2 = $members->firstWhere('id', $member2->id); $this->assertNotNull($foundMember1); $this->assertNotNull($foundMember2); $this->assertEquals(3, $foundMember1->notes_count); $this->assertEquals(0, $foundMember2->notes_count); } public function test_notes_returned_newest_first(): void { $admin = $this->createAdminUser(); $member = Member::factory()->create(); // Create notes with different timestamps $oldestNote = Note::factory()->forMember($member)->byAuthor($admin)->create([ 'content' => 'Oldest note', 'created_at' => now()->subDays(3), ]); $middleNote = Note::factory()->forMember($member)->byAuthor($admin)->create([ 'content' => 'Middle note', 'created_at' => now()->subDays(1), ]); $newestNote = Note::factory()->forMember($member)->byAuthor($admin)->create([ 'content' => 'Newest note', 'created_at' => now(), ]); $response = $this->actingAs($admin)->getJson( route('admin.members.notes.index', $member) ); $response->assertStatus(200); $notes = $response->json('notes'); // Verify ordering (newest first) $this->assertEquals('Newest note', $notes[0]['content']); $this->assertEquals('Middle note', $notes[1]['content']); $this->assertEquals('Oldest note', $notes[2]['content']); } public function test_notes_index_returns_author_name_and_created_at(): void { $admin = $this->createAdminUser(); $otherAdmin = User::factory()->create(['name' => '測試管理員']); $otherAdmin->assignRole('admin'); $member = Member::factory()->create(); Note::factory()->forMember($member)->byAuthor($admin)->create(['content' => 'Note by admin']); Note::factory()->forMember($member)->byAuthor($otherAdmin)->create(['content' => 'Note by other']); $response = $this->actingAs($admin)->getJson( route('admin.members.notes.index', $member) ); $response->assertStatus(200); $notes = $response->json('notes'); // Each note must have author.name and created_at for display foreach ($notes as $note) { $this->assertNotEmpty($note['author']['name']); $this->assertNotEmpty($note['created_at']); } // Verify different authors are represented $authorNames = array_column(array_column($notes, 'author'), 'name'); $this->assertContains($admin->name, $authorNames); $this->assertContains('測試管理員', $authorNames); } public function test_notes_index_returns_empty_array_for_member_with_no_notes(): void { $admin = $this->createAdminUser(); $member = Member::factory()->create(); $response = $this->actingAs($admin)->getJson( route('admin.members.notes.index', $member) ); $response->assertStatus(200); $response->assertJson(['notes' => []]); $this->assertCount(0, $response->json('notes')); } public function test_member_list_renders_history_panel_directives(): void { $admin = $this->createAdminUser(); Member::factory()->create(); $response = $this->actingAs($admin)->get(route('admin.members.index')); $response->assertStatus(200); $response->assertSee('toggleHistory', false); $response->assertSee('historyOpen', false); $response->assertSee('x-collapse', false); $response->assertSee('searchQuery', false); $response->assertSee('filteredNotes', false); $response->assertSee('尚無備註', false); $response->assertSee('aria-expanded', false); $response->assertSee('notes-panel-', false); } public function test_store_note_returns_note_with_author_for_cache_sync(): void { $admin = $this->createAdminUser(); $member = Member::factory()->create(); $response = $this->actingAs($admin)->postJson( route('admin.members.notes.store', $member), ['content' => 'Cache sync test note'] ); $response->assertStatus(201); $note = $response->json('note'); // All fields needed for frontend cache sync $this->assertArrayHasKey('id', $note); $this->assertArrayHasKey('content', $note); $this->assertArrayHasKey('created_at', $note); $this->assertArrayHasKey('author', $note); $this->assertArrayHasKey('name', $note['author']); $this->assertEquals($admin->name, $note['author']['name']); } }