libra/tests/Feature/Admin/BlockedTimesTest.php

519 lines
16 KiB
PHP

<?php
use App\Enums\ConsultationStatus;
use App\Models\AdminLog;
use App\Models\BlockedTime;
use App\Models\Consultation;
use App\Models\User;
use Livewire\Volt\Volt;
beforeEach(function () {
$this->admin = User::factory()->admin()->create();
});
// ===========================================
// Access Tests
// ===========================================
test('admin can access blocked times page', function () {
$this->actingAs($this->admin)
->get(route('admin.settings.blocked-times'))
->assertOk();
});
test('non-admin cannot access blocked times page', function () {
$client = User::factory()->individual()->create();
$this->actingAs($client)
->get(route('admin.settings.blocked-times'))
->assertForbidden();
});
test('unauthenticated user cannot access blocked times page', function () {
$this->get(route('admin.settings.blocked-times'))
->assertRedirect(route('login'));
});
// ===========================================
// Create All-Day Block Tests
// ===========================================
test('admin can create an all-day block', function () {
$this->actingAs($this->admin);
$blockDate = today()->addDays(5)->format('Y-m-d');
$initialCount = BlockedTime::count();
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', $blockDate)
->set('is_all_day', true)
->set('reason', 'Holiday')
->call('save')
->assertHasNoErrors();
expect(BlockedTime::count())->toBe($initialCount + 1);
$blocked = BlockedTime::latest()->first();
expect($blocked)->not->toBeNull()
->and($blocked->isAllDay())->toBeTrue()
->and($blocked->reason)->toBe('Holiday');
});
// ===========================================
// Create Time-Range Block Tests
// ===========================================
test('admin can create a time-range block', function () {
$this->actingAs($this->admin);
$initialCount = BlockedTime::count();
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', today()->addDays(3)->format('Y-m-d'))
->set('is_all_day', false)
->set('start_time', '09:00')
->set('end_time', '12:00')
->set('reason', 'Morning meeting')
->call('save')
->assertHasNoErrors();
expect(BlockedTime::count())->toBe($initialCount + 1);
$blocked = BlockedTime::latest()->first();
expect($blocked)->not->toBeNull()
->and($blocked->isAllDay())->toBeFalse()
->and($blocked->start_time)->toContain('09:00')
->and($blocked->end_time)->toContain('12:00')
->and($blocked->reason)->toBe('Morning meeting');
});
test('admin can create block without reason', function () {
$this->actingAs($this->admin);
$initialCount = BlockedTime::count();
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', today()->addDays(2)->format('Y-m-d'))
->set('is_all_day', true)
->set('reason', '')
->call('save')
->assertHasNoErrors();
expect(BlockedTime::count())->toBe($initialCount + 1);
$blocked = BlockedTime::latest()->first();
expect($blocked)->not->toBeNull()
->and($blocked->reason)->toBeNull();
});
// ===========================================
// Edit Blocked Time Tests
// ===========================================
test('admin can edit an existing blocked time', function () {
$blocked = BlockedTime::factory()->allDay()->create([
'block_date' => today()->addDays(5),
'reason' => 'Original reason',
]);
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('openEditModal', $blocked->id)
->set('reason', 'Updated reason')
->call('save')
->assertHasNoErrors();
$blocked->refresh();
expect($blocked->reason)->toBe('Updated reason');
});
test('admin can change block from all-day to time-range', function () {
$blocked = BlockedTime::factory()->allDay()->create([
'block_date' => today()->addDays(5),
]);
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('openEditModal', $blocked->id)
->set('is_all_day', false)
->set('start_time', '14:00')
->set('end_time', '18:00')
->call('save')
->assertHasNoErrors();
$blocked->refresh();
expect($blocked->isAllDay())->toBeFalse()
->and($blocked->start_time)->toContain('14:00')
->and($blocked->end_time)->toContain('18:00');
});
test('admin can edit past blocked time date', function () {
$blocked = BlockedTime::factory()->past()->create();
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->call('openEditModal', $blocked->id)
->set('reason', 'Updated past block')
->call('save')
->assertHasNoErrors();
$blocked->refresh();
expect($blocked->reason)->toBe('Updated past block');
});
// ===========================================
// Delete Blocked Time Tests
// ===========================================
test('admin can delete a blocked time', function () {
$blocked = BlockedTime::factory()->create();
$blockedId = $blocked->id;
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('confirmDelete', $blockedId)
->call('delete')
->assertHasNoErrors();
expect(BlockedTime::find($blockedId))->toBeNull();
});
// ===========================================
// Validation Tests
// ===========================================
test('cannot create block with end time before start time', function () {
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', today()->addDays(5)->format('Y-m-d'))
->set('is_all_day', false)
->set('start_time', '14:00')
->set('end_time', '10:00')
->call('save')
->assertHasErrors(['end_time']);
});
test('cannot create block for past dates', function () {
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', today()->subDays(5)->format('Y-m-d'))
->set('is_all_day', true)
->call('save')
->assertHasErrors(['block_date']);
});
test('can create block for today', function () {
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', today()->format('Y-m-d'))
->set('is_all_day', true)
->call('save')
->assertHasNoErrors();
expect(BlockedTime::where('block_date', today())->exists())->toBeTrue();
});
test('reason field respects 255 character max length', function () {
$this->actingAs($this->admin);
$longReason = str_repeat('a', 256);
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', today()->addDays(5)->format('Y-m-d'))
->set('is_all_day', true)
->set('reason', $longReason)
->call('save')
->assertHasErrors(['reason']);
});
// ===========================================
// List View Tests
// ===========================================
test('list displays all blocked times sorted by date', function () {
BlockedTime::factory()->create(['block_date' => today()->addDays(10)]);
BlockedTime::factory()->create(['block_date' => today()->addDays(2)]);
BlockedTime::factory()->create(['block_date' => today()->addDays(5)]);
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->set('filter', 'all');
$blockedTimes = $component->viewData('blockedTimes');
expect($blockedTimes)->toHaveCount(3);
});
test('filter by upcoming shows only future blocks', function () {
BlockedTime::factory()->upcoming()->create();
BlockedTime::factory()->today()->create();
BlockedTime::factory()->past()->create();
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->set('filter', 'upcoming');
$blockedTimes = $component->viewData('blockedTimes');
expect($blockedTimes)->toHaveCount(2)
->and($blockedTimes->every(fn ($b) => $b->block_date->gte(today())))->toBeTrue();
});
test('filter by past shows only past blocks', function () {
BlockedTime::factory()->upcoming()->create();
BlockedTime::factory()->today()->create();
BlockedTime::factory()->past()->create();
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->set('filter', 'past');
$blockedTimes = $component->viewData('blockedTimes');
expect($blockedTimes)->toHaveCount(1)
->and($blockedTimes->every(fn ($b) => $b->block_date->lt(today())))->toBeTrue();
});
test('filter by all shows all blocks', function () {
BlockedTime::factory()->upcoming()->create();
BlockedTime::factory()->today()->create();
BlockedTime::factory()->past()->create();
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->set('filter', 'all');
$blockedTimes = $component->viewData('blockedTimes');
expect($blockedTimes)->toHaveCount(3);
});
test('list shows reason if provided', function () {
BlockedTime::factory()->create([
'block_date' => today()->addDays(5),
'reason' => 'Personal vacation',
]);
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->assertSee('Personal vacation');
});
// ===========================================
// Audit Log Tests
// ===========================================
test('audit log is created when blocked time is created', function () {
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', today()->addDays(5)->format('Y-m-d'))
->set('is_all_day', true)
->set('reason', 'Test')
->call('save');
$log = AdminLog::where('target_type', 'blocked_time')->first();
expect($log)->not->toBeNull()
->and($log->admin_id)->toBe($this->admin->id)
->and($log->action)->toBe('create');
});
test('audit log is created when blocked time is updated', function () {
$blocked = BlockedTime::factory()->create([
'block_date' => today()->addDays(5),
]);
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('openEditModal', $blocked->id)
->set('reason', 'Updated')
->call('save');
$log = AdminLog::where('target_type', 'blocked_time')
->where('action', 'update')
->first();
expect($log)->not->toBeNull()
->and($log->target_id)->toBe($blocked->id);
});
test('audit log is created when blocked time is deleted', function () {
$blocked = BlockedTime::factory()->create();
$blockedId = $blocked->id;
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('confirmDelete', $blockedId)
->call('delete');
$log = AdminLog::where('target_type', 'blocked_time')
->where('action', 'delete')
->first();
expect($log)->not->toBeNull()
->and($log->target_id)->toBe($blockedId);
});
// ===========================================
// Pending Booking Warning Tests
// ===========================================
test('warning displayed when blocking date with pending consultations', function () {
$client = User::factory()->individual()->create();
$blockDate = today()->addDays(5);
Consultation::factory()->create([
'user_id' => $client->id,
'booking_date' => $blockDate,
'booking_time' => '10:00:00',
'status' => ConsultationStatus::Pending,
]);
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', $blockDate->format('Y-m-d'))
->set('is_all_day', true)
->call('checkPendingBookings');
expect($component->get('pendingBookings'))->toHaveCount(1)
->and($component->get('pendingBookings')[0]['client'])->toBe($client->full_name);
});
test('warning shows pending bookings within time range', function () {
$client = User::factory()->individual()->create();
$blockDate = today()->addDays(5);
// Consultation within blocked range (10:00 is between 09:00 and 12:00)
Consultation::factory()->create([
'user_id' => $client->id,
'booking_date' => $blockDate,
'booking_time' => '10:00:00',
'status' => ConsultationStatus::Pending,
]);
// Consultation outside blocked range (15:00 is not between 09:00 and 12:00)
Consultation::factory()->create([
'user_id' => $client->id,
'booking_date' => $blockDate,
'booking_time' => '15:00:00',
'status' => ConsultationStatus::Pending,
]);
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', $blockDate->format('Y-m-d'))
->set('is_all_day', false)
->set('start_time', '09:00')
->set('end_time', '12:00')
->call('checkPendingBookings');
expect($component->get('pendingBookings'))->toHaveCount(1);
});
// ===========================================
// Modal Tests
// ===========================================
test('create modal opens with default values', function () {
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->call('openCreateModal');
expect($component->get('showModal'))->toBeTrue()
->and($component->get('editingId'))->toBeNull()
->and($component->get('is_all_day'))->toBeTrue()
->and($component->get('block_date'))->toBe(today()->format('Y-m-d'));
});
test('edit modal opens with existing values', function () {
$blocked = BlockedTime::factory()->timeRange('10:00', '14:00')->create([
'block_date' => today()->addDays(5),
'reason' => 'Test reason',
]);
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->call('openEditModal', $blocked->id);
expect($component->get('showModal'))->toBeTrue()
->and($component->get('editingId'))->toBe($blocked->id)
->and($component->get('is_all_day'))->toBeFalse()
->and($component->get('start_time'))->toBe('10:00')
->and($component->get('end_time'))->toBe('14:00')
->and($component->get('reason'))->toBe('Test reason');
});
test('modal closes and resets on close', function () {
$blocked = BlockedTime::factory()->create();
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->call('openEditModal', $blocked->id)
->call('closeModal');
expect($component->get('showModal'))->toBeFalse()
->and($component->get('editingId'))->toBeNull();
});
// ===========================================
// Multiple Blocks on Same Date Tests
// ===========================================
test('multiple time-range blocks on same date handled correctly', function () {
$blockDate = today()->addDays(5);
BlockedTime::factory()->timeRange('09:00', '12:00')->create([
'block_date' => $blockDate,
]);
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', $blockDate->format('Y-m-d'))
->set('is_all_day', false)
->set('start_time', '14:00')
->set('end_time', '17:00')
->call('save')
->assertHasNoErrors();
expect(BlockedTime::where('block_date', $blockDate)->count())->toBe(2);
});