package dyzurbot import ( "testing" "time" ) func warsaw(t *testing.T) *time.Location { t.Helper() loc, err := time.LoadLocation("Europe/Warsaw") if err != nil { t.Skipf("Europe/Warsaw tzdata unavailable: %v", err) } return loc } func TestNextFire(t *testing.T) { loc := warsaw(t) at := func(y int, mo time.Month, d, h, mi int) time.Time { return time.Date(y, mo, d, h, mi, 0, 0, loc) } tests := []struct { name string now time.Time weekday time.Weekday hour int want time.Time }{ { "same weekday before hour -> today", at(2026, 6, 15, 8, 0), time.Monday, 9, // Mon 08:00 at(2026, 6, 15, 9, 0), }, { "same weekday exactly at hour -> next week (strictly after)", at(2026, 6, 15, 9, 0), time.Monday, 9, at(2026, 6, 22, 9, 0), }, { "same weekday after hour -> next week", at(2026, 6, 15, 10, 0), time.Monday, 9, at(2026, 6, 22, 9, 0), }, { "later weekday -> upcoming target weekday", at(2026, 6, 17, 12, 0), time.Monday, 9, // Wed -> next Mon at(2026, 6, 22, 9, 0), }, { "day before -> next day", at(2026, 6, 21, 23, 0), time.Monday, 9, // Sun 23:00 -> Mon 09:00 at(2026, 6, 22, 9, 0), }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got := nextFire(tc.now, tc.weekday, tc.hour, loc) if !got.Equal(tc.want) { t.Errorf("nextFire = %s, want %s", got.In(loc), tc.want.In(loc)) } if !got.After(tc.now) { t.Errorf("nextFire %s not strictly after now %s", got.In(loc), tc.now.In(loc)) } }) } } // Across the spring-forward (2026-03-29 in Warsaw, clocks jump 02:00->03:00), // the next Monday 09:00 must still be 09:00 wall-clock in Warsaw, proving the // fire time is recomputed in the zone rather than by +7d UTC arithmetic. func TestNextFireAcrossDST(t *testing.T) { loc := warsaw(t) now := time.Date(2026, 3, 28, 12, 0, 0, 0, loc) // Sat before DST got := nextFire(now, time.Monday, 9, loc).In(loc) if got.Weekday() != time.Monday || got.Hour() != 9 || got.Minute() != 0 { t.Fatalf("got %s, want Monday 09:00 Warsaw", got) } if y, m, d := got.Date(); y != 2026 || m != time.March || d != 30 { t.Errorf("got date %04d-%02d-%02d, want 2026-03-30", y, m, d) } } func TestReminderText(t *testing.T) { // Week of 2026-06-29; target lands inside it. target := day(t, "2026-06-30") rota := func(p1, p2 string) Rota { return Rota{Weeks: []DutyWeek{week(t, "2026-06-29", p1, p2)}} } tests := []struct { name string rota Rota target time.Time wantOK bool want string }{ { "both people", rota("Ala", "Bartek"), target, true, "Przypomnienie: w tygodniu od 2026-06-29 dyżur sprzątają Ala i Bartek.", }, { "one person", rota("Ala", ""), target, true, "Przypomnienie: w tygodniu od 2026-06-29 dyżur sprząta Ala.", }, { "nobody assigned", rota("", ""), target, true, "Przypomnienie: w tygodniu od 2026-06-29 dyżur jest nieobsadzony.", }, { "no matching week -> skip", rota("Ala", "Bartek"), day(t, "2026-09-01"), false, "", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got, ok := reminderText(tc.rota, tc.target) if ok != tc.wantOK { t.Fatalf("ok = %v, want %v", ok, tc.wantOK) } if ok && got != tc.want { t.Errorf("reminderText = %q, want %q", got, tc.want) } }) } } func TestReminderTextMentions(t *testing.T) { target := day(t, "2026-06-30") people := []Person{ {Name: "Ala", Handle: "@ala"}, // stored with leading @ {Name: "Bartek", Handle: "bart"}, // stored without @ -> normalized {Name: "Cela", Handle: ""}, // present but no handle -> plain name } rota := func(p1, p2 string) Rota { return Rota{Weeks: []DutyWeek{week(t, "2026-06-29", p1, p2)}, People: people} } tests := []struct { name string rota Rota want string }{ { "both have handles", rota("Ala", "Bartek"), "Przypomnienie: w tygodniu od 2026-06-29 dyżur sprzątają @ala i @bart.", }, { "one handle, one missing -> mixed", rota("Ala", "Cela"), "Przypomnienie: w tygodniu od 2026-06-29 dyżur sprzątają @ala i Cela.", }, { "name not in people list -> plain name", rota("Ala", "Zenon"), "Przypomnienie: w tygodniu od 2026-06-29 dyżur sprzątają @ala i Zenon.", }, { "single person with handle", rota("Bartek", ""), "Przypomnienie: w tygodniu od 2026-06-29 dyżur sprząta @bart.", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got, ok := reminderText(tc.rota, target) if !ok { t.Fatalf("ok = false, want true") } if got != tc.want { t.Errorf("reminderText = %q, want %q", got, tc.want) } }) } } func TestParseWeekday(t *testing.T) { tests := []struct { in string want time.Weekday wantErr bool }{ {"Monday", time.Monday, false}, {"Sunday", time.Sunday, false}, {"monday", time.Monday, false}, // case-insensitive {"Funday", 0, true}, {"", 0, true}, } for _, tc := range tests { t.Run(tc.in, func(t *testing.T) { got, err := parseWeekday(tc.in) if (err != nil) != tc.wantErr { t.Fatalf("err = %v, wantErr %v", err, tc.wantErr) } if err == nil && got != tc.want { t.Errorf("parseWeekday(%q) = %v, want %v", tc.in, got, tc.want) } }) } }