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 "amadan.net/oficina/home/connect"
5 "amadan.net/rastrillo/rastrillo"
6 "context"
7 "html"
8 "net/http"
9 "net/http/httptest"
10 "net/url"
11 "strings"
12 "testing"
13)
14
15func TestHomeLocalPluginRoundTripAndSignout(t *testing.T) {
16 f := newHomeFixture(t)
17 if w := f.request(t, "GET", "/account/home", nil, false); w.Code != 303 {
18 t.Fatal(w.Code)
19 }
20 if w := f.request(t, "POST", "/signin", url.Values{"address": {signedInAddress}, "force": {"1"}}, false); w.Code != 303 {
21 t.Fatal(w.Code)
22 }
23 u, err := url.Parse(f.app.testMailer(t).lastLink(t))
24 if err != nil {
25 t.Fatal(err)
26 }
27 // Opening the link draws the confirm door under LocalGuard and
28 // spends nothing; its Sign in button POSTs the token back, and
29 // that POST is the local callback.
30 open := f.request(t, "GET", u.RequestURI(), nil, false)
31 m := confirmForm.FindStringSubmatch(open.Body.String())
32 if open.Code != 200 || m == nil || !strings.Contains(open.Body.String(), "estilo-door") {
33 t.Fatalf("Home-local link: %d, want the confirm door\n%s", open.Code, open.Body.String())
34 }
35 verify := f.request(t, "POST", html.UnescapeString(m[1]), url.Values{"token": {html.UnescapeString(m[2])}}, false)
36 if verify.Code != 303 || verify.Header().Get("Location") != connect.LocalCompletePath {
37 t.Fatal("local callback", verify.Code, verify.Header().Get("Location"))
38 }
39 completed := f.request(t, "GET", connect.LocalCompletePath, nil, false)
40 if completed.Code != 303 || completed.Header().Get("Location") != "/account/home" {
41 t.Fatal("local completion", completed.Code, completed.Header().Get("Location"))
42 }
43 if page := f.request(t, "GET", "/account/home", nil, false); page.Code != 200 {
44 t.Fatal("local account unavailable", page.Code)
45 }
46 var count int64
47 f.app.db.Model(&Member{}).Count(&count)
48 if count != 1 {
49 t.Fatal("local member missing", count)
50 }
51 f.app.db.Table("sheets").Count(&count)
52 if count != 0 {
53 t.Fatal("sign-in seeded content", count)
54 }
55 f.link(t, "round-trip")
56 // With Home configured, the app's Sign out ends the app session and
57 // then chains to the credential origin (home/connect 01166fc, spec
58 // §9.5): a 307, so the POST survives the hop, to {Issuer}/signout.
59 // That hop is what stops the signed-out page's "Sign in" passing
60 // silently back through a live id.* session.
61 out := f.request(t, "POST", "/signout", nil, false)
62 if out.Code != http.StatusTemporaryRedirect || out.Header().Get("Location") != homeTestIssuer+"/signout" {
63 t.Fatal("signout", out.Code, out.Header().Get("Location"))
64 }
65 if page := f.request(t, "GET", "/signed-out", nil, false); page.Code != 200 || page.Header().Get("Location") != "" {
66 t.Fatal("signout restarted Home")
67 }
68}
69func TestHomeDeletedMemberIsNeverReassigned(t *testing.T) {
70 f := newHomeFixture(t)
71 old := f.local(t, signedInAddress)
72 f.link(t, "deleted-person")
73 if err := f.app.db.Exec("DELETE FROM members WHERE id=?", old.ID).Error; err != nil {
74 t.Fatal(err)
75 }
76 replacement, err := f.app.memberFor(signedInAddress)
77 if err != nil {
78 t.Fatal(err)
79 }
80 if replacement.ID <= old.ID {
81 t.Fatal("member IDs reused")
82 }
83 if _, err := f.app.home.Resolve(context.Background(), "deleted-person"); err == nil {
84 t.Fatal("deleted account resolved to replacement")
85 }
86}
87func TestHomeFrameworkPolicyAndDuplicateCookies(t *testing.T) {
88 f := newHomeFixture(t)
89 f.local(t, signedInAddress)
90 mux := http.NewServeMux()
91 mux.Handle("/", f.app.Router())
92 handler, close, err := rastrillo.Handler(rastrillo.Options{Mux: mux})
93 if err != nil {
94 t.Fatal(err)
95 }
96 defer close()
97 r := httptest.NewRequest("GET", testOrigin+"/signin/local", nil)
98 w := httptest.NewRecorder()
99 handler.ServeHTTP(w, r)
100 got := w.Header().Get("Content-Security-Policy")
101 if !strings.Contains(got, "form-action 'self' "+homeTestIssuer) || !strings.Contains(got, "frame-ancestors 'none'") {
102 t.Fatal("Home policy lost", got)
103 }
104 r = httptest.NewRequest("GET", testOrigin+"/account/home", nil)
105 for _, c := range f.jar {
106 r.AddCookie(c)
107 r.AddCookie(c)
108 }
109 w = httptest.NewRecorder()
110 handler.ServeHTTP(w, r)
111 if w.Code != 400 {
112 t.Fatal("duplicate session accepted", w.Code)
113 }
114}
115
116func TestHomeDisabledRefusesRetainedOpaqueSession(t *testing.T) {
117 f := newHomeFixture(t)
118 f.local(t, signedInAddress)
119 f.link(t, "retained-home")
120 f.jar = map[string]*http.Cookie{}
121 start := f.request(t, "GET", "/auth/start", nil, false)
122 if w := f.exchange(t, start, "retained-home"); w.Code != 303 {
123 t.Fatal(w.Code)
124 }
125 f.app.home = nil
126 f.app.homeRoutes = nil
127 f.app.homeSessions = nil
128 var before, after int64
129 f.app.db.Model(&Member{}).Count(&before)
130 w := f.request(t, "GET", "/sheets", nil, false)
131 if w.Code != 403 {
132 t.Fatal("retained Home session accepted", w.Code)
133 }
134 f.app.db.Model(&Member{}).Count(&after)
135 if after != before {
136 t.Fatal("opaque member created")
137 }
138}
139func TestHomeStorageFailureDoesNotRevokeSession(t *testing.T) {
140 f := newHomeFixture(t)
141 f.local(t, signedInAddress)
142 f.link(t, "storage-home")
143 f.jar = map[string]*http.Cookie{}
144 if w := f.exchange(t, f.request(t, "GET", "/auth/start", nil, false), "storage-home"); w.Code != 303 {
145 t.Fatal(w.Code)
146 }
147 cookie := f.jar[f.app.homeSessions.CookieName()]
148 if err := f.app.db.Exec("ALTER TABLE home_identity_links RENAME TO unavailable_links").Error; err != nil {
149 t.Fatal(err)
150 }
151 w := f.request(t, "GET", "/sheets", nil, false)
152 if w.Code != 503 {
153 t.Fatal("storage response", w.Code)
154 }
155 if f.jar[cookie.Name] == nil || f.jar[cookie.Name].Value != cookie.Value {
156 t.Fatal("storage failure revoked session")
157 }
158 if err := f.app.db.Exec("ALTER TABLE unavailable_links RENAME TO home_identity_links").Error; err != nil {
159 t.Fatal(err)
160 }
161 if w := f.request(t, "GET", "/sheets", nil, false); w.Code != 200 {
162 t.Fatal("session did not recover", w.Code)
163 }
164}
165