oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1package calendartest
2
3import (
4 "testing"
5 "time"
6
7 calendar "amadan.net/oficina/calendar/internal/calendar"
8)
9
10// The two hours a year that break calendars, pinned against
11// Europe/Dublin 2026 — DST starts 29 March and ends 25 October, both at
12// 01:00 UTC.
13//
14// There is no correct answer here and Go does not promise one, so this
15// test does not assert correctness. It asserts THE ANSWER, so that a Go
16// upgrade or a tzdata bump that changes it fails loudly here rather
17// than moving somebody's meeting quietly.
18//
19// The two cases resolve differently and that is the point of testing
20// both: the gap takes the pre-transition offset, the fold takes the
21// post-transition one.
22func TestDSTGapAndFold(t *testing.T) {
23 const dublin = "Europe/Dublin"
24
25 t.Run("the gap moves forward", func(t *testing.T) {
26 // Local 01:00 jumps to 02:00, so 01:30 never happens.
27 got, err := calendar.ToUTC("2026-03-29T01:30", dublin, false)
28 if err != nil {
29 t.Fatalf("ToUTC: %v", err)
30 }
31 if want := int64(1774747800); got != want {
32 t.Errorf("gap 01:30: unix = %d, want %d", got, want)
33 }
34 // Reading it back must land AFTER the written time, never
35 // before: an event in the gap that moved backwards would
36 // happen before the person expected it.
37 back, err := calendar.InZone(got, dublin)
38 if err != nil {
39 t.Fatalf("InZone: %v", err)
40 }
41 if h, m := back.Hour(), back.Minute(); h != 2 || m != 30 {
42 t.Errorf("gap 01:30 reads back as %02d:%02d, want 02:30", h, m)
43 }
44 })
45
46 t.Run("the fold takes the second occurrence", func(t *testing.T) {
47 // Local 02:00 falls back to 01:00, so 01:30 happens twice:
48 // once at 00:30 UTC (offset +1) and once at 01:30 UTC
49 // (offset 0). Go returns the second.
50 got, err := calendar.ToUTC("2026-10-25T01:30", dublin, false)
51 if err != nil {
52 t.Fatalf("ToUTC: %v", err)
53 }
54 if want := int64(1792891800); got != want {
55 t.Errorf("fold 01:30: unix = %d, want %d (the second occurrence)", got, want)
56 }
57 })
58}
59
60// A day is not always 24 hours long. On the two DST nights it is 23 or
61// 25, and a day view that added a fixed duration would run an hour into
62// the next day or stop an hour short of the end of this one.
63func TestDayBoundsFollowDSTRatherThanTwentyFourHours(t *testing.T) {
64 const dublin = "Europe/Dublin"
65 for _, tc := range []struct {
66 name string
67 day time.Time
68 hours float64
69 }{
70 {"an ordinary day is 24 hours", time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC), 24},
71 {"the spring-forward day is 23", time.Date(2026, 3, 29, 12, 0, 0, 0, time.UTC), 23},
72 {"the fall-back day is 25", time.Date(2026, 10, 25, 12, 0, 0, 0, time.UTC), 25},
73 } {
74 t.Run(tc.name, func(t *testing.T) {
75 start, end, err := calendar.DayBounds(tc.day, dublin)
76 if err != nil {
77 t.Fatalf("DayBounds: %v", err)
78 }
79 if got := float64(end-start) / 3600; got != tc.hours {
80 t.Errorf("day length = %v hours, want %v", got, tc.hours)
81 }
82 })
83 }
84}
85
86// An all-day event is a date, not an instant. Storing it as a timed
87// event at midnight UTC is the bug that shows a birthday on the wrong
88// day for everybody west of Greenwich.
89func TestAllDayIsMidnightInItsOwnZone(t *testing.T) {
90 dublin, err := calendar.ToUTC("2026-09-02", "Europe/Dublin", true)
91 if err != nil {
92 t.Fatalf("ToUTC Dublin: %v", err)
93 }
94 tokyo, err := calendar.ToUTC("2026-09-02", "Asia/Tokyo", true)
95 if err != nil {
96 t.Fatalf("ToUTC Tokyo: %v", err)
97 }
98 if dublin == tokyo {
99 t.Error("the same date in two zones produced the same instant; all-day is being stored as UTC midnight")
100 }
101 // Tokyo is ahead, so its midnight comes first.
102 if tokyo >= dublin {
103 t.Errorf("Tokyo midnight (%d) should precede Dublin midnight (%d)", tokyo, dublin)
104 }
105}
106
107// An empty zone is UTC, never the server's local zone — which is an
108// accident of provisioning and would shift every unzoned event's day
109// boundaries the first time the fleet moved region.
110func TestEmptyZoneIsUTCNotTheServers(t *testing.T) {
111 empty, err := calendar.ToUTC("2026-09-02T09:00", "", false)
112 if err != nil {
113 t.Fatalf("ToUTC: %v", err)
114 }
115 utc, err := calendar.ToUTC("2026-09-02T09:00", "UTC", false)
116 if err != nil {
117 t.Fatalf("ToUTC: %v", err)
118 }
119 if empty != utc {
120 t.Errorf("empty zone = %d, UTC = %d; they must agree", empty, utc)
121 }
122}
123
124func TestUnknownZoneIsRefused(t *testing.T) {
125 if _, err := calendar.ToUTC("2026-09-02T09:00", "Mars/Olympus_Mons", false); err == nil {
126 t.Error("an unknown zone was accepted")
127 }
128}
129