oficina / docs Public

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

Plain git — no account needed to clone.

Download

Download this file

1package docs_test
2
3import (
4 "net/http"
5 "strings"
6 "testing"
7
8 "github.com/carlosframework/docs/internal/docs"
9)
10
11// anon is a request with no cookies at all — nobody, arriving from
12// somewhere else. The whole point of a public link is that this works, so
13// the helper exists to make every test below say "signed out" in one word.
14func anon(t *testing.T, app *docs.App, path string) *http.Response {
15 t.Helper()
16 rec := get(t, app, nil, path)
17 return rec.Result()
18}
19
20// linkFor publishes a document and returns its public path, collected the
21// one way the app offers it: the share page rendered immediately after the
22// publish. Under ruling 16 the database holds a SHA-256 and cannot produce
23// a URL, so there is no second chance and no store-level shortcut — the
24// test has to take the same one shot a person does.
25func linkFor(t *testing.T, app *docs.App, cookies []*http.Cookie, sqid string) string {
26 t.Helper()
27 if rec := post(t, app, cookies, "/d/"+sqid+"/share/link", "on=1"); rec.Code != http.StatusSeeOther {
28 t.Fatalf("POST link: status %d, want 303", rec.Code)
29 }
30 return readLink(t, get(t, app, cookies, "/d/"+sqid+"/share").Body.String())
31}
32
33// readLink pulls a /p/... path out of a rendered share page.
34func readLink(t *testing.T, body string) string {
35 t.Helper()
36 i := strings.Index(body, "/p/")
37 if i < 0 {
38 t.Fatal("the share page does not show the public link it just created")
39 }
40 rest := body[i:]
41 end := strings.IndexAny(rest, `"'< `)
42 if end < 0 {
43 t.Fatal("could not read the public link off the share page")
44 }
45 return rest[:end]
46}
47
48// The feature, end to end: Paul's "being able to share read-only publicly".
49// A signed-out stranger with the link reads the document.
50func TestAPublicLinkLetsASignedOutStrangerRead(t *testing.T) {
51 app, _ := docs.NewForTest(t)
52 alice := signInCookies(t, app, "alice@example.com")
53 sqid := createDoc(t, app, alice, "Minutes")
54
55 // Before the link exists, the app has no public door at all.
56 if res := anon(t, app, "/d/"+sqid); res.StatusCode == http.StatusOK {
57 t.Fatal("a signed-out request read a document through the private route")
58 }
59
60 path := linkFor(t, app, alice, sqid)
61 res := anon(t, app, path)
62 if res.StatusCode != http.StatusOK {
63 t.Fatalf("GET %s signed out: status %d, want 200", path, res.StatusCode)
64 }
65}
66
67// Read-only means read-only, and the interesting half is that it holds
68// against the routes rather than only against the rendering. Ruling 15:
69// every write has a named actor, and a link holds no name.
70func TestAPublicLinkCarriesNoWrite(t *testing.T) {
71 app, _ := docs.NewForTest(t)
72 alice := signInCookies(t, app, "alice@example.com")
73 sqid := createDoc(t, app, alice, "Minutes")
74 linkFor(t, app, alice, sqid)
75
76 // Holding the link does not admit its holder to any authenticated
77 // route: it is a capability on one document, not a session.
78 for _, path := range []string{
79 "/",
80 "/d/" + sqid,
81 "/d/" + sqid + "/share",
82 "/d/" + sqid + "/updates",
83 } {
84 if res := anon(t, app, path); res.StatusCode == http.StatusOK {
85 t.Errorf("signed out, %s answered 200; a link is not a session", path)
86 }
87 }
88
89 // And the public page offers no editor to drive one with.
90 //
91 // "editor-entry" used to be in this list and appears on no page in
92 // this app — a ban that could never fail. The mount element and the
93 // real served script path are what actually matter.
94 body := get(t, app, nil, linkFor(t, app, alice, sqid)).Body.String()
95 for _, banned := range []string{"docs-text docs-editor", "data-sqid", docs.TestEditorScriptPath(), "/updates"} {
96 if strings.Contains(body, banned) {
97 t.Errorf("the public page ships %q; it is a reading surface", banned)
98 }
99 }
100}
101
102// A wrong token is indistinguishable from a document that does not exist —
103// spec §4.2's rule, which the public route has to keep too or it becomes
104// an oracle for which documents are published.
105func TestAWrongTokenIsANotFound(t *testing.T) {
106 app, _ := docs.NewForTest(t)
107 alice := signInCookies(t, app, "alice@example.com")
108 sqid := createDoc(t, app, alice, "Minutes")
109 path := linkFor(t, app, alice, sqid)
110
111 for _, bad := range []string{"/p/", "/p/nope", "/p/" + strings.Repeat("a", 43), path + "x"} {
112 if res := anon(t, app, bad); res.StatusCode != http.StatusNotFound {
113 t.Errorf("GET %s answered %d, want exactly 404", bad, res.StatusCode)
114 }
115 }
116}
117
118// The refusals are byte-identical, which is the invariant the branch
119// claims and nothing previously checked. The old test asserted only
120// "!= 200", so a 500 from a database error would have passed it while
121// telling a stranger that this token means something.
122//
123// Unknown, revoked, and pointing-at-a-deleted-document must be one answer.
124// The differences between them are exactly the oracle §4.2 exists to deny:
125// which documents were ever published, and which were withdrawn.
126func TestPublicRefusalsAreIndistinguishable(t *testing.T) {
127 app, _ := docs.NewForTest(t)
128 alice := signInCookies(t, app, "alice@example.com")
129
130 type answer struct{ status, body, ctype string }
131 capture := func(path string) answer {
132 rec := get(t, app, nil, path)
133 return answer{
134 status: rec.Result().Status,
135 body: rec.Body.String(),
136 ctype: rec.Header().Get("Content-Type"),
137 }
138 }
139
140 // (i) a syntactically valid token that was never minted.
141 unknown := capture("/p/" + strings.Repeat("A", 22))
142
143 // (ii) a real token, revoked.
144 revokedSqid := createDoc(t, app, alice, "Revoked")
145 revokedPath := linkFor(t, app, alice, revokedSqid)
146 post(t, app, alice, "/d/"+revokedSqid+"/share/link", "on=0")
147 revoked := capture(revokedPath)
148
149 // (iii) a live token whose document has been deleted underneath it.
150 deletedSqid := createDoc(t, app, alice, "Deleted")
151 deletedPath := linkFor(t, app, alice, deletedSqid)
152 app.TestSoftDelete(t, deletedSqid)
153 deleted := capture(deletedPath)
154
155 if unknown.status != "404 Not Found" {
156 t.Fatalf("an unknown token answered %q, want 404", unknown.status)
157 }
158 for name, got := range map[string]answer{"revoked": revoked, "deleted-document": deleted} {
159 if got != unknown {
160 t.Errorf("%s answers differently from an unknown token:\n got: %+v\nwant: %+v", name, got, unknown)
161 }
162 }
163}
164
165// The published page shows the document's actual content.
166//
167// Every other test here publishes an EMPTY document, so if ByToken's join
168// ever scanned links.id into Document.ID the wrong document would be
169// replayed and every one of them would still pass with a 200. GORM emits
170// SELECT documents.* here today; nothing held it.
171func TestAPublicLinkShowsTheDocumentsContent(t *testing.T) {
172 app, _ := docs.NewForTest(t)
173 alice := signInCookies(t, app, "alice@example.com")
174 sqid := createDoc(t, app, alice, "Minutes")
175 app.TestWrite(t, alice, sqid, "hello")
176
177 body := get(t, app, nil, linkFor(t, app, alice, sqid)).Body.String()
178 if !strings.Contains(body, "hello") {
179 t.Error("the published page does not contain the document's text")
180 }
181 if strings.Contains(body, sqid) {
182 t.Error("the published page names the private sqid")
183 }
184}
185
186// A published URL is shown once and never again — ruling 16 as a
187// behaviour rather than as a column type. Reloading the share page after
188// collecting it must show the document as published WITHOUT the URL.
189func TestThePublicURLIsShownExactlyOnce(t *testing.T) {
190 app, _ := docs.NewForTest(t)
191 alice := signInCookies(t, app, "alice@example.com")
192 sqid := createDoc(t, app, alice, "Minutes")
193
194 path := linkFor(t, app, alice, sqid) // consumes the one showing
195
196 again := get(t, app, alice, "/d/"+sqid+"/share").Body.String()
197 if strings.Contains(again, path) {
198 t.Error("the share page showed the public URL a second time")
199 }
200 if strings.Contains(again, "/p/") {
201 t.Error("the share page still prints a /p/ URL after the one showing")
202 }
203 // But it still says the document is published, or the owner cannot
204 // tell a published document from an unpublished one.
205 //
206 // The catalog key, not the word: NewForTest wires no BaseCatalog, so
207 // strings render as their keys — and "published" matched the key
208 // "docs.share.public.published" whatever the page actually said,
209 // which made this assertion unfalsifiable.
210 if !strings.Contains(again, "docs.share.public.published") {
211 t.Error("the share page does not say the document is published")
212 }
213 // And the link itself still works — shown once, not valid once.
214 if res := anon(t, app, path); res.StatusCode != http.StatusOK {
215 t.Errorf("the link stopped working after being shown: %d", res.StatusCode)
216 }
217}
218
219// Nothing anywhere stores the plaintext token. The point of ruling 16,
220// asserted against the database rather than against the code that writes
221// to it.
222func TestTheTokenIsNotStoredInPlaintext(t *testing.T) {
223 app, _ := docs.NewForTest(t)
224 alice := signInCookies(t, app, "alice@example.com")
225 sqid := createDoc(t, app, alice, "Minutes")
226 path := linkFor(t, app, alice, sqid)
227 token := strings.TrimPrefix(path, "/p/")
228
229 if app.TestRowContains(t, "links", token) {
230 t.Error("the plaintext token is in the links table")
231 }
232}
233
234// Turning it off is the revoke, and it has to be immediate: a link that
235// keeps working after the owner switched it off is the failure that makes
236// people distrust the whole control.
237func TestTurningThePublicLinkOffClosesIt(t *testing.T) {
238 app, _ := docs.NewForTest(t)
239 alice := signInCookies(t, app, "alice@example.com")
240 sqid := createDoc(t, app, alice, "Minutes")
241 path := linkFor(t, app, alice, sqid)
242
243 if res := anon(t, app, path); res.StatusCode != http.StatusOK {
244 t.Fatalf("precondition: the link does not work (%d)", res.StatusCode)
245 }
246 if rec := post(t, app, alice, "/d/"+sqid+"/share/link", "on=0"); rec.Code != http.StatusSeeOther {
247 t.Fatalf("POST link off: status %d, want 303", rec.Code)
248 }
249 if res := anon(t, app, path); res.StatusCode == http.StatusOK {
250 t.Error("the link still reads the document after being turned off")
251 }
252
253 // Turning it back on mints a NEW token: the old one stays dead, so
254 // "off" is a revocation rather than a pause somebody can undo for a
255 // stranger who kept the URL.
256 again := linkFor(t, app, alice, sqid)
257 if again == path {
258 t.Error("re-enabling reissued the same token; the revoked link came back")
259 }
260 if res := anon(t, app, path); res.StatusCode == http.StatusOK {
261 t.Error("the old token works again after the link was re-enabled")
262 }
263}
264
265// Publishing over a live link rotates it. This is the property the store
266// promises ("a fresh token every time") and the one the off/on test above
267// cannot see: with the row deleted in between, any implementation mints a
268// new token, so that test passes on a Publish that reuses an existing one.
269// A mutation that made Publish return the existing row survived it, which
270// is why this test exists.
271//
272// It is the rotation an owner needs when a link has gone somewhere they
273// did not intend: republish, and the copy that escaped stops working.
274func TestPublishingAgainRotatesALiveLink(t *testing.T) {
275 app, _ := docs.NewForTest(t)
276 alice := signInCookies(t, app, "alice@example.com")
277 sqid := createDoc(t, app, alice, "Minutes")
278
279 first := linkFor(t, app, alice, sqid)
280 second := linkFor(t, app, alice, sqid) // no Unpublish in between
281 if second == first {
282 t.Fatal("publishing over a live link reissued the same token")
283 }
284 if res := anon(t, app, first); res.StatusCode == http.StatusOK {
285 t.Error("the rotated-away token still reads the document")
286 }
287 if res := anon(t, app, second); res.StatusCode != http.StatusOK {
288 t.Errorf("the new token does not work: status %d", res.StatusCode)
289 }
290}
291
292// Only the owner controls it. A writer grantee cannot publish somebody
293// else's document to the whole internet — the same rule that stops them
294// re-sharing it to one person, and it matters far more here.
295func TestOnlyTheOwnerMayPublish(t *testing.T) {
296 app, _ := docs.NewForTest(t)
297 alice := signInCookies(t, app, "alice@example.com")
298 sqid := createDoc(t, app, alice, "Minutes")
299 bob := signInCookies(t, app, "bob@example.com")
300 post(t, app, alice, "/d/"+sqid+"/share", "address=bob@example.com&role=writer")
301
302 for _, c := range []struct{ path, body string }{
303 {"/d/" + sqid + "/share/link", "on=1"},
304 {"/d/" + sqid + "/share/link", "on=0"},
305 {"/d/" + sqid + "/share/revoke", "address=alice@example.com"},
306 {"/d/" + sqid + "/share", "address=eve@example.com&role=reader"},
307 } {
308 if rec := post(t, app, bob, c.path, c.body); rec.Code != http.StatusNotFound {
309 t.Errorf("a writer grantee reached POST %s (%s): status %d, want 404", c.path, c.body, rec.Code)
310 }
311 }
312}
313
314// The public page must not carry who else can reach the document. The
315// share list is the owner's alone, and a published page is the widest
316// audience this app has.
317func TestThePublicPageLeaksNoAudience(t *testing.T) {
318 app, _ := docs.NewForTest(t)
319 alice := signInCookies(t, app, "alice@example.com")
320 sqid := createDoc(t, app, alice, "Minutes")
321 post(t, app, alice, "/d/"+sqid+"/share", "address=bob@example.com&role=writer")
322 path := linkFor(t, app, alice, sqid)
323
324 body := get(t, app, nil, path).Body.String()
325 for _, secret := range []string{"alice@example.com", "bob@example.com", sqid} {
326 if strings.Contains(body, secret) {
327 t.Errorf("the public page names %q", secret)
328 }
329 }
330}
331