Files
usher-manage-stack/tests/Feature/Admin/MemberNoteTest.php
gbanyan 46973c2f85 test(03-01): add feature tests for note history panel
- Test notes index returns author name and created_at for display
- Test empty state response (empty array when no notes)
- Test Blade view renders all Alpine.js history panel directives
- Test store endpoint returns complete note data for cache sync
- All 11 tests pass (7 existing + 4 new)
2026-02-13 12:59:39 +08:00

289 lines
9.7 KiB
PHP

<?php
namespace Tests\Feature\Admin;
use App\Models\Member;
use App\Models\Note;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class MemberNoteTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->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']);
}
}