Add tests for town sync job and search endpoint
All checks were successful
linter / quality (pull_request) Successful in 1m37s
security / Dependency Audit (pull_request) Successful in 1m36s
security / Static Analysis (pull_request) Successful in 1m25s
tests / ci (8.4) (pull_request) Successful in 1m57s
tests / ci (8.5) (pull_request) Successful in 1m31s

- SyncTownsTest: covers API syncing, pagination, upsert, retired towns, error logging, and success logging
- TownSearchTest: covers auth requirement, min query length, prefix matching, state filtering, retired town exclusion, result limit, response shape, and ordering
- Update TownFactory with retired() and inState() states
This commit is contained in:
2026-03-06 04:22:49 +00:00
parent a37ada96e2
commit 72fb80b673
3 changed files with 289 additions and 1 deletions

View File

@@ -17,7 +17,26 @@ class TownFactory extends Factory
public function definition(): array public function definition(): array
{ {
return [ return [
// 'town_pid' => 'TWN'.$this->faker->unique()->numerify('####'),
'town_name' => $this->faker->city(),
'state' => $this->faker->numberBetween(1, 8),
'population' => $this->faker->numberBetween(100, 500000),
'town_class' => $this->faker->numberBetween(0, 5),
'date_retired' => null,
]; ];
} }
public function retired(): static
{
return $this->state(fn (array $attributes) => [
'date_retired' => now()->subDays(30),
]);
}
public function inState(\App\Enums\AustralianState $state): static
{
return $this->state(fn (array $attributes) => [
'state' => $state->value,
]);
}
} }

View File

