package calendartest import ( "net/http" "net/url" "regexp" "strings" "testing" ) // createdEvent is the redirect a successful quick add sends: /events/ // followed by an id and nothing else. // // The obvious check — HasPrefix "/events/" — is worthless here, because // the failure this file is guarding against redirects to // /events/new?text=..., which has that prefix too. Asserting the prefix // passes whether the event was created or refused. var createdEvent = regexp.MustCompile(`^/events/\d+$`) // One sentence in, one event out — through the real router, with no // script anywhere in the path. This is the scriptless floor, and it is // the product rather than a fallback. func TestQuickAddCreatesAnEventFromASentence(t *testing.T) { app, session := signedIn(t, "someone@example.com") h := app.Handler() rec := postAs(t, h, session, "/events/quick", url.Values{ "text": {"Board meeting with doc@tito.io tomorrow at 3pm in McCloskeys"}, }) if rec.Code != http.StatusSeeOther { t.Fatalf("quick add: status %d, want 303\n%s", rec.Code, rec.Body.String()) } loc := rec.Header().Get("Location") if !strings.HasPrefix(loc, "/events/") { t.Fatalf("quick add redirected to %q, want the new event", loc) } page := getAs(t, h, session, loc).Body.String() for _, want := range []string{"Board meeting", "McCloskeys", "doc@tito.io"} { if !strings.Contains(page, want) { t.Errorf("the created event does not show %q:\n%s", want, page) } } // The one thing that could mislead somebody has to be on the page. if !strings.Contains(page, "Nobody here was emailed") { t.Error("the event page does not say that attendees were not emailed") } } // The one rule, extended to attendees: an external attendee's address is // who this team MEETS WITH, which is exactly what the bucket must not // give up. Greps the raw file, not a query — a column that was never // sealed looks identical through Go. func TestQuickAddSealsAttendeeAddresses(t *testing.T) { app, session := signedIn(t, "someone@example.com") h := app.Handler() postAs(t, h, session, "/events/quick", url.Values{ "text": {"Coffee with secret.person@elsewhere.example at 11am"}, }) if found, where := app.GrepDatabaseForTest(t, "secret.person@elsewhere.example"); found { t.Errorf("an attendee's address is readable in %s — it was not sealed", where) } // The keyed fingerprint must not be the address in another alphabet // either: an unkeyed SHA-256 of an address is trivially reversed // from a wordlist, which is why seal.Fingerprint is an HMAC. if found, where := app.GrepDatabaseForTest(t, "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"); found { t.Errorf("something in %s looks like an unkeyed digest", where) } } // The live pane is a FRAGMENT. Rendered through the page templates it // would arrive wrapped in a second whole document and the browser would // paste an inside the open one. func TestTheQuickAddPaneIsAFragmentNotAPage(t *testing.T) { app, session := signedIn(t, "someone@example.com") h := app.Handler() rec := postAs(t, h, session, "/events/quick/parse", url.Values{ "text": {"Standup tomorrow at 9:30"}, }) if rec.Code != http.StatusOK { t.Fatalf("parse: status %d, want 200", rec.Code) } body := rec.Body.String() if strings.Contains(strings.ToLower(body), " opens with no script. A div with a class does not. if !strings.Contains(body, "
") { t.Errorf("%s builds the popover from something other than
, "+ "so it needs script to open", path) } } } // A sentence with nothing usable in it must not create a nameless row. func TestQuickAddWithNoTextGoesToTheForm(t *testing.T) { app, session := signedIn(t, "someone@example.com") h := app.Handler() rec := postAs(t, h, session, "/events/quick", url.Values{"text": {" "}}) if rec.Code != http.StatusSeeOther { t.Fatalf("empty quick add: status %d, want 303", rec.Code) } if got := rec.Header().Get("Location"); got != "/events/new" { t.Errorf("empty quick add went to %q, want /events/new", got) } } // A run of days is a real event, created and visible. This is the whole // point of the range work, and it is asserted through the router rather // than against the parser because the bug it replaced was NOT in the // parse: an all-day event whose end equals its start is refused by the // validator, so the sentence used to bounce to the long form and // nothing was ever created. func TestQuickAddCreatesAnAllDayRun(t *testing.T) { app, session := signedIn(t, "someone@example.com") h := app.Handler() rec := postAs(t, h, session, "/events/quick", url.Values{ "text": {"london tues-thurs"}, }) loc := rec.Header().Get("Location") if rec.Code != http.StatusSeeOther || !createdEvent.MatchString(loc) { t.Fatalf("quick add went to %q with status %d, want the new event", loc, rec.Code) } page := getAs(t, h, session, loc).Body.String() if !strings.Contains(page, "london") { t.Errorf("the created event does not show its title:\n%s", page) } // The stranded word is the symptom everybody sees: before the range // patterns existed the title came out as "london -thurs". if strings.Contains(page, "thurs") { t.Errorf("the end of the range was left in the title:\n%s", page) } } // A single all-day sentence has to create an event too. It did not: the // parser wrote "2027-03-12T00:00" for both ends, which is neither a // DATE nor a non-empty range. func TestQuickAddCreatesASingleAllDayEvent(t *testing.T) { app, session := signedIn(t, "someone@example.com") h := app.Handler() rec := postAs(t, h, session, "/events/quick", url.Values{ "text": {"Team offsite all day on 12 March"}, }) if loc := rec.Header().Get("Location"); !createdEvent.MatchString(loc) { t.Fatalf("an all-day sentence went to %q instead of creating an event", loc) } } // The pane prints the last day a person is actually busy, not the // exclusive end. Telling somebody their trip runs to Friday when it ends // on Thursday is the one thing a preview must not do. func TestTheQuickAddPaneShowsTheInclusiveLastDay(t *testing.T) { app, session := signedIn(t, "someone@example.com") h := app.Handler() body := postAs(t, h, session, "/events/quick/parse", url.Values{ "text": {"london tues-thurs"}, }).Body.String() if !strings.Contains(body, "Thursday") { t.Errorf("the pane does not name the last day of the run:\n%s", body) } if strings.Contains(body, "Friday") { t.Errorf("the pane printed the exclusive end as the last day:\n%s", body) } if !strings.Contains(body, "all day") { t.Errorf("the pane does not say the run is all day:\n%s", body) } }