- AdminMemberController index() now includes withCount('notes')
- Created MemberNoteTest with 7 feature tests
- Tests cover: creation, retrieval, validation, audit logging, authorization, N+1 prevention, ordering
- All new tests passing (7/7)
- No regressions in existing test suite
206 lines
6.6 KiB
PHP
206 lines
6.6 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']);
|
|
}
|
|
}
|