oficina / sheets Public

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

Plain git — no account needed to clone.

Download

Download this file

1package sheets
2
3import (
4 "fmt"
5 "net/http"
6 "net/http/httptest"
7 "net/url"
8 "strings"
9 "testing"
10)
11
12// The one rule, through a side door. Spec §8.3: stored plaintext, the
13// replicated database becomes a bundle of live read capabilities to
14// sealed sheets.
15func TestALinkTokenIsNeverStored(t *testing.T) {
16 app, cookies := signIn(t)
17 s, _ := newSheet(t, app, cookies, "Budget")
18
19 token, err := app.createLink(s.ID, SheetViewer, signedInMember(t, app))
20 if err != nil {
21 t.Fatalf("createLink: %v", err)
22 }
23 if len(token) < 40 {
24 t.Fatalf("token is %d characters; 32 random bytes should be longer", len(token))
25 }
26
27 var stored string
28 if err := app.db.Raw("SELECT token_hash FROM share_links LIMIT 1").Row().Scan(&stored); err != nil {
29 t.Fatalf("read back: %v", err)
30 }
31 if stored == token {
32 t.Fatal("the token itself is in the database")
33 }
34 if strings.Contains(stored, token) || strings.Contains(token, stored) {
35 t.Fatal("the stored value contains the token")
36 }
37 if stored != hashLinkToken(token) {
38 t.Fatal("the stored value is not the token's hash")
39 }
40}
41
42func TestTwoLinksNeverCollide(t *testing.T) {
43 app, cookies := signIn(t)
44 s, _ := newSheet(t, app, cookies, "Budget")
45 me := signedInMember(t, app)
46
47 seen := map[string]bool{}
48 for i := 0; i < 20; i++ {
49 token, err := app.createLink(s.ID, SheetViewer, me)
50 if err != nil {
51 t.Fatalf("createLink: %v", err)
52 }
53 if seen[token] {
54 t.Fatal("crypto/rand produced the same token twice")
55 }
56 seen[token] = true
57 }
58}
59
60// Ruling 12: never editor. Every version row carries an actor, and an
61// anonymous editor would write unattributed rows into version history.
62func TestALinkCanNeverEdit(t *testing.T) {
63 app, cookies := signIn(t)
64 s, _ := newSheet(t, app, cookies, "Budget")
65 me := signedInMember(t, app)
66
67 for _, role := range []SheetRole{SheetEditor, SheetOwner, SheetRole("admin"), SheetRole("")} {
68 if _, err := app.createLink(s.ID, role, me); err == nil {
69 t.Errorf("a %q link was created", role)
70 }
71 }
72 for _, role := range []SheetRole{SheetViewer, SheetCommenter} {
73 if _, err := app.createLink(s.ID, role, me); err != nil {
74 t.Errorf("a %q link was refused: %v", role, err)
75 }
76 }
77}
78
79// A view link mints nothing: no session, no member, no directory entry.
80func TestALinkOpensTheSheetWithNoAccount(t *testing.T) {
81 app, cookies := signIn(t)
82 s, cells := newSheet(t, app, cookies, "Budget")
83 postCell(t, app, cookies, cells, `{"r":0,"c":0,"raw":"=6*7"}`)
84
85 token, err := app.createLink(s.ID, SheetViewer, signedInMember(t, app))
86 if err != nil {
87 t.Fatalf("createLink: %v", err)
88 }
89
90 // No cookies at all.
91 rec := do(t, app, httptest.NewRequest(http.MethodGet, "/l/"+token, nil))
92 if rec.Code != http.StatusOK {
93 t.Fatalf("GET /l/<token> = %d, want 200", rec.Code)
94 }
95 body := rec.Body.String()
96 if !strings.Contains(body, "Budget") {
97 t.Error("the link page does not show the sheet's title")
98 }
99 if !strings.Contains(body, ">42<") {
100 t.Error("the link page does not show the sheet's computed values")
101 }
102
103 var members int64
104 app.db.Model(&Member{}).Count(&members)
105 // Only the signed-in owner. Opening a link must not mint anybody.
106 if members != 1 {
107 t.Fatalf("opening a link left %d members, want 1 — a view link mints nothing", members)
108 }
109}
110
111// The link page must offer no way to write, and the write routes must
112// refuse a link holder, who has no session at all.
113func TestALinkHolderCannotWrite(t *testing.T) {
114 app, cookies := signIn(t)
115 s, cells := newSheet(t, app, cookies, "Budget")
116 token, err := app.createLink(s.ID, SheetCommenter, signedInMember(t, app))
117 if err != nil {
118 t.Fatalf("createLink: %v", err)
119 }
120
121 body := do(t, app, httptest.NewRequest(http.MethodGet, "/l/"+token, nil)).Body.String()
122 if strings.Contains(body, "<form") {
123 t.Error("the link page carries a form; a link is read-only")
124 }
125 if strings.Contains(body, "edit=") {
126 t.Error("the link page offers an edit link")
127 }
128
129 // And the real write routes, with no session.
130 if rec := postCell(t, app, nil, cells, `{"r":0,"c":0,"raw":"sneaky"}`); rec.Code == http.StatusOK {
131 t.Error("a cell was written with no session")
132 }
133 form := url.Values{"r": {"0"}, "c": {"0"}, "raw": {"sneaky"}, "page": {"1"}}
134 if rec := do(t, app, postForm(fmt.Sprintf("/sheets/%d/table", s.ID), form)); rec.Code == http.StatusSeeOther {
135 t.Error("the scriptless table accepted a write with no session")
136 }
137}
138
139func TestARevokedLinkGivesNothing(t *testing.T) {
140 app, cookies := signIn(t)
141 s, _ := newSheet(t, app, cookies, "Budget")
142 token, err := app.createLink(s.ID, SheetViewer, signedInMember(t, app))
143 if err != nil {
144 t.Fatalf("createLink: %v", err)
145 }
146 if rec := do(t, app, httptest.NewRequest(http.MethodGet, "/l/"+token, nil)); rec.Code != http.StatusOK {
147 t.Fatalf("the link did not work before revoking: %d", rec.Code)
148 }
149
150 links, err := app.linksOn(s.ID)
151 if err != nil || len(links) != 1 {
152 t.Fatalf("linksOn: %v (%d links)", err, len(links))
153 }
154 if err := app.revokeLink(s.ID, links[0].ID); err != nil {
155 t.Fatalf("revokeLink: %v", err)
156 }
157 if rec := do(t, app, httptest.NewRequest(http.MethodGet, "/l/"+token, nil)); rec.Code != http.StatusNotFound {
158 t.Fatalf("a revoked link still serves: %d", rec.Code)
159 }
160 // And it disappears from the share screen's live list.
161 live, err := app.linksOn(s.ID)
162 if err != nil {
163 t.Fatalf("linksOn: %v", err)
164 }
165 if len(live) != 0 {
166 t.Fatalf("a revoked link is still listed as live")
167 }
168}
169
170// One answer for every failure, so the route is not an oracle for which
171// tokens were ever real.
172func TestABadTokenIs404(t *testing.T) {
173 app, _ := signIn(t)
174 for _, token := range []string{"nope", "", strings.Repeat("A", 43), "../../etc/passwd"} {
175 path := "/l/" + url.PathEscape(token)
176 rec := do(t, app, httptest.NewRequest(http.MethodGet, path, nil))
177 if rec.Code != http.StatusNotFound {
178 t.Errorf("GET %s = %d, want 404", path, rec.Code)
179 }
180 }
181}
182
183// The token is shown once because only its hash is kept. That is a
184// property of the design, so it is asserted rather than described.
185func TestTheTokenIsShownOnceAndNotAgain(t *testing.T) {
186 app, cookies := signIn(t)
187 s, _ := newSheet(t, app, cookies, "Budget")
188
189 form := url.Values{"role": {string(SheetViewer)}}
190 rec := do(t, app, as(postForm(sharePath(s)+"/link", form), cookies))
191 if rec.Code != http.StatusOK {
192 t.Fatalf("creating a link = %d: %s", rec.Code, rec.Body.String())
193 }
194
195 // The response to the POST is the one place it appears.
196 if !strings.Contains(rec.Body.String(), "/l/") {
197 t.Fatal("minting a link did not show the new link")
198 }
199
200 // Loading the screen again cannot.
201 plain := do(t, app, as(httptest.NewRequest(http.MethodGet, sharePath(s), nil), cookies)).Body.String()
202 if strings.Contains(plain, "/l/") {
203 t.Fatal("the share screen showed a link URL without being handed the token — " +
204 "something is storing it")
205 }
206 if !strings.Contains(plain, "link") {
207 t.Error("the share screen does not mention the live link at all")
208 }
209}
210
211// TestTheShareScreenOnlyEchoesItsOwnToken is deliberately gone.
212//
213// It asserted that a token minted for one sheet is not echoed onto
214// another sheet's share screen — a real property while the screen read
215// a token out of its query string. The screen no longer reads one at
216// all, so no production change could make that test fail: it would have
217// sat in the suite passing and proving nothing, which is the shape this
218// branch has been finding all week. TestTheShareScreenIgnoresATokenInIts
219// Query, in sharelink_url_test.go, asserts the stronger property that
220// replaced it.
221
222// Only an owner may mint or revoke a link — it is the same power as
223// granting access, in a different shape.
224func TestOnlyAnOwnerCanMintALink(t *testing.T) {
225 app, cookies := signIn(t)
226 s, _ := newSheet(t, app, cookies, "Budget")
227 guestOn(t, app, s.ID, "ada@elsewhere.example", SheetEditor)
228 guestCookies := signInAs(t, app, "ada@elsewhere.example")
229
230 form := url.Values{"role": {string(SheetViewer)}}
231 if rec := do(t, app, as(postForm(sharePath(s)+"/link", form), guestCookies)); rec.Code != http.StatusNotFound {
232 t.Errorf("an editor minted a link: %d", rec.Code)
233 }
234 links, err := app.linksOn(s.ID)
235 if err != nil {
236 t.Fatalf("linksOn: %v", err)
237 }
238 if len(links) != 0 {
239 t.Fatal("an editor's attempt created a link anyway")
240 }
241}
242