@@ -0,0 +1,150 @@
<?php
namespace Tests\Feature\Jobs;
use App\Jobs\SyncTowns;
use App\Models\Town;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Tests\TestCase;
class SyncTownsTest extends TestCase
{
use RefreshDatabase;
private const API_URL = 'https://services-ap1.arcgis.com/ypkPEy1AmwPKGNNv/arcgis/rest/services/Town_Point/FeatureServer/0/query*';
private function makeTownFeature(array $overrides = []): array
{
return [
'attributes' => array_merge([
'town_pid' => 'TWN0001',
'town_name' => 'Perth',
'state' => 5,
'population' => 2000000,
'town_class' => 1,
'date_retired' => null,
], $overrides),
];
}
public function test_syncs_towns_from_api(): void
{
Http::fake([
self::API_URL => Http::sequence()
->push(['features' => [
$this->makeTownFeature(['town_pid' => 'TWN0001', 'town_name' => 'Perth', 'state' => 5]),
$this->makeTownFeature(['town_pid' => 'TWN0002', 'town_name' => 'Fremantle', 'state' => 5]),
]])
->push(['features' => []]),
]);
(new SyncTowns)->handle();
$this->assertDatabaseHas('towns', ['town_pid' => 'TWN0001', 'town_name' => 'Perth']);
$this->assertDatabaseHas('towns', ['town_pid' => 'TWN0002', 'town_name' => 'Fremantle']);
$this->assertDatabaseCount('towns', 2);
}
public function test_paginates_through_all_pages(): void
{
$pageOne = [];
for ($i = 1; $i <= 1000; $i++) {
$pageOne[] = $this->makeTownFeature(['town_pid' => 'TWN'.str_pad($i, 4, '0', STR_PAD_LEFT)]);
}
Http::fake([
self::API_URL => Http::sequence()
->push(['features' => $pageOne])
->push(['features' => [
$this->makeTownFeature(['town_pid' => 'TWN1001', 'town_name' => 'Albany']),
]])
->push(['features' => []]),
]);
(new SyncTowns)->handle();
$this->assertDatabaseCount('towns', 1001);
$this->assertDatabaseHas('towns', ['town_pid' => 'TWN1001', 'town_name' => 'Albany']);
}
public function test_upserts_existing_towns_with_updated_data(): void
{
Town::factory()->create([
'town_pid' => 'TWN0001',
'town_name' => 'Old Name',
'state' => 5,
]);
Http::fake([
self::API_URL => Http::sequence()
->push(['features' => [
$this->makeTownFeature(['town_pid' => 'TWN0001', 'town_name' => 'Perth', 'population' => 2100000]),
]])
->push(['features' => []]),
]);
(new SyncTowns)->handle();
$this->assertDatabaseCount('towns', 1);
$this->assertDatabaseHas('towns', ['town_pid' => 'TWN0001', 'town_name' => 'Perth', 'population' => 2100000]);
}
public function test_marks_retired_towns_with_date_retired(): void
{
$retiredAt = Carbon::parse('2024-01-15')->startOfDay();
Http::fake([
self::API_URL => Http::sequence()
->push(['features' => [
$this->makeTownFeature([
'town_pid' => 'TWN0001',
'town_name' => 'OldTown',
'date_retired' => $retiredAt->getTimestampMs(),
]),
]])
->push(['features' => []]),
]);
(new SyncTowns)->handle();
$town = Town::where('town_pid', 'TWN0001')->first();
$this->assertNotNull($town->date_retired);
}
public function test_logs_error_and_stops_when_api_fails(): void
{
Log::spy();
Http::fake([
self::API_URL => Http::response(null, 500),
]);
(new SyncTowns)->handle();
$this->assertDatabaseCount('towns', 0);
Log::shouldHaveReceived('error')
->once()
->with('SyncTowns: API request failed', \Mockery::any());
}
public function test_logs_synced_count_on_success(): void
{
Log::spy();
Http::fake([
self::API_URL => Http::sequence()
->push(['features' => [
$this->makeTownFeature(['town_pid' => 'TWN0001']),
$this->makeTownFeature(['town_pid' => 'TWN0002']),
]])
->push(['features' => []]),
]);
(new SyncTowns)->handle();
Log::shouldHaveReceived('info')->once()->with('SyncTowns: synced 2 towns.');
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Tests\Feature\Towns;
use App\Enums\AustralianState;
use App\Models\Town;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class TownSearchTest extends TestCase
{
use RefreshDatabase;
private function actingAsUser(): static
{
return $this->actingAs(User::factory()->create());
}
public function test_requires_authentication(): void
{
$this->get('/towns/search?query=Perth')->assertRedirect('/login');
}
public function test_returns_empty_array_for_query_shorter_than_two_characters(): void
{
Town::factory()->create(['town_name' => 'Perth']);
$this->actingAsUser()
->getJson('/towns/search?query=P')
->assertOk()
->assertExactJson([]);
}
public function test_returns_matching_towns_by_prefix(): void
{
Town::factory()->create(['town_name' => 'Perth', 'state' => AustralianState::WA->value]);
Town::factory()->create(['town_name' => 'Pertham', 'state' => AustralianState::WA->value]);
Town::factory()->create(['town_name' => 'Sydney', 'state' => AustralianState::NSW->value]);
$response = $this->actingAsUser()
->getJson('/towns/search?query=Per')
->assertOk();
$this->assertCount(2, $response->json());
$this->assertSame('Perth, WA', $response->json('0.label'));
$this->assertSame('Pertham, WA', $response->json('1.label'));
}
public function test_filters_results_by_state(): void
{
Town::factory()->create(['town_name' => 'Port Hedland', 'state' => AustralianState::WA->value]);
Town::factory()->create(['town_name' => 'Port Augusta', 'state' => AustralianState::SA->value]);
$response = $this->actingAsUser()
->getJson('/towns/search?query=Port&state='.AustralianState::WA->value)
->assertOk();
$this->assertCount(1, $response->json());
$this->assertSame('Port Hedland', $response->json('0.name'));
}
public function test_excludes_retired_towns(): void
{
Town::factory()->create(['town_name' => 'Perth', 'state' => AustralianState::WA->value]);
Town::factory()->retired()->create(['town_name' => 'Perthville', 'state' => AustralianState::WA->value]);
$response = $this->actingAsUser()
->getJson('/towns/search?query=Per')
->assertOk();
$this->assertCount(1, $response->json());
$this->assertSame('Perth', $response->json('0.name'));
}
public function test_returns_at_most_ten_results(): void
{
Town::factory()->count(15)->create([
'town_name' => 'Perth',
'state' => AustralianState::WA->value,
]);
$response = $this->actingAsUser()
->getJson('/towns/search?query=Per')
->assertOk();
$this->assertCount(10, $response->json());
}
public function test_response_includes_name_state_and_label_fields(): void
{
Town::factory()->create(['town_name' => 'Perth', 'state' => AustralianState::WA->value]);
$response = $this->actingAsUser()
->getJson('/towns/search?query=Per')
->assertOk();
$result = $response->json('0');
$this->assertArrayHasKey('name', $result);
$this->assertArrayHasKey('state', $result);
$this->assertArrayHasKey('label', $result);
$this->assertSame('Perth', $result['name']);
$this->assertSame('WA', $result['state']);
$this->assertSame('Perth, WA', $result['label']);
}
public function test_results_are_ordered_alphabetically(): void
{
Town::factory()->create(['town_name' => 'Pertham', 'state' => AustralianState::WA->value]);
Town::factory()->create(['town_name' => 'Perth', 'state' => AustralianState::WA->value]);
$response = $this->actingAsUser()
->getJson('/towns/search?query=Per')
->assertOk();
$this->assertSame('Perth', $response->json('0.name'));
$this->assertSame('Pertham', $response->json('1.name'));
}
}