From 1ca8e77ede4fca14fb7126ebfe5bb638a0f8b0c8 Mon Sep 17 00:00:00 2001 From: edwh Date: Tue, 8 Sep 2026 14:36:13 +0100 Subject: [PATCH] fix: stop CI failing on forked PRs for want of a geocoding key CircleCI does not pass project environment variables to jobs built from forked pull requests, so GOOGLE_API_CONSOLE_KEY is empty on those runs. Every geocode then returns false, group creation 422s on groups.geocode_failed, and the Playwright suite fails at its first createGroup() - so all 50 tests fail and the job times out, looking exactly like the contributor having broken something. That is what is happening on #906, whose diff removes eleven unused component registrations and cannot affect group creation at all: none of the changed components is reachable from the create page. Adds a StubGeocoder answering from a fixed table, used only when GEOCODER_STUB is set, which CI sets only when it has no key. It keeps the ForceGeocodeFailure sentinel failing, and falls back to London for an unlisted place so a new test fails on what it is testing rather than on the stub. Co-Authored-By: Claude Opus 5 (1M context) --- .circleci/config.yml | 10 +++++ app/Helpers/StubGeocoder.php | 49 +++++++++++++++++++++ app/Providers/AppServiceProvider.php | 4 +- config/restarters.php | 5 +++ tests/Unit/StubGeocoderTest.php | 66 ++++++++++++++++++++++++++++ 5 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 app/Helpers/StubGeocoder.php create mode 100644 tests/Unit/StubGeocoderTest.php 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')); + } +}