test(11-01): add planning agenda red coverage
This commit is contained in:
parent
66e3846614
commit
c5c3bbe2d0
1 changed files with 271 additions and 0 deletions
271
backend/internal/web/handlers_planning_test.go
Normal file
271
backend/internal/web/handlers_planning_test.go
Normal file
|
|
@ -0,0 +1,271 @@
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"backend/internal/auth"
|
||||||
|
"backend/internal/db/sqlc"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newPlanningTestRouter(q *sqlc.Queries, store *auth.Store, now func() time.Time) http.Handler {
|
||||||
|
authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
|
||||||
|
tabloDeps := TablosDeps{Queries: q}
|
||||||
|
taskDeps := TasksDeps{Queries: q}
|
||||||
|
etapeDeps := EtapesDeps{Queries: q}
|
||||||
|
eventDeps := EventsDeps{Queries: q}
|
||||||
|
planningDeps := PlanningDeps{Queries: q, Now: now}
|
||||||
|
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, etapeDeps, eventDeps, planningDeps, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||||
|
if err != nil {
|
||||||
|
panic("newPlanningTestRouter: " + err.Error())
|
||||||
|
}
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
func fixedPlanningNow() time.Time {
|
||||||
|
return time.Date(2026, time.May, 16, 9, 0, 0, 0, time.Local)
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertPlanningTestTablo(t *testing.T, ctx context.Context, q *sqlc.Queries, user sqlc.User, title, color string) sqlc.Tablo {
|
||||||
|
t.Helper()
|
||||||
|
tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
Title: title,
|
||||||
|
Description: pgtype.Text{Valid: false},
|
||||||
|
Color: pgtype.Text{String: color, Valid: color != ""},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("InsertTablo: %v", err)
|
||||||
|
}
|
||||||
|
return tablo
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanningRequiresAuth(t *testing.T) {
|
||||||
|
pool, cleanup := setupTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
q := sqlc.New(pool)
|
||||||
|
store := auth.NewStore(q)
|
||||||
|
router := newPlanningTestRouter(q, store, fixedPlanningNow)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/planning", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusSeeOther {
|
||||||
|
t.Fatalf("GET /planning status = %d; want 303", rec.Code)
|
||||||
|
}
|
||||||
|
if got := rec.Header().Get("Location"); got != "/login" {
|
||||||
|
t.Fatalf("GET /planning Location = %q; want /login", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanningDefaultsToUpcomingFourteenDays(t *testing.T) {
|
||||||
|
pool, cleanup := setupTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
q := sqlc.New(pool)
|
||||||
|
store := auth.NewStore(q)
|
||||||
|
router := newPlanningTestRouter(q, store, fixedPlanningNow)
|
||||||
|
|
||||||
|
user := preInsertUser(t, ctx, q, "planningdefault@example.com", "correct-horse-12")
|
||||||
|
tablo := insertPlanningTestTablo(t, ctx, q, user, "Default Window", "#2563eb")
|
||||||
|
insertEventTestEvent(t, ctx, q, tablo.ID, "Starts Today", "2026-05-16", "09:30", "", "", "")
|
||||||
|
insertEventTestEvent(t, ctx, q, tablo.ID, "Ends Window", "2026-05-29", "10:00", "", "", "")
|
||||||
|
insertEventTestEvent(t, ctx, q, tablo.ID, "Outside Window", "2026-05-30", "10:00", "", "", "")
|
||||||
|
sessionCookie := sessionCookieForUser(t, ctx, store, user)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/planning", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("GET /planning status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
body := rec.Body.String()
|
||||||
|
for _, want := range []string{"Planning", "May 16 - May 29, 2026", "Starts Today", "Ends Window"} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("planning default range missing %q; body: %.800s", want, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(body, "Outside Window") {
|
||||||
|
t.Fatalf("planning default range included event outside 14-day window; body: %.800s", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanningListsOwnedEventsChronologically(t *testing.T) {
|
||||||
|
pool, cleanup := setupTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
q := sqlc.New(pool)
|
||||||
|
store := auth.NewStore(q)
|
||||||
|
router := newPlanningTestRouter(q, store, fixedPlanningNow)
|
||||||
|
|
||||||
|
user := preInsertUser(t, ctx, q, "planningorder@example.com", "correct-horse-12")
|
||||||
|
blueTablo := insertPlanningTestTablo(t, ctx, q, user, "Blue Launch", "#123abc")
|
||||||
|
redTablo := insertPlanningTestTablo(t, ctx, q, user, "Red Studio", "")
|
||||||
|
insertEventTestEvent(t, ctx, q, redTablo.ID, "Gamma", "2026-05-18", "09:00", "", "Studio", "Hidden description")
|
||||||
|
insertEventTestEvent(t, ctx, q, blueTablo.ID, "Beta", "2026-05-17", "11:00", "12:00", "Office", "")
|
||||||
|
insertEventTestEvent(t, ctx, q, blueTablo.ID, "Alpha", "2026-05-17", "11:00", "", "", "")
|
||||||
|
insertEventTestEvent(t, ctx, q, blueTablo.ID, "Early", "2026-05-17", "09:30", "", "HQ", "")
|
||||||
|
sessionCookie := sessionCookieForUser(t, ctx, store, user)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/planning?start=2026-05-16", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("GET /planning status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
body := rec.Body.String()
|
||||||
|
assertBodyOrder(t, body, "Early", "Alpha", "Beta", "Gamma")
|
||||||
|
for _, want := range []string{"09:30", "11:00", "11:00-12:00", "Blue Launch", "#123abc", "Red Studio", "Office", "HQ", "/tablos/" + blueTablo.ID.String() + "/events?month=2026-05", "/tablos/" + redTablo.ID.String() + "/events?month=2026-05"} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("planning agenda missing %q; body: %.1200s", want, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(body, "Hidden description") {
|
||||||
|
t.Fatalf("planning agenda rendered description snippet; body: %.1200s", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanningDoesNotLeakOtherUsersEvents(t *testing.T) {
|
||||||
|
pool, cleanup := setupTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
q := sqlc.New(pool)
|
||||||
|
store := auth.NewStore(q)
|
||||||
|
router := newPlanningTestRouter(q, store, fixedPlanningNow)
|
||||||
|
|
||||||
|
owner := preInsertUser(t, ctx, q, "planningowner@example.com", "correct-horse-12")
|
||||||
|
other := preInsertUser(t, ctx, q, "planningother@example.com", "correct-horse-12")
|
||||||
|
ownedTablo := insertPlanningTestTablo(t, ctx, q, owner, "Owned Planning", "#2563eb")
|
||||||
|
foreignTablo := insertPlanningTestTablo(t, ctx, q, other, "Foreign Planning", "#b91c1c")
|
||||||
|
insertEventTestEvent(t, ctx, q, ownedTablo.ID, "Owned Event", "2026-05-20", "09:30", "", "", "")
|
||||||
|
insertEventTestEvent(t, ctx, q, foreignTablo.ID, "Foreign Event", "2026-05-20", "09:30", "", "", "")
|
||||||
|
sessionCookie := sessionCookieForUser(t, ctx, store, owner)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/planning?start=2026-05-16", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("GET /planning status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
body := rec.Body.String()
|
||||||
|
if !strings.Contains(body, "Owned Event") {
|
||||||
|
t.Fatalf("planning agenda missing owned event; body: %.800s", body)
|
||||||
|
}
|
||||||
|
if strings.Contains(body, "Foreign Event") {
|
||||||
|
t.Fatalf("planning agenda leaked foreign event; body: %.800s", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanningNavigationLinks(t *testing.T) {
|
||||||
|
pool, cleanup := setupTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
q := sqlc.New(pool)
|
||||||
|
store := auth.NewStore(q)
|
||||||
|
router := newPlanningTestRouter(q, store, fixedPlanningNow)
|
||||||
|
|
||||||
|
user := preInsertUser(t, ctx, q, "planningnav@example.com", "correct-horse-12")
|
||||||
|
sessionCookie := sessionCookieForUser(t, ctx, store, user)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/planning?start=2026-05-16", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("GET /planning status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
body := rec.Body.String()
|
||||||
|
for _, want := range []string{"Previous 14 days", "Today", "Next 14 days", "start=2026-05-02", "href=\"/planning\"", "start=2026-05-30"} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("planning navigation missing %q; body: %.1000s", want, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanningEmptyState(t *testing.T) {
|
||||||
|
pool, cleanup := setupTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
q := sqlc.New(pool)
|
||||||
|
store := auth.NewStore(q)
|
||||||
|
router := newPlanningTestRouter(q, store, fixedPlanningNow)
|
||||||
|
|
||||||
|
user := preInsertUser(t, ctx, q, "planningempty@example.com", "correct-horse-12")
|
||||||
|
sessionCookie := sessionCookieForUser(t, ctx, store, user)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/planning?start=2026-05-16", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("GET /planning status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
body := rec.Body.String()
|
||||||
|
for _, want := range []string{"No events in this range", "Use the navigation controls to browse another 14-day window.", "Previous 14 days", "Today", "Next 14 days"} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("planning empty state missing %q; body: %.1000s", want, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanningInvalidStartFallsBackToToday(t *testing.T) {
|
||||||
|
pool, cleanup := setupTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
q := sqlc.New(pool)
|
||||||
|
store := auth.NewStore(q)
|
||||||
|
router := newPlanningTestRouter(q, store, fixedPlanningNow)
|
||||||
|
|
||||||
|
user := preInsertUser(t, ctx, q, "planninginvalid@example.com", "correct-horse-12")
|
||||||
|
sessionCookie := sessionCookieForUser(t, ctx, store, user)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/planning?start=not-a-date", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("GET /planning invalid start status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(rec.Body.String(), "May 16 - May 29, 2026") {
|
||||||
|
t.Fatalf("invalid start did not fall back to today range; body: %.1000s", rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertBodyOrder(t *testing.T, body string, values ...string) {
|
||||||
|
t.Helper()
|
||||||
|
last := -1
|
||||||
|
for _, value := range values {
|
||||||
|
idx := strings.Index(body, value)
|
||||||
|
if idx == -1 {
|
||||||
|
t.Fatalf("body missing %q; body: %.1200s", value, body)
|
||||||
|
}
|
||||||
|
if idx <= last {
|
||||||
|
t.Fatalf("body order mismatch: %q appeared at %d after prior index %d; body: %.1200s", value, idx, last, body)
|
||||||
|
}
|
||||||
|
last = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue