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 "net/http"
5 "net/url"
6 "strings"
7 "testing"
8)
9
10// §8.2 item 2: "Connect your Oficina account" is offered with Home
11// configured, to a local session, and never to a Home session, whose
12// account is already the connection. Sheets used to offer it to both.
13func TestConnectYourOficinaAccountIsHiddenForHomeSessions(t *testing.T) {
14 f := newHomeFixture(t)
15 f.local(t, signedInAddress)
16 if body := f.request(t, "GET", "/sheets", nil, false).Body.String(); !strings.Contains(body, `<a estilo-menu-item href="/account/home"><span estilo-menu-label>Connect your Oficina account</span></a>`) {
17 t.Error("a local session is not offered the connection")
18 }
19 g := newHomeFixture(t)
20 homeSignedIn(t, g)
21 body := g.request(t, "GET", "/sheets", nil, false).Body.String()
22 if !strings.Contains(body, "<details estilo-account") {
23 t.Fatal("the Home session is not signed in")
24 }
25 if strings.Contains(body, "/account/home") || strings.Contains(body, "Home account") {
26 t.Error("a Home session is offered to connect the account it signed in with")
27 }
28 app, cookies := signIn(t)
29 if body := page(t, app, cookies, "/sheets").Body.String(); strings.Contains(body, "/account/home") {
30 t.Error("a standalone instance offers a connection to a Home it does not have")
31 }
32}
33
34// Home #74, decision 2: with Home configured, a signed-out "/" starts
35// sign-in instead of showing a landing page — the switcher's single-team
36// hop lands on the root. Signed in, it is the list, both ways.
37func TestTheRootStartsSignInWhenHomeIsConfigured(t *testing.T) {
38 f := newHomeFixture(t)
39 if rec := f.request(t, "GET", "/", nil, false); rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signin?return_to=%2F" {
40 t.Fatalf("signed-out / with Home = %d %q, want 303 /signin?return_to=%%2F", rec.Code, rec.Header().Get("Location"))
41 }
42 homeSignedIn(t, f)
43 if rec := f.request(t, "GET", "/", nil, false); rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/sheets" {
44 t.Fatalf("signed-in / with Home = %d %q, want 303 /sheets", rec.Code, rec.Header().Get("Location"))
45 }
46 g := newHomeFixture(t)
47 g.local(t, signedInAddress)
48 if rec := g.request(t, "GET", "/", nil, false); rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/sheets" {
49 t.Fatalf("locally signed-in / with Home = %d %q, want 303 /sheets", rec.Code, rec.Header().Get("Location"))
50 }
51 // Standalone keeps its landing page for a stranger.
52 if rec := page(t, New(Config{}), nil, "/"); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "<h1>Sheets</h1>") {
53 t.Fatalf("standalone signed-out / = %d, want the landing page", rec.Code)
54 }
55}
56
57// §9.2's last row: somebody already signed in never sees the form. With
58// Home, connect's /signin does it (home/connect 01166fc); standalone,
59// Sheets' own handler does, to a safe return_to or the list, and never
60// to a sign-in page (which would loop).
61func TestSigninWhenAlreadySignedIn(t *testing.T) {
62 app, cookies := signIn(t)
63 for query, want := range map[string]string{
64 "": "/sheets",
65 "?return_to=%2Fsheets%2F4": "/sheets/4",
66 "?return_to=%2Fsignin": "/sheets",
67 "?return_to=%2F%2Fevil.test": "/sheets",
68 } {
69 rec := page(t, app, cookies, "/signin"+query)
70 if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != want {
71 t.Errorf("signed-in /signin%s = %d %q, want 303 %s", query, rec.Code, rec.Header().Get("Location"), want)
72 }
73 }
74 // The outcome pages still render: a keymail failure must be able to
75 // offer the email path to somebody holding a session.
76 if rec := page(t, app, cookies, "/signin?force=1&err=keymail"); rec.Code != http.StatusOK {
77 t.Errorf("?force=1&err=keymail = %d, want the form", rec.Code)
78 }
79 f := newHomeFixture(t)
80 homeSignedIn(t, f)
81 if rec := f.request(t, "GET", "/signin?return_to=%2Fsheets%2F4", nil, false); rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/sheets/4" {
82 t.Errorf("Home session /signin = %d %q, want 303 /sheets/4", rec.Code, rec.Header().Get("Location"))
83 }
84}
85
86// §9.2: Sign out lands on the signed-out page, "You’re signed out", with
87// an empty corner and a way back in — standalone too, where rastrillo's
88// auth sends it to the sign-in form.
89func TestSignOutLandsOnTheSignedOutPage(t *testing.T) {
90 app, cookies := signIn(t)
91 req := as(postForm("/signout", nil), cookies)
92 req.Header.Set("Origin", testOrigin)
93 rec := do(t, app, req)
94 if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signed-out" {
95 t.Fatalf("sign out = %d %q, want 303 /signed-out", rec.Code, rec.Header().Get("Location"))
96 }
97 if after := page(t, app, cookies, "/sheets"); after.Code == http.StatusOK {
98 t.Fatal("the session survived sign-out")
99 }
100 body := page(t, app, nil, "/signed-out").Body.String()
101 for _, want := range []string{"<h1>You’re signed out</h1>", "You’ve signed out of Sheets on this device.", `<a rst-btn="primary" href="/signin">Sign in</a>`} {
102 if !strings.Contains(body, want) {
103 t.Errorf("the signed-out page lacks %s", want)
104 }
105 }
106}
107
108// A Home user whose admission is revoked while their session is live must
109// not bounce between "/" and "/signin" forever (Astra, #39 finding 2).
110// "/" sent them to /signin because identity refused them; connect's
111// /signin saw the still-live cookie and sent them back to "/". The root
112// now goes through the same guard as every signed-in page, which ends
113// the stale session, so the redirects terminate at a fresh sign-in.
114func TestARevokedHomeUserIsNotBouncedBetweenTheRootAndSignIn(t *testing.T) {
115 f := newHomeFixture(t)
116 member := homeSignedIn(t, f)
117 f.app.admit = func(string) bool { return false }
118 if err := f.app.db.Where("member_id = ?", member.ID).Delete(&Grant{}).Error; err != nil {
119 t.Fatal(err)
120 }
121 path, seen := "/", map[string]int{}
122 for hop := 0; ; hop++ {
123 if hop == 8 {
124 t.Fatalf("still redirecting after 8 hops: %v", seen)
125 }
126 seen[path]++
127 if seen[path] > 1 {
128 t.Fatalf("redirect loop: %s visited twice (%v)", path, seen)
129 }
130 rec := f.request(t, "GET", path, nil, false)
131 loc := rec.Header().Get("Location")
132 if rec.Code != http.StatusSeeOther || loc == "" {
133 break
134 }
135 u, err := url.Parse(loc)
136 if err != nil {
137 t.Fatal(err)
138 }
139 if u.Host != "" {
140 // Off to id.* for a fresh sign-in: the loop is over.
141 if u.Host != "id.example.com" {
142 t.Fatalf("left for %s", loc)
143 }
144 break
145 }
146 path = u.RequestURI()
147 }
148 if _, ok := f.jar[f.app.homeSessions.CookieName()]; ok {
149 t.Fatal("the revoked session survived")
150 }
151}
152
153// Astra's door-copy review: the account-connection start page's action
154// is the verb the page is about, "Connect", not a bare "Continue".
155func TestConnectStartPageActionSaysConnect(t *testing.T) {
156 f := newHomeFixture(t)
157 f.local(t, signedInAddress)
158 w := f.request(t, "GET", "/account/home", nil, false)
159 if w.Code != http.StatusOK {
160 t.Fatal(w.Code)
161 }
162 body := w.Body.String()
163 if !strings.Contains(body, "Connect your Oficina account") {
164 t.Fatal("not the connection start page")
165 }
166 if !strings.Contains(body, ">Connect</button>") {
167 t.Error("the start action does not say Connect")
168 }
169 if strings.Contains(body, ">Continue</button>") {
170 t.Error("the start action still says Continue")
171 }
172}
173