xtablo-source/backend/internal/web/handlers_events_test.go
2026-05-16 07:26:49 +02:00

764 lines
27 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package web
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
"backend/internal/auth"
"backend/internal/db/sqlc"
"backend/templates"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"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, PlanningDeps{Queries: q}, 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 eventTestDate(t *testing.T, raw string) pgtype.Date {
t.Helper()
parsed, err := time.Parse("2006-01-02", raw)
if err != nil {
t.Fatalf("eventTestDate(%q): %v", raw, err)
}
return pgtype.Date{Time: parsed, Valid: true}
}
func eventTestTime(t *testing.T, raw string) pgtype.Time {
t.Helper()
parsed, err := time.Parse("15:04", raw)
if err != nil {
t.Fatalf("eventTestTime(%q): %v", raw, err)
}
micros := int64(parsed.Hour())*int64(time.Hour/time.Microsecond) + int64(parsed.Minute())*int64(time.Minute/time.Microsecond)
return pgtype.Time{Microseconds: micros, Valid: true}
}
func insertEventTestEvent(t *testing.T, ctx context.Context, q *sqlc.Queries, tabloID uuid.UUID, title, date, start, end, location, description string) sqlc.Event {
t.Helper()
var endTime pgtype.Time
if end != "" {
endTime = eventTestTime(t, end)
}
event, err := q.CreateEvent(ctx, sqlc.CreateEventParams{
TabloID: tabloID,
Title: title,
EventDate: eventTestDate(t, date),
StartTime: eventTestTime(t, start),
EndTime: endTime,
Description: pgtype.Text{String: description, Valid: description != ""},
Location: pgtype.Text{String: location, Valid: location != ""},
})
if err != nil {
t.Fatalf("CreateEvent: %v", err)
}
return event
}
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)
}
}
func TestEventEditRendersInlineForm(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, "eventedit@example.com", "correct-horse-12")
tablo := insertEventTestTablo(t, ctx, q, user, "Event Edit Tablo")
event := insertEventTestEvent(t, ctx, q, tablo.ID, "Planning Review", "2026-05-21", "09:30", "10:30", "Studio", "Discuss schedule")
sessionCookie := sessionCookieForUser(t, ctx, store, user)
req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events/"+event.ID.String()+"/edit?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 event edit status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
for _, want := range []string{"Save event changes", "Planning Review", "2026-05-21", "09:30", "Studio", "Discuss schedule"} {
if !strings.Contains(body, want) {
t.Errorf("edit form missing %q; body: %.800s", want, body)
}
}
}
func TestEventUpdateChangesCalendarPlacement(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, "eventupdate@example.com", "correct-horse-12")
tablo := insertEventTestTablo(t, ctx, q, user, "Event Update Tablo")
event := insertEventTestEvent(t, ctx, q, tablo.ID, "Original Review", "2026-05-21", "09:30", "", "Studio", "Discuss schedule")
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": {"Moved Review"},
"event_date": {"2026-05-24"},
"start_time": {"14:15"},
"end_time": {"15:00"},
"location": {"HQ"},
"description": {"Updated details"},
"_csrf": {csrfToken},
}
req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/events/"+event.ID.String()+"?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 event update status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
if !strings.Contains(body, "Moved Review") {
t.Fatalf("updated event title missing from refreshed calendar; body: %.800s", body)
}
if strings.Contains(body, "Original Review") {
t.Fatalf("old event title still present after update; body: %.800s", body)
}
}
func TestEventUpdateRejectsInvalidEndTime(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, "eventupdateinvalid@example.com", "correct-horse-12")
tablo := insertEventTestTablo(t, ctx, q, user, "Event Update Invalid Tablo")
event := insertEventTestEvent(t, ctx, q, tablo.ID, "Invalid Update", "2026-05-21", "09:30", "", "", "")
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 Update"},
"event_date": {"2026-05-21"},
"start_time": {"10:00"},
"end_time": {"10:00"},
"_csrf": {csrfToken},
}
req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/events/"+event.ID.String()+"?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 update 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 TestEventDeleteRemovesFromCalendarAndDatabase(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, "eventdelete@example.com", "correct-horse-12")
tablo := insertEventTestTablo(t, ctx, q, user, "Event Delete Tablo")
event := insertEventTestEvent(t, ctx, q, tablo.ID, "Delete Me", "2026-05-21", "09:30", "", "", "")
sessionCookie := sessionCookieForUser(t, ctx, store, user)
csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", []*http.Cookie{sessionCookie})
confirmReq := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events/"+event.ID.String()+"/delete-confirm?month=2026-05", nil)
confirmReq.Header.Set("HX-Request", "true")
confirmReq.AddCookie(sessionCookie)
confirmRec := httptest.NewRecorder()
router.ServeHTTP(confirmRec, confirmReq)
if confirmRec.Code != http.StatusOK {
t.Fatalf("GET delete confirm status = %d; want 200; body: %.500s", confirmRec.Code, confirmRec.Body.String())
}
if !strings.Contains(confirmRec.Body.String(), "Delete event?") {
t.Fatalf("delete confirmation copy missing; body: %.800s", confirmRec.Body.String())
}
form := url.Values{"_csrf": {csrfToken}}
req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/events/"+event.ID.String()+"/delete?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 event delete status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
}
if strings.Contains(rec.Body.String(), "Delete Me") {
t.Fatalf("deleted event title still present in refreshed calendar; body: %.800s", rec.Body.String())
}
_, err := q.GetEventByID(ctx, sqlc.GetEventByIDParams{ID: event.ID, TabloID: tablo.ID})
if !errors.Is(err, pgx.ErrNoRows) {
t.Fatalf("GetEventByID after delete err = %v; want pgx.ErrNoRows", err)
}
}
func TestEventMutationOwnershipReturns404(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, "eventmutationowner@example.com", "correct-horse-12")
nonOwner := preInsertUser(t, ctx, q, "eventmutationnonowner@example.com", "correct-horse-12")
tablo := insertEventTestTablo(t, ctx, q, owner, "Private Mutation Tablo")
event := insertEventTestEvent(t, ctx, q, tablo.ID, "Private Event", "2026-05-21", "09:30", "", "", "")
nonOwnerCookie := sessionCookieForUser(t, ctx, store, nonOwner)
csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/new", []*http.Cookie{nonOwnerCookie})
getPaths := []string{
"/tablos/" + tablo.ID.String() + "/events/" + event.ID.String() + "/edit?month=2026-05",
"/tablos/" + tablo.ID.String() + "/events/" + event.ID.String() + "/delete-confirm?month=2026-05",
}
for _, path := range getPaths {
req := httptest.NewRequest(http.MethodGet, path, nil)
req.AddCookie(nonOwnerCookie)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("non-owner GET %s status = %d; want 404", path, rec.Code)
}
}
postBodies := map[string]url.Values{
"/tablos/" + tablo.ID.String() + "/events/" + event.ID.String() + "?month=2026-05": {
"title": {"Private Event"},
"event_date": {"2026-05-21"},
"start_time": {"09:30"},
"_csrf": {csrfToken},
},
"/tablos/" + tablo.ID.String() + "/events/" + event.ID.String() + "/delete?month=2026-05": {
"_csrf": {csrfToken},
},
}
for path, form := range postBodies {
req := httptest.NewRequest(http.MethodPost, path, 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.StatusNotFound {
t.Fatalf("non-owner POST %s status = %d; want 404", path, rec.Code)
}
}
}
func TestEventsMonthNavigationPushesMonthParam(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, "eventnav@example.com", "correct-horse-12")
tablo := insertEventTestTablo(t, ctx, q, user, "Event Navigation 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 events month status = %d; want 200", rec.Code)
}
body := rec.Body.String()
for _, want := range []string{"month=2026-04", "month=2026-06", "Previous month: April 2026", "Next month: June 2026"} {
if !strings.Contains(body, want) {
t.Errorf("month navigation missing %q; body: %.800s", want, body)
}
}
}
func TestEventNewFromDayPrefillsDate(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, "eventprefill@example.com", "correct-horse-12")
tablo := insertEventTestTablo(t, ctx, q, user, "Event Prefill Tablo")
sessionCookie := sessionCookieForUser(t, ctx, store, user)
req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events/new?date=2026-05-20&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 event new status = %d; want 200", rec.Code)
}
if !strings.Contains(rec.Body.String(), `value="2026-05-20"`) {
t.Fatalf("date input was not prefilled from day query; body: %.800s", rec.Body.String())
}
}
func TestEventCreateSubmittedDateOverridesQueryDefault(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, "eventsubmitteddate@example.com", "correct-horse-12")
tablo := insertEventTestTablo(t, ctx, q, user, "Submitted Date 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": {"Submitted Date"},
"event_date": {"2026-05-21"},
"start_time": {"09:30"},
"_csrf": {csrfToken},
}
req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/events?date=2026-05-20&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 event create status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
}
events, err := q.ListEventsByTabloRange(ctx, sqlc.ListEventsByTabloRangeParams{
TabloID: tablo.ID,
EventDate: eventTestDate(t, "2026-05-01"),
EventDate_2: eventTestDate(t, "2026-05-31"),
})
if err != nil {
t.Fatalf("ListEventsByTabloRange: %v", err)
}
if len(events) != 1 {
t.Fatalf("events len = %d; want 1", len(events))
}
if got := templates.FormatEventDate(events[0].EventDate); got != "2026-05-21" {
t.Fatalf("stored event date = %s; want 2026-05-21", got)
}
}
func TestEventsCalendarShowsMoreIndicator(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, "eventoverflow@example.com", "correct-horse-12")
tablo := insertEventTestTablo(t, ctx, q, user, "Overflow Tablo")
sessionCookie := sessionCookieForUser(t, ctx, store, user)
for i, title := range []string{"Overflow 1", "Overflow 2", "Overflow 3", "Overflow 4"} {
insertEventTestEvent(t, ctx, q, tablo.ID, title, "2026-05-20", "09:0"+string(rune('0'+i)), "", "", "")
}
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 events overflow status = %d; want 200", rec.Code)
}
body := rec.Body.String()
for _, want := range []string{"Overflow 1", "Overflow 2", "Overflow 3", "+1 more"} {
if !strings.Contains(body, want) {
t.Errorf("overflow calendar missing %q; body: %.800s", want, body)
}
}
if strings.Contains(body, "Overflow 4") {
t.Fatalf("fourth event title should be hidden behind overflow indicator; body: %.800s", body)
}
}
func TestListUserEventsRangeReturnsOnlyOwnedTablos(t *testing.T) {
pool, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
q := sqlc.New(pool)
owner := preInsertUser(t, ctx, q, "eventrangeowner@example.com", "correct-horse-12")
other := preInsertUser(t, ctx, q, "eventrangeother@example.com", "correct-horse-12")
ownedTablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
UserID: owner.ID,
Title: "Owned Calendar",
Description: pgtype.Text{Valid: false},
Color: pgtype.Text{String: "#123abc", Valid: true},
})
if err != nil {
t.Fatalf("InsertTablo owned: %v", err)
}
foreignTablo := insertEventTestTablo(t, ctx, q, other, "Foreign Calendar")
insertEventTestEvent(t, ctx, q, ownedTablo.ID, "Owned Range Event", "2026-05-20", "09:30", "", "", "")
insertEventTestEvent(t, ctx, q, foreignTablo.ID, "Foreign Range Event", "2026-05-20", "09:30", "", "", "")
rows, err := q.ListUserEventsRange(ctx, sqlc.ListUserEventsRangeParams{
UserID: owner.ID,
StartDate: eventTestDate(t, "2026-05-01"),
EndDate: eventTestDate(t, "2026-05-31"),
})
if err != nil {
t.Fatalf("ListUserEventsRange: %v", err)
}
if len(rows) != 1 {
t.Fatalf("rows len = %d; want 1", len(rows))
}
row := rows[0]
if row.Title != "Owned Range Event" {
t.Fatalf("row title = %q; want Owned Range Event", row.Title)
}
if row.TabloTitle != "Owned Calendar" {
t.Fatalf("row tablo title = %q; want Owned Calendar", row.TabloTitle)
}
if !row.TabloColor.Valid || row.TabloColor.String != "#123abc" {
t.Fatalf("row tablo color = %#v; want #123abc", row.TabloColor)
}
}
func TestTabloDetailEventsTabFullPageFallback(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, "eventfullpage@example.com", "correct-horse-12")
tablo := insertEventTestTablo(t, ctx, q, user, "Events Full Page Tablo")
sessionCookie := sessionCookieForUser(t, ctx, store, user)
req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", nil)
req.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("GET full events page status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
for _, want := range []string{"Back to tablos", "Overview", "Tasks", "Files", "Events", "May 2026", `id="tab-content"`} {
if !strings.Contains(body, want) {
t.Errorf("full events page missing %q; body: %.800s", want, body)
}
}
}
func TestEventContentIsEscaped(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, "eventescaped@example.com", "correct-horse-12")
tablo := insertEventTestTablo(t, ctx, q, user, "Escaped Event Tablo")
insertEventTestEvent(t, ctx, q, tablo.ID, "<script>alert(1)</script>", "2026-05-20", "09:30", "", "", "")
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 escaped event status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
if strings.Contains(body, "<script>alert(1)</script>") {
t.Fatalf("event title rendered as raw script; body: %.800s", body)
}
if !strings.Contains(body, "&lt;script&gt;alert(1)&lt;/script&gt;") {
t.Fatalf("escaped event title missing; body: %.800s", body)
}
}
func TestEventWithoutEndTimeDisplaysOnlyStartTime(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, "eventnoend@example.com", "correct-horse-12")
tablo := insertEventTestTablo(t, ctx, q, user, "No End Event Tablo")
event := insertEventTestEvent(t, ctx, q, tablo.ID, "Open End", "2026-05-20", "09:30", "", "", "")
sessionCookie := sessionCookieForUser(t, ctx, store, user)
req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events/"+event.ID.String()+"/edit?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 event edit status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
if !strings.Contains(body, `name="start_time" value="09:30"`) {
t.Fatalf("start time missing for event without end time; body: %.800s", body)
}
if strings.Contains(body, "09:30-") || strings.Contains(body, "09:30 ") {
t.Fatalf("event without end time rendered an inferred range; body: %.800s", body)
}
}
func TestEventsOrderedByStartTimeThenTitle(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, "eventorder@example.com", "correct-horse-12")
tablo := insertEventTestTablo(t, ctx, q, user, "Ordered Events Tablo")
insertEventTestEvent(t, ctx, q, tablo.ID, "Beta", "2026-05-20", "10:00", "", "", "")
insertEventTestEvent(t, ctx, q, tablo.ID, "Gamma", "2026-05-20", "09:00", "", "", "")
insertEventTestEvent(t, ctx, q, tablo.ID, "Alpha", "2026-05-20", "09:00", "", "", "")
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 ordered events status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
alpha := strings.Index(body, "Alpha")
gamma := strings.Index(body, "Gamma")
beta := strings.Index(body, "Beta")
if alpha == -1 || gamma == -1 || beta == -1 {
t.Fatalf("ordered event titles missing; Alpha=%d Gamma=%d Beta=%d body: %.800s", alpha, gamma, beta, body)
}
if !(alpha < gamma && gamma < beta) {
t.Fatalf("events not ordered by start time then title; Alpha=%d Gamma=%d Beta=%d body: %.800s", alpha, gamma, beta, body)
}
}