diff --git a/backend/internal/web/handlers_events_test.go b/backend/internal/web/handlers_events_test.go new file mode 100644 index 0000000..f50ab0a --- /dev/null +++ b/backend/internal/web/handlers_events_test.go @@ -0,0 +1,184 @@ +package web + +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" +) + +func newEventTestRouter(q *sqlc.Queries, store *auth.Store) 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} + router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, etapeDeps, eventDeps, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost") + if err != nil { + panic("newEventTestRouter: " + err.Error()) + } + return router +} + +func insertEventTestTablo(t *testing.T, ctx context.Context, q *sqlc.Queries, user sqlc.User, title 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{Valid: false}, + }) + if err != nil { + t.Fatalf("InsertTablo: %v", err) + } + return tablo +} + +func sessionCookieForUser(t *testing.T, ctx context.Context, store *auth.Store, user sqlc.User) *http.Cookie { + t.Helper() + cookieVal, _, err := store.Create(ctx, user.ID) + if err != nil { + t.Fatalf("store.Create: %v", err) + } + return &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} +} + +func TestEventsTabRendersMonthGrid(t *testing.T) { + pool, cleanup := setupTestDB(t) + defer cleanup() + + ctx := context.Background() + q := sqlc.New(pool) + store := auth.NewStore(q) + router := newEventTestRouter(q, store) + + user := preInsertUser(t, ctx, q, "eventsgrid@example.com", "correct-horse-12") + tablo := insertEventTestTablo(t, ctx, q, user, "Events Grid Tablo") + sessionCookie := sessionCookieForUser(t, ctx, store, user) + + req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", 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}/events status = %d; want 200", rec.Code) + } + body := rec.Body.String() + for _, want := range []string{"Events", "May 2026", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "New event"} { + if !strings.Contains(body, want) { + t.Errorf("events calendar missing %q; body: %.800s", want, body) + } + } +} + +func TestEventCreateRendersTitleInCalendar(t *testing.T) { + pool, cleanup := setupTestDB(t) + defer cleanup() + + ctx := context.Background() + q := sqlc.New(pool) + store := auth.NewStore(q) + router := newEventTestRouter(q, store) + + user := preInsertUser(t, ctx, q, "eventcreate@example.com", "correct-horse-12") + tablo := insertEventTestTablo(t, ctx, q, user, "Event Create Tablo") + sessionCookie := sessionCookieForUser(t, ctx, store, user) + csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", []*http.Cookie{sessionCookie}) + + form := url.Values{ + "title": {"Kickoff"}, + "event_date": {"2026-05-20"}, + "start_time": {"09:30"}, + "end_time": {""}, + "location": {"Office"}, + "description": {"Planning"}, + "_csrf": {csrfToken}, + } + req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", 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}/events status = %d; want 200; body: %.500s", rec.Code, rec.Body.String()) + } + if !strings.Contains(rec.Body.String(), "Kickoff") { + t.Fatalf("created event title missing from refreshed calendar; body: %.800s", rec.Body.String()) + } +} + +func TestEventCreateRejectsEndTimeBeforeOrEqualStart(t *testing.T) { + pool, cleanup := setupTestDB(t) + defer cleanup() + + ctx := context.Background() + q := sqlc.New(pool) + store := auth.NewStore(q) + router := newEventTestRouter(q, store) + + user := preInsertUser(t, ctx, q, "eventinvalid@example.com", "correct-horse-12") + tablo := insertEventTestTablo(t, ctx, q, user, "Event Invalid Tablo") + sessionCookie := sessionCookieForUser(t, ctx, store, user) + csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", []*http.Cookie{sessionCookie}) + + form := url.Values{ + "title": {"Invalid"}, + "event_date": {"2026-05-20"}, + "start_time": {"10:00"}, + "end_time": {"10:00"}, + "_csrf": {csrfToken}, + } + req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", 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("invalid event create status = %d; want 422", rec.Code) + } + if !strings.Contains(rec.Body.String(), "End time must be after the start time.") { + t.Fatalf("validation copy missing; body: %.800s", rec.Body.String()) + } +} + +func TestEventsTabOwnershipReturns404(t *testing.T) { + pool, cleanup := setupTestDB(t) + defer cleanup() + + ctx := context.Background() + q := sqlc.New(pool) + store := auth.NewStore(q) + router := newEventTestRouter(q, store) + + owner := preInsertUser(t, ctx, q, "eventsowner@example.com", "correct-horse-12") + nonOwner := preInsertUser(t, ctx, q, "eventsnonowner@example.com", "correct-horse-12") + tablo := insertEventTestTablo(t, ctx, q, owner, "Private Events Tablo") + nonOwnerCookie := sessionCookieForUser(t, ctx, store, nonOwner) + + req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", nil) + req.AddCookie(nonOwnerCookie) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + if rec.Code != http.StatusNotFound { + t.Fatalf("non-owner GET /tablos/{id}/events status = %d; want 404", rec.Code) + } +}