libra/tests/Feature/Client/BookingSubmissionTest.php

369 lines
12 KiB
PHP

<?php
use App\Enums\ConsultationStatus;
use App\Mail\BookingSubmittedMail;
use App\Mail\NewBookingRequestMail;
use App\Models\AdminLog;
use App\Models\Consultation;
use App\Models\User;
use App\Models\WorkingHour;
use Illuminate\Support\Facades\Mail;
use Livewire\Volt\Volt;
beforeEach(function () {
// Setup working hours for Monday (day 1)
WorkingHour::factory()->create([
'day_of_week' => 1,
'start_time' => '09:00',
'end_time' => '17:00',
'is_active' => true,
]);
});
// Navigation Tests
test('client sees book now link in navigation', function () {
$client = User::factory()->individual()->create();
$this->actingAs($client)
->get(route('client.dashboard'))
->assertOk()
->assertSee(__('navigation.book_now'));
});
test('book now link navigates to booking page', function () {
$client = User::factory()->individual()->create();
$this->actingAs($client)
->get(route('client.consultations.book'))
->assertOk();
});
test('admin does not see book now link', function () {
$admin = User::factory()->admin()->create();
$this->actingAs($admin)
->get(route('admin.dashboard'))
->assertOk()
->assertDontSee(__('navigation.book_now'));
});
test('guest cannot access booking form', function () {
$this->get(route('client.consultations.book'))
->assertRedirect(route('login'));
});
test('authenticated client can access booking form', function () {
$client = User::factory()->individual()->create();
$this->actingAs($client)
->get(route('client.consultations.book'))
->assertOk();
});
test('authenticated client can submit booking request', function () {
Mail::fake();
$client = User::factory()->individual()->create();
User::factory()->admin()->create();
$monday = now()->next('Monday')->format('Y-m-d');
$this->actingAs($client);
Volt::test('client.consultations.book')
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute with my employer.')
->call('showConfirm')
->assertSet('showConfirmation', true)
->call('submit')
->assertRedirect(route('client.consultations.index'));
expect(Consultation::where('user_id', $client->id)->exists())->toBeTrue();
Mail::assertQueued(BookingSubmittedMail::class);
Mail::assertQueued(NewBookingRequestMail::class);
});
test('booking is created with pending status', function () {
Mail::fake();
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
$this->actingAs($client);
Volt::test('client.consultations.book')
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->call('showConfirm')
->call('submit');
$consultation = Consultation::where('user_id', $client->id)->first();
expect($consultation->status)->toBe(ConsultationStatus::Pending);
});
test('problem summary is required', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
$this->actingAs($client);
Volt::test('client.consultations.book')
->call('selectSlot', $monday, '10:00')
->set('problemSummary', '')
->call('showConfirm')
->assertHasErrors(['problemSummary' => 'required']);
});
test('problem summary must be at least 20 characters', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
$this->actingAs($client);
Volt::test('client.consultations.book')
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'Too short')
->call('showConfirm')
->assertHasErrors(['problemSummary' => 'min']);
});
test('problem summary cannot exceed 2000 characters', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
$this->actingAs($client);
Volt::test('client.consultations.book')
->call('selectSlot', $monday, '10:00')
->set('problemSummary', str_repeat('a', 2001))
->call('showConfirm')
->assertHasErrors(['problemSummary' => 'max']);
});
test('client cannot book more than once per day', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
// Create existing booking
Consultation::factory()->pending()->create([
'user_id' => $client->id,
'booking_date' => $monday,
'booking_time' => '09:00',
]);
$this->actingAs($client);
Volt::test('client.consultations.book')
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->call('showConfirm')
->assertHasErrors(['selectedDate']);
});
test('client can book on different day if already booked on another', function () {
Mail::fake();
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
$nextMonday = now()->next('Monday')->addWeek()->format('Y-m-d');
// Create existing booking for this Monday
Consultation::factory()->pending()->create([
'user_id' => $client->id,
'booking_date' => $monday,
'booking_time' => '09:00',
]);
$this->actingAs($client);
Volt::test('client.consultations.book')
->call('selectSlot', $nextMonday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->call('showConfirm')
->assertSet('showConfirmation', true);
});
test('client cannot book unavailable slot', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
// Create booking that takes the slot
Consultation::factory()->approved()->create([
'booking_date' => $monday,
'booking_time' => '10:00',
]);
$this->actingAs($client);
Volt::test('client.consultations.book')
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->call('showConfirm')
->assertHasErrors(['selectedTime']);
});
test('confirmation step displays before final submission', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
$this->actingAs($client);
Volt::test('client.consultations.book')
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->assertSet('showConfirmation', false)
->call('showConfirm')
->assertSet('showConfirmation', true);
});
test('user can go back from confirmation to edit', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
$this->actingAs($client);
Volt::test('client.consultations.book')
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->call('showConfirm')
->assertSet('showConfirmation', true)
->set('showConfirmation', false)
->assertSet('showConfirmation', false);
});
test('user can clear slot selection', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
$this->actingAs($client);
Volt::test('client.consultations.book')
->call('selectSlot', $monday, '10:00')
->assertSet('selectedDate', $monday)
->assertSet('selectedTime', '10:00')
->call('clearSelection')
->assertSet('selectedDate', null)
->assertSet('selectedTime', null);
});
test('success message shown after submission', function () {
Mail::fake();
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
$this->actingAs($client);
Volt::test('client.consultations.book')
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->call('showConfirm')
->call('submit')
->assertSessionHas('success');
});
test('audit log entry is created on booking submission', function () {
Mail::fake();
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
$this->actingAs($client);
Volt::test('client.consultations.book')
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->call('showConfirm')
->call('submit');
$consultation = Consultation::where('user_id', $client->id)->first();
expect(AdminLog::query()
->where('action', 'create')
->where('target_type', 'consultation')
->where('target_id', $consultation->id)
->whereNull('admin_id')
->exists()
)->toBeTrue();
});
test('emails are sent to client and admin after submission', function () {
Mail::fake();
$client = User::factory()->individual()->create();
$admin = User::factory()->admin()->create();
$monday = now()->next('Monday')->format('Y-m-d');
$this->actingAs($client);
Volt::test('client.consultations.book')
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->call('showConfirm')
->call('submit');
Mail::assertQueued(BookingSubmittedMail::class, function ($mail) use ($client) {
return $mail->hasTo($client->email);
});
Mail::assertQueued(NewBookingRequestMail::class, function ($mail) use ($admin) {
return $mail->hasTo($admin->email);
});
});
test('booking fails if slot is taken during submission (race condition)', function () {
Mail::fake();
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
$this->actingAs($client);
$component = Volt::test('client.consultations.book')
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->call('showConfirm')
->assertSet('showConfirmation', true);
// Another user takes the slot
Consultation::factory()->approved()->create([
'booking_date' => $monday,
'booking_time' => '10:00',
]);
$component->call('submit')
->assertHasErrors(['selectedTime'])
->assertSet('showConfirmation', false);
expect(Consultation::where('user_id', $client->id)->exists())->toBeFalse();
});
test('booking fails if user already booked during submission (race condition)', function () {
Mail::fake();
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
$this->actingAs($client);
$component = Volt::test('client.consultations.book')
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->call('showConfirm')
->assertSet('showConfirmation', true);
// User books another slot on same day (from another browser tab)
Consultation::factory()->pending()->create([
'user_id' => $client->id,
'booking_date' => $monday,
'booking_time' => '11:00',
]);
$component->call('submit')
->assertHasErrors(['selectedTime'])
->assertSet('showConfirmation', false);
// Should only have the one booking from the "other tab"
expect(Consultation::where('user_id', $client->id)->count())->toBe(1);
});