apilogs
This commit is contained in:
65
tests/Unit/ApiLoggerTest.php
Normal file
65
tests/Unit/ApiLoggerTest.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
use App\Models\ApiLog;
|
||||
use App\Services\ApiLogger;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->apiLogger = new ApiLogger;
|
||||
});
|
||||
|
||||
it('logs a successful GET request', function (): void {
|
||||
Http::fake(['https://example.com/data' => Http::response(['ok' => true])]);
|
||||
|
||||
$this->apiLogger->send('test_service', 'GET', 'https://example.com/data', fn () => Http::get('https://example.com/data'));
|
||||
|
||||
$log = ApiLog::first();
|
||||
expect($log)->not->toBeNull()
|
||||
->and($log->service)->toBe('test_service')
|
||||
->and($log->method)->toBe('GET')
|
||||
->and($log->url)->toBe('https://example.com/data')
|
||||
->and($log->status_code)->toBe(200)
|
||||
->and($log->error)->toBeNull()
|
||||
->and($log->duration_ms)->toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('logs a failed request and re-throws the exception', function (): void {
|
||||
Http::fake(['https://example.com/fail' => fn () => throw new RuntimeException('connection refused')]);
|
||||
|
||||
expect(fn () => $this->apiLogger->send(
|
||||
'test_service', 'GET', 'https://example.com/fail',
|
||||
fn () => Http::get('https://example.com/fail')
|
||||
))->toThrow(RuntimeException::class, 'connection refused');
|
||||
|
||||
$log = ApiLog::first();
|
||||
expect($log)->not->toBeNull()
|
||||
->and($log->status_code)->toBeNull()
|
||||
->and($log->error)->toBe('connection refused');
|
||||
});
|
||||
|
||||
it('logs a POST request with correct method', function (): void {
|
||||
Http::fake(['https://example.com/token' => Http::response(['token' => 'abc'], 201)]);
|
||||
|
||||
$this->apiLogger->send('test_service', 'POST', 'https://example.com/token', fn () => Http::post('https://example.com/token', ['key' => 'val']));
|
||||
|
||||
expect(ApiLog::first()->method)->toBe('POST');
|
||||
});
|
||||
|
||||
it('records duration in milliseconds', function (): void {
|
||||
Http::fake(['https://example.com/slow' => Http::response([])]);
|
||||
|
||||
$this->apiLogger->send('test_service', 'GET', 'https://example.com/slow', fn () => Http::get('https://example.com/slow'));
|
||||
|
||||
expect(ApiLog::first()->duration_ms)->toBeInt()->toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('upcases the method', function (): void {
|
||||
Http::fake(['https://example.com/*' => Http::response([])]);
|
||||
|
||||
$this->apiLogger->send('test_service', 'get', 'https://example.com/x', fn () => Http::get('https://example.com/x'));
|
||||
|
||||
expect(ApiLog::first()->method)->toBe('GET');
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
use App\Models\Station;
|
||||
use App\Models\StationPrice;
|
||||
use App\Models\StationPriceCurrent;
|
||||
use App\Services\ApiLogger;
|
||||
use App\Services\FuelPriceService;
|
||||
use App\Services\StationTaggingService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
@@ -12,7 +13,7 @@ use Illuminate\Support\Facades\Http;
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->service = new FuelPriceService(new StationTaggingService());
|
||||
$this->service = new FuelPriceService(new StationTaggingService, new ApiLogger);
|
||||
});
|
||||
|
||||
it('fetches and caches an access token', function (): void {
|
||||
@@ -20,15 +21,15 @@ it('fetches and caches an access token', function (): void {
|
||||
'*/oauth/generate_access_token' => Http::response([
|
||||
'data' => [
|
||||
'access_token' => 'test-token-abc',
|
||||
'expires_in' => 3600,
|
||||
'expires_in' => 3600,
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
$token = $this->service->getAccessToken();
|
||||
|
||||
expect($token)->toBe('test-token-abc');
|
||||
expect(Cache::get('fuel_finder_access_token'))->toBe('test-token-abc');
|
||||
expect($token)->toBe('test-token-abc')
|
||||
->and(Cache::get('fuel_finder_access_token'))->toBe('test-token-abc');
|
||||
});
|
||||
|
||||
it('returns cached token without hitting API', function (): void {
|
||||
@@ -45,29 +46,29 @@ it('returns cached token without hitting API', function (): void {
|
||||
it('upserts stations from API batch response', function (): void {
|
||||
$apiStations = [
|
||||
[
|
||||
'node_id' => 'abc123',
|
||||
'trading_name' => 'Village Garage',
|
||||
'brand_name' => 'Village Garage',
|
||||
'node_id' => 'abc123',
|
||||
'trading_name' => 'Village Garage',
|
||||
'brand_name' => 'Village Garage',
|
||||
'is_same_trading_and_brand_name' => true,
|
||||
'is_motorway_service_station' => false,
|
||||
'is_motorway_service_station' => false,
|
||||
'is_supermarket_service_station' => false,
|
||||
'temporary_closure' => false,
|
||||
'permanent_closure' => false,
|
||||
'permanent_closure_date' => null,
|
||||
'public_phone_number' => null,
|
||||
'location' => [
|
||||
'temporary_closure' => false,
|
||||
'permanent_closure' => false,
|
||||
'permanent_closure_date' => null,
|
||||
'public_phone_number' => null,
|
||||
'location' => [
|
||||
'address_line_1' => '1 High Street',
|
||||
'address_line_2' => null,
|
||||
'city' => 'London',
|
||||
'county' => null,
|
||||
'country' => 'England',
|
||||
'postcode' => 'SW1A 1AA',
|
||||
'latitude' => 51.5,
|
||||
'longitude' => -0.1,
|
||||
'city' => 'London',
|
||||
'county' => null,
|
||||
'country' => 'England',
|
||||
'postcode' => 'SW1A 1AA',
|
||||
'latitude' => 51.5,
|
||||
'longitude' => -0.1,
|
||||
],
|
||||
'amenities' => [],
|
||||
'opening_times'=> null,
|
||||
'fuel_types' => ['E10', 'E5'],
|
||||
'amenities' => [],
|
||||
'opening_times' => null,
|
||||
'fuel_types' => ['E10', 'E5'],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -83,32 +84,184 @@ it('upserts stations from API batch response', function (): void {
|
||||
|
||||
it('tags supermarket stations during upsert', function (): void {
|
||||
$apiStations = [[
|
||||
'node_id' => 'tesco1',
|
||||
'trading_name' => 'TESCO',
|
||||
'brand_name' => 'TESCO',
|
||||
'node_id' => 'tesco1',
|
||||
'trading_name' => 'TESCO',
|
||||
'brand_name' => 'TESCO',
|
||||
'is_same_trading_and_brand_name' => true,
|
||||
'is_motorway_service_station' => false,
|
||||
'is_motorway_service_station' => false,
|
||||
'is_supermarket_service_station' => true,
|
||||
'temporary_closure' => false,
|
||||
'permanent_closure' => false,
|
||||
'permanent_closure_date' => null,
|
||||
'public_phone_number' => null,
|
||||
'location' => [
|
||||
'temporary_closure' => false,
|
||||
'permanent_closure' => false,
|
||||
'permanent_closure_date' => null,
|
||||
'public_phone_number' => null,
|
||||
'location' => [
|
||||
'address_line_1' => '1 Tesco Way',
|
||||
'address_line_2' => null,
|
||||
'city' => 'Bristol',
|
||||
'county' => null,
|
||||
'country' => 'England',
|
||||
'postcode' => 'BS1 1AA',
|
||||
'latitude' => 51.45,
|
||||
'longitude' => -2.6,
|
||||
'city' => 'Bristol',
|
||||
'county' => null,
|
||||
'country' => 'England',
|
||||
'postcode' => 'BS1 1AA',
|
||||
'latitude' => 51.45,
|
||||
'longitude' => -2.6,
|
||||
],
|
||||
'amenities' => [],
|
||||
'opening_times'=> null,
|
||||
'fuel_types' => ['E10'],
|
||||
'amenities' => [],
|
||||
'opening_times' => null,
|
||||
'fuel_types' => ['E10'],
|
||||
]];
|
||||
|
||||
$this->service->upsertStations($apiStations);
|
||||
|
||||
expect(Station::find('tesco1')->is_supermarket)->toBeTrue();
|
||||
});
|
||||
|
||||
// --- pollPrices ---
|
||||
|
||||
it('inserts new price records and upserts current prices on poll', function (): void {
|
||||
Cache::put('fuel_finder_access_token', 'tok', 3540);
|
||||
|
||||
Http::fake([
|
||||
'*/pfs/fuel-prices*' => Http::sequence()
|
||||
->push([
|
||||
[
|
||||
'node_id' => 'sta1',
|
||||
'fuel_prices' => [
|
||||
[
|
||||
'fuel_type' => 'E10',
|
||||
'price' => 142.9,
|
||||
'price_last_updated' => '2026-04-04T10:00:00.000Z',
|
||||
'price_change_effective_timestamp' => '2026-04-04T10:00:00.000Z',
|
||||
],
|
||||
],
|
||||
],
|
||||
])
|
||||
->push([]),
|
||||
]);
|
||||
|
||||
Station::factory()->create(['node_id' => 'sta1']);
|
||||
|
||||
$inserted = $this->service->pollPrices();
|
||||
|
||||
expect($inserted)->toBe(1)
|
||||
->and(StationPrice::count())->toBe(1)
|
||||
->and(StationPriceCurrent::where('station_id', 'sta1')->where('fuel_type', 'e10')->value('price_pence'))->toBe(14290);
|
||||
});
|
||||
|
||||
it('does not insert a history row when price is unchanged', function (): void {
|
||||
Cache::put('fuel_finder_access_token', 'tok', 3540);
|
||||
|
||||
Station::factory()->create(['node_id' => 'sta1']);
|
||||
|
||||
StationPriceCurrent::insert([
|
||||
'station_id' => 'sta1',
|
||||
'fuel_type' => 'e10',
|
||||
'price_pence' => 14290,
|
||||
'price_effective_at' => now()->subHour(),
|
||||
'price_reported_at' => now()->subHour(),
|
||||
'recorded_at' => now()->subHour(),
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'*/pfs/fuel-prices*' => Http::sequence()
|
||||
->push([
|
||||
[
|
||||
'node_id' => 'sta1',
|
||||
'fuel_prices' => [
|
||||
[
|
||||
'fuel_type' => 'E10',
|
||||
'price' => 142.9,
|
||||
'price_last_updated' => '2026-04-04T10:00:00.000Z',
|
||||
'price_change_effective_timestamp' => '2026-04-04T10:00:00.000Z',
|
||||
],
|
||||
],
|
||||
],
|
||||
])
|
||||
->push([]),
|
||||
]);
|
||||
|
||||
$inserted = $this->service->pollPrices();
|
||||
|
||||
expect($inserted)->toBe(0)
|
||||
->and(StationPrice::count())->toBe(0);
|
||||
});
|
||||
|
||||
it('inserts a history row when price has changed', function (): void {
|
||||
Cache::put('fuel_finder_access_token', 'tok', 3540);
|
||||
|
||||
Station::factory()->create(['node_id' => 'sta1']);
|
||||
|
||||
StationPriceCurrent::insert([
|
||||
'station_id' => 'sta1',
|
||||
'fuel_type' => 'e10',
|
||||
'price_pence' => 14290,
|
||||
'price_effective_at' => now()->subHour(),
|
||||
'price_reported_at' => now()->subHour(),
|
||||
'recorded_at' => now()->subHour(),
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'*/pfs/fuel-prices*' => Http::sequence()
|
||||
->push([
|
||||
[
|
||||
'node_id' => 'sta1',
|
||||
'fuel_prices' => [
|
||||
[
|
||||
'fuel_type' => 'E10',
|
||||
'price' => 139.9,
|
||||
'price_last_updated' => '2026-04-04T12:00:00.000Z',
|
||||
'price_change_effective_timestamp' => '2026-04-04T12:00:00.000Z',
|
||||
],
|
||||
],
|
||||
],
|
||||
])
|
||||
->push([]),
|
||||
]);
|
||||
|
||||
$inserted = $this->service->pollPrices();
|
||||
|
||||
expect($inserted)->toBe(1)
|
||||
->and(StationPrice::first()->price_pence)->toBe(13990)
|
||||
->and(StationPriceCurrent::where('station_id', 'sta1')->value('price_pence'))->toBe(13990);
|
||||
});
|
||||
|
||||
it('skips unknown fuel types without error', function (): void {
|
||||
Cache::put('fuel_finder_access_token', 'tok', 3540);
|
||||
|
||||
Station::factory()->create(['node_id' => 'sta1']);
|
||||
|
||||
Http::fake([
|
||||
'*/pfs/fuel-prices*' => Http::sequence()
|
||||
->push([
|
||||
[
|
||||
'node_id' => 'sta1',
|
||||
'fuel_prices' => [
|
||||
[
|
||||
'fuel_type' => 'UNKNOWN_FUEL',
|
||||
'price' => 150.0,
|
||||
'price_last_updated' => '2026-04-04T10:00:00.000Z',
|
||||
'price_change_effective_timestamp' => '2026-04-04T10:00:00.000Z',
|
||||
],
|
||||
],
|
||||
],
|
||||
])
|
||||
->push([]),
|
||||
]);
|
||||
|
||||
$inserted = $this->service->pollPrices();
|
||||
|
||||
expect($inserted)->toBe(0)
|
||||
->and(StationPrice::count())->toBe(0);
|
||||
});
|
||||
|
||||
it('stops pagination when an empty batch is returned', function (): void {
|
||||
Cache::put('fuel_finder_access_token', 'tok', 3540);
|
||||
|
||||
Http::fake([
|
||||
'*/pfs/fuel-prices*' => Http::sequence()
|
||||
->push([])
|
||||
->push([['node_id' => 'never', 'fuel_prices' => []]]),
|
||||
]);
|
||||
|
||||
$this->service->pollPrices();
|
||||
|
||||
Http::assertSentCount(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user