package web // handlers_tablos_test.go — Wave 0 integration test scaffold for TABLO-01..06. // // All tests are expected to FAIL at runtime until Plans 02 and 03 implement the // actual handlers and routes. This file is the RED baseline; Plans 02/03 turn it green. // // Pattern: mirrors handlers_auth_test.go exactly — setupTestDB, preInsertUser, // getCSRFToken, and newTestRouter are all reused from that file (same package). import ( "context" "net/http" "net/http/httptest" "net/url" "os" "strings" "testing" "backend/internal/auth" "backend/internal/db/sqlc" "github.com/jackc/pgx/v5/pgtype" ) // newTabloTestRouter builds a router with TablosDeps wired for tablo handler tests. func newTabloTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler { authDeps := AuthDeps{Queries: q, Store: store, Secure: false} tabloDeps := TablosDeps{Queries: q} return NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, TasksDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost") } // loginUser signs up a user and returns the session cookie set after signup. // Reuses the signup flow (same as handlers_auth_test.go integration tests). func loginUser(t *testing.T, router http.Handler, email, password string) []*http.Cookie { t.Helper() csrfToken, csrfCookies := getCSRFToken(t, router, "/signup", nil) form := url.Values{ "email": {email}, "password": {password}, "_csrf": {csrfToken}, } req := httptest.NewRequest(http.MethodPost, "/signup", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") for _, c := range csrfCookies { req.AddCookie(c) } rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusSeeOther { t.Fatalf("loginUser: signup status = %d; want 303", rec.Code) } // Collect all cookies from the response (session + gorilla_csrf). var allCookies []*http.Cookie allCookies = append(allCookies, csrfCookies...) allCookies = append(allCookies, rec.Result().Cookies()...) return allCookies } // ---- TestTabloList ---- // TestTabloList verifies that an authenticated GET / returns the user's tablos // newest-first, and that tablos owned by other users are NOT present (TABLO-01). func TestTabloList(t *testing.T) { pool, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() q := sqlc.New(pool) store := auth.NewStore(q) router := newTabloTestRouter(q, store) // Create two users. userA := preInsertUser(t, ctx, q, "lista@example.com", "correct-horse-12") userB := preInsertUser(t, ctx, q, "listb@example.com", "correct-horse-12") // Insert tablos for userA. tabloA1, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{ UserID: userA.ID, Title: "TabloA First", Description: pgtype.Text{Valid: false}, Color: pgtype.Text{Valid: false}, }) if err != nil { t.Fatalf("InsertTablo tabloA1: %v", err) } tabloA2, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{ UserID: userA.ID, Title: "TabloA Second", Description: pgtype.Text{Valid: false}, Color: pgtype.Text{Valid: false}, }) if err != nil { t.Fatalf("InsertTablo tabloA2: %v", err) } // Insert a tablo for userB — must NOT appear in userA's list. _, err = q.InsertTablo(ctx, sqlc.InsertTabloParams{ UserID: userB.ID, Title: "TabloB Private", Description: pgtype.Text{Valid: false}, Color: pgtype.Text{Valid: false}, }) if err != nil { t.Fatalf("InsertTablo tabloB: %v", err) } // Log in as userA. cookieVal, _, storeErr := store.Create(ctx, userA.ID) if storeErr != nil { t.Fatalf("store.Create: %v", storeErr) } sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} req := httptest.NewRequest(http.MethodGet, "/", nil) req.AddCookie(sessionCookie) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("GET / status = %d; want 200", rec.Code) } body := rec.Body.String() // Both of userA's tablos must appear. if !strings.Contains(body, tabloA1.Title) { t.Errorf("body missing tabloA1 title %q", tabloA1.Title) } if !strings.Contains(body, tabloA2.Title) { t.Errorf("body missing tabloA2 title %q", tabloA2.Title) } // userB's tablo must NOT appear. if strings.Contains(body, "TabloB Private") { t.Errorf("body must NOT contain userB's tablo title") } // Newest-first ordering (D-03): tabloA2 was inserted after tabloA1. // Check that tabloA2 appears before tabloA1 in the response body. idx1 := strings.Index(body, tabloA1.Title) idx2 := strings.Index(body, tabloA2.Title) if idx1 != -1 && idx2 != -1 && idx2 > idx1 { t.Errorf("tablos not in newest-first order: tabloA2 index=%d, tabloA1 index=%d", idx2, idx1) } } // ---- TestTabloList_Empty ---- // TestTabloList_Empty verifies the empty state copy when the user has no tablos (TABLO-01). func TestTabloList_Empty(t *testing.T) { pool, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() q := sqlc.New(pool) store := auth.NewStore(q) router := newTabloTestRouter(q, store) user := preInsertUser(t, ctx, q, "empty@example.com", "correct-horse-12") cookieVal, _, err := store.Create(ctx, user.ID) if err != nil { t.Fatalf("store.Create: %v", err) } sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} req := httptest.NewRequest(http.MethodGet, "/", nil) req.AddCookie(sessionCookie) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("GET / status = %d; want 200", rec.Code) } body := rec.Body.String() if !strings.Contains(body, "No tablos yet") { t.Errorf("empty state missing 'No tablos yet'; body: %.300s", body) } if !strings.Contains(body, "Create your first tablo to get started.") { t.Errorf("empty state missing CTA text; body: %.300s", body) } } // ---- TestTabloCreate ---- // TestTabloCreate verifies HTMX-driven create (TABLO-02, TABLO-06). // Sub-asserts cover: // - HTMX POST returns 200 with card fragment + HX-Retarget + HX-Reswap + OOB form clear // - Non-HTMX POST returns 303 to / // - DB row exists with user_id = caller func TestTabloCreate(t *testing.T) { pool, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() q := sqlc.New(pool) store := auth.NewStore(q) router := newTabloTestRouter(q, store) user := preInsertUser(t, ctx, q, "create@example.com", "correct-horse-12") t.Run("HTMX create", func(t *testing.T) { cookieVal, _, err := store.Create(ctx, user.ID) if err != nil { t.Fatalf("store.Create: %v", err) } sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} csrfToken, csrfCookies := getCSRFToken(t, router, "/", []*http.Cookie{sessionCookie}) form := url.Values{ "title": {"My New Tablo"}, "description": {"A description"}, "color": {"#6366f1"}, "_csrf": {csrfToken}, } req := httptest.NewRequest(http.MethodPost, "/tablos", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("HX-Request", "true") for _, c := range csrfCookies { req.AddCookie(c) } rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("HTMX POST /tablos status = %d; want 200", rec.Code) } body := rec.Body.String() if !strings.Contains(body, "My New Tablo") { t.Errorf("response body missing new tablo title; body: %.300s", body) } // HX-Retarget must be #tablos-list (dual-target swap per RESEARCH Pattern 4). if hxRetarget := rec.Header().Get("HX-Retarget"); hxRetarget != "#tablos-list" { t.Errorf("HX-Retarget = %q; want #tablos-list", hxRetarget) } // HX-Reswap must be afterbegin. if hxReswap := rec.Header().Get("HX-Reswap"); hxReswap != "afterbegin" { t.Errorf("HX-Reswap = %q; want afterbegin", hxReswap) } // OOB element to clear the form slot must be present. if !strings.Contains(body, `id="create-form-slot"`) { t.Errorf("response body missing OOB form-clear element (id=create-form-slot); body: %.500s", body) } if !strings.Contains(body, `hx-swap-oob="true"`) { t.Errorf("response body missing hx-swap-oob attribute; body: %.500s", body) } // DB row must exist with user_id = caller (TABLO-02). tablos, err := q.ListTablosByUser(ctx, user.ID) if err != nil { t.Fatalf("ListTablosByUser: %v", err) } if len(tablos) == 0 { t.Fatal("no tablo row in DB after create") } if tablos[0].Title != "My New Tablo" { t.Errorf("DB tablo title = %q; want My New Tablo", tablos[0].Title) } if tablos[0].UserID != user.ID { t.Errorf("DB tablo user_id = %v; want %v", tablos[0].UserID, user.ID) } }) t.Run("non-HTMX create", func(t *testing.T) { // Delete any existing tablos to keep state clean. tablos, _ := q.ListTablosByUser(ctx, user.ID) for _, tbl := range tablos { _ = q.DeleteTablo(ctx, sqlc.DeleteTabloParams{ID: tbl.ID, UserID: tbl.UserID}) } cookieVal, _, err := store.Create(ctx, user.ID) if err != nil { t.Fatalf("store.Create: %v", err) } sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} csrfToken, csrfCookies := getCSRFToken(t, router, "/", []*http.Cookie{sessionCookie}) form := url.Values{ "title": {"Non-HTMX Tablo"}, "_csrf": {csrfToken}, } req := httptest.NewRequest(http.MethodPost, "/tablos", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") // No HX-Request header — non-HTMX path. for _, c := range csrfCookies { req.AddCookie(c) } rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusSeeOther { t.Fatalf("non-HTMX POST /tablos status = %d; want 303", rec.Code) } if loc := rec.Header().Get("Location"); loc != "/" { t.Errorf("Location = %q; want /", loc) } }) } // ---- TestTabloCreate_Validation ---- // TestTabloCreate_Validation verifies that POST /tablos with empty title returns // 422 with an error message (TABLO-02 validation, T-03-03). func TestTabloCreate_Validation(t *testing.T) { pool, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() q := sqlc.New(pool) store := auth.NewStore(q) router := newTabloTestRouter(q, store) user := preInsertUser(t, ctx, q, "createval@example.com", "correct-horse-12") cookieVal, _, err := store.Create(ctx, user.ID) if err != nil { t.Fatalf("store.Create: %v", err) } sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} csrfToken, csrfCookies := getCSRFToken(t, router, "/", []*http.Cookie{sessionCookie}) // Empty title. form := url.Values{ "title": {""}, "_csrf": {csrfToken}, } req := httptest.NewRequest(http.MethodPost, "/tablos", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("HX-Request", "true") for _, c := range csrfCookies { req.AddCookie(c) } rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusUnprocessableEntity { t.Fatalf("status = %d; want 422", rec.Code) } if !strings.Contains(rec.Body.String(), "Title is required.") { t.Errorf("body missing 'Title is required.'; got: %.300s", rec.Body.String()) } // No DB row should have been inserted. tablos, _ := q.ListTablosByUser(ctx, user.ID) if len(tablos) != 0 { t.Errorf("tablo count = %d; want 0 (no insert on validation error)", len(tablos)) } } // ---- TestTabloDetail_Owner ---- // TestTabloDetail_Owner verifies that the owner of a tablo can view its detail page (TABLO-03). func TestTabloDetail_Owner(t *testing.T) { pool, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() q := sqlc.New(pool) store := auth.NewStore(q) router := newTabloTestRouter(q, store) user := preInsertUser(t, ctx, q, "owner@example.com", "correct-horse-12") tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{ UserID: user.ID, Title: "Owners Detail Tablo", Description: pgtype.Text{Valid: false}, Color: pgtype.Text{Valid: false}, }) if err != nil { t.Fatalf("InsertTablo: %v", err) } cookieVal, _, storeErr := store.Create(ctx, user.ID) if storeErr != nil { t.Fatalf("store.Create: %v", storeErr) } sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String(), nil) req.AddCookie(sessionCookie) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("GET /tablos/{id} as owner: status = %d; want 200", rec.Code) } if !strings.Contains(rec.Body.String(), "Owners Detail Tablo") { t.Errorf("body missing tablo title; got: %.300s", rec.Body.String()) } } // ---- TestTabloDetail_NonOwner ---- // TestTabloDetail_NonOwner verifies that a different authenticated user gets 404 // (not 403 — no information leakage per D-04) when accessing another user's tablo (TABLO-03). func TestTabloDetail_NonOwner(t *testing.T) { pool, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() q := sqlc.New(pool) store := auth.NewStore(q) router := newTabloTestRouter(q, store) // User A creates a tablo. userA := preInsertUser(t, ctx, q, "nonowner_a@example.com", "correct-horse-12") tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{ UserID: userA.ID, Title: "UserA's Secret Tablo", Description: pgtype.Text{Valid: false}, Color: pgtype.Text{Valid: false}, }) if err != nil { t.Fatalf("InsertTablo: %v", err) } // User B tries to access it. userB := preInsertUser(t, ctx, q, "nonowner_b@example.com", "correct-horse-12") cookieVal, _, storeErr := store.Create(ctx, userB.ID) if storeErr != nil { t.Fatalf("store.Create for userB: %v", storeErr) } sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String(), nil) req.AddCookie(sessionCookie) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) // D-04: 404 (not 403) — no information leakage. if rec.Code != http.StatusNotFound { t.Fatalf("GET /tablos/{id} as non-owner: status = %d; want 404", rec.Code) } } // ---- TestTabloDetail_InvalidID ---- // TestTabloDetail_InvalidID verifies that a non-UUID path param returns 404 (TABLO-03). func TestTabloDetail_InvalidID(t *testing.T) { pool, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() q := sqlc.New(pool) store := auth.NewStore(q) router := newTabloTestRouter(q, store) user := preInsertUser(t, ctx, q, "invalidid@example.com", "correct-horse-12") cookieVal, _, err := store.Create(ctx, user.ID) if err != nil { t.Fatalf("store.Create: %v", err) } sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} req := httptest.NewRequest(http.MethodGet, "/tablos/not-a-uuid", nil) req.AddCookie(sessionCookie) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("GET /tablos/not-a-uuid: status = %d; want 404", rec.Code) } } // ---- TestTabloUpdate ---- // TestTabloUpdate verifies that POST /tablos/{id} updates title and description (TABLO-04). // Checks: 200 response with updated title in body; DB row updated; updated_at refreshed. func TestTabloUpdate(t *testing.T) { pool, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() q := sqlc.New(pool) store := auth.NewStore(q) router := newTabloTestRouter(q, store) user := preInsertUser(t, ctx, q, "update@example.com", "correct-horse-12") tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{ UserID: user.ID, Title: "Original Title", Description: pgtype.Text{Valid: true, String: "Original description"}, Color: pgtype.Text{Valid: false}, }) if err != nil { t.Fatalf("InsertTablo: %v", err) } cookieVal, _, storeErr := store.Create(ctx, user.ID) if storeErr != nil { t.Fatalf("store.Create: %v", storeErr) } sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String(), []*http.Cookie{sessionCookie}) form := url.Values{ "title": {"Updated Title"}, "description": {"Updated description"}, "_csrf": {csrfToken}, } req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String(), strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("HX-Request", "true") for _, c := range csrfCookies { req.AddCookie(c) } rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("POST /tablos/{id} status = %d; want 200", rec.Code) } if !strings.Contains(rec.Body.String(), "Updated Title") { t.Errorf("body missing updated title; got: %.300s", rec.Body.String()) } // Verify DB state. updated, err := q.GetTabloByID(ctx, sqlc.GetTabloByIDParams{ID: tablo.ID, UserID: user.ID}) if err != nil { t.Fatalf("GetTabloByID after update: %v", err) } if updated.Title != "Updated Title" { t.Errorf("DB title = %q; want 'Updated Title'", updated.Title) } if !updated.Description.Valid || updated.Description.String != "Updated description" { t.Errorf("DB description = %v/%q; want valid 'Updated description'", updated.Description.Valid, updated.Description.String) } // updated_at must be >= original (Pitfall 7). if updated.UpdatedAt.Time.Before(tablo.UpdatedAt.Time) || updated.UpdatedAt.Time.Equal(tablo.UpdatedAt.Time) { // In tests the DB is local; allow same time but not earlier. // We do a lenient check: updated_at must be >= original. if updated.UpdatedAt.Time.Before(tablo.UpdatedAt.Time) { t.Errorf("updated_at did not advance: was %v, got %v", tablo.UpdatedAt.Time, updated.UpdatedAt.Time) } } } // ---- TestTabloDeleteConfirm ---- // TestTabloDeleteConfirm verifies that GET /tablos/{id}/delete-confirm returns a // confirmation fragment with "Delete tablo?" and "Yes, delete" (TABLO-05, D-07). func TestTabloDeleteConfirm(t *testing.T) { pool, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() q := sqlc.New(pool) store := auth.NewStore(q) router := newTabloTestRouter(q, store) user := preInsertUser(t, ctx, q, "delconfirm@example.com", "correct-horse-12") tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{ UserID: user.ID, Title: "To Be Deleted", Description: pgtype.Text{Valid: false}, Color: pgtype.Text{Valid: false}, }) if err != nil { t.Fatalf("InsertTablo: %v", err) } cookieVal, _, storeErr := store.Create(ctx, user.ID) if storeErr != nil { t.Fatalf("store.Create: %v", storeErr) } sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/delete-confirm", nil) req.Header.Set("HX-Request", "true") req.AddCookie(sessionCookie) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("GET /tablos/{id}/delete-confirm: status = %d; want 200", rec.Code) } body := rec.Body.String() if !strings.Contains(body, "Delete tablo?") { t.Errorf("confirm fragment missing 'Delete tablo?'; body: %.300s", body) } if !strings.Contains(body, "Yes, delete") { t.Errorf("confirm fragment missing 'Yes, delete'; body: %.300s", body) } } // ---- TestTabloDelete ---- // TestTabloDelete verifies that POST /tablos/{id}/delete hard-deletes the row (TABLO-05). // Sub-asserts: // - HTMX: 200 + HX-Redirect:/ (or HX-Retarget #tablos-list for in-list deletes) // - Non-HTMX: 303 to / // - DB row deleted func TestTabloDelete(t *testing.T) { pool, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() q := sqlc.New(pool) store := auth.NewStore(q) router := newTabloTestRouter(q, store) user := preInsertUser(t, ctx, q, "delete@example.com", "correct-horse-12") t.Run("HTMX delete", func(t *testing.T) { tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{ UserID: user.ID, Title: "Tablo to Delete HTMX", Description: pgtype.Text{Valid: false}, Color: pgtype.Text{Valid: false}, }) if err != nil { t.Fatalf("InsertTablo: %v", err) } cookieVal, _, storeErr := store.Create(ctx, user.ID) if storeErr != nil { t.Fatalf("store.Create: %v", storeErr) } sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} csrfToken, csrfCookies := getCSRFToken(t, router, "/", []*http.Cookie{sessionCookie}) form := url.Values{"_csrf": {csrfToken}} req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/delete", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("HX-Request", "true") for _, c := range csrfCookies { req.AddCookie(c) } rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("HTMX DELETE status = %d; want 200", rec.Code) } // Must have HX-Redirect header pointing back to /. hxRedirect := rec.Header().Get("HX-Redirect") if hxRedirect != "/" { t.Errorf("HX-Redirect = %q; want /", hxRedirect) } // DB row must be gone. _, err = q.GetTabloByID(ctx, sqlc.GetTabloByIDParams{ID: tablo.ID, UserID: user.ID}) if err == nil { t.Error("tablo row still exists in DB after delete") } }) t.Run("non-HTMX delete", func(t *testing.T) { tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{ UserID: user.ID, Title: "Tablo to Delete Plain", Description: pgtype.Text{Valid: false}, Color: pgtype.Text{Valid: false}, }) if err != nil { t.Fatalf("InsertTablo: %v", err) } cookieVal, _, storeErr := store.Create(ctx, user.ID) if storeErr != nil { t.Fatalf("store.Create: %v", storeErr) } sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} csrfToken, csrfCookies := getCSRFToken(t, router, "/", []*http.Cookie{sessionCookie}) form := url.Values{"_csrf": {csrfToken}} req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/delete", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") // No HX-Request — non-HTMX path. for _, c := range csrfCookies { req.AddCookie(c) } rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusSeeOther { t.Fatalf("non-HTMX DELETE status = %d; want 303", rec.Code) } if loc := rec.Header().Get("Location"); loc != "/" { t.Errorf("Location = %q; want /", loc) } // DB row must be gone. _, err = q.GetTabloByID(ctx, sqlc.GetTabloByIDParams{ID: tablo.ID, UserID: user.ID}) if err == nil { t.Error("tablo row still exists in DB after non-HTMX delete") } }) }