xtablo-source/backend/internal/web/handlers_planning_test.go
2026-05-16 07:24:23 +02:00

271 lines
9.6 KiB
Go

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
}
}