oficina / docs Public

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

Plain git — no account needed to clone.

Download

Download this file

1package docs
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "regexp"
7 "strings"
8 "testing"
9)
10
11// browser is a cookie jar over the real router, asking for pages the way a
12// browser does (Accept: text/html, the app's own Host).
13type browser struct {
14 t *testing.T
15 app *App
16 jar map[string]*http.Cookie
17}
18
19func newBrowser(t *testing.T, app *App) *browser {
20 return &browser{t: t, app: app, jar: map[string]*http.Cookie{}}
21}
22
23func (b *browser) do(method, path, form string) *httptest.ResponseRecorder {
24 req := httptest.NewRequest(method, DefaultOrigin+path, strings.NewReader(form))
25 req.Header.Set("Accept", "text/html")
26 req.Header.Set("Sec-Fetch-Site", "same-origin")
27 if form != "" {
28 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
29 }
30 for _, c := range b.jar {
31 req.AddCookie(c)
32 }
33 rec := httptest.NewRecorder()
34 b.app.Handler().ServeHTTP(rec, req)
35 for _, c := range rec.Result().Cookies() {
36 if c.MaxAge < 0 {
37 delete(b.jar, c.Name)
38 } else {
39 b.jar[c.Name] = c
40 }
41 }
42 return rec
43}
44
45func (b *browser) signIn(address string) {
46 link, err := b.app.TestSignIn(b.t, address)
47 if err != nil {
48 b.t.Fatal(err)
49 }
50 verifyPath, verifyForm := confirmForm(link)
51 b.do("POST", verifyPath, verifyForm)
52 b.do("GET", localLinkCompletePath, "")
53}
54
55// Review finding (Astra, #65): a guest whose last share is revoked still
56// holds a valid session, and admission rightly refuses it — so GET /
57// sends them to /signin. The already-signed-in shortcut must not send
58// them straight back to /, or the two redirect to each other forever.
59// Skipping the form needs a session the signed-in routes would admit.
60func TestARevokedGuestIsNotBouncedBetweenTheListAndSignIn(t *testing.T) {
61 app, err := NewForTestWith(t, Config{AllowedAddresses: "alice@example.com"})
62 if err != nil {
63 t.Fatal(err)
64 }
65 alice := newBrowser(t, app)
66 alice.signIn("alice@example.com")
67 created := alice.do("POST", "/documents", "title=Minutes")
68 sqid := strings.TrimPrefix(created.Header().Get("Location"), "/d/")
69 if rec := alice.do("POST", "/d/"+sqid+"/share", "address=guest@example.com&role=reader"); rec.Code != http.StatusSeeOther {
70 t.Fatalf("share: %d", rec.Code)
71 }
72
73 guest := newBrowser(t, app)
74 guest.signIn("guest@example.com")
75 if rec := guest.do("GET", "/d/"+sqid, ""); rec.Code != http.StatusOK {
76 t.Fatalf("the guest cannot read the shared document before revocation: %d", rec.Code)
77 }
78 // Already signed in and admitted: /signin goes straight back.
79 if rec := guest.do("GET", "/signin", ""); rec.Code != http.StatusSeeOther {
80 t.Fatalf("an admitted guest was shown the sign-in form again: %d", rec.Code)
81 }
82
83 if rec := alice.do("POST", "/d/"+sqid+"/share/revoke", "address=guest@example.com"); rec.Code != http.StatusSeeOther {
84 t.Fatalf("revoke: %d", rec.Code)
85 }
86 list := guest.do("GET", "/", "")
87 if list.Code != http.StatusSeeOther || !strings.HasPrefix(list.Header().Get("Location"), "/signin") {
88 t.Fatalf("a revoked guest reached the list: %d %q", list.Code, list.Header().Get("Location"))
89 }
90 signin := guest.do("GET", list.Header().Get("Location"), "")
91 if signin.Code != http.StatusOK {
92 t.Fatalf("GET %s: %d to %q — the list and sign-in redirect to each other", list.Header().Get("Location"), signin.Code, signin.Header().Get("Location"))
93 }
94 if !strings.Contains(signin.Body.String(), `<form rst-form estilo-signin-form`) {
95 t.Fatal("the revoked guest is not offered a way to sign in as someone else")
96 }
97}
98
99// Review finding (Astra, #65): "Use a different address" on the sent page
100// went to a bare /signin, which a fresh start treats as "remember the
101// list", so the deep link was lost. The retry keeps the remembered page.
102func TestUseADifferentAddressKeepsTheDeepLink(t *testing.T) {
103 app, _ := NewForTest(t)
104 owner := newBrowser(t, app)
105 owner.signIn("alice@example.com")
106 created := owner.do("POST", "/documents", "title=Minutes")
107 deep := created.Header().Get("Location") + "?from=mail"
108
109 b := newBrowser(t, app)
110 signin := b.do("GET", deep, "").Header().Get("Location")
111 b.do("GET", signin, "")
112 sent := b.do("POST", "/signin", "address=typo@example.com")
113 sentPage := b.do("GET", sent.Header().Get("Location"), "")
114 m := regexp.MustCompile(`<p><a href="([^"]+)">Use a different address</a></p>`).FindStringSubmatch(sentPage.Body.String())
115 if m == nil {
116 t.Fatalf("no \"Use a different address\" link on the sent page (%d)", sentPage.Code)
117 }
118 again := strings.ReplaceAll(m[1], "&amp;", "&")
119 if rec := b.do("GET", again, ""); rec.Code != http.StatusOK {
120 t.Fatalf("GET %s: %d", again, rec.Code)
121 }
122 // The right address this time, in the same browser.
123 link, err := app.TestSignIn(t, "alice@example.com")
124 if err != nil {
125 t.Fatal(err)
126 }
127 verifyPath, verifyForm := confirmForm(link)
128 b.do("POST", verifyPath, verifyForm)
129 landed := b.do("GET", localLinkCompletePath, "")
130 if landed.Header().Get("Location") != deep {
131 t.Fatalf("after \"Use a different address\" (%s) the sign-in landed on %q, want %q", again, landed.Header().Get("Location"), deep)
132 }
133}
134
135// The same loop through Home's /signin, which has the same shortcut: a
136// guest admitted only by a share, signed in locally, then revoked.
137func TestARevokedGuestIsNotBouncedThroughHomeSignIn(t *testing.T) {
138 f := newRouterHomeFixture(t)
139 alice := f.browser()
140 alice.localSignin(t, "alice@example.com")
141 created := alice.do("POST", "/documents", "title=Minutes")
142 sqid := strings.TrimPrefix(created.Header().Get("Location"), "/d/")
143 if rec := alice.do("POST", "/d/"+sqid+"/share", "address=carol@example.com&role=reader"); rec.Code != http.StatusSeeOther {
144 t.Fatalf("share: %d", rec.Code)
145 }
146 carol := f.browser()
147 carol.localSignin(t, "carol@example.com")
148 if rec := carol.do("GET", "/signin", ""); rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/" {
149 t.Fatalf("an admitted guest was not sent straight back: %d %q", rec.Code, rec.Header().Get("Location"))
150 }
151 if rec := alice.do("POST", "/d/"+sqid+"/share/revoke", "address=carol@example.com"); rec.Code != http.StatusSeeOther {
152 t.Fatalf("revoke: %d", rec.Code)
153 }
154 list := carol.do("GET", "/", "")
155 if list.Code != http.StatusSeeOther || !strings.HasPrefix(list.Header().Get("Location"), "/signin") {
156 t.Fatalf("a revoked guest reached the list: %d %q", list.Code, list.Header().Get("Location"))
157 }
158 next := carol.do("GET", list.Header().Get("Location"), "")
159 if next.Code == http.StatusSeeOther && next.Header().Get("Location") == "/" {
160 t.Fatal("Home's /signin sent a refused session straight back to / — the two redirect to each other")
161 }
162}
163