package callstest import ( "bytes" "net/http" "net/url" "os" "path/filepath" "regexp" "strings" "testing" ) var linkInPage = regexp.MustCompile(`http://app\.test/j/([A-Za-z0-9_-]{43})`) func createMeeting(t *testing.T, c *client, title, starts string) (sqid, token string) { t.Helper() rec := c.post("/meetings", url.Values{"title": {title}, "starts_at": {starts}}) if rec.Code != http.StatusOK { t.Fatalf("POST /meetings: %d\n%s", rec.Code, rec.Body.String()) } m := linkInPage.FindStringSubmatch(rec.Body.String()) if m == nil { t.Fatalf("no link on the page:\n%s", rec.Body.String()) } s := regexp.MustCompile(`/m/([23456789a-z]{10})/join`).FindStringSubmatch(rec.Body.String()) return s[1], m[1] } func TestCreateShowsTheLinkOnceAndTheListNever(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) c.signIn("paul@example.com") _, token := createMeeting(t, c, "Board meeting", "") rec := c.get("/") if strings.Contains(rec.Body.String(), token) { t.Fatal("the list shows a link token") } if !strings.Contains(rec.Body.String(), "Board meeting") { t.Fatal("the list does not show the title") } } // TestStartCallShowsTheLinkOnceAndListsItUnderUpcoming is the one-click // button above "New meeting": a member POSTs with no form fields at // all, and gets the same link-once page a scheduled meeting's creation // does, with a title Calls minted itself. func TestStartCallShowsTheLinkOnceAndListsItUnderUpcoming(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) c.signIn("paul@example.com") rec := c.post("/meetings/start", nil) if rec.Code != http.StatusOK { t.Fatalf("POST /meetings/start: %d\n%s", rec.Code, rec.Body.String()) } body := rec.Body.String() if !linkInPage.MatchString(body) { t.Fatalf("no link on the page:\n%s", body) } if !strings.Contains(body, "Call, ") { t.Fatalf("page title does not start \"Call, \":\n%s", body) } rec = c.get("/") if !strings.Contains(rec.Body.String(), "Call, ") { t.Fatal("the list does not show the started call under Upcoming") } } func TestCreateRefusesAnEmptyTitleInPlace(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) c.signIn("paul@example.com") rec := c.post("/meetings", url.Values{"title": {" "}}) if rec.Code != http.StatusUnprocessableEntity || !strings.Contains(rec.Body.String(), "Give the meeting a title") { t.Fatalf("empty title: %d", rec.Code) } } func TestReplaceLinkNeedsItsConfirmPageAndKillsTheOldLink(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) c.signIn("paul@example.com") sqid, old := createMeeting(t, c, "x", "") if rec := c.get("/meetings/" + sqid + "/links/replace"); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "Replace the link") { t.Fatalf("confirm page: %d", rec.Code) } rec := c.post("/meetings/"+sqid+"/links", nil) if rec.Code != http.StatusOK || !linkInPage.MatchString(rec.Body.String()) { t.Fatalf("replace: %d", rec.Code) } if rec := x.browser(t).get("/j/" + old); rec.Code != http.StatusNotFound { t.Fatalf("old link after replace: %d, want 404", rec.Code) } } func TestEndMeetingNeedsItsConfirmPageAndRefusesJoinsAfter(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) c.signIn("paul@example.com") sqid, tok := createMeeting(t, c, "x", "") if rec := c.get("/meetings/" + sqid + "/end"); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "End meeting for everyone") { t.Fatalf("confirm page: %d", rec.Code) } if rec := c.post("/meetings/"+sqid+"/end", nil); rec.Code != http.StatusSeeOther { t.Fatalf("end: %d", rec.Code) } if rec := x.browser(t).get("/j/" + tok); rec.Code != http.StatusNotFound { t.Fatalf("link after end: %d, want 404", rec.Code) } if rec := c.get("/"); !strings.Contains(rec.Body.String(), "Ended meetings (1)") { t.Fatal("ended meeting not in the collapsed section") } } func TestCrossOriginPostIsRefused(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) c.signIn("paul@example.com") sqid, _ := createMeeting(t, c, "x", "") req := httptestPost("/meetings/"+sqid+"/end", nil) req.Header.Set("Origin", "https://evil.example") rec := c.do(req) if rec.Code != http.StatusForbidden { t.Fatalf("cross-origin end: %d, want 403", rec.Code) } } // The one rule (spec §0.1): the database file and its WAL never hold a // plaintext title. func TestTheDatabaseNeverHoldsAPlaintextTitle(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) c.signIn("paul@example.com") const secret = "Quarterly board meeting about the acquisition" createMeeting(t, c, secret, "") grep := func(when string) { for _, suffix := range []string{"", "-wal", "-shm"} { b, err := os.ReadFile(x.dbPath + suffix) // A missing -wal or -shm is ordinary — SQLite only keeps // them while there is unchecked WAL content. The primary // file missing is not ordinary: a wrong path would read // nothing and this test would pass having grepped nothing // at all, which is the one way a privacy gate can lie // (review C1-8, codex round 2 C2-4). if suffix != "" && os.IsNotExist(err) { continue } if err != nil { t.Fatalf("%s: reading %s: %v", when, filepath.Base(x.dbPath+suffix), err) } if bytes.Contains(b, []byte(secret)) { t.Errorf("%s: title in plaintext in %s", when, filepath.Base(x.dbPath+suffix)) } } } grep("before checkpoint") if _, err := x.sqlDB().Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil { t.Fatal(err) } grep("after checkpoint") }