admin = User::factory()->admin()->create(); $this->client = User::factory()->individual()->create(); $this->timeline = Timeline::factory()->create(['user_id' => $this->client->id]); }); // =========================================== // View & Access Tests // =========================================== test('admin can view timeline show page', function () { $this->actingAs($this->admin) ->get(route('admin.timelines.show', $this->timeline)) ->assertOk(); }); test('admin can view timeline with updates', function () { $update = TimelineUpdate::factory()->create([ 'timeline_id' => $this->timeline->id, 'admin_id' => $this->admin->id, 'update_text' => 'First update text here', ]); $this->actingAs($this->admin); Volt::test('admin.timelines.show', ['timeline' => $this->timeline]) ->assertSee('First update text here') ->assertSee($this->admin->full_name); }); test('non-admin cannot access timeline show page', function () { $this->actingAs($this->client) ->get(route('admin.timelines.show', $this->timeline)) ->assertForbidden(); }); test('guest cannot access timeline show page', function () { $this->get(route('admin.timelines.show', $this->timeline)) ->assertRedirect(route('login')); }); // =========================================== // Add Update Tests // =========================================== test('admin can add update with valid text', function () { Notification::fake(); $this->actingAs($this->admin); Volt::test('admin.timelines.show', ['timeline' => $this->timeline]) ->set('updateText', 'This is a valid update text with enough characters.') ->call('addUpdate') ->assertHasNoErrors(); expect(TimelineUpdate::where('timeline_id', $this->timeline->id)->count())->toBe(1); $update = TimelineUpdate::where('timeline_id', $this->timeline->id)->first(); expect($update->update_text)->toContain('This is a valid update text with enough characters.'); expect($update->admin_id)->toBe($this->admin->id); }); test('admin can add update with minimum 10 characters', function () { Notification::fake(); $this->actingAs($this->admin); Volt::test('admin.timelines.show', ['timeline' => $this->timeline]) ->set('updateText', '1234567890') ->call('addUpdate') ->assertHasNoErrors(); expect(TimelineUpdate::where('timeline_id', $this->timeline->id)->exists())->toBeTrue(); }); test('cannot add update with empty text', function () { $this->actingAs($this->admin); Volt::test('admin.timelines.show', ['timeline' => $this->timeline]) ->set('updateText', '') ->call('addUpdate') ->assertHasErrors(['updateText' => 'required']); }); test('cannot add update with less than 10 characters', function () { $this->actingAs($this->admin); Volt::test('admin.timelines.show', ['timeline' => $this->timeline]) ->set('updateText', 'short') ->call('addUpdate') ->assertHasErrors(['updateText' => 'min']); }); test('admin name is automatically recorded when adding update', function () { Notification::fake(); $this->actingAs($this->admin); Volt::test('admin.timelines.show', ['timeline' => $this->timeline]) ->set('updateText', 'This is a valid update text.') ->call('addUpdate') ->assertHasNoErrors(); $update = TimelineUpdate::where('timeline_id', $this->timeline->id)->first(); expect($update->admin_id)->toBe($this->admin->id); expect($update->admin->id)->toBe($this->admin->id); }); test('timestamp is automatically recorded when adding update', function () { Notification::fake(); $this->actingAs($this->admin); $beforeTime = now()->subSecond(); Volt::test('admin.timelines.show', ['timeline' => $this->timeline]) ->set('updateText', 'This is a valid update text.') ->call('addUpdate') ->assertHasNoErrors(); $update = TimelineUpdate::where('timeline_id', $this->timeline->id)->first(); expect($update->created_at)->not->toBeNull(); expect($update->created_at->isAfter($beforeTime))->toBeTrue(); }); test('update text is cleared after adding update', function () { Notification::fake(); $this->actingAs($this->admin); $component = Volt::test('admin.timelines.show', ['timeline' => $this->timeline]) ->set('updateText', 'This is a valid update text.') ->call('addUpdate') ->assertHasNoErrors(); expect($component->get('updateText'))->toBe(''); }); // =========================================== // Edit Update Tests // =========================================== test('admin can edit existing update', function () { Notification::fake(); $update = TimelineUpdate::factory()->create([ 'timeline_id' => $this->timeline->id, 'admin_id' => $this->admin->id, 'update_text' => 'Original text here.', ]); $this->actingAs($this->admin); Volt::test('admin.timelines.show', ['timeline' => $this->timeline]) ->call('editUpdate', $update->id) ->set('updateText', 'Updated text with new content.') ->call('saveEdit') ->assertHasNoErrors(); $update->refresh(); expect($update->update_text)->toContain('Updated text with new content.'); }); test('edit preserves original created_at timestamp', function () { Notification::fake(); $update = TimelineUpdate::factory()->create([ 'timeline_id' => $this->timeline->id, 'admin_id' => $this->admin->id, 'update_text' => 'Original text here.', ]); $originalCreatedAt = $update->created_at->toDateTimeString(); $this->actingAs($this->admin); // Wait a moment to ensure time difference sleep(1); Volt::test('admin.timelines.show', ['timeline' => $this->timeline]) ->call('editUpdate', $update->id) ->set('updateText', 'Updated text with new content.') ->call('saveEdit') ->assertHasNoErrors(); $update->refresh(); expect($update->created_at->toDateTimeString())->toBe($originalCreatedAt); }); test('edit updates the updated_at timestamp', function () { Notification::fake(); $update = TimelineUpdate::factory()->create([ 'timeline_id' => $this->timeline->id, 'admin_id' => $this->admin->id, 'update_text' => 'Original text here.', ]); $originalUpdatedAt = $update->updated_at; $this->actingAs($this->admin); // Wait a moment to ensure time difference sleep(1); Volt::test('admin.timelines.show', ['timeline' => $this->timeline]) ->call('editUpdate', $update->id) ->set('updateText', 'Updated text with new content.') ->call('saveEdit') ->assertHasNoErrors(); $update->refresh(); expect($update->updated_at->gt($originalUpdatedAt))->toBeTrue(); }); test('cannot change admin on edit', function () { Notification::fake(); $otherAdmin = User::factory()->admin()->create(); $update = TimelineUpdate::factory()->create([ 'timeline_id' => $this->timeline->id, 'admin_id' => $otherAdmin->id, 'update_text' => 'Original text here.', ]); $this->actingAs($this->admin); Volt::test('admin.timelines.show', ['timeline' => $this->timeline]) ->call('editUpdate', $update->id) ->set('updateText', 'Updated text with new content.') ->call('saveEdit') ->assertHasNoErrors(); $update->refresh(); expect($update->admin_id)->toBe($otherAdmin->id); }); test('cancel edit clears form', function () { $update = TimelineUpdate::factory()->create([ 'timeline_id' => $this->timeline->id, 'admin_id' => $this->admin->id, 'update_text' => 'Original text here.', ]); $this->actingAs($this->admin); $component = Volt::test('admin.timelines.show', ['timeline' => $this->timeline]) ->call('editUpdate', $update->id) ->call('cancelEdit'); expect($component->get('editingUpdateId'))->toBeNull(); expect($component->get('updateText'))->toBe(''); }); test('edit update loads text into form', function () { $update = TimelineUpdate::factory()->create([ 'timeline_id' => $this->timeline->id, 'admin_id' => $this->admin->id, 'update_text' => 'Original text here.', ]); $this->actingAs($this->admin); $component = Volt::test('admin.timelines.show', ['timeline' => $this->timeline]) ->call('editUpdate', $update->id); expect($component->get('editingUpdateId'))->toBe($update->id); expect($component->get('updateText'))->toBe('Original text here.'); }); // =========================================== // HTML Sanitization Tests // =========================================== test('html is sanitized when adding update', function () { Notification::fake(); $this->actingAs($this->admin); $maliciousText = 'Valid update text here.'; Volt::test('admin.timelines.show', ['timeline' => $this->timeline]) ->set('updateText', $maliciousText) ->call('addUpdate') ->assertHasNoErrors(); $update = TimelineUpdate::where('timeline_id', $this->timeline->id)->first(); expect($update->update_text)->not->toContain('Updated safe text.'; Volt::test('admin.timelines.show', ['timeline' => $this->timeline]) ->call('editUpdate', $update->id) ->set('updateText', $maliciousText) ->call('saveEdit') ->assertHasNoErrors(); $update->refresh(); expect($update->update_text)->not->toContain('