From 72fb80b67338b4616582510b7bda4683b4fca7d3 Mon Sep 17 00:00:00 2001 From: Tim Basten Date: Fri, 6 Mar 2026 04:22:49 +0000 Subject: [PATCH] Add tests for town sync job and search endpoint - 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 --- database/factories/TownFactory.php | 21 +++- tests/Feature/Jobs/SyncTownsTest.php | 150 +++++++++++++++++++++++++ tests/Feature/Towns/TownSearchTest.php | 119 ++++++++++++++++++++ 3 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/Jobs/SyncTownsTest.php create mode 100644 tests/Feature/Towns/TownSearchTest.php diff --git a/database/factories/TownFactory.php b/database/factories/TownFactory.php index 9d8732a..4892455 100644 --- a/database/factories/TownFactory.php +++ b/database/factories/TownFactory.php @@ -17,7 +17,26 @@ class TownFactory extends Factory public function definition(): array { 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, + ]); + } } diff --git a/tests/Feature/Jobs/SyncTownsTest.php b/tests/Feature/Jobs/SyncTownsTest.php new file mode 100644 index 0000000..7798d8e --- /dev/null +++ b/tests/Feature/Jobs/SyncTownsTest.php @@ -0,0 +1,150 @@ + 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.'); + } +} diff --git a/tests/Feature/Towns/TownSearchTest.php b/tests/Feature/Towns/TownSearchTest.php new file mode 100644 index 0000000..63c134d --- /dev/null +++ b/tests/Feature/Towns/TownSearchTest.php @@ -0,0 +1,119 @@ +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')); + } +}