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 "net/http"
5 "net/url"
6 "regexp"
7 "strings"
8 "testing"
9)
10
11// createdEvent is the redirect a successful quick add sends: /events/
12// followed by an id and nothing else.
13//
14// The obvious check — HasPrefix "/events/" — is worthless here, because
15// the failure this file is guarding against redirects to
16// /events/new?text=..., which has that prefix too. Asserting the prefix
17// passes whether the event was created or refused.
18var createdEvent = regexp.MustCompile(`^/events/\d+$`)
19
20// One sentence in, one event out — through the real router, with no
21// script anywhere in the path. This is the scriptless floor, and it is
22// the product rather than a fallback.
23func TestQuickAddCreatesAnEventFromASentence(t *testing.T) {
24 app, session := signedIn(t, "someone@example.com")
25 h := app.Handler()
26
27 rec := postAs(t, h, session, "/events/quick", url.Values{
28 "text": {"Board meeting with doc@tito.io tomorrow at 3pm in McCloskeys"},
29 })
30 if rec.Code != http.StatusSeeOther {
31 t.Fatalf("quick add: status %d, want 303\n%s", rec.Code, rec.Body.String())
32 }
33 loc := rec.Header().Get("Location")
34 if !strings.HasPrefix(loc, "/events/") {
35 t.Fatalf("quick add redirected to %q, want the new event", loc)
36 }
37
38 page := getAs(t, h, session, loc).Body.String()
39 for _, want := range []string{"Board meeting", "McCloskeys", "doc@tito.io"} {
40 if !strings.Contains(page, want) {
41 t.Errorf("the created event does not show %q:\n%s", want, page)
42 }
43 }
44 // The one thing that could mislead somebody has to be on the page.
45 if !strings.Contains(page, "Nobody here was emailed") {
46 t.Error("the event page does not say that attendees were not emailed")
47 }
48}
49
50// The one rule, extended to attendees: an external attendee's address is
51// who this team MEETS WITH, which is exactly what the bucket must not
52// give up. Greps the raw file, not a query — a column that was never
53// sealed looks identical through Go.
54func TestQuickAddSealsAttendeeAddresses(t *testing.T) {
55 app, session := signedIn(t, "someone@example.com")
56 h := app.Handler()
57
58 postAs(t, h, session, "/events/quick", url.Values{
59 "text": {"Coffee with secret.person@elsewhere.example at 11am"},
60 })
61
62 if found, where := app.GrepDatabaseForTest(t, "secret.person@elsewhere.example"); found {
63 t.Errorf("an attendee's address is readable in %s — it was not sealed", where)
64 }
65 // The keyed fingerprint must not be the address in another alphabet
66 // either: an unkeyed SHA-256 of an address is trivially reversed
67 // from a wordlist, which is why seal.Fingerprint is an HMAC.
68 if found, where := app.GrepDatabaseForTest(t,
69 "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"); found {
70 t.Errorf("something in %s looks like an unkeyed digest", where)
71 }
72}
73
74// The live pane is a FRAGMENT. Rendered through the page templates it
75// would arrive wrapped in a second whole document and the browser would
76// paste an <html> inside the open one.
77func TestTheQuickAddPaneIsAFragmentNotAPage(t *testing.T) {
78 app, session := signedIn(t, "someone@example.com")
79 h := app.Handler()
80
81 rec := postAs(t, h, session, "/events/quick/parse", url.Values{
82 "text": {"Standup tomorrow at 9:30"},
83 })
84 if rec.Code != http.StatusOK {
85 t.Fatalf("parse: status %d, want 200", rec.Code)
86 }
87 body := rec.Body.String()
88 if strings.Contains(strings.ToLower(body), "<html") {
89 t.Errorf("the pane came back as a whole page:\n%s", body)
90 }
91 // And it says what it understood, which is the entire point of it.
92 for _, want := range []string{"Standup", "09:30"} {
93 if !strings.Contains(body, want) {
94 t.Errorf("the pane does not mention %q:\n%s", want, body)
95 }
96 }
97}
98
99// The box has to be on the page a person is looking at, and it has to be
100// a real form pointing at a real route — not a button waiting for
101// script to give it meaning.
102func TestTheQuickAddBoxIsOnTheViewsAndNeedsNoScript(t *testing.T) {
103 app, session := signedIn(t, "someone@example.com")
104 h := app.Handler()
105
106 for _, path := range []string{"/today", "/agenda"} {
107 body := follow(t, h, session, path).Body.String()
108 if !strings.Contains(body, "cal-quick") {
109 t.Errorf("%s has no quick add box", path)
110 }
111 if !strings.Contains(body, `action="/events/quick"`) {
112 t.Errorf("%s has a quick add box that posts nowhere", path)
113 }
114 // A <details> opens with no script. A div with a class does not.
115 if !strings.Contains(body, "<details cal-quick>") {
116 t.Errorf("%s builds the popover from something other than <details>, "+
117 "so it needs script to open", path)
118 }
119 }
120}
121
122// A sentence with nothing usable in it must not create a nameless row.
123func TestQuickAddWithNoTextGoesToTheForm(t *testing.T) {
124 app, session := signedIn(t, "someone@example.com")
125 h := app.Handler()
126
127 rec := postAs(t, h, session, "/events/quick", url.Values{"text": {" "}})
128 if rec.Code != http.StatusSeeOther {
129 t.Fatalf("empty quick add: status %d, want 303", rec.Code)
130 }
131 if got := rec.Header().Get("Location"); got != "/events/new" {
132 t.Errorf("empty quick add went to %q, want /events/new", got)
133 }
134}
135
136// A run of days is a real event, created and visible. This is the whole
137// point of the range work, and it is asserted through the router rather
138// than against the parser because the bug it replaced was NOT in the
139// parse: an all-day event whose end equals its start is refused by the
140// validator, so the sentence used to bounce to the long form and
141// nothing was ever created.
142func TestQuickAddCreatesAnAllDayRun(t *testing.T) {
143 app, session := signedIn(t, "someone@example.com")
144 h := app.Handler()
145
146 rec := postAs(t, h, session, "/events/quick", url.Values{
147 "text": {"london tues-thurs"},
148 })
149 loc := rec.Header().Get("Location")
150 if rec.Code != http.StatusSeeOther || !createdEvent.MatchString(loc) {
151 t.Fatalf("quick add went to %q with status %d, want the new event",
152 loc, rec.Code)
153 }
154 page := getAs(t, h, session, loc).Body.String()
155 if !strings.Contains(page, "london") {
156 t.Errorf("the created event does not show its title:\n%s", page)
157 }
158 // The stranded word is the symptom everybody sees: before the range
159 // patterns existed the title came out as "london -thurs".
160 if strings.Contains(page, "thurs") {
161 t.Errorf("the end of the range was left in the title:\n%s", page)
162 }
163}
164
165// A single all-day sentence has to create an event too. It did not: the
166// parser wrote "2027-03-12T00:00" for both ends, which is neither a
167// DATE nor a non-empty range.
168func TestQuickAddCreatesASingleAllDayEvent(t *testing.T) {
169 app, session := signedIn(t, "someone@example.com")
170 h := app.Handler()
171
172 rec := postAs(t, h, session, "/events/quick", url.Values{
173 "text": {"Team offsite all day on 12 March"},
174 })
175 if loc := rec.Header().Get("Location"); !createdEvent.MatchString(loc) {
176 t.Fatalf("an all-day sentence went to %q instead of creating an event", loc)
177 }
178}
179
180// The pane prints the last day a person is actually busy, not the
181// exclusive end. Telling somebody their trip runs to Friday when it ends
182// on Thursday is the one thing a preview must not do.
183func TestTheQuickAddPaneShowsTheInclusiveLastDay(t *testing.T) {
184 app, session := signedIn(t, "someone@example.com")
185 h := app.Handler()
186
187 body := postAs(t, h, session, "/events/quick/parse", url.Values{
188 "text": {"london tues-thurs"},
189 }).Body.String()
190
191 if !strings.Contains(body, "Thursday") {
192 t.Errorf("the pane does not name the last day of the run:\n%s", body)
193 }
194 if strings.Contains(body, "Friday") {
195 t.Errorf("the pane printed the exclusive end as the last day:\n%s", body)
196 }
197 if !strings.Contains(body, "all day") {
198 t.Errorf("the pane does not say the run is all day:\n%s", body)
199 }
200}
201