This commit is contained in:
@@ -12,7 +12,7 @@ beforeEach(function () {
|
||||
$this->token = User::factory()->create()->createToken('test-token')->plainTextToken;
|
||||
});
|
||||
|
||||
test('lists active routes with nested company, destinations, time slots and pricing', function () {
|
||||
test('searches active routes with nested company, destinations, time slots and pricing', function () {
|
||||
$route = EvRoute::factory()->create(['is_active' => true]);
|
||||
EvRoute::factory()->create(['is_active' => false]);
|
||||
|
||||
@@ -26,17 +26,18 @@ test('lists active routes with nested company, destinations, time slots and pric
|
||||
]);
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$this->token}")
|
||||
->getJson('/api/v1/routes')
|
||||
->postJson('/api/v1/routes/search')
|
||||
->assertSuccessful()
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.id', $route->id)
|
||||
->assertJsonPath('data.0.company.id', $route->ev_company_id)
|
||||
->assertJsonPath('data.0.from_destination.id', $route->from_destination_id)
|
||||
->assertJsonPath('data.0.to_destination.id', $route->to_destination_id)
|
||||
->assertJsonPath('data.0.time_slots.0.id', $slot->id)
|
||||
->assertJsonPath('data.0.time_slots.0.is_active', true)
|
||||
->assertJsonPath('data.0.pricing.0.vehicle_option', 'front_seat')
|
||||
->assertJsonPath('data.0.pricing.0.price', '12000.00');
|
||||
->assertJsonCount(1, 'routes.data')
|
||||
->assertJsonPath('routes.data.0.id', $route->id)
|
||||
->assertJsonPath('routes.data.0.company.id', $route->ev_company_id)
|
||||
->assertJsonPath('routes.data.0.from_destination.id', $route->from_destination_id)
|
||||
->assertJsonPath('routes.data.0.to_destination.id', $route->to_destination_id)
|
||||
->assertJsonPath('routes.data.0.time_slots.0.id', $slot->id)
|
||||
->assertJsonPath('routes.data.0.time_slots.0.is_active', true)
|
||||
->assertJsonPath('routes.data.0.pricing.0.vehicle_option', 'front_seat')
|
||||
->assertJsonPath('routes.data.0.pricing.0.price', '12000.00')
|
||||
->assertJsonCount(0, 'return_routes.data');
|
||||
});
|
||||
|
||||
test('filters routes by company, from, and to', function () {
|
||||
@@ -68,24 +69,211 @@ test('filters routes by company, from, and to', function () {
|
||||
]);
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$this->token}")
|
||||
->getJson('/api/v1/routes?'.http_build_query([
|
||||
->postJson('/api/v1/routes/search', [
|
||||
'company' => $companyA->id,
|
||||
'from' => $yangon->id,
|
||||
'to' => $mandalay->id,
|
||||
]))
|
||||
])
|
||||
->assertSuccessful()
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.id', $matching->id);
|
||||
->assertJsonCount(1, 'routes.data')
|
||||
->assertJsonPath('routes.data.0.id', $matching->id);
|
||||
});
|
||||
|
||||
test('filters routes by time_slot', function () {
|
||||
$morning = DepartureTimeSlot::factory()->create(['time' => '06:00']);
|
||||
$evening = DepartureTimeSlot::factory()->create(['time' => '18:00']);
|
||||
|
||||
$morningRoute = EvRoute::factory()->create(['is_active' => true]);
|
||||
$morningRoute->timeSlots()->attach($morning->id, ['is_active' => true]);
|
||||
|
||||
$eveningRoute = EvRoute::factory()->create(['is_active' => true]);
|
||||
$eveningRoute->timeSlots()->attach($evening->id, ['is_active' => true]);
|
||||
|
||||
// Attached but inactive on this route — must not match.
|
||||
$inactivePivotRoute = EvRoute::factory()->create(['is_active' => true]);
|
||||
$inactivePivotRoute->timeSlots()->attach($morning->id, ['is_active' => false]);
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$this->token}")
|
||||
->postJson('/api/v1/routes/search', ['time_slot' => '06:00'])
|
||||
->assertSuccessful()
|
||||
->assertJsonCount(1, 'routes.data')
|
||||
->assertJsonPath('routes.data.0.id', $morningRoute->id);
|
||||
});
|
||||
|
||||
test('round trip: returns both routes and return_routes, swapped from/to', function () {
|
||||
$company = EvCompany::factory()->create();
|
||||
$yangon = Destination::factory()->create();
|
||||
$mandalay = Destination::factory()->create();
|
||||
|
||||
$outbound = EvRoute::factory()->create([
|
||||
'ev_company_id' => $company->id,
|
||||
'from_destination_id' => $yangon->id,
|
||||
'to_destination_id' => $mandalay->id,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$return = EvRoute::factory()->create([
|
||||
'ev_company_id' => $company->id,
|
||||
'from_destination_id' => $mandalay->id,
|
||||
'to_destination_id' => $yangon->id,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$this->token}")
|
||||
->postJson('/api/v1/routes/search', [
|
||||
'round_trip' => true,
|
||||
'from' => $yangon->id,
|
||||
'to' => $mandalay->id,
|
||||
])
|
||||
->assertSuccessful()
|
||||
->assertJsonCount(1, 'routes.data')
|
||||
->assertJsonPath('routes.data.0.id', $outbound->id)
|
||||
->assertJsonCount(1, 'return_routes.data')
|
||||
->assertJsonPath('return_routes.data.0.id', $return->id);
|
||||
});
|
||||
|
||||
test('response includes the distinct companies and time_slots actually available for the from-to pair', function () {
|
||||
$yangon = Destination::factory()->create();
|
||||
$mandalay = Destination::factory()->create();
|
||||
|
||||
$companyA = EvCompany::factory()->create(['name' => 'Alpha EV']);
|
||||
$companyB = EvCompany::factory()->create(['name' => 'Beta EV']);
|
||||
$morning = DepartureTimeSlot::factory()->create(['time' => '06:00']);
|
||||
$evening = DepartureTimeSlot::factory()->create(['time' => '18:00']);
|
||||
$inactiveSlot = DepartureTimeSlot::factory()->create(['time' => '12:00']);
|
||||
|
||||
$routeA = EvRoute::factory()->create([
|
||||
'ev_company_id' => $companyA->id,
|
||||
'from_destination_id' => $yangon->id,
|
||||
'to_destination_id' => $mandalay->id,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$routeA->timeSlots()->attach([$morning->id => ['is_active' => true], $inactiveSlot->id => ['is_active' => false]]);
|
||||
|
||||
$routeB = EvRoute::factory()->create([
|
||||
'ev_company_id' => $companyB->id,
|
||||
'from_destination_id' => $yangon->id,
|
||||
'to_destination_id' => $mandalay->id,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$routeB->timeSlots()->attach($evening->id, ['is_active' => true]);
|
||||
|
||||
// Unrelated pair — must not leak into the facets.
|
||||
$bagan = Destination::factory()->create();
|
||||
$unrelated = EvRoute::factory()->create([
|
||||
'from_destination_id' => $yangon->id,
|
||||
'to_destination_id' => $bagan->id,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$unrelated->timeSlots()->attach(DepartureTimeSlot::factory()->create(['time' => '09:00'])->id, ['is_active' => true]);
|
||||
|
||||
// Applying a company filter narrows `routes.data` but must not narrow
|
||||
// the facets themselves — facets always reflect the full from-to pair.
|
||||
$response = $this->withHeader('Authorization', "Bearer {$this->token}")
|
||||
->postJson('/api/v1/routes/search', [
|
||||
'from' => $yangon->id,
|
||||
'to' => $mandalay->id,
|
||||
'company' => $companyA->id,
|
||||
])
|
||||
->assertSuccessful()
|
||||
->assertJsonCount(1, 'routes.data')
|
||||
->json();
|
||||
|
||||
expect(collect($response['filters']['companies'])->pluck('id')->sort()->values()->all())
|
||||
->toBe([$companyA->id, $companyB->id]);
|
||||
// Facet shape is trimmed to id/name/mm_name — not the full company resource.
|
||||
expect(array_keys($response['filters']['companies'][0]))->toBe(['id', 'name', 'mm_name']);
|
||||
expect(collect($response['filters']['time_slots'])->pluck('time')->all())
|
||||
->toBe(['06:00', '18:00']); // sorted by time, inactive pivot and unrelated pair excluded
|
||||
});
|
||||
|
||||
test('filter options are empty when from/to are not both given', function () {
|
||||
$this->withHeader('Authorization', "Bearer {$this->token}")
|
||||
->postJson('/api/v1/routes/search')
|
||||
->assertSuccessful()
|
||||
->assertJson(['filters' => ['companies' => [], 'time_slots' => []]]);
|
||||
});
|
||||
|
||||
test('round trip: return_filters reflect the swapped to-from pair', function () {
|
||||
$yangon = Destination::factory()->create();
|
||||
$mandalay = Destination::factory()->create();
|
||||
$returnCompany = EvCompany::factory()->create();
|
||||
|
||||
EvRoute::factory()->create([
|
||||
'from_destination_id' => $yangon->id,
|
||||
'to_destination_id' => $mandalay->id,
|
||||
'is_active' => true,
|
||||
]);
|
||||
EvRoute::factory()->create([
|
||||
'ev_company_id' => $returnCompany->id,
|
||||
'from_destination_id' => $mandalay->id,
|
||||
'to_destination_id' => $yangon->id,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$response = $this->withHeader('Authorization', "Bearer {$this->token}")
|
||||
->postJson('/api/v1/routes/search', [
|
||||
'round_trip' => true,
|
||||
'from' => $yangon->id,
|
||||
'to' => $mandalay->id,
|
||||
])
|
||||
->assertSuccessful()
|
||||
->json();
|
||||
|
||||
expect(collect($response['return_filters']['companies'])->pluck('id')->all())
|
||||
->toBe([$returnCompany->id]);
|
||||
});
|
||||
|
||||
test('round trip: routes and return_routes paginate independently via page and return_page', function () {
|
||||
$company = EvCompany::factory()->create();
|
||||
$yangon = Destination::factory()->create();
|
||||
$mandalay = Destination::factory()->create();
|
||||
|
||||
// 20 outbound routes (2 pages of 15), only 3 return routes (1 page).
|
||||
EvRoute::factory()->count(20)->create([
|
||||
'ev_company_id' => $company->id,
|
||||
'from_destination_id' => $yangon->id,
|
||||
'to_destination_id' => $mandalay->id,
|
||||
'is_active' => true,
|
||||
]);
|
||||
EvRoute::factory()->count(3)->create([
|
||||
'ev_company_id' => $company->id,
|
||||
'from_destination_id' => $mandalay->id,
|
||||
'to_destination_id' => $yangon->id,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
// page=2 must give the 2nd page of routes (5 remaining), while
|
||||
// return_routes — with no return_page given — must still return its own
|
||||
// full page 1 (all 3), not an empty slice at offset 2.
|
||||
$this->withHeader('Authorization', "Bearer {$this->token}")
|
||||
->postJson('/api/v1/routes/search', [
|
||||
'round_trip' => true,
|
||||
'from' => $yangon->id,
|
||||
'to' => $mandalay->id,
|
||||
'page' => 2,
|
||||
])
|
||||
->assertSuccessful()
|
||||
->assertJsonCount(5, 'routes.data')
|
||||
->assertJsonPath('routes.meta.current_page', 2)
|
||||
->assertJsonCount(3, 'return_routes.data')
|
||||
->assertJsonPath('return_routes.meta.current_page', 1);
|
||||
});
|
||||
|
||||
test('round_trip without from and to is rejected', function () {
|
||||
$this->withHeader('Authorization', "Bearer {$this->token}")
|
||||
->postJson('/api/v1/routes/search', ['round_trip' => true])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['from', 'to']);
|
||||
});
|
||||
|
||||
test('paginates routes', function () {
|
||||
EvRoute::factory()->count(20)->create(['is_active' => true]);
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$this->token}")
|
||||
->getJson('/api/v1/routes')
|
||||
->postJson('/api/v1/routes/search')
|
||||
->assertSuccessful()
|
||||
->assertJsonCount(15, 'data')
|
||||
->assertJsonPath('meta.total', 20);
|
||||
->assertJsonCount(15, 'routes.data')
|
||||
->assertJsonPath('routes.meta.total', 20);
|
||||
});
|
||||
|
||||
test('shows a single active route', function () {
|
||||
@@ -148,7 +336,7 @@ test('lists a route\'s time slots with the pivot active flag', function () {
|
||||
test('routes endpoints reject unauthenticated requests', function () {
|
||||
$route = EvRoute::factory()->create(['is_active' => true]);
|
||||
|
||||
$this->getJson('/api/v1/routes')->assertUnauthorized();
|
||||
$this->postJson('/api/v1/routes/search')->assertUnauthorized();
|
||||
$this->getJson("/api/v1/routes/{$route->id}")->assertUnauthorized();
|
||||
$this->getJson("/api/v1/routes/{$route->id}/pricing")->assertUnauthorized();
|
||||
$this->getJson("/api/v1/routes/{$route->id}/time-slots")->assertUnauthorized();
|
||||
|
||||
Reference in New Issue
Block a user