oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1package callstest
2
3import (
4 "bytes"
5 "net/http"
6 "net/url"
7 "os"
8 "path/filepath"
9 "regexp"
10 "strings"
11 "testing"
12)
13
14var linkInPage = regexp.MustCompile(`http://app\.test/j/([A-Za-z0-9_-]{43})`)
15
16func createMeeting(t *testing.T, c *client, title, starts string) (sqid, token string) {
17 t.Helper()
18 rec := c.post("/meetings", url.Values{"title": {title}, "starts_at": {starts}})
19 if rec.Code != http.StatusOK {
20 t.Fatalf("POST /meetings: %d\n%s", rec.Code, rec.Body.String())
21 }
22 m := linkInPage.FindStringSubmatch(rec.Body.String())
23 if m == nil {
24 t.Fatalf("no link on the page:\n%s", rec.Body.String())
25 }
26 s := regexp.MustCompile(`/m/([23456789a-z]{10})/join`).FindStringSubmatch(rec.Body.String())
27 return s[1], m[1]
28}
29
30func TestCreateShowsTheLinkOnceAndTheListNever(t *testing.T) {
31 x := newHarness(t, "paul@example.com")
32 c := x.browser(t)
33 c.signIn("paul@example.com")
34 _, token := createMeeting(t, c, "Board meeting", "")
35 rec := c.get("/")
36 if strings.Contains(rec.Body.String(), token) {
37 t.Fatal("the list shows a link token")
38 }
39 if !strings.Contains(rec.Body.String(), "Board meeting") {
40 t.Fatal("the list does not show the title")
41 }
42}
43
44// TestStartCallShowsTheLinkOnceAndListsItUnderUpcoming is the one-click
45// button above "New meeting": a member POSTs with no form fields at
46// all, and gets the same link-once page a scheduled meeting's creation
47// does, with a title Calls minted itself.
48func TestStartCallShowsTheLinkOnceAndListsItUnderUpcoming(t *testing.T) {
49 x := newHarness(t, "paul@example.com")
50 c := x.browser(t)
51 c.signIn("paul@example.com")
52 rec := c.post("/meetings/start", nil)
53 if rec.Code != http.StatusOK {
54 t.Fatalf("POST /meetings/start: %d\n%s", rec.Code, rec.Body.String())
55 }
56 body := rec.Body.String()
57 if !linkInPage.MatchString(body) {
58 t.Fatalf("no link on the page:\n%s", body)
59 }
60 if !strings.Contains(body, "Call, ") {
61 t.Fatalf("page title does not start \"Call, \":\n%s", body)
62 }
63 rec = c.get("/")
64 if !strings.Contains(rec.Body.String(), "Call, ") {
65 t.Fatal("the list does not show the started call under Upcoming")
66 }
67}
68
69func TestCreateRefusesAnEmptyTitleInPlace(t *testing.T) {
70 x := newHarness(t, "paul@example.com")
71 c := x.browser(t)
72 c.signIn("paul@example.com")
73 rec := c.post("/meetings", url.Values{"title": {" "}})
74 if rec.Code != http.StatusUnprocessableEntity || !strings.Contains(rec.Body.String(), "Give the meeting a title") {
75 t.Fatalf("empty title: %d", rec.Code)
76 }
77}
78
79func TestReplaceLinkNeedsItsConfirmPageAndKillsTheOldLink(t *testing.T) {
80 x := newHarness(t, "paul@example.com")
81 c := x.browser(t)
82 c.signIn("paul@example.com")
83 sqid, old := createMeeting(t, c, "x", "")
84 if rec := c.get("/meetings/" + sqid + "/links/replace"); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "Replace the link") {
85 t.Fatalf("confirm page: %d", rec.Code)
86 }
87 rec := c.post("/meetings/"+sqid+"/links", nil)
88 if rec.Code != http.StatusOK || !linkInPage.MatchString(rec.Body.String()) {
89 t.Fatalf("replace: %d", rec.Code)
90 }
91 if rec := x.browser(t).get("/j/" + old); rec.Code != http.StatusNotFound {
92 t.Fatalf("old link after replace: %d, want 404", rec.Code)
93 }
94}
95
96func TestEndMeetingNeedsItsConfirmPageAndRefusesJoinsAfter(t *testing.T) {
97 x := newHarness(t, "paul@example.com")
98 c := x.browser(t)
99 c.signIn("paul@example.com")
100 sqid, tok := createMeeting(t, c, "x", "")
101 if rec := c.get("/meetings/" + sqid + "/end"); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "End meeting for everyone") {
102 t.Fatalf("confirm page: %d", rec.Code)
103 }
104 if rec := c.post("/meetings/"+sqid+"/end", nil); rec.Code != http.StatusSeeOther {
105 t.Fatalf("end: %d", rec.Code)
106 }
107 if rec := x.browser(t).get("/j/" + tok); rec.Code != http.StatusNotFound {
108 t.Fatalf("link after end: %d, want 404", rec.Code)
109 }
110 if rec := c.get("/"); !strings.Contains(rec.Body.String(), "Ended meetings (1)") {
111 t.Fatal("ended meeting not in the collapsed section")
112 }
113}
114
115func TestCrossOriginPostIsRefused(t *testing.T) {
116 x := newHarness(t, "paul@example.com")
117 c := x.browser(t)
118 c.signIn("paul@example.com")
119 sqid, _ := createMeeting(t, c, "x", "")
120 req := httptestPost("/meetings/"+sqid+"/end", nil)
121 req.Header.Set("Origin", "https://evil.example")
122 rec := c.do(req)
123 if rec.Code != http.StatusForbidden {
124 t.Fatalf("cross-origin end: %d, want 403", rec.Code)
125 }
126}
127
128// The one rule (spec §0.1): the database file and its WAL never hold a
129// plaintext title.
130func TestTheDatabaseNeverHoldsAPlaintextTitle(t *testing.T) {
131 x := newHarness(t, "paul@example.com")
132 c := x.browser(t)
133 c.signIn("paul@example.com")
134 const secret = "Quarterly board meeting about the acquisition"
135 createMeeting(t, c, secret, "")
136 grep := func(when string) {
137 for _, suffix := range []string{"", "-wal", "-shm"} {
138 b, err := os.ReadFile(x.dbPath + suffix)
139 // A missing -wal or -shm is ordinary — SQLite only keeps
140 // them while there is unchecked WAL content. The primary
141 // file missing is not ordinary: a wrong path would read
142 // nothing and this test would pass having grepped nothing
143 // at all, which is the one way a privacy gate can lie
144 // (review C1-8, codex round 2 C2-4).
145 if suffix != "" && os.IsNotExist(err) {
146 continue
147 }
148 if err != nil {
149 t.Fatalf("%s: reading %s: %v", when, filepath.Base(x.dbPath+suffix), err)
150 }
151 if bytes.Contains(b, []byte(secret)) {
152 t.Errorf("%s: title in plaintext in %s", when, filepath.Base(x.dbPath+suffix))
153 }
154 }
155 }
156 grep("before checkpoint")
157 if _, err := x.sqlDB().Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil {
158 t.Fatal(err)
159 }
160 grep("after checkpoint")
161}
162