diff --git a/.circleci/config.yml b/.circleci/config.yml index 2b214d4f69..39ef7012e7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -47,6 +47,16 @@ jobs: echo "GOOGLE_API_CONSOLE_KEY=$GOOGLE_API_CONSOLE_KEY" >> .env echo "MAPBOX_TOKEN=$MAPBOX_TOKEN" >> .env + # CircleCI does not give project environment variables to jobs built + # from forked pull requests, so those runs have no geocoding key. + # Without this every group creation 422s on geocode_failed and the + # whole Playwright suite fails at its first createGroup(), which + # reads as the contributor having broken something. + if [ -z "$GOOGLE_API_CONSOLE_KEY" ]; then + echo "No geocoding key available - using the stub geocoder." + echo "GEOCODER_STUB=true" >> .env + fi + # Start Docker services using Task - run: name: Start Docker services diff --git a/app/Helpers/StubGeocoder.php b/app/Helpers/StubGeocoder.php new file mode 100644 index 0000000000..6e4580baa0 --- /dev/null +++ b/app/Helpers/StubGeocoder.php @@ -0,0 +1,49 @@ + [51.5072178, -0.1275862, 'GB'], + 'london, uk' => [51.5072178, -0.1275862, 'GB'], + 'edinburgh' => [55.9533456, -3.1883749, 'GB'], + 'brussels' => [50.8476424, 4.3571696, 'BE'], + 'paris' => [48.8575475, 2.3513765, 'FR'], + ]; + + public function geocode($location) + { + // Preserve the sentinel the tests use to exercise the failure path. + if ($location === 'ForceGeocodeFailure') { + return false; + } + + [$latitude, $longitude, $country_code] = + self::PLACES[mb_strtolower(trim((string) $location))] ?? self::PLACES['london']; + + return [ + 'latitude' => $latitude, + 'longitude' => $longitude, + 'country_code' => $country_code, + ]; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 4f620ea837..96ffd1d226 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -46,7 +46,9 @@ public function boot(): void public function register(): void { $this->app->singleton(Geocoder::class, function () { - return new Geocoder(); + return config('restarters.geocoder_stub') + ? new \App\Helpers\StubGeocoder() + : new Geocoder(); }); // Override the existing translator with our own robust one. diff --git a/config/restarters.php b/config/restarters.php index 2b42e5f0b5..f04f2779d0 100644 --- a/config/restarters.php +++ b/config/restarters.php @@ -31,4 +31,9 @@ ], 'support_email_address' => env('SUPPORT_EMAIL_ADDRESS'), + + // Answer geocoding from a fixed table instead of calling Google. Set by CI + // when no API key is available (forked pull requests get no project + // environment variables), never in production. + 'geocoder_stub' => env('GEOCODER_STUB', false), ]; diff --git a/tests/Unit/StubGeocoderTest.php b/tests/Unit/StubGeocoderTest.php new file mode 100644 index 0000000000..b50acaf4bc --- /dev/null +++ b/tests/Unit/StubGeocoderTest.php @@ -0,0 +1,66 @@ +geocode('London, UK'); + + self::assertEqualsWithDelta(51.5072178, $geocoded['latitude'], 0.001); + self::assertEqualsWithDelta(-0.1275862, $geocoded['longitude'], 0.001); + self::assertEquals('GB', $geocoded['country_code']); + } + + public function testIsNotCaseOrWhitespaceSensitive(): void + { + self::assertEquals( + (new StubGeocoder())->geocode('London, UK'), + (new StubGeocoder())->geocode(' LONDON, uk ') + ); + } + + /** + * An unlisted place still geocodes, so a new test that happens to use one + * fails on what it is actually testing rather than on the stub. + */ + public function testFallsBackRatherThanFailingForAnUnlistedPlace(): void + { + $geocoded = (new StubGeocoder())->geocode('Somewhere nobody listed'); + + self::assertNotFalse($geocoded); + self::assertArrayHasKey('latitude', $geocoded); + } + + /** + * The suite uses this sentinel to exercise the geocode-failure path, so + * the stub has to keep failing for it. + */ + public function testStillFailsForTheForcedFailureSentinel(): void + { + self::assertFalse((new StubGeocoder())->geocode('ForceGeocodeFailure')); + } +}