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
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:
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
150
tests/Feature/Jobs/SyncTownsTest.php
Normal file
150
tests/Feature/Jobs/SyncTownsTest.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
119
tests/Feature/Towns/TownSearchTest.php
Normal file
119
tests/Feature/Towns/TownSearchTest.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user