package clock import ( "testing" "time" ) func TestNow_ReturnsUTC(t *testing.T) { t.Parallel() now := Now() if now.Location() != time.UTC { t.Errorf("clock.Now() returned time in %v, expected UTC", now.Location()) } // UTC is a fixed-offset zone: the zone abbreviation and offset must both be // the canonical UTC values so SQL TIMESTAMPTZ comparisons and duration math // never drift. name, off := now.Zone() if name != "UTC" || off != 0 { t.Errorf("clock.Now() returned zone %q offset %d, expected UTC offset 0", name, off) } } // TestLondon_Location_IsEuropeLondon proves the single London location used for // wall-clock business decisions is the IANA Europe/London zone (which carries // the full DST rule table for GMT<->BST). func TestLondon_Location_IsEuropeLondon(t *testing.T) { t.Parallel() if got := London.String(); got != "Europe/London" { t.Errorf("clock.London.String() = %q, expected %q", got, "Europe/London") } } // TestLondon_DSTTransitions pins the two 2026 Europe/London transitions: // // Spring forward (2026-03-29 01:00 GMT -> 02:00 BST): // - 2026-03-29 00:30 UTC = 01:30 GMT, offset +0 (still winter time) // - 2026-03-29 01:00 UTC = 02:00 BST, offset +1 (the transition instant) // Fall back (2026-10-25 02:00 BST -> 01:00 GMT): // - 2026-10-25 00:30 UTC = 01:30 BST, offset +1 (still summer time) // - 2026-10-25 01:00 UTC = 01:00 GMT, offset +0 (the ambiguous hour, which // Go resolves to the SECOND occurrence) // // These instants are the boundary of every London wall-clock decision the app // makes (closing hours, default-hours effective dates, today summaries), so // clock.London must get them exactly right. func TestLondon_DSTTransitions(t *testing.T) { t.Parallel() cases := []struct { name string utc time.Time wantWall string // London wall-clock, "2006-01-02 15:04" wantZone string // "GMT" or "BST" wantOffsetH int }{ {"spring before", time.Date(2026, 3, 29, 0, 30, 0, 0, time.UTC), "2026-03-29 00:30", "GMT", 0}, {"spring transition", time.Date(2026, 3, 29, 1, 0, 0, 0, time.UTC), "2026-03-29 02:00", "BST", 1}, {"autumn before", time.Date(2026, 10, 25, 0, 30, 0, 0, time.UTC), "2026-10-25 01:30", "BST", 1}, {"autumn ambiguous->GMT", time.Date(2026, 10, 25, 1, 0, 0, 0, time.UTC), "2026-10-25 01:00", "GMT", 0}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { l := tc.utc.In(London) if got := l.Format("2006-01-02 15:04"); got != tc.wantWall { t.Errorf("%s UTC in London = %q, expected wall-clock %q", tc.utc.Format(time.RFC3339), got, tc.wantWall) } zone, off := l.Zone() if zone != tc.wantZone { t.Errorf("%s UTC in London zone = %q, expected %q", tc.utc.Format(time.RFC3339), zone, tc.wantZone) } if off != tc.wantOffsetH*3600 { t.Errorf("%s UTC in London offset = %ds, expected %dh", tc.utc.Format(time.RFC3339), off, tc.wantOffsetH) } }) } // Sanity: the two wall-clock times on the autumn transition day share the // same 01:xx wall hour (the ambiguous hour) but at different UTC instants — // proving Go resolves the fold to the second occurrence when converting the // UTC instant back to London. fold1 := time.Date(2026, 10, 25, 0, 30, 0, 0, time.UTC).In(London).Format("15:04") // 01:30 BST fold2 := time.Date(2026, 10, 25, 1, 30, 0, 0, time.UTC).In(London).Format("15:04") // 01:30 GMT if fold1 != "01:30" || fold2 != "01:30" { t.Errorf("expected both sides of the autumn fold to show 01:30, got %q and %q", fold1, fold2) } } func TestNow_IsReasonable(t *testing.T) { t.Parallel() now := Now() // Check it's within the last minute (reasonable) since := time.Since(now) if since < 0 || since > time.Minute { t.Errorf("clock.Now() returned unreasonable time: %v ago", since) } }