package utils import ( "testing" "time" ) // The customer contract asks for epoch milliseconds, UTC, integer. This // database stores IST wall-clock digits (see DBNow), so the conversion is the // one place a 5h30m error can enter every timestamp the app renders — which is // exactly the defect class §3.3 of the contract warns about after it produced // "yesterday's work shown as today" on the miler app. func TestEpochMillisTreatsStoredWallClockAsIST(t *testing.T) { // A row written as 2026-09-05 14:30:00 by this database means 2:30pm in // Chennai, whatever zone the time.Time happens to carry. stored := time.Date(2026, 9, 5, 14, 30, 0, 0, time.UTC) got := EpochMillis(stored) want := time.Date(2026, 9, 5, 9, 0, 0, 0, time.UTC).UnixMilli() // 14:30 IST == 09:00 UTC if got != want { t.Errorf("EpochMillis(2026-09-05 14:30 stored) = %d, want %d (a %d ms error)", got, want, got-want) } } func TestEpochMillisAgreesForBothTaggings(t *testing.T) { // The driver can hand back the same instant either tagged UTC with IST // digits, or correctly tagged +05:30. Both must name one moment, or a // column type change silently shifts every timestamp on the tracking screen. ist := ISTLocation() taggedUTC := time.Date(2026, 9, 5, 14, 30, 0, 0, time.UTC) taggedIST := time.Date(2026, 9, 5, 14, 30, 0, 0, ist) if EpochMillis(taggedUTC) != EpochMillis(taggedIST) { t.Errorf("EpochMillis disagrees on tagging: utc-tagged=%d ist-tagged=%d", EpochMillis(taggedUTC), EpochMillis(taggedIST)) } } func TestEpochMillisPtrKeepsNullNull(t *testing.T) { // A nil timestamp must stay null in JSON. Coercing it to 0 renders as // 1 Jan 1970 on the timeline, which reads as a real event. if got := EpochMillisPtr(nil); got != nil { t.Errorf("EpochMillisPtr(nil) = %v, want nil", got) } var zero time.Time if got := EpochMillisPtr(&zero); got != nil { t.Errorf("EpochMillisPtr(zero time) = %v, want nil", got) } stored := time.Date(2026, 9, 5, 14, 30, 0, 0, time.UTC) got := EpochMillisPtr(&stored) if got == nil || *got != EpochMillis(stored) { t.Errorf("EpochMillisPtr(%v) = %v, want %d", stored, got, EpochMillis(stored)) } } func TestFormatISTWindowMatchesTheDesign(t *testing.T) { // "2:00 – 4:00 PM" — en dash, spaced, meridiem stated once when both ends // share it. The client renders this string verbatim. cases := []struct { name string fromH, fromM int toH, toM int want string }{ {"afternoon window", 14, 0, 16, 0, "2:00 – 4:00 PM"}, {"morning window", 8, 0, 10, 0, "8:00 – 10:00 AM"}, {"straddles noon", 10, 0, 14, 0, "10:00 AM – 2:00 PM"}, {"half hour bounds", 18, 30, 20, 30, "6:30 – 8:30 PM"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { from := time.Date(2026, 9, 5, tc.fromH, tc.fromM, 0, 0, time.UTC) to := time.Date(2026, 9, 5, tc.toH, tc.toM, 0, 0, time.UTC) if got := FormatISTWindow(from, to); got != tc.want { t.Errorf("FormatISTWindow = %q, want %q", got, tc.want) } }) } } func TestFormatISTDayUsesRelativeWordsOnlyWhenTrue(t *testing.T) { now := ISTNow() if got := FormatISTDay(now); got != "Today" { t.Errorf("FormatISTDay(now) = %q, want \"Today\"", got) } if got := FormatISTDay(now.AddDate(0, 0, 1)); got != "Tomorrow" { t.Errorf("FormatISTDay(tomorrow) = %q, want \"Tomorrow\"", got) } // Three days out has no relative word — it falls back to the dated form, // which must not read as "Today". got := FormatISTDay(now.AddDate(0, 0, 3)) if got == "Today" || got == "Tomorrow" { t.Errorf("FormatISTDay(+3 days) = %q, want a dated label", got) } } // FormatISTDay crossing a month boundary is the case a naive day+1 comparison // gets wrong: 30 September plus one day is 1 October, not 31 September. func TestFormatISTDayAcrossMonthEnd(t *testing.T) { now := ISTNow() tomorrow := now.AddDate(0, 0, 1) if now.Month() == tomorrow.Month() { t.Skip("not a month boundary today; the AddDate path is exercised by the general case") } if got := FormatISTDay(tomorrow); got != "Tomorrow" { t.Errorf("FormatISTDay across a month end = %q, want \"Tomorrow\"", got) } }