Files
memby/server/internal/api/calendar_test.go

150 lines
5.3 KiB
Go

package api
import (
"encoding/json"
"testing"
"time"
"github.com/ponzischeme89/memby/server/internal/sonarr"
)
func calendarEpisode(id int, air time.Time, title string) sonarr.Episode {
utc := air.UTC()
return sonarr.Episode{
ID: id,
SeriesID: id,
SeasonNumber: 1,
EpisodeNumber: id,
Title: title,
AirDateUTC: &utc,
Monitored: true,
Series: sonarr.Series{ID: id, Title: title},
}
}
func TestBuildCalendarMonthDescribesTheGrid(t *testing.T) {
location := time.UTC
// August 2026 begins on a Saturday and runs 31 days.
month := time.Date(2026, 8, 1, 0, 0, 0, 0, location)
now := time.Date(2026, 8, 11, 9, 0, 0, 0, location)
calendar := buildCalendarMonth(nil, month, now, location, nil)
if !calendar.Available {
t.Fatal("a configured Sonarr must report the month as available")
}
if calendar.Month != "2026-08" || calendar.Label != "August 2026" {
t.Fatalf("unexpected identity: %+v", calendar)
}
if calendar.FirstWeekday != int(time.Saturday) || calendar.DayCount != 31 {
t.Fatalf("unexpected grid shape: %+v", calendar)
}
if calendar.Previous != "2026-07" || calendar.Next != "2026-09" {
t.Fatalf("unexpected neighbours: %+v", calendar)
}
if calendar.Today != "2026-08-11" {
t.Fatalf("today was not marked: %+v", calendar)
}
if calendar.Days == nil {
t.Fatal("days must encode as an array rather than null")
}
}
func TestBuildCalendarMonthCountsFebruaryInALeapYear(t *testing.T) {
location := time.UTC
now := time.Date(2028, 2, 1, 0, 0, 0, 0, location)
calendar := buildCalendarMonth(nil, now, now, location, nil)
if calendar.DayCount != 29 {
t.Fatalf("expected 29 days, got %d", calendar.DayCount)
}
}
func TestBuildCalendarMonthOmitsTodayOutsideTheMonth(t *testing.T) {
location := time.UTC
month := time.Date(2026, 10, 1, 0, 0, 0, 0, location)
now := time.Date(2026, 8, 11, 9, 0, 0, 0, location)
if today := buildCalendarMonth(nil, month, now, location, nil).Today; today != "" {
t.Fatalf("a month the household is not in must carry no today: %q", today)
}
}
// The arrows are what stop a held D-pad walking Sonarr into the next decade, so the edge of
// the range must be reported as having no neighbour rather than as one more request.
func TestBuildCalendarMonthStopsAtTheTravelLimit(t *testing.T) {
location := time.UTC
now := time.Date(2026, 8, 11, 9, 0, 0, 0, location)
last := time.Date(2027, 8, 1, 0, 0, 0, 0, location)
if next := buildCalendarMonth(nil, last, now, location, nil).Next; next != "" {
t.Fatalf("expected no next month at the limit, got %q", next)
}
first := time.Date(2025, 8, 1, 0, 0, 0, 0, location)
if previous := buildCalendarMonth(nil, first, now, location, nil).Previous; previous != "" {
t.Fatalf("expected no previous month at the limit, got %q", previous)
}
}
func TestBuildCalendarMonthGroupsEpisodesByLocalDay(t *testing.T) {
location := time.FixedZone("NZST", 12*60*60)
month := time.Date(2026, 8, 1, 0, 0, 0, 0, location)
now := time.Date(2026, 8, 11, 9, 0, 0, 0, location)
// Two on the 12th local, one on the 13th, and one that is the 31st in UTC but already
// September where the household lives.
episodes := []sonarr.Episode{
calendarEpisode(2, time.Date(2026, 8, 12, 21, 0, 0, 0, location), "Harbour"),
calendarEpisode(1, time.Date(2026, 8, 12, 20, 0, 0, 0, location), "Northbound"),
calendarEpisode(3, time.Date(2026, 8, 13, 20, 0, 0, 0, location), "Deep Water"),
calendarEpisode(4, time.Date(2026, 8, 31, 20, 0, 0, 0, time.UTC), "Rolled Over"),
}
calendar := buildCalendarMonth(episodes, month, now, location, nil)
if len(calendar.Days) != 2 {
t.Fatalf("expected two populated days, got %d: %+v", len(calendar.Days), calendar.Days)
}
if calendar.Days[0].Date != "2026-08-12" || calendar.Days[0].Day != 12 {
t.Fatalf("unexpected first day: %+v", calendar.Days[0])
}
if len(calendar.Days[0].Items) != 2 {
t.Fatalf("expected two episodes on the 12th: %+v", calendar.Days[0])
}
// Within a day the order is Sonarr's air time, which is the order they will be watched.
var first sonarrScheduleItem
if err := json.Unmarshal(calendar.Days[0].Items[0], &first); err != nil {
t.Fatal(err)
}
if first.Name != "Northbound" {
t.Fatalf("episodes within a day must be ordered by air time: %+v", first)
}
if first.Type != "MembySonarrEpisode" || first.MembyPlayable {
t.Fatalf("a calendar card must be the schedule row's informational item: %+v", first)
}
if calendar.Days[1].Date != "2026-08-13" {
t.Fatalf("unexpected second day: %+v", calendar.Days[1])
}
}
func TestParseCalendarMonthDefaultsToThePresent(t *testing.T) {
location := time.UTC
now := time.Date(2026, 8, 11, 9, 0, 0, 0, location)
month, ok := parseCalendarMonth(" ", now, location)
if !ok || month.Format("2006-01") != "2026-08" {
t.Fatalf("unexpected default month: %v %v", month, ok)
}
}
func TestParseCalendarMonthRefusesNonsenseAndDistantMonths(t *testing.T) {
location := time.UTC
now := time.Date(2026, 8, 11, 9, 0, 0, 0, location)
for _, value := range []string{"2026-13", "August", "2026-08-11", "2031-01", "2020-01"} {
if _, ok := parseCalendarMonth(value, now, location); ok {
t.Fatalf("expected %q to be refused", value)
}
}
for _, value := range []string{"2026-09", "2027-08", "2025-08"} {
if _, ok := parseCalendarMonth(value, now, location); !ok {
t.Fatalf("expected %q to be accepted", value)
}
}
}