Gap fill: three no-infrastructure unit tests that run without TEST_DATABASE_URL or S3_ENDPOINT:
- backend/templates/files_helpers_test.go — formatBytes boundary cases (B/KB/MB/GB)
- backend/internal/files/store_unit_test.go — byteCountReader accumulation, io.ErrUnexpectedEOF
guard for small files, and MultiReader body reconstruction after 512-byte sniff
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Expand stubbedFileStorer with deletedKey tracking and deleteErr injection field
- Implement TestFileDownload (FILE-04): 302 redirect to presigned URL
- Implement TestFileDownload_NonOwner: non-owner gets 404
- Implement TestFileDelete (FILE-05): HTMX delete, S3+DB both deleted
- Implement TestFileDelete_S3Failure: S3 error does not abort DB delete, 200 returned
- Implement TestFileOwnership (FILE-06): non-owner gets 404 on all three routes
maxPos + 100 could silently overflow to a negative value when maxPos
approached MaxInt32. Added a maxAllowedPosition guard that returns a
validation error before the InsertTask call if the column position space
is exhausted.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
A description of spaces-only was being stored as a valid non-null DB value
because the empty-string check ran before trimming. Now consistent with how
other nullable text fields are handled.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Without these headers, HTMX used the form's own hx-target="#column-{status}"
+ hx-swap="beforeend", appending the error form into the task column and
destroying all visible task cards. The error form now lands back in the
add-task slot where it belongs.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Both the single-task branch and the main loop were using _, _ = to
discard UpdateTask errors. Now both log the error and return 500 so
the client is never shown a false success when DB writes fail.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The raw fmt.Fprintf bypassed templ's auto-escaping pipeline and was
inconsistent with every other handler. Added TaskCardGone(taskID uuid.UUID)
to tasks.templ and updated TaskDeleteHandler to use it. Ran just generate.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Both handlers were missing the mandatory ParseForm call before reading
PostFormValue. This caused gorilla/csrf (which reads the body for CSRF
token validation) to consume the body, leaving PostFormValue to return
empty strings. TaskReorderHandler was used as the correct reference.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Remove t.Skip("handlers_tasks not yet implemented") from TestTaskOrderPersists
- Full test suite green: go test ./... exits 0, no FAIL lines
- All 9 TestTask* tests active (skip on missing TEST_DATABASE_URL per existing pattern)
- TaskReorderHandler: POST /tablos/{id}/tasks/reorder updates status+position
- Fetches existing task via GetTaskByID before UpdateTask (T-04-08 mass assignment guard)
- Supports both array form (task_id[]/task_col[]) and single-value form (task_id/status/position)
- Invalid UUIDs silently skipped (D-05); tasks from other tablos skipped (T-04-10)
- Returns updated KanbanBoard outerHTML for HTMX swap
- Remove t.Skip from TestTaskReorderCrossColumn and TestTaskReorderSameColumn
- Add handlers_tasks.go: TasksDeps, TaskNewFormHandler, TaskCancelNewHandler, TaskCreateHandler, TaskShowHandler, TaskDeleteConfirmHandler, TaskDeleteHandler, plus stub Edit/Update/Reorder handlers
- Add task routes to router.go (static before parametric per Pitfall 1)
- Add TasksDeps param to NewRouter; update main.go and all test callers
- Move TaskColumns/TaskColumnLabels to templates package to avoid import cycle
- handlers_tasks_test.go: 9 TestTask* functions (TASK-01..07 + IDOR) all skip
- TasksDeps stub struct declared in test file for Plan 02 wiring
- tasks_forms.go: TaskCreateForm, TaskCreateErrors, TaskUpdateForm, TaskUpdateErrors structs
- go build ./... passes; go test -run TestTask exits 0 with all 9 SKIP
- TabloUpdateHandler: capture user from loadOwnedTablo (was discarded with _)
- Pass captured user to TabloDetailPage on non-HTMX validation error path
instead of nil, preventing broken layout (no logout button/email shown)
- TabloUpdateHandler: pass tablo.Color to UpdateTablo to preserve color on update (CR-01)
- loadOwnedTablo: pass GetTabloByIDParams{ID, UserID} to DB query (WR-01 call site)
- TabloDeleteHandler: pass DeleteTabloParams{ID, UserID} to DB query (WR-02 call site)
- TabloDeleteHandler: on DB error with HX-Request, render TabloDeleteConfirmFragment
instead of plain http.Error to avoid broken HTMX DOM state (CR-03)
- renderTabloCreateError: log secondary ListTablosByUser fetch failure (WR-03)
- TablosCreateHandler: validate color with isValidCSSColor (hex only) and surface
TabloCreateErrors.Color field error to prevent CSS injection (WR-04)
- Add isValidCSSColor helper using ^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$ regex
- Update test call sites for GetTabloByID and DeleteTablo new param types
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- UpdateTablo SQL: add color = \$4 so color is preserved across title/description edits
- GetTabloByID SQL: add AND user_id = \$2 to push ownership enforcement into the DB layer
- DeleteTablo SQL: add AND user_id = \$2 to push authorization into the DB layer
- sqlc bindings regenerated (UpdateTabloParams+Color, GetTabloByIDParams, DeleteTabloParams)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- loadOwnedTablo helper: uuid.Parse, GetTabloByID, ownership check (D-04: 404 not 403)
- TabloDetailHandler: GET /tablos/{id} renders detail page
- TabloEditTitleHandler/ShowTitleHandler: GET /tablos/{id}/edit-title|show-title fragments
- TabloEditDescHandler/ShowDescHandler: GET /tablos/{id}/edit-desc|show-desc fragments
- TabloUpdateHandler: POST /tablos/{id} — validates, updates DB, renders matching zone fragment
- TabloDeleteConfirmHandler/CancelHandler: GET /tablos/{id}/delete-confirm|delete-cancel
- TabloDeleteHandler: POST /tablos/{id}/delete — deletes row, HX-Redirect:/ or 303
- router.go: 9 new routes in RequireAuth group, static-before-parametric order preserved
- Fix [Rule 1 - Bug]: test title "Owner's Tablo" caused HTML entity mismatch — changed to "Owners Detail Tablo"
- go test ./internal/web/... -run TestTablo: 10/10 PASS; full suite: all PASS
- TestLoadCSRFKey_* in internal/auth for env key loading
- TestCSRF_*MissingToken / TestCSRF_*ValidToken for all three POST routes
- TestForms_ContainCSRFField for hidden _csrf input in rendered HTML
- TestRouter_CSRFMountedAfterResolveSession for middleware order (D-24)
- TestCSRF_HeaderFallback for X-CSRF-Token header support
- Add gorilla/csrf v1.7.3 dependency
- Add LogoutHandler: deletes session row (D-06), clears cookie, redirects to /login
- Protect GET / inside RequireAuth group; remove old top-level registration
- Add POST /logout inside same RequireAuth group (D-22: POST-only logout)
- Update Layout signature to accept *auth.User; render logout form + email when authed
- Update Index template to accept *auth.User and show "Signed in as {email}"
- Update SignupPage/LoginPage to pass nil to Layout (auth pages are unauthed)
- Update IndexHandler to pull user from auth.Authed(ctx) and pass to template
- Update TestIndex_RendersHxGet -> TestIndex_UnauthRedirects (GET / now protected)
- AUTH-04 (logout) and AUTH-05 (protected /) are now closed
- TestLogout_Success: POST /logout with valid cookie -> 303, cookie cleared, session deleted
- TestLogout_UnauthRedirectsToLogin: POST /logout without cookie -> 303 from RequireAuth
- TestLogout_HXRedirect: HTMX logout -> 200 + HX-Redirect: /login
- TestLogout_AfterLogoutSubsequentRequestUnauth: stale cookie blocked after logout
- TestProtected_HomeUnauthRedirects: GET / without session -> 303 /login
- TestProtected_HomeUnauthHXRedirect: HTMX GET / without session -> 200 + HX-Redirect
- TestProtected_HomeAuthRendersUserEmail: authed GET / -> 200 with user email
- TestLayout_LogoutFormVisibleWhenAuthed: Layout with user shows logout form
- TestLayout_LogoutFormHiddenWhenUnauthed: Layout with nil user hides logout form
- auth_login.templ: LoginPage + LoginFormFragment (mirrors signup shape)
- LoginForm + LoginErrors types added to templates/auth_forms.go
- LoginPageHandler + LoginPostHandler in handlers_auth.go
- Rate-limit check before user lookup (D-16, T-2-14)
- Single errInvalidCreds constant for D-20 enumeration defense
- Session rotation via Store.Rotate on success (D-10, T-2-04)
- HTMX-aware redirect and fragment responses (D-19, D-21)
- AuthDeps extended with Limiter *auth.LimiterStore field
- router.go: GET /login in RedirectIfAuthed group (D-23)
- main.go: LimiterStore created with janitor goroutine (D-16)
- Export NewLimiterStoreWithClock + SetLimiterClock for cross-package tests
- 12 TestLogin_* integration tests all pass with real DB
- templates/layout.templ: base HTML shell per UI-SPEC §Base Layout Contract
(max-w-5xl container, slate-50 header, slate-200 borders, footer copy,
/static/tailwind.css in <head>, /static/htmx.min.js deferred at body end —
D-10: HTMX never loaded from a CDN)
- templates/index.templ: root page consuming @ui.Card and @ui.Button for the
canonical HTMX demo (UI-SPEC §Component Library Contract canonical block)
- templates/fragments.templ: TimeFragment renders <span> with RFC3339 UTC
timestamp; templ auto-escapes interpolation (T-01-13)
- internal/web/handlers.go: HealthzHandler (200 ok / 503 degraded per D-20,
2s Ping timeout), IndexHandler, DemoTimeHandler with injected clock
- internal/web/router.go: Pinger interface; NewRouter wires
RequestIDMiddleware → RealIP → SlogLoggerMiddleware → Recoverer (D-08
+ Pitfall 6 — chi middleware.Logger deliberately NOT registered) and
routes /, /healthz, /demo/time, /static/* via http.FileServer (T-01-08
path traversal blocked by http.Dir)
All six handler tests + ui package tests are GREEN under default go test.
- Remove //go:build red_gate tag from internal/web/handlers_test.go and
internal/db/pool_test.go now that consumer symbols are about to exist
- go mod tidy after real importers land (deferred from Plan 01-01 per
Codex concern #1) — chi/v5, templ, pgx/v5, google/uuid now in require list
- internal/db/pool.go: NewPool(ctx, dsn) builds a pgxpool.Pool with
MaxConns=10, MinConns=1; no eager Ping (RESEARCH Pitfall 2)
- internal/web/slog.go: NewSlogHandler returns JSON when env='production',
text otherwise; pure helper, no slog.SetDefault side effect
- internal/web/middleware.go: RequestIDMiddleware (UUIDv4 → ctx +
X-Request-ID header), LoggerFromContext helper, SlogLoggerMiddleware
factory using chi WrapResponseWriter; field allowlist per V7/T-01-09
- handlers_test.go (//go:build red_gate): TestHealthz_OK, TestHealthz_Down,
TestIndex_RendersHxGet, TestDemoTime_Fragment, TestRequestID_HeaderSet,
TestSlog_HandlerSwitch — reference web.HealthzHandler / NewRouter /
NewSlogHandler / Pinger to be implemented in Plan 01-03
- pool_test.go (//go:build red_gate): TestPool_Connects with t.Skip
fallback when DATABASE_URL is unset
- Build tag isolates the RED state from default 'go test ./...' (Codex #3)