diff --git a/modules/mesh-mail-reader/cmd/mesh-mail-reader/integration_db_name_test.go b/modules/mesh-mail-reader/cmd/mesh-mail-reader/integration_db_name_test.go new file mode 100644 index 00000000..afbe3dc7 --- /dev/null +++ b/modules/mesh-mail-reader/cmd/mesh-mail-reader/integration_db_name_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "crypto/sha256" + "fmt" + "testing" +) + +// integrationDatabaseName builds a one-off postgres database name for an integration test: unique (n, a +// counter that only ever grows, sits at its very end, where nothing can truncate it away) and never over +// 63 bytes — postgres's own identifier limit (NAMEDATALEN - 1) — however long testName is. A short hash of +// testName keeps the name traceable to its test without spending the length budget a long test name would +// (issue 1517, fixed per the lead's try feedback on 63980bf5): the earlier name, +// "mesh_mail_reader_test_"+lower(testName)+"_"+n, ran past 63 bytes for +// TestPrepareOnADatabaseAtMigration0001AppliesWhatFollows, and postgres silently truncated both of that +// test's two calls (n=1, n=2) to the SAME 63-byte name — the second call's own `drop ... with (force)` +// then dropped the first call's database while its store was still connected to it, "terminating +// connection due to administrator command" (SQLSTATE 57P01). TestIntegrationDatabaseNameStaysUniqueAndWithinPostgresLimit +// proves both properties, with no database, including for that exact test's own long name. +func integrationDatabaseName(testName string, n int64) string { + sum := sha256.Sum256([]byte(testName)) + return fmt.Sprintf("mmr_test_%x_%d", sum[:4], n) +} + +// TestIntegrationDatabaseNameStaysUniqueAndWithinPostgresLimit (1517, fixed per the lead's try feedback on +// 63980bf5): for a very long test name — the exact one whose two calls collided in try 63980bf5 — two +// successive counters give two different names, and both stay at or under postgres's own 63-byte limit. A +// plain unit test (no `//go:build integration`, no database) so it runs on every `go test ./...`. +func TestIntegrationDatabaseNameStaysUniqueAndWithinPostgresLimit(t *testing.T) { + const long = "TestPrepareOnADatabaseAtMigration0001AppliesWhatFollowsAndThenSomeEvenLongerSuffixToBeSure" + a := integrationDatabaseName(long, 1) + b := integrationDatabaseName(long, 2) + if a == b { + t.Fatalf("two different counters gave the same name %q: the second drop would kill the first's connection (SQLSTATE 57P01)", a) + } + for _, name := range []string{a, b} { + if len(name) > 63 { + t.Fatalf("name %q is %d bytes, want at most 63 (postgres's own identifier limit)", name, len(name)) + } + } +} diff --git a/modules/mesh-mail-reader/cmd/mesh-mail-reader/pgstore_integration_test.go b/modules/mesh-mail-reader/cmd/mesh-mail-reader/pgstore_integration_test.go index 94ee4e46..2faa5b71 100644 --- a/modules/mesh-mail-reader/cmd/mesh-mail-reader/pgstore_integration_test.go +++ b/modules/mesh-mail-reader/cmd/mesh-mail-reader/pgstore_integration_test.go @@ -10,7 +10,6 @@ package main import ( "context" - "fmt" "net/url" "os" "reflect" @@ -32,9 +31,9 @@ func testDatabaseURL(t *testing.T) string { // freshDatabaseCounter makes freshIntegrationDatabase's own name unique across every call, even two calls // within the SAME test (t.Name() alone is not: TestPrepareOnADatabaseAtMigration0001AppliesWhatFollows -// calls it twice, for "fresh" and "stepped", and two databases of the same name collided — the first -// call's own pool was still open on it when the second tried to drop and recreate it, "database ... is -// being accessed by other users", SQLSTATE 55006; the coordinator's own fix for issue 1517). +// calls it twice, for "fresh" and "stepped") — combined with integrationDatabaseName's own 63-byte limit +// (issue 1517, fixed per the lead's try feedback on 63980bf5: this test's own long name, truncated by +// postgres past that limit, once made both calls' names the same). var freshDatabaseCounter atomic.Int64 // freshIntegrationDatabase makes a new, empty database on the server MESH_TEST_POSTGRES names, and returns @@ -51,7 +50,7 @@ func freshIntegrationDatabase(t *testing.T) string { } defer admin.Close() n := freshDatabaseCounter.Add(1) - name := fmt.Sprintf("mesh_mail_reader_test_%s_%d", strings.ToLower(strings.ReplaceAll(t.Name(), "/", "_")), n) + name := integrationDatabaseName(t.Name(), n) if _, err := admin.Exec(ctx, "drop database if exists "+name+" with (force)"); err != nil { t.Fatal(err) }