| 1 | package calls |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | "time" |
| 7 | ) |
| 8 | |
| 9 | // TestCallTitle is the review follow-up for "Start a call": |
| 10 | // callstest's black-box test only ever checked the title for a |
| 11 | // "Call, " prefix, so this pins the exact format over a fixed time |
| 12 | // and a few zones, including one that rolls the date over. |
| 13 | func TestCallTitle(t *testing.T) { |
| 14 | dublin, err := time.LoadLocation("Europe/Dublin") |
| 15 | if err != nil { |
| 16 | t.Fatal(err) |
| 17 | } |
| 18 | tokyo, err := time.LoadLocation("Asia/Tokyo") |
| 19 | if err != nil { |
| 20 | t.Fatal(err) |
| 21 | } |
| 22 | cases := []struct { |
| 23 | name string |
| 24 | now time.Time |
| 25 | zone *time.Location |
| 26 | want string |
| 27 | wantZone string |
| 28 | }{ |
| 29 | { |
| 30 | // Not "Call, 30 Sep 20:47 UTC" (f86d4da until 2026-10-02): |
| 31 | // the zone is data, not words. |
| 32 | name: "no browser zone: the instance zone, kept but not in the title", |
| 33 | now: time.Date(2026, 9, 30, 20, 47, 0, 0, time.UTC), |
| 34 | zone: nil, |
| 35 | want: "Call, 30 Sep 20:47", |
| 36 | wantZone: "UTC", |
| 37 | }, |
| 38 | { |
| 39 | name: "UTC", |
| 40 | now: time.Date(2026, 9, 10, 9, 5, 0, 0, time.UTC), |
| 41 | zone: time.UTC, |
| 42 | want: "Call, 10 Sep 09:05", |
| 43 | }, |
| 44 | { |
| 45 | name: "single-digit day is not zero-padded", |
| 46 | now: time.Date(2026, 9, 3, 9, 5, 0, 0, time.UTC), |
| 47 | zone: time.UTC, |
| 48 | want: "Call, 3 Sep 09:05", |
| 49 | }, |
| 50 | { |
| 51 | name: "a zone ahead of UTC, same day", |
| 52 | now: time.Date(2026, 9, 10, 9, 15, 0, 0, time.UTC), |
| 53 | zone: dublin, // Irish summer time is UTC+1 in September |
| 54 | want: "Call, 10 Sep 10:15", |
| 55 | wantZone: "Europe/Dublin", |
| 56 | }, |
| 57 | { |
| 58 | name: "a zone far enough ahead to roll the date over", |
| 59 | now: time.Date(2026, 9, 10, 23, 30, 0, 0, time.UTC), |
| 60 | zone: tokyo, // UTC+9, no DST |
| 61 | want: "Call, 11 Sep 08:30", |
| 62 | }, |
| 63 | } |
| 64 | for _, c := range cases { |
| 65 | t.Run(c.name, func(t *testing.T) { |
| 66 | got := callTitle(c.now, c.zone, time.UTC) |
| 67 | if got.Text != c.want { |
| 68 | t.Errorf("callTitle(%s, %s) = %q, want %q", c.now, c.zone, got.Text, c.want) |
| 69 | } |
| 70 | if c.wantZone != "" && got.Zone != c.wantZone { |
| 71 | t.Errorf("callTitle(%s, %s) zone = %q, want %q", c.now, c.zone, got.Zone, c.wantZone) |
| 72 | } |
| 73 | }) |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | // TestCallZone: the browser's zone is used only when it is a real IANA |
| 78 | // zone; anything else falls back to the instance zone (and its name). |
| 79 | func TestCallZone(t *testing.T) { |
| 80 | if callZone("Europe/Dublin") == nil { |
| 81 | t.Error("a real zone was refused") |
| 82 | } |
| 83 | for _, bad := range []string{"", "Local", "Mars/Olympus", "../../etc/passwd", "Europe/Dublin\n"} { |
| 84 | if callZone(bad) != nil { |
| 85 | t.Errorf("callZone(%q) accepted", bad) |
| 86 | } |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | // TestCallTitleNeverNamesItsZone: whatever zone the title is written in, |
| 91 | // the title is the day and time and nothing after them — the reported |
| 92 | // "Call, 2 Oct 15:40 Europe/Dublin" — and the zone is returned beside it. |
| 93 | func TestCallTitleNeverNamesItsZone(t *testing.T) { |
| 94 | dublin, _ := time.LoadLocation("Europe/Dublin") |
| 95 | now := time.Date(2026, 10, 2, 14, 40, 0, 0, time.UTC) |
| 96 | for _, local := range []*time.Location{nil, dublin, time.UTC} { |
| 97 | got := callTitle(now, local, dublin) |
| 98 | if got.Text != "Call, 2 Oct 15:40" && local != time.UTC { |
| 99 | t.Errorf("callTitle in %v = %q", local, got.Text) |
| 100 | } |
| 101 | if strings.Contains(got.Text, "/") || strings.HasSuffix(got.Text, "UTC") { |
| 102 | t.Errorf("callTitle in %v names its zone: %q", local, got.Text) |
| 103 | } |
| 104 | if got.Zone == "" { |
| 105 | t.Errorf("callTitle in %v kept no zone", local) |
| 106 | } |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | // TestAutoTitleIsAFlagNotAReading: a meeting started with no title is |
| 111 | // marked when it is made, and a meeting someone titled is not — even |
| 112 | // one whose title reads exactly like an automatic one, which is why |
| 113 | // this is a flag and not a pattern match on the words. |
| 114 | func TestAutoTitleIsAFlagNotAReading(t *testing.T) { |
| 115 | s, _ := testStore(t) |
| 116 | ctx := t.Context() |
| 117 | auto, _, err := s.createCall(ctx, autoTitle{Text: "Call, 2 Oct 15:40", Zone: "Europe/Dublin"}, "person:1") |
| 118 | if err != nil { |
| 119 | t.Fatal(err) |
| 120 | } |
| 121 | named, _, err := s.createMeeting(ctx, "Call, 2 Oct 15:40", nil, "person:1") |
| 122 | if err != nil { |
| 123 | t.Fatal(err) |
| 124 | } |
| 125 | for _, c := range []struct { |
| 126 | m Meeting |
| 127 | auto bool |
| 128 | zone string |
| 129 | }{{auto, true, "Europe/Dublin"}, {named, false, ""}} { |
| 130 | got, ok, err := s.meetingBySqid(ctx, c.m.Sqid) |
| 131 | if err != nil || !ok { |
| 132 | t.Fatalf("read back %s: %v %v", c.m.Sqid, ok, err) |
| 133 | } |
| 134 | if got.AutoTitle != c.auto || got.TitleZone != c.zone { |
| 135 | t.Errorf("meeting %s: auto %v zone %q, want %v %q", c.m.Sqid, got.AutoTitle, got.TitleZone, c.auto, c.zone) |
| 136 | } |
| 137 | if s.title(got) != "Call, 2 Oct 15:40" { |
| 138 | t.Errorf("title read back as %q", s.title(got)) |
| 139 | } |
| 140 | } |
| 141 | } |
| 142 | |