Files
fuel-price/tests/Feature/Api/StationControllerTest.php
Ovidiu U 5acb99c9e3
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
Remove obsolete Livewire fuel search components and consolidate pricing tiers
- Delete unused Livewire Search test and fuel type select Blade component
- Move subscription webhook listener from EventServiceProvider to AppServiceProvider
- Add FUEL_TYPES global config to app layout for client-side use
- Add Billable trait to User model and include email_verified_at in fillable
- Implement monthly/annual cadence toggle with pricing display and smart CTA routing on homepage
- Update VerifyApiKeyMiddlewareTest to use e10 instead of petrol
- Refactor PollFuelPrices to auto-refresh stale stations based on last_seen_at
- Add incremental polling with cached timestamp and effective-start-timestamp param to FuelPriceService
- Normalize amenities/fuel_types from API objects to flat arrays, skip stations missing required fields
- Log response body on API failures in ApiLogger
- Default homepage sort to 'reliable' instead of 'price'
2026-04-20 14:12:15 +01:00

195 lines
7.1 KiB
PHP

<?php
use App\Enums\FuelType;
use App\Models\Station;
use App\Models\StationPriceCurrent;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->withHeaders(['X-Api-Key' => config('app.api_secret_key')]);
});
it('returns stations near coordinates filtered by fuel type', function () {
$station = Station::factory()->create(['lat' => 52.555064, 'lng' => -0.256119]);
StationPriceCurrent::factory()->create([
'station_id' => $station->node_id,
'fuel_type' => FuelType::B7Standard,
'price_pence' => 14500,
]);
$this->getJson('/api/stations?lat=52.555064&lng=-0.256119&fuel_type=b7_standard&radius=10&sort=price')
->assertOk()
->assertJsonStructure([
'data' => [['station_id', 'name', 'brand', 'is_supermarket', 'lat', 'lng', 'distance_km', 'fuel_type', 'price_pence', 'price', 'price_updated_at']],
'meta' => ['count', 'fuel_type', 'radius_km', 'lowest_pence'],
])
->assertJsonPath('data.0.price_pence', 14500)
->assertJsonPath('meta.fuel_type', 'b7_standard');
});
it('excludes stations with no matching fuel type', function () {
$station = Station::factory()->create(['lat' => 52.555064, 'lng' => -0.256119]);
StationPriceCurrent::factory()->create([
'station_id' => $station->node_id,
'fuel_type' => FuelType::E10, // not diesel
'price_pence' => 13800,
]);
$this->getJson('/api/stations?lat=52.555064&lng=-0.256119&fuel_type=b7_standard&radius=10')
->assertOk()
->assertJsonPath('meta.count', 0);
});
it('excludes temporarily closed stations', function () {
$closed = Station::factory()->create([
'lat' => 52.555064, 'lng' => -0.256119,
'temporary_closure' => true,
]);
StationPriceCurrent::factory()->create([
'station_id' => $closed->node_id,
'fuel_type' => FuelType::B7Standard,
'price_pence' => 14200,
]);
$this->getJson('/api/stations?lat=52.555064&lng=-0.256119&fuel_type=b7_standard&radius=10')
->assertOk()
->assertJsonPath('meta.count', 0);
});
it('excludes stations beyond radius', function () {
// Station ~100km north
$farStation = Station::factory()->create(['lat' => 53.5, 'lng' => -0.256119]);
StationPriceCurrent::factory()->create([
'station_id' => $farStation->node_id,
'fuel_type' => FuelType::B7Standard,
'price_pence' => 14200,
]);
$this->getJson('/api/stations?lat=52.555064&lng=-0.256119&fuel_type=b7_standard&radius=10')
->assertOk()
->assertJsonPath('meta.count', 0);
});
it('sorts by price when sort=price', function () {
$sLat = 52.555;
$sLng = -0.256;
$cheap = Station::factory()->create(['lat' => $sLat, 'lng' => $sLng]);
$expensive = Station::factory()->create(['lat' => $sLat + 0.001, 'lng' => $sLng]);
StationPriceCurrent::factory()->create(['station_id' => $cheap->node_id, 'fuel_type' => FuelType::B7Standard, 'price_pence' => 13900]);
StationPriceCurrent::factory()->create(['station_id' => $expensive->node_id, 'fuel_type' => FuelType::B7Standard, 'price_pence' => 14500]);
$this->getJson("/api/stations?lat={$sLat}&lng={$sLng}&fuel_type=b7_standard&radius=10&sort=price")
->assertOk()
->assertJsonPath('data.0.price_pence', 13900);
});
it('logs a search record for each request', function () {
$station = Station::factory()->create(['lat' => 52.555064, 'lng' => -0.256119]);
StationPriceCurrent::factory()->create(['station_id' => $station->node_id, 'fuel_type' => FuelType::B7Standard, 'price_pence' => 14500]);
$this->getJson('/api/stations?lat=52.555064&lng=-0.256119&fuel_type=b7_standard&radius=10');
$this->assertDatabaseHas('searches', [
'lat_bucket' => '52.56',
'lng_bucket' => '-0.26',
'fuel_type' => 'b7_standard',
'results_count' => 1,
'lowest_pence' => 14500,
'highest_pence' => 14500,
]);
});
it('returns 422 when required params are missing', function () {
$this->getJson('/api/stations?lat=52.5')
->assertUnprocessable();
});
it('resolves a full postcode to coordinates and returns nearby stations', function () {
$station = Station::factory()->create(['lat' => 51.5010, 'lng' => -0.1415]);
StationPriceCurrent::factory()->create([
'station_id' => $station->node_id,
'fuel_type' => FuelType::E10,
'price_pence' => 14200,
]);
Http::fake([
'api.postcodes.io/postcodes/SW1A1AA' => Http::response([
'status' => 200,
'result' => ['postcode' => 'SW1A 1AA', 'latitude' => 51.5010, 'longitude' => -0.1415],
]),
]);
$this->getJson('/api/stations?postcode=SW1A+1AA&fuel_type=e10&radius=1')
->assertOk()
->assertJsonPath('meta.count', 1);
});
it('resolves an outcode to coordinates', function () {
$station = Station::factory()->create(['lat' => 51.5010, 'lng' => -0.1415]);
StationPriceCurrent::factory()->create([
'station_id' => $station->node_id,
'fuel_type' => FuelType::E10,
'price_pence' => 14200,
]);
Http::fake([
'api.postcodes.io/outcodes/SW1A' => Http::response([
'status' => 200,
'result' => ['outcode' => 'SW1A', 'latitude' => 51.5010, 'longitude' => -0.1415],
]),
]);
$this->getJson('/api/stations?postcode=SW1A&fuel_type=e10&radius=1')
->assertOk()
->assertJsonPath('meta.count', 1);
});
it('returns 422 when postcode cannot be resolved', function () {
Http::fake([
'api.postcodes.io/*' => Http::response(['status' => 404, 'error' => 'Postcode not found'], 404),
]);
$this->getJson('/api/stations?postcode=ZZ99+9ZZ&fuel_type=e10')
->assertUnprocessable()
->assertJsonValidationErrors(['postcode']);
});
it('includes resolved lat and lng in meta', function () {
$station = Station::factory()->create(['lat' => 52.555064, 'lng' => -0.256119]);
StationPriceCurrent::factory()->create([
'station_id' => $station->node_id,
'fuel_type' => FuelType::B7Standard,
'price_pence' => 14500,
]);
$this->getJson('/api/stations?lat=52.555064&lng=-0.256119&fuel_type=b7_standard&radius=10')
->assertOk()
->assertJsonPath('meta.lat', 52.555064)
->assertJsonPath('meta.lng', -0.256119);
});
it('includes resolved lat and lng in meta when postcode is provided', function () {
$station = Station::factory()->create(['lat' => 51.5010, 'lng' => -0.1415]);
StationPriceCurrent::factory()->create([
'station_id' => $station->node_id,
'fuel_type' => FuelType::E10,
'price_pence' => 14200,
]);
Http::fake([
'api.postcodes.io/postcodes/SW1A1AA' => Http::response([
'status' => 200,
'result' => ['postcode' => 'SW1A 1AA', 'latitude' => 51.5010, 'longitude' => -0.1415],
]),
]);
$this->getJson('/api/stations?postcode=SW1A+1AA&fuel_type=e10&radius=1')
->assertOk()
->assertJsonPath('meta.lat', 51.5010)
->assertJsonPath('meta.lng', -0.1415);
});