user = User::factory()->create(); $this->token = $this->user->createToken('test-token')->plainTextToken; } public function test_calendar_slots_are_auto_created_when_trip_is_created() { $tripData = [ 'name' => 'Test Trip', 'description' => 'Test Description', 'start_date' => '2024-01-01', 'end_date' => '2024-01-03' ]; $response = $this->withHeaders([ 'Authorization' => 'Bearer ' . $this->token, 'Accept' => 'application/json' ])->postJson('/api/trips', $tripData); $response->assertStatus(201); $tripId = $response->json('data.id'); // Check that 3 calendar slots were created for this trip (Jan 1, 2, 3) $this->assertEquals(3, CalendarSlot::where('trip_id', $tripId)->count()); $slots = CalendarSlot::where('trip_id', $tripId) ->orderBy('slot_order') ->get(); $this->assertEquals('Day 1', $slots[0]->name); $this->assertEquals('2024-01-01', $slots[0]->slot_date->format('Y-m-d')); $this->assertEquals(1, $slots[0]->slot_order); $this->assertEquals('Day 2', $slots[1]->name); $this->assertEquals('2024-01-02', $slots[1]->slot_date->format('Y-m-d')); $this->assertEquals(2, $slots[1]->slot_order); $this->assertEquals('Day 3', $slots[2]->name); $this->assertEquals('2024-01-03', $slots[2]->slot_date->format('Y-m-d')); $this->assertEquals(3, $slots[2]->slot_order); } public function test_calendar_slots_are_updated_when_trip_dates_change() { $trip = Trip::factory()->create([ 'created_by_user_id' => $this->user->id, 'start_date' => '2024-01-01', 'end_date' => '2024-01-02' ]); // Initially should have 2 slots $this->assertCount(2, $trip->calendarSlots); // Update trip dates $response = $this->withHeaders([ 'Authorization' => 'Bearer ' . $this->token, 'Accept' => 'application/json' ])->putJson("/api/trips/{$trip->id}", [ 'name' => $trip->name, 'start_date' => '2024-02-01', 'end_date' => '2024-02-04' ]); $response->assertStatus(200); // Should now have 4 slots with new dates $trip->refresh(); $slots = $trip->calendarSlots()->orderBy('slot_order')->get(); $this->assertCount(4, $slots); $this->assertEquals('2024-02-01', $slots[0]->slot_date->format('Y-m-d')); $this->assertEquals('2024-02-02', $slots[1]->slot_date->format('Y-m-d')); $this->assertEquals('2024-02-03', $slots[2]->slot_date->format('Y-m-d')); $this->assertEquals('2024-02-04', $slots[3]->slot_date->format('Y-m-d')); } public function test_can_list_calendar_slots_for_trip() { $trip = Trip::factory()->create([ 'created_by_user_id' => $this->user->id, 'start_date' => '2024-01-01', 'end_date' => '2024-01-03' ]); $response = $this->withHeaders([ 'Authorization' => 'Bearer ' . $this->token, 'Accept' => 'application/json' ])->getJson("/api/trips/{$trip->id}/calendar-slots"); $response->assertStatus(200) ->assertJsonCount(3, 'data') ->assertJsonPath('data.0.name', 'Day 1') ->assertJsonPath('data.1.name', 'Day 2') ->assertJsonPath('data.2.name', 'Day 3'); } public function test_can_update_calendar_slot_name() { $trip = Trip::factory()->create([ 'created_by_user_id' => $this->user->id, 'start_date' => '2024-01-01', 'end_date' => '2024-01-01' ]); $slot = $trip->calendarSlots()->first(); $response = $this->withHeaders([ 'Authorization' => 'Bearer ' . $this->token, 'Accept' => 'application/json' ])->putJson("/api/calendar-slots/{$slot->id}", [ 'name' => 'Arrival Day' ]); $response->assertStatus(200) ->assertJsonPath('data.name', 'Arrival Day'); $this->assertDatabaseHas('calendar_slots', [ 'id' => $slot->id, 'name' => 'Arrival Day' ]); } public function test_cannot_access_calendar_slots_of_other_users_trip() { $otherUser = User::factory()->create(); $otherTrip = Trip::factory()->create([ 'created_by_user_id' => $otherUser->id, 'start_date' => '2024-01-01', 'end_date' => '2024-01-02' ]); $response = $this->withHeaders([ 'Authorization' => 'Bearer ' . $this->token, 'Accept' => 'application/json' ])->getJson("/api/trips/{$otherTrip->id}/calendar-slots"); $response->assertStatus(403); } public function test_cannot_update_calendar_slot_of_other_users_trip() { $otherUser = User::factory()->create(); $otherTrip = Trip::factory()->create([ 'created_by_user_id' => $otherUser->id, 'start_date' => '2024-01-01', 'end_date' => '2024-01-01' ]); $slot = $otherTrip->calendarSlots()->first(); $response = $this->withHeaders([ 'Authorization' => 'Bearer ' . $this->token, 'Accept' => 'application/json' ])->putJson("/api/calendar-slots/{$slot->id}", [ 'name' => 'Hacked Name' ]); $response->assertStatus(403); } public function test_no_slots_created_for_trip_without_dates() { $tripData = [ 'name' => 'Trip without dates', 'description' => 'No dates set' ]; $response = $this->withHeaders([ 'Authorization' => 'Bearer ' . $this->token, 'Accept' => 'application/json' ])->postJson('/api/trips', $tripData); $response->assertStatus(201); $tripId = $response->json('data.id'); // No slots should be created $this->assertDatabaseCount('calendar_slots', 0); $slots = CalendarSlot::where('trip_id', $tripId)->get(); $this->assertCount(0, $slots); } public function test_slots_created_when_dates_added_to_trip() { $trip = Trip::factory()->create([ 'created_by_user_id' => $this->user->id, 'start_date' => null, 'end_date' => null ]); // Initially no slots $this->assertCount(0, $trip->calendarSlots); // Add dates to trip $response = $this->withHeaders([ 'Authorization' => 'Bearer ' . $this->token, 'Accept' => 'application/json' ])->putJson("/api/trips/{$trip->id}", [ 'name' => $trip->name, 'start_date' => '2024-03-01', 'end_date' => '2024-03-02' ]); $response->assertStatus(200); // Now should have 2 slots $trip->refresh(); $this->assertCount(2, $trip->calendarSlots); } public function test_calendar_slots_include_planned_items_with_plannable_item() { $trip = Trip::factory()->create([ 'created_by_user_id' => $this->user->id, 'start_date' => '2024-01-01', 'end_date' => '2024-01-01' ]); // Create a plannable item and schedule it $plannableItem = \App\Models\PlannableItem::factory()->create([ 'trip_id' => $trip->id, 'name' => 'Eiffel Tower' ]); $response = $this->withHeaders([ 'Authorization' => 'Bearer ' . $this->token, 'Accept' => 'application/json' ])->postJson('/api/planned-items', [ 'plannable_item_id' => $plannableItem->id, 'trip_id' => $trip->id, 'start_datetime' => '2024-01-01 14:00:00', 'end_datetime' => '2024-01-01 16:00:00', ]); $response->assertStatus(201); // Fetch calendar slots and verify relationships are loaded $slotsResponse = $this->withHeaders([ 'Authorization' => 'Bearer ' . $this->token, 'Accept' => 'application/json' ])->getJson("/api/trips/{$trip->id}/calendar-slots"); $slotsResponse->assertStatus(200); // Find the slot we just created $slots = $slotsResponse->json('data'); $scheduledSlot = collect($slots)->firstWhere('name', 'Eiffel Tower'); $this->assertNotNull($scheduledSlot); $this->assertArrayHasKey('planned_items', $scheduledSlot); $this->assertCount(1, $scheduledSlot['planned_items']); $this->assertArrayHasKey('plannable_item', $scheduledSlot['planned_items'][0]); $this->assertEquals('Eiffel Tower', $scheduledSlot['planned_items'][0]['plannable_item']['name']); } public function test_calendar_slots_ordered_by_date_and_time() { $trip = Trip::factory()->create([ 'created_by_user_id' => $this->user->id, 'start_date' => '2024-01-01', 'end_date' => '2024-01-02' ]); // Create plannable items $item1 = \App\Models\PlannableItem::factory()->create(['trip_id' => $trip->id, 'name' => 'Breakfast']); $item2 = \App\Models\PlannableItem::factory()->create(['trip_id' => $trip->id, 'name' => 'Lunch']); $item3 = \App\Models\PlannableItem::factory()->create(['trip_id' => $trip->id, 'name' => 'Dinner Day 1']); $item4 = \App\Models\PlannableItem::factory()->create(['trip_id' => $trip->id, 'name' => 'Breakfast Day 2']); // Schedule in non-chronological order $this->actingAs($this->user)->postJson('/api/planned-items', [ 'plannable_item_id' => $item3->id, 'trip_id' => $trip->id, 'start_datetime' => '2024-01-01 19:00:00', 'end_datetime' => '2024-01-01 21:00:00', ]); $this->actingAs($this->user)->postJson('/api/planned-items', [ 'plannable_item_id' => $item1->id, 'trip_id' => $trip->id, 'start_datetime' => '2024-01-01 08:00:00', 'end_datetime' => '2024-01-01 09:00:00', ]); $this->actingAs($this->user)->postJson('/api/planned-items', [ 'plannable_item_id' => $item4->id, 'trip_id' => $trip->id, 'start_datetime' => '2024-01-02 08:00:00', 'end_datetime' => '2024-01-02 09:00:00', ]); $this->actingAs($this->user)->postJson('/api/planned-items', [ 'plannable_item_id' => $item2->id, 'trip_id' => $trip->id, 'start_datetime' => '2024-01-01 12:00:00', 'end_datetime' => '2024-01-01 13:00:00', ]); // Fetch slots and verify they're ordered correctly $response = $this->withHeaders([ 'Authorization' => 'Bearer ' . $this->token, 'Accept' => 'application/json' ])->getJson("/api/trips/{$trip->id}/calendar-slots"); $response->assertStatus(200); $slots = $response->json('data'); $scheduledSlots = collect($slots)->filter(fn($slot) => $slot['datetime_start'] !== null)->values(); // Should be ordered: Breakfast (Day 1, 8am), Lunch (Day 1, 12pm), Dinner (Day 1, 7pm), Breakfast (Day 2, 8am) $this->assertEquals('Breakfast', $scheduledSlots[0]['name']); $this->assertEquals('Lunch', $scheduledSlots[1]['name']); $this->assertEquals('Dinner Day 1', $scheduledSlots[2]['name']); $this->assertEquals('Breakfast Day 2', $scheduledSlots[3]['name']); } }