package handlers import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/Silo-Server/silo-server/internal/catalog" ) type stubCalendarRepo struct { calls int last catalog.CalendarFilter events []catalog.CalendarEvent err error } func (r *stubCalendarRepo) ListEvents(_ context.Context, f catalog.CalendarFilter) ([]catalog.CalendarEvent, error) { r.calls++ r.last = f return r.events, r.err } type stubCalendarImageResolver struct { resolveURLCalls int resolveURLsCalls int lastPaths []string lastVariant string urls map[string]string } func (r *stubCalendarImageResolver) ResolveImageURL(_ context.Context, path string, _ string) string { r.resolveURLCalls++ return r.urls[path] } func (r *stubCalendarImageResolver) ResolveImageURLs(_ context.Context, paths []string, variant string) map[string]string { r.resolveURLsCalls++ r.lastPaths = append([]string(nil), paths...) r.lastVariant = variant result := make(map[string]string, len(paths)) for _, path := range paths { if url, ok := r.urls[path]; ok { result[path] = url } } return result } func TestHandleGetCalendar_RejectsEndBeforeStart(t *testing.T) { t.Parallel() repo := &stubCalendarRepo{} handler := &CalendarHandler{repo: repo} req := httptest.NewRequest(http.MethodGet, "/calendar?start=2026-04-12&end=2026-04-06", nil) rec := httptest.NewRecorder() handler.HandleGetCalendar(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest) } if repo.calls != 0 { t.Fatalf("expected repo not to be called, got %d calls", repo.calls) } } func TestHandleGetCalendar_RejectsRangesLongerThan31Days(t *testing.T) { t.Parallel() repo := &stubCalendarRepo{} handler := &CalendarHandler{repo: repo} req := httptest.NewRequest(http.MethodGet, "/calendar?start=2026-04-01&end=2026-05-02", nil) rec := httptest.NewRecorder() handler.HandleGetCalendar(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest) } if repo.calls != 0 { t.Fatalf("expected repo not to be called, got %d calls", repo.calls) } } func TestHandleGetCalendar_ReturnsEmptyEvents(t *testing.T) { t.Parallel() repo := &stubCalendarRepo{} handler := &CalendarHandler{repo: repo} req := httptest.NewRequest(http.MethodGet, "/calendar?start=2026-04-06&end=2026-04-12", nil) rec := httptest.NewRecorder() handler.HandleGetCalendar(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } var resp calendarResponse if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode response: %v", err) } if len(resp.Events) != 0 { t.Fatalf("events len = %d, want 0", len(resp.Events)) } } func TestHandleGetCalendar_GroupsEventsAndBatchResolvesCardPosters(t *testing.T) { t.Parallel() repo := &stubCalendarRepo{ events: []catalog.CalendarEvent{ { ContentID: "episode-1", Type: "episode", Title: "Series", SeriesID: ptrString("series-1"), SeasonNumber: ptrInt(1), EpisodeNumber: ptrInt(1), AirDate: time.Date(2026, time.April, 6, 0, 0, 0, 0, time.UTC), PosterPath: "images/poster/original.jpg", PosterThumbhash: "thumb-a", IsPremiere: true, }, { ContentID: "episode-2", Type: "episode", Title: "Series", SeriesID: ptrString("series-1"), SeasonNumber: ptrInt(1), EpisodeNumber: ptrInt(2), AirDate: time.Date(2026, time.April, 6, 0, 0, 0, 0, time.UTC), PosterPath: "images/poster/original.jpg", PosterThumbhash: "thumb-a", IsFinale: true, }, }, } resolver := &stubCalendarImageResolver{ urls: map[string]string{ "images/poster/w300.jpg": "https://cdn.example/poster-card.jpg", }, } detailSvc := &catalog.DetailService{} detailSvc.SetImageResolver(resolver) handler := &CalendarHandler{repo: repo, detailSvc: detailSvc} req := httptest.NewRequest(http.MethodGet, "/calendar?start=2026-04-06&end=2026-04-12", nil) rec := httptest.NewRecorder() handler.HandleGetCalendar(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } if resolver.resolveURLsCalls != 1 { t.Fatalf("ResolveImageURLs calls = %d, want 1", resolver.resolveURLsCalls) } if resolver.resolveURLCalls != 0 { t.Fatalf("ResolveImageURL calls = %d, want 0", resolver.resolveURLCalls) } if len(resolver.lastPaths) != 1 || resolver.lastPaths[0] != "images/poster/w300.jpg" { t.Fatalf("lastPaths = %#v, want [images/poster/w300.jpg]", resolver.lastPaths) } if resolver.lastVariant != "card" { t.Fatalf("variant = %q, want card", resolver.lastVariant) } var resp calendarResponse if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode response: %v", err) } if len(resp.Events) != 1 { t.Fatalf("days len = %d, want 1", len(resp.Events)) } if got := len(resp.Events[0].Items); got != 2 { t.Fatalf("items len = %d, want 2", got) } for i, item := range resp.Events[0].Items { if item.PosterURL != "https://cdn.example/poster-card.jpg" { t.Fatalf("item %d poster_url = %q, want resolved card URL", i, item.PosterURL) } } } func ptrString(value string) *string { return &value } func ptrInt(value int) *int { return &value }