369 lines
12 KiB
PHP
369 lines
12 KiB
PHP
<?php
|
|
|
|
use App\Enums\ConsultationStatus;
|
|
use App\Mail\BookingSubmittedMail;
|
|
use App\Mail\NewBookingAdminEmail;
|
|
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(NewBookingAdminEmail::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(NewBookingAdminEmail::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);
|
|
});
|