117 lines
4.1 KiB
Go
117 lines
4.1 KiB
Go
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)
|
||
}
|
||
}
|