Files
miti99bot/internal/modules/lolschedule/parse_date_test.go
T
tiennm99 be13a597c7 feat(modules): port lolschedule (5 commands; daily-push cron deferred)
Phase 6e (final sub-phase of port-plan Phase 06). LoL esports match
schedule via lolesports.com's persisted API.

- internal/modules/lolschedule:
  - api_client.go: HTTP client with cache-first lookup (120s fresh
    window, 60-min stale fallback). Cache record shape matches JS so
    cross-runtime KV migration round-trips.
  - parse_date.go: ICT-anchored date parser. Accepts dd-mm-yyyy,
    dd/mm/yyyy, ddmmyyyy; trailing month/year may be omitted (default
    to current ICT month/year). Rejects impossible dates (Apr 31, Feb
    29 in non-leap, etc.).
  - format.go: Today (grouped by league) and Week (grouped by league
    -> day) renderers. Major-league filter (LCK/LPL/LEC/LCS/Worlds/
    MSI/etc.) keeps replies under Telegram's 4096-char limit. All
    user-influenced strings HTML-escaped.
  - subscribers.go: Idempotent add/remove/list keyed by chat id.
  - handlers.go: 5 commands (`/lolschedule [date]`,
    `/lolschedule_today`, `/lolschedule_week`,
    `/lolschedule_subscribe`, `/lolschedule_unsubscribe`).
- 22 tests across api-client (cache hit / miss / stale fallback /
  hard fail / show filter / non-JSON), parse-date (full and
  short formats, defaults, rejections, ICT anchor), format (event
  line states, league ordering, week grouping, HTML escape, major
  filter), subscribers (idempotent add/remove), handlers (HTML
  reply, error path, subscribe/unsubscribe round-trip).

Daily-push cron deferred to Phase 09 (Cloud Scheduler). Subscribers
are still collected so the push lights up the moment the cron infra
lands. Deps doesn't currently expose a *bot.Bot reference; that is
the prerequisite that Phase 09 will solve.

go test -race -count=1 ./... clean (19 packages); golangci-lint clean.
2026-05-09 17:14:01 +07:00

117 lines
3.2 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 lolschedule
import (
"strings"
"testing"
"time"
)
// fixed reference now: 2026-05-09 12:00 UTC = 19:00 ICT (still May 9 ICT).
var refNow = time.Date(2026, 5, 9, 12, 0, 0, 0, time.UTC)
func TestParseScheduleDate_EmptyReturnsToday(t *testing.T) {
got := ParseScheduleDate("", refNow)
if !got.OK {
t.Fatalf("empty should be OK; err=%q", got.Error)
}
want := time.Date(2026, 5, 9, 0, 0, 0, 0, IctLocation).UTC()
if !got.Date.Equal(want) {
t.Errorf("empty → %v, want %v", got.Date, want)
}
}
func TestParseScheduleDate_FullFormats(t *testing.T) {
cases := []string{"15-06-2026", "15/06/2026", "15062026"}
want := time.Date(2026, 6, 15, 0, 0, 0, 0, IctLocation).UTC()
for _, in := range cases {
got := ParseScheduleDate(in, refNow)
if !got.OK {
t.Errorf("%q: not OK: %s", in, got.Error)
continue
}
if !got.Date.Equal(want) {
t.Errorf("%q → %v, want %v", in, got.Date, want)
}
}
}
func TestParseScheduleDate_DayOnly_DefaultsMonthYear(t *testing.T) {
got := ParseScheduleDate("15", refNow)
if !got.OK {
t.Fatalf("not OK: %s", got.Error)
}
want := time.Date(2026, 5, 15, 0, 0, 0, 0, IctLocation).UTC()
if !got.Date.Equal(want) {
t.Errorf("15 → %v, want %v", got.Date, want)
}
}
func TestParseScheduleDate_DayMonth_DefaultsYear(t *testing.T) {
got := ParseScheduleDate("15-06", refNow)
if !got.OK {
t.Fatalf("not OK: %s", got.Error)
}
want := time.Date(2026, 6, 15, 0, 0, 0, 0, IctLocation).UTC()
if !got.Date.Equal(want) {
t.Errorf("15-06 → %v, want %v", got.Date, want)
}
// 4-digit unbroken form: ddmm
got2 := ParseScheduleDate("1506", refNow)
if !got2.OK || !got2.Date.Equal(want) {
t.Errorf("1506 → %v ok=%v, want %v", got2.Date, got2.OK, want)
}
}
func TestParseScheduleDate_RejectsImpossibleDate(t *testing.T) {
got := ParseScheduleDate("31-04-2026", refNow) // April has only 30 days
if got.OK {
t.Errorf("31-04-2026 should be rejected; got %v", got.Date)
}
if !strings.Contains(got.Error, "does not exist") {
t.Errorf("error should mention non-existent date: %q", got.Error)
}
}
func TestParseScheduleDate_RejectsInvalidValues(t *testing.T) {
tests := []struct {
in string
hint string
}{
{"abc", "dd-mm-yyyy"},
{"32-01-2026", "must be 131"},
{"15-13-2026", "must be 112"},
{"15-06-1900", "Invalid year"},
{"-15", "Invalid date"}, // empty leading part
{"15--06", "Invalid date"},
}
for _, tt := range tests {
got := ParseScheduleDate(tt.in, refNow)
if got.OK {
t.Errorf("%q should be rejected", tt.in)
continue
}
if !strings.Contains(got.Error, tt.hint) {
t.Errorf("%q error %q missing hint %q", tt.in, got.Error, tt.hint)
}
}
}
func TestIctDayStartOf(t *testing.T) {
// 2026-05-09 19:00 ICT == 12:00 UTC. Start of ICT day = 2026-05-09 00:00 ICT
// = 2026-05-08 17:00 UTC.
got := ictDayStartOf(refNow)
want := time.Date(2026, 5, 8, 17, 0, 0, 0, time.UTC)
if !got.Equal(want) {
t.Errorf("ictDayStartOf = %v, want %v", got, want)
}
}
func TestAddDays(t *testing.T) {
base := time.Date(2026, 5, 9, 12, 30, 0, 0, time.UTC)
got := addDays(base, 3)
want := time.Date(2026, 5, 12, 12, 30, 0, 0, time.UTC)
if !got.Equal(want) {
t.Errorf("addDays(3) = %v, want %v", got, want)
}
}