test(server): runs leave no residue — auto-cleanup for every resource the suite creates
Symptom: repeated full-suite runs poisoned the dev database with leaked rows (duplicate Prefs Scope libraries, colliding device names, saved-filter name conflicts on rerun) because tests created rows but never deleted them, and stale-binary runs left rows the new cleanup could not match. - test_helpers: library creation now registers t.Cleanup DELETE (the leak root cause — createTestLibraryWithFolder never cleaned up). Suite-wide cleanup passes for libraries/collections/devices/ saved-filters run both at setup (pre-pass against legacy residue) and in t.Cleanup (no per-run residue). Preserve the admin's real libraries; delete only convention-named test rows. - setup seeds canonical system collections every run keyed on query_type (idempotent — heals renames/stale tombstones), moved out of the preserved-admin branch so it runs for every suite start. - dashboard_integration: scope-testing library renamed into the test-row convention; ordering assertions on the restored "Want to Read" collection preserved. - filters_test: filter names moved into the convention (rerun-safe — name-uniqueness 409 no longer trips second runs); five .(string) type assertions panic-proofed with require.True guards (a panic here used to kill the whole test process mid-run, which is why a stack of "never ran" failures surfaced at once later). - device_test: device names moved into the convention (ILIKE %test% hit them; duplicate-device-name conflicts gone).
This commit is contained in:
1 parent
7ce73213ae
commit
843f35bbfc
5 files changed
+214
-16
No files matched your search
@@ -352,7 +352,7 @@ func (s *DashboardIntegrationTestSuite) TestUpdatePreferences_EmptyLibraryIDReje
|
|||||||
|
|
||||||
func (s *DashboardIntegrationTestSuite) TestGetPreferences_GlobalAndPerLibraryIndependent() {
|
func (s *DashboardIntegrationTestSuite) TestGetPreferences_GlobalAndPerLibraryIndependent() {
|
||||||
token := s.setup.Token
|
token := s.setup.Token
|
||||||
libraryID := createTestLibraryWithFolder(s.T(), s.setup.Server, token, "Prefs Scope Library", false)
|
libraryID := createTestLibraryWithFolder(s.T(), s.setup.Server, token, "Prefs Scope Test Library", false)
|
||||||
|
|
||||||
// Global scope: 15.
|
// Global scope: 15.
|
||||||
globalBody, _ := json.Marshal(map[string]interface{}{
|
globalBody, _ := json.Marshal(map[string]interface{}{
|
||||||
@@ -413,6 +413,119 @@ func (s *DashboardIntegrationTestSuite) TestGetPreferences_GlobalAndPerLibraryIn
|
|||||||
assert.Equal(s.T(), float64(15), globalPrefs["items_per_section"], "global scope should stay independent")
|
assert.Equal(s.T(), float64(15), globalPrefs["items_per_section"], "global scope should stay independent")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDeleteSystemCollection_TombstonesAndRestores pins the system-
|
||||||
|
// collection permanence contract: DELETE hides the section everywhere
|
||||||
|
// (tombstone) and restore brings back the EXACT SAME row (id stable),
|
||||||
|
// never a fresh one.
|
||||||
|
func newHTTPClient() *http.Client { return &http.Client{} }
|
||||||
|
func (s *DashboardIntegrationTestSuite) TestDeleteSystemCollection_TombstonesAndRestores() {
|
||||||
|
token := s.setup.Token
|
||||||
|
libraryID := createTestLibraryWithFolder(s.T(), s.setup.Server, token, "Test Library", false)
|
||||||
|
|
||||||
|
collectionID := func(sectionID string) string {
|
||||||
|
url := fmt.Sprintf("%s/api/dashboard/sections?library_id=%s", s.setup.Server.URL, libraryID)
|
||||||
|
req, _ := http.NewRequest("GET", url, nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
resp, err := newHTTPClient().Do(req)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body)
|
||||||
|
require.Equal(s.T(), http.StatusOK, resp.StatusCode)
|
||||||
|
var response map[string]interface{}
|
||||||
|
require.NoError(s.T(), json.NewDecoder(resp.Body).Decode(&response))
|
||||||
|
for _, sec := range response["sections"].([]interface{}) {
|
||||||
|
section := sec.(map[string]interface{})
|
||||||
|
if section["id"] == sectionID {
|
||||||
|
return section["collection_id"].(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
hasSection := func(sectionID string) bool {
|
||||||
|
url := fmt.Sprintf("%s/api/dashboard/sections?library_id=%s", s.setup.Server.URL, libraryID)
|
||||||
|
req, _ := http.NewRequest("GET", url, nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
resp, err := newHTTPClient().Do(req)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body)
|
||||||
|
var response map[string]interface{}
|
||||||
|
require.NoError(s.T(), json.NewDecoder(resp.Body).Decode(&response))
|
||||||
|
for _, sec := range response["sections"].([]interface{}) {
|
||||||
|
if sec.(map[string]interface{})["id"] == sectionID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
idBefore := collectionID("Continue Reading")
|
||||||
|
require.NotEmpty(s.T(), idBefore, "Continue Reading should exist")
|
||||||
|
|
||||||
|
// DELETE a system collection = tombstone (204, behaves like a delete).
|
||||||
|
delReq, _ := http.NewRequest("DELETE", s.setup.Server.URL+"/api/collections/"+idBefore, nil)
|
||||||
|
delReq.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
delResp, err := newHTTPClient().Do(delReq)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
defer func(Body io.ReadCloser) { _ = Body.Close() }(delResp.Body)
|
||||||
|
assert.Equal(s.T(), http.StatusNoContent, delResp.StatusCode)
|
||||||
|
|
||||||
|
// Invisible on the dashboard.
|
||||||
|
assert.False(s.T(), hasSection("Continue Reading"), "tombstoned section must vanish")
|
||||||
|
|
||||||
|
// Restore brings back the EXACT SAME row.
|
||||||
|
restoreBody, _ := json.Marshal(map[string]string{"collection_name": "Continue Reading"})
|
||||||
|
req, _ := http.NewRequest("POST", s.setup.Server.URL+"/api/dashboard/restore-system-collection", bytes.NewBuffer(restoreBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
restoreResp, err := newHTTPClient().Do(req)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
defer func(Body io.ReadCloser) { _ = Body.Close() }(restoreResp.Body)
|
||||||
|
require.Equal(s.T(), http.StatusOK, restoreResp.StatusCode)
|
||||||
|
|
||||||
|
idAfter := collectionID("Continue Reading")
|
||||||
|
assert.Equal(s.T(), idBefore, idAfter, "restore must resurrect the SAME row id")
|
||||||
|
assert.True(s.T(), hasSection("Continue Reading"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDeleteUserCollection_HardDelete pins the user-collection contract:
|
||||||
|
// user collections hard-delete exactly as before (no tombstone, no restore).
|
||||||
|
func (s *DashboardIntegrationTestSuite) TestDeleteUserCollection_HardDelete() {
|
||||||
|
token := s.setup.Token
|
||||||
|
|
||||||
|
createBody, _ := json.Marshal(map[string]interface{}{"name": "Test User Shelf", "type": "ebooks"})
|
||||||
|
req, _ := http.NewRequest("POST", s.setup.Server.URL+"/api/collections", bytes.NewBuffer(createBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
createResp, err := newHTTPClient().Do(req)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
defer func(Body io.ReadCloser) { _ = Body.Close() }(createResp.Body)
|
||||||
|
require.Equal(s.T(), http.StatusCreated, createResp.StatusCode)
|
||||||
|
|
||||||
|
var created map[string]interface{}
|
||||||
|
require.NoError(s.T(), json.NewDecoder(createResp.Body).Decode(&created))
|
||||||
|
collectionID := created["id"].(string)
|
||||||
|
|
||||||
|
delReq, _ := http.NewRequest("DELETE", s.setup.Server.URL+"/api/collections/"+collectionID, nil)
|
||||||
|
delReq.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
delResp, err := newHTTPClient().Do(delReq)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
defer func(Body io.ReadCloser) { _ = Body.Close() }(delResp.Body)
|
||||||
|
assert.Equal(s.T(), http.StatusNoContent, delResp.StatusCode)
|
||||||
|
|
||||||
|
listReq, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/collections", nil)
|
||||||
|
listReq.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
listResp, err := newHTTPClient().Do(listReq)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
defer func(Body io.ReadCloser) { _ = Body.Close() }(listResp.Body)
|
||||||
|
var list struct {
|
||||||
|
Collections []map[string]interface{} `json:"collections"`
|
||||||
|
}
|
||||||
|
require.NoError(s.T(), json.NewDecoder(listResp.Body).Decode(&list))
|
||||||
|
for _, c := range list.Collections {
|
||||||
|
assert.NotEqual(s.T(), collectionID, c["id"], "hard-deleted user collection must be gone")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func TestDashboardIntegrationTestSuite(t *testing.T) {
|
func TestDashboardIntegrationTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(DashboardIntegrationTestSuite))
|
suite.Run(t, new(DashboardIntegrationTestSuite))
|
||||||
}
|
}
|
||||||
@@ -512,7 +512,7 @@ func TestRegenerateDeviceToken_Forbidden(t *testing.T) {
|
|||||||
defer setup.Server.Close()
|
defer setup.Server.Close()
|
||||||
|
|
||||||
// Create device for user 1
|
// Create device for user 1
|
||||||
device1 := setup.CreateDevice(t, "User1 Device", "koreader", "user1-device")
|
device1 := setup.CreateDevice(t, "User1 Device", "koreader", "user1-test-device")
|
||||||
|
|
||||||
// Create a second user with different credentials
|
// Create a second user with different credentials
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func TestSavedFilters(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("POST /api/saved-filters creates filter", func(t *testing.T) {
|
t.Run("POST /api/saved-filters creates filter", func(t *testing.T) {
|
||||||
reqBody := map[string]interface{}{
|
reqBody := map[string]interface{}{
|
||||||
"name": "My Sci-Fi Books",
|
"name": "My Sci-Fi Test Books",
|
||||||
"resource_type": "media-items",
|
"resource_type": "media-items",
|
||||||
"filters": map[string]string{
|
"filters": map[string]string{
|
||||||
"genre_filter": "Science Fiction",
|
"genre_filter": "Science Fiction",
|
||||||
@@ -71,7 +71,7 @@ func TestSavedFilters(t *testing.T) {
|
|||||||
var filter map[string]interface{}
|
var filter map[string]interface{}
|
||||||
err = json.NewDecoder(resp.Body).Decode(&filter)
|
err = json.NewDecoder(resp.Body).Decode(&filter)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "My Sci-Fi Books", filter["name"])
|
assert.Equal(t, "My Sci-Fi Test Books", filter["name"])
|
||||||
assert.Equal(t, "media-items", filter["resource_type"])
|
assert.Equal(t, "media-items", filter["resource_type"])
|
||||||
assert.NotEmpty(t, filter["id"])
|
assert.NotEmpty(t, filter["id"])
|
||||||
// Filters field will be JSON object (JSONB as raw bytes)
|
// Filters field will be JSON object (JSONB as raw bytes)
|
||||||
@@ -113,7 +113,7 @@ func TestSavedFilters(t *testing.T) {
|
|||||||
t.Run("PUT /api/saved-filters/:id updates filter", func(t *testing.T) {
|
t.Run("PUT /api/saved-filters/:id updates filter", func(t *testing.T) {
|
||||||
// Create filter first
|
// Create filter first
|
||||||
createReq := map[string]interface{}{
|
createReq := map[string]interface{}{
|
||||||
"name": "Original Name",
|
"name": "Original Test Name",
|
||||||
"resource_type": "media-items",
|
"resource_type": "media-items",
|
||||||
"filters": map[string]string{"search": "original"},
|
"filters": map[string]string{"search": "original"},
|
||||||
}
|
}
|
||||||
@@ -134,11 +134,13 @@ func TestSavedFilters(t *testing.T) {
|
|||||||
var createdFilter map[string]interface{}
|
var createdFilter map[string]interface{}
|
||||||
err = json.NewDecoder(createResp.Body).Decode(&createdFilter)
|
err = json.NewDecoder(createResp.Body).Decode(&createdFilter)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
filterID := createdFilter["id"].(string)
|
filterIDRaw, ok := createdFilter["id"]
|
||||||
|
require.True(t, ok, "create filter response should carry id: %v", createdFilter)
|
||||||
|
filterID := filterIDRaw.(string)
|
||||||
|
|
||||||
// Update filter
|
// Update filter
|
||||||
updateReq := map[string]interface{}{
|
updateReq := map[string]interface{}{
|
||||||
"name": "Updated Name",
|
"name": "Updated Test Name",
|
||||||
"resource_type": "media-items",
|
"resource_type": "media-items",
|
||||||
"filters": map[string]string{"search": "updated"},
|
"filters": map[string]string{"search": "updated"},
|
||||||
}
|
}
|
||||||
@@ -159,7 +161,7 @@ func TestSavedFilters(t *testing.T) {
|
|||||||
var updatedFilter map[string]interface{}
|
var updatedFilter map[string]interface{}
|
||||||
err = json.NewDecoder(updateResp.Body).Decode(&updatedFilter)
|
err = json.NewDecoder(updateResp.Body).Decode(&updatedFilter)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "Updated Name", updatedFilter["name"])
|
assert.Equal(t, "Updated Test Name", updatedFilter["name"])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("DELETE /api/saved-filters/:id deletes filter", func(t *testing.T) {
|
t.Run("DELETE /api/saved-filters/:id deletes filter", func(t *testing.T) {
|
||||||
@@ -184,7 +186,9 @@ func TestSavedFilters(t *testing.T) {
|
|||||||
var createdFilter map[string]interface{}
|
var createdFilter map[string]interface{}
|
||||||
err = json.NewDecoder(createResp.Body).Decode(&createdFilter)
|
err = json.NewDecoder(createResp.Body).Decode(&createdFilter)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
filterID := createdFilter["id"].(string)
|
filterIDRaw, ok := createdFilter["id"]
|
||||||
|
require.True(t, ok, "create filter response should carry id: %v", createdFilter)
|
||||||
|
filterID := filterIDRaw.(string)
|
||||||
|
|
||||||
// Delete filter
|
// Delete filter
|
||||||
deleteHTTP, _ := http.NewRequest("DELETE", setup.Server.URL+"/api/saved-filters/"+filterID, nil)
|
deleteHTTP, _ := http.NewRequest("DELETE", setup.Server.URL+"/api/saved-filters/"+filterID, nil)
|
||||||
@@ -223,7 +227,9 @@ func TestSavedFilters(t *testing.T) {
|
|||||||
var createdFilter map[string]interface{}
|
var createdFilter map[string]interface{}
|
||||||
err = json.NewDecoder(createResp.Body).Decode(&createdFilter)
|
err = json.NewDecoder(createResp.Body).Decode(&createdFilter)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
filterID := createdFilter["id"].(string)
|
filterIDRaw, ok := createdFilter["id"]
|
||||||
|
require.True(t, ok, "create filter response should carry id: %v", createdFilter)
|
||||||
|
filterID := filterIDRaw.(string)
|
||||||
|
|
||||||
// Admin user tries to delete regular user's filter
|
// Admin user tries to delete regular user's filter
|
||||||
deleteHTTP, _ := http.NewRequest("DELETE", setup.Server.URL+"/api/saved-filters/"+filterID, nil)
|
deleteHTTP, _ := http.NewRequest("DELETE", setup.Server.URL+"/api/saved-filters/"+filterID, nil)
|
||||||
@@ -260,7 +266,9 @@ func TestSavedFilters(t *testing.T) {
|
|||||||
var createdFilter map[string]interface{}
|
var createdFilter map[string]interface{}
|
||||||
err = json.NewDecoder(createResp.Body).Decode(&createdFilter)
|
err = json.NewDecoder(createResp.Body).Decode(&createdFilter)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
filterID := createdFilter["id"].(string)
|
filterIDRaw, ok := createdFilter["id"]
|
||||||
|
require.True(t, ok, "create filter response should carry id: %v", createdFilter)
|
||||||
|
filterID := filterIDRaw.(string)
|
||||||
|
|
||||||
// Now retrieve the filter by ID
|
// Now retrieve the filter by ID
|
||||||
getReq, _ := http.NewRequest("GET", setup.Server.URL+"/api/saved-filters/"+filterID, nil)
|
getReq, _ := http.NewRequest("GET", setup.Server.URL+"/api/saved-filters/"+filterID, nil)
|
||||||
@@ -324,7 +332,7 @@ func TestSavedFilters(t *testing.T) {
|
|||||||
t.Run("GET /api/saved-filters/:id from different user returns 404", func(t *testing.T) {
|
t.Run("GET /api/saved-filters/:id from different user returns 404", func(t *testing.T) {
|
||||||
// Create filter with admin user (setup.Token)
|
// Create filter with admin user (setup.Token)
|
||||||
reqBody := map[string]interface{}{
|
reqBody := map[string]interface{}{
|
||||||
"name": "Admin Filter",
|
"name": "Admin Test Filter",
|
||||||
"resource_type": "media-items",
|
"resource_type": "media-items",
|
||||||
"filters": map[string]string{"search": "test"},
|
"filters": map[string]string{"search": "test"},
|
||||||
}
|
}
|
||||||
@@ -340,7 +348,9 @@ func TestSavedFilters(t *testing.T) {
|
|||||||
var createdFilter map[string]interface{}
|
var createdFilter map[string]interface{}
|
||||||
err = json.NewDecoder(createResp.Body).Decode(&createdFilter)
|
err = json.NewDecoder(createResp.Body).Decode(&createdFilter)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
filterID := createdFilter["id"].(string)
|
filterIDRaw, ok := createdFilter["id"]
|
||||||
|
require.True(t, ok, "create filter response should carry id: %v", createdFilter)
|
||||||
|
filterID := filterIDRaw.(string)
|
||||||
// Try to access with regular user (setup.RegularToken)
|
// Try to access with regular user (setup.RegularToken)
|
||||||
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/api/saved-filters/"+filterID, nil)
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/api/saved-filters/"+filterID, nil)
|
||||||
httpReq.Header.Set("Authorization", "Bearer "+setup.RegularToken)
|
httpReq.Header.Set("Authorization", "Bearer "+setup.RegularToken)
|
||||||
|
|||||||
@@ -109,6 +109,18 @@ func createTestLibraryWithFolder(t *testing.T, ts *httptest.Server, token, name
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
libraryID := libResponse["id"].(string)
|
libraryID := libResponse["id"].(string)
|
||||||
|
|
||||||
|
// Every library a test creates must not outlive that test — delete
|
||||||
|
// it on cleanup so runs leave no residue in the dev database.
|
||||||
|
t.Cleanup(func() {
|
||||||
|
delHTTP, _ := http.NewRequest("DELETE", fmt.Sprintf("%s/api/libraries/%s", ts.URL, libraryID), nil)
|
||||||
|
delHTTP.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
delResp, err := client.Do(delHTTP)
|
||||||
|
if err == nil {
|
||||||
|
_, _ = io.Copy(io.Discard, delResp.Body)
|
||||||
|
_ = delResp.Body.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if withFolder {
|
if withFolder {
|
||||||
folderReq := map[string]interface{}{
|
folderReq := map[string]interface{}{
|
||||||
"folder_path": "/app/uploads",
|
"folder_path": "/app/uploads",
|
||||||
|
|||||||
@@ -295,6 +295,26 @@ func createDefaultCollectionsForUser(t *testing.T, db *database.Queries, userID
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, col := range defaultCollections {
|
for _, col := range defaultCollections {
|
||||||
|
// Idempotent by the STABLE identity (user + query_type): existing
|
||||||
|
// rows (renamed, tombstoned) heal to canonical metadata in place;
|
||||||
|
// insert only when the query type has no row. A blind insert here
|
||||||
|
// hit UNIQUE(user_id, name) on every preserved-admin rerun.
|
||||||
|
existing, gerr := db.GetSystemCollectionByQueryType(ctx, database.GetSystemCollectionByQueryTypeParams{
|
||||||
|
UserID: userID,
|
||||||
|
QueryType: pgtype.Text{String: col.QueryType, Valid: true},
|
||||||
|
})
|
||||||
|
if gerr == nil {
|
||||||
|
_, uerr := db.RestoreSystemCollectionRow(ctx, database.RestoreSystemCollectionRowParams{
|
||||||
|
ID: existing.ID,
|
||||||
|
Name: col.Name,
|
||||||
|
Description: pgtype.Text{String: col.Description, Valid: true},
|
||||||
|
Icon: pgtype.Text{String: col.Icon, Valid: true},
|
||||||
|
Color: pgtype.Text{String: col.Color, Valid: true},
|
||||||
|
Priority: pgtype.Int4{Int32: col.Priority, Valid: true},
|
||||||
|
})
|
||||||
|
require.NoError(t, uerr, "Should restore default collection: "+col.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
_, err := db.CreateSystemCollection(ctx, database.CreateSystemCollectionParams{
|
_, err := db.CreateSystemCollection(ctx, database.CreateSystemCollectionParams{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Name: col.Name,
|
Name: col.Name,
|
||||||
@@ -627,9 +647,41 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
|||||||
require.NoError(t, cerr, "Failed to create admin test user")
|
require.NoError(t, cerr, "Failed to create admin test user")
|
||||||
adminUUID, err = uuid.FromBytes(adminUser.ID.Bytes[:])
|
adminUUID, err = uuid.FromBytes(adminUser.ID.Bytes[:])
|
||||||
require.NoError(t, err, "Failed to parse admin user UUID")
|
require.NoError(t, err, "Failed to parse admin user UUID")
|
||||||
// Default collections are created exactly once per user; the
|
}
|
||||||
// preserved dev admin already has its set from the first run.
|
// The canonical system-collection set is ensured on EVERY run: the
|
||||||
createDefaultCollectionsForUser(t, queries, pgtype.UUID{Bytes: adminUUID, Valid: true})
|
// helper is idempotent by (user, query_type) and heals tombstoned or
|
||||||
|
// renamed rows, so suite runs can't accumulate naming-drift copies.
|
||||||
|
createDefaultCollectionsForUser(t, queries, pgtype.UUID{Bytes: adminUUID, Valid: true})
|
||||||
|
|
||||||
|
// Delete test-created COLLECTIONS (names containing "test"): without
|
||||||
|
// this, every suite run leaves its collections behind and the next
|
||||||
|
// run's creates die on UNIQUE(user_id, name) — the "collections
|
||||||
|
// 500-panic" rot. System collections are skipped (permanent rows).
|
||||||
|
// Same convention as the library pass: names containing "test".
|
||||||
|
adminColls, cerr := queries.GetCollectionsByUser(ctx, pgtype.UUID{Bytes: adminUUID, Valid: true})
|
||||||
|
if cerr == nil {
|
||||||
|
for _, coll := range adminColls {
|
||||||
|
if !coll.IsSystemCollection.Bool && strings.Contains(strings.ToLower(coll.Name), "test") {
|
||||||
|
queries.DeleteCollection(ctx, coll.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same passes for test DEVICES (identifier/name containing "test")
|
||||||
|
// and test SAVED FILTERS (name containing "test"): both accumulated
|
||||||
|
// across runs and poisoned later runs (duplicate identifiers, 409s,
|
||||||
|
// non-empty "initially empty" assertions).
|
||||||
|
_, derr := dbPool.Exec(ctx,
|
||||||
|
"DELETE FROM devices WHERE user_id = $1 AND (device_identifier ILIKE '%test%' OR device_name ILIKE '%test%')",
|
||||||
|
adminUUID)
|
||||||
|
if derr != nil {
|
||||||
|
t.Logf("device cleanup failed: %v", derr)
|
||||||
|
}
|
||||||
|
_, ferr := dbPool.Exec(ctx,
|
||||||
|
"DELETE FROM saved_filters WHERE user_id = $1 AND name ILIKE '%test%'",
|
||||||
|
adminUUID)
|
||||||
|
if ferr != nil {
|
||||||
|
t.Logf("saved-filter cleanup failed: %v", ferr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create fresh regular test user
|
// Create fresh regular test user
|
||||||
@@ -680,6 +732,17 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same pass for test-created COLLECTIONS (the "collections
|
||||||
|
// 500-panic" rot: they lingered forever and poisoned later runs).
|
||||||
|
adminColls, cerr := queries.GetCollectionsByUser(ctx, pgtype.UUID{Bytes: adminUUID, Valid: true})
|
||||||
|
if cerr == nil {
|
||||||
|
for _, coll := range adminColls {
|
||||||
|
if !coll.IsSystemCollection.Bool && strings.Contains(strings.ToLower(coll.Name), "test") {
|
||||||
|
queries.DeleteCollection(ctx, coll.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := setup.Close(); err != nil {
|
if err := setup.Close(); err != nil {
|
||||||
t.Errorf("Failed to cleanup test server: %v", err)
|
t.Errorf("Failed to cleanup test server: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user