fix(tests): uuid-suffixed websocket collection self-deletes

TestWebSocketUserScopedBroadcast creates "Admin Collection <uuid8>" on
the shared admin — deliberately NOT named with "test" or every run
would 409 on UNIQUE(user_id, name) against stranded past-run rows
(the comment in the test says exactly that), but it never deleted its
row, so the same runs accrued ten "Admin Collection" forever-rows the
name sweep can never touch. cleanupCollectionByID gives tests an
owner-scoped RLS-bridge delete-at-backstory: same minimal contract as
cleanupDeviceByIdentifier, for rows whose name must stay unique across
runs. Deleted the ten standing rows (owner-scoped, exactly what the
cleanup path does).

What CANNOT be swept, audited safe: the dev admin's real hand-made
collections (Saga reading-orders, Podcast 2, Smart Collection (Books))
carry no sweepable names; devices rely on identifier/token prefixes
tests own exclusively; the rls-phone hand account persists because the
transient-user sweep matches only @tests.bookhoard.internal /
@example.com emails.
This commit is contained in:
John O'Keefe committed 2026-11-02 17:54:30 -05:00
1 parent 7d88ec5184
commit 06ec312863
2 files changed
+26

No files matched your search

+22
View File
@@ -111,6 +111,28 @@ func cleanupDeviceByIdentifier(t *testing.T, setup *TestServerSetup, identifier
}) })
} }
// cleanupCollectionByID deletes one collections row at test end, scoped
// through the RLS bridge to its owner — collections RLS (2026-10-31)
// makes unscoped writes invisible (no-op). Use for a collection a test
// attaches to the shared admin whose name CANNOT carry the "test"
// marker the setup-time name sweep keys on (uuid-suffixed uniqueness
// against stale UNIQUE(user_id, name) rows from past runs). Rows that
// survive their test also get stranded forever — the 10 accumulated
// "Admin Collection <uuid>" rows were exactly this.
func cleanupCollectionByID(t *testing.T, setup *TestServerSetup, ownerID [16]byte, collectionID string) {
t.Cleanup(func() {
cid, err := uuid.Parse(collectionID)
if err != nil {
t.Logf("cleanupCollectionByID: unparsable id %q — %v", collectionID, err)
return
}
scope := rls.WithUser(context.Background(), pgtype.UUID{Bytes: ownerID, Valid: true})
if err := setup.DB.DeleteCollection(scope, pgtype.UUID{Bytes: cid, Valid: true}); err != nil {
t.Logf("cleanupCollectionByID: delete failed for %s — %v", collectionID, err)
}
})
}
// Close cleans up all resources in the correct order // Close cleans up all resources in the correct order
func (s *TestServerSetup) Close() error { func (s *TestServerSetup) Close() error {
s.mu.Lock() s.mu.Lock()
+4
View File
@@ -241,6 +241,10 @@ func TestWebSocketUserScopedBroadcast(t *testing.T) {
err = json.NewDecoder(collectionResp.Body).Decode(&collectionResult) err = json.NewDecoder(collectionResp.Body).Decode(&collectionResult)
require.NoError(t, err) require.NoError(t, err)
collectionID := collectionResult["id"].(string) collectionID := collectionResult["id"].(string)
// The uuid suffix keeps the name off every run's UNIQUE(user_id,name),
// which also keeps it out of the name-based cleanup sweep — self-delete
// instead (owner-scoped, exactly who created it).
cleanupCollectionByID(t, setup, getTestUserID(t, setup.DB), collectionID)
// Create a test book via API // Create a test book via API
bookID := createTestMediaItemID(t, setup) bookID := createTestMediaItemID(t, setup)