Http::response([ 'status' => 200, 'result' => [['admin_district' => 'Peterborough']], ]), ]); // Two searches share a bucket; a third is in a different bucket. Search::factory()->count(2)->create(['lat_bucket' => 52.54, 'lng_bucket' => -0.21, 'area_label' => null]); Search::factory()->create(['lat_bucket' => 51.50, 'lng_bucket' => -0.14, 'area_label' => null]); $this->artisan('searches:backfill-areas')->assertSuccessful(); expect(Search::whereNull('area_label')->count())->toBe(0) ->and(Search::where('area_label', 'Peterborough')->count())->toBe(3); // One reverse-geocode call per distinct bucket, not per row. Http::assertSentCount(2); }); it('leaves searches that already have an area label untouched', function () { Http::fake([ '*/postcodes?*' => Http::response([ 'status' => 200, 'result' => [['admin_district' => 'Peterborough']], ]), ]); Search::factory()->create(['lat_bucket' => 52.54, 'lng_bucket' => -0.21, 'area_label' => 'Manchester']); $this->artisan('searches:backfill-areas')->assertSuccessful(); expect(Search::where('area_label', 'Manchester')->count())->toBe(1); Http::assertNothingSent(); }); it('reports when there is nothing to backfill', function () { $this->artisan('searches:backfill-areas') ->expectsOutputToContain('No searches need an area label.') ->assertSuccessful(); });