Summary
When run against the Spanner emulator (per setup-spanner-emulator.sh), 60 Spanner Outbox and Inbox tests fail at fixture setup with System.InvalidOperationException : Your default credentials were not found. To set up Application Default Credentials, see https://cloud.google.com/docs/authentication/external/set-up-adc. — the test helpers construct SpannerConnection directly from the connection string, which does not opt into EmulatorDetection, so the gRPC client tries to load ADC instead of routing to SPANNER_EMULATOR_HOST.
The fix is small and contained: route Outbox/Inbox test helpers through SpannerConnectionStringBuilder { EmulatorDetection = EmulatorDetection.EmulatorOrProduction } the way the BoxProvisioning Spanner tests already do.
Repro
docker compose -f docker-compose-spanner.yaml up -d
bash ./setup-spanner-emulator.sh
SPANNER_EMULATOR_HOST=localhost:9010 GOOGLE_CLOUD_PROJECT=brighter-tests \
dotnet test tests/Paramore.Brighter.Gcp.Tests/Paramore.Brighter.Gcp.Tests.csproj -f net9.0 \
--filter "FullyQualifiedName~Spanner"
Result on multi-tenancy@c6335eb00: Failed: 60, Passed: 33, Total: 93. The 33 passing are exactly the Spanner/BoxProvisioning/ tests.
Failure breakdown
| Group |
Count |
Paramore.Brighter.Gcp.Tests.Outbox.SpannerBinary.{Sync,Async} |
24 |
Paramore.Brighter.Gcp.Tests.Outbox.SpannerText.{Sync,Async} |
24 |
Paramore.Brighter.Gcp.Tests.Spanner.Inbox.SpannerInbox{,Async}Test |
12 |
| Total |
60 |
All fail with the identical "credentials were not found" message at fixture-init time.
Root cause (located)
The failing test helpers construct the connection without enabling emulator detection. Example — tests/Paramore.Brighter.Gcp.Tests/Spanner/Inbox/SpannerInboxAsyncTest.cs:23:
protected override async Task CreateInboxTableAsync(RelationalDatabaseConfiguration configuration)
{
await using var connection = new SpannerConnection(configuration.ConnectionString); // <-- no EmulatorDetection
await connection.OpenAsync();
...
}
The Spanner BoxProvisioning tests (which all pass) use the pattern that works. Example — tests/Paramore.Brighter.Gcp.Tests/Spanner/BoxProvisioning/When_spanner_outbox_provisioner_runs_on_fresh_database_it_should_create_outbox_table.cs:
private SpannerConnection CreateConnection()
{
var builder = new SpannerConnectionStringBuilder(_connectionString)
{
EmulatorDetection = EmulatorDetection.EmulatorOrProduction
};
return new SpannerConnection(builder);
}
EmulatorDetection.EmulatorOrProduction makes the SDK check SPANNER_EMULATOR_HOST and bypass ADC when set; without it, the SDK falls back to ADC and fails when no creds are configured.
Proposed fix
Apply the EmulatorDetection.EmulatorOrProduction connection-builder pattern to the Outbox/Inbox test setup helpers (test-only change). Candidate files:
tests/Paramore.Brighter.Gcp.Tests/Spanner/Inbox/SpannerInboxTest.cs (sync helpers)
tests/Paramore.Brighter.Gcp.Tests/Spanner/Inbox/SpannerInboxAsyncTest.cs (async helpers)
- The Outbox test base/builder helpers used by
Outbox/Spanner{Binary,Text}/{Sync,Async}/ — likely tests/Paramore.Brighter.Gcp.Tests/Outbox/... shared fixture (worth a grep -rn 'new SpannerConnection' tests/Paramore.Brighter.Gcp.Tests).
Consider extracting the builder pattern into a tests/Paramore.Brighter.Gcp.Tests/Spanner/SpannerTestConnectionFactory.cs helper used by every test that constructs a SpannerConnection.
Out-of-scope question for a future ADR: should the production-side SpannerInbox / SpannerOutbox honour SPANNER_EMULATOR_HOST by default, or is that strictly a test concern? Today the test-only fix is the right scope.
Setup-script gap (not the root cause but worth noting)
setup-spanner-emulator.sh exports env vars in its own subshell. Running bash ./setup-spanner-emulator.sh does NOT export to the caller; you must either source ./setup-spanner-emulator.sh or prefix env inline. This is documented at the end of the script, but a reader may miss it. (The emulator detection fix above subsumes this — once EmulatorDetection is on the connection, the env var is the SDK's contract, not the test runner's.)
Out-of-scope context
Uncovered during the VERIFY phase of spec 0029 (multi-tenancy migration-history scope) on branch multi-tenancy (PR #4155). That spec touches only src/Paramore.Brighter.BoxProvisioning*/ — zero Spanner Outbox/Inbox code — so the failures are pre-existing and orthogonal. The Spanner/BoxProvisioning/ subset (--filter "FullyQualifiedName~Spanner.BoxProvisioning") is 33/33 on both TFMs on the same branch.
Summary
When run against the Spanner emulator (per
setup-spanner-emulator.sh), 60 Spanner Outbox and Inbox tests fail at fixture setup withSystem.InvalidOperationException : Your default credentials were not found. To set up Application Default Credentials, see https://cloud.google.com/docs/authentication/external/set-up-adc.— the test helpers constructSpannerConnectiondirectly from the connection string, which does not opt intoEmulatorDetection, so the gRPC client tries to load ADC instead of routing toSPANNER_EMULATOR_HOST.The fix is small and contained: route Outbox/Inbox test helpers through
SpannerConnectionStringBuilder { EmulatorDetection = EmulatorDetection.EmulatorOrProduction }the way the BoxProvisioning Spanner tests already do.Repro
Result on
multi-tenancy@c6335eb00:Failed: 60, Passed: 33, Total: 93. The 33 passing are exactly theSpanner/BoxProvisioning/tests.Failure breakdown
Paramore.Brighter.Gcp.Tests.Outbox.SpannerBinary.{Sync,Async}Paramore.Brighter.Gcp.Tests.Outbox.SpannerText.{Sync,Async}Paramore.Brighter.Gcp.Tests.Spanner.Inbox.SpannerInbox{,Async}TestAll fail with the identical "credentials were not found" message at fixture-init time.
Root cause (located)
The failing test helpers construct the connection without enabling emulator detection. Example —
tests/Paramore.Brighter.Gcp.Tests/Spanner/Inbox/SpannerInboxAsyncTest.cs:23:The Spanner BoxProvisioning tests (which all pass) use the pattern that works. Example —
tests/Paramore.Brighter.Gcp.Tests/Spanner/BoxProvisioning/When_spanner_outbox_provisioner_runs_on_fresh_database_it_should_create_outbox_table.cs:EmulatorDetection.EmulatorOrProductionmakes the SDK checkSPANNER_EMULATOR_HOSTand bypass ADC when set; without it, the SDK falls back to ADC and fails when no creds are configured.Proposed fix
Apply the
EmulatorDetection.EmulatorOrProductionconnection-builder pattern to the Outbox/Inbox test setup helpers (test-only change). Candidate files:tests/Paramore.Brighter.Gcp.Tests/Spanner/Inbox/SpannerInboxTest.cs(sync helpers)tests/Paramore.Brighter.Gcp.Tests/Spanner/Inbox/SpannerInboxAsyncTest.cs(async helpers)Outbox/Spanner{Binary,Text}/{Sync,Async}/— likelytests/Paramore.Brighter.Gcp.Tests/Outbox/...shared fixture (worth agrep -rn 'new SpannerConnection' tests/Paramore.Brighter.Gcp.Tests).Consider extracting the builder pattern into a
tests/Paramore.Brighter.Gcp.Tests/Spanner/SpannerTestConnectionFactory.cshelper used by every test that constructs aSpannerConnection.Out-of-scope question for a future ADR: should the production-side
SpannerInbox/SpannerOutboxhonourSPANNER_EMULATOR_HOSTby default, or is that strictly a test concern? Today the test-only fix is the right scope.Setup-script gap (not the root cause but worth noting)
setup-spanner-emulator.shexports env vars in its own subshell. Runningbash ./setup-spanner-emulator.shdoes NOT export to the caller; you must eithersource ./setup-spanner-emulator.shor prefix env inline. This is documented at the end of the script, but a reader may miss it. (The emulator detection fix above subsumes this — onceEmulatorDetectionis on the connection, the env var is the SDK's contract, not the test runner's.)Out-of-scope context
Uncovered during the VERIFY phase of spec 0029 (multi-tenancy migration-history scope) on branch
multi-tenancy(PR #4155). That spec touches onlysrc/Paramore.Brighter.BoxProvisioning*/— zero Spanner Outbox/Inbox code — so the failures are pre-existing and orthogonal. TheSpanner/BoxProvisioning/subset (--filter "FullyQualifiedName~Spanner.BoxProvisioning") is 33/33 on both TFMs on the same branch.