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 "context"
5 "net/http"
6 "net/http/httptest"
7 "path/filepath"
8 "sync"
9 "testing"
10
11 rdb "amadan.net/rastrillo/rastrillo/db"
12 "amadan.net/rastrillo/rastrillo/migrate"
13
14 "amadan.net/oficina/sheets/internal/sheets/seal"
15)
16
17// newTestApp builds an App over a real, migrated, throwaway database
18// and a deterministic all-zero DEK.
19//
20// Nil auth: these tests exercise storage, services and rendering, not
21// sign-in — appWithRecorder (begin_test.go) is the helper for that. A
22// nil auth means Router mounts no auth handlers and identity() answers
23// "nobody", which is why routes needing an actor take one explicitly in
24// these tests rather than reading a session.
25func newTestApp(t *testing.T) *App {
26 t.Helper()
27 d, err := rdb.Open(filepath.Join(t.TempDir(), "test.db"), quietLogger())
28 if err != nil {
29 t.Fatalf("db.Open: %v", err)
30 }
31 t.Cleanup(func() { d.Close() })
32 if _, err := migrate.Apply(context.Background(), d, BootSchema); err != nil {
33 t.Fatalf("migrate: %v", err)
34 }
35 s, err := seal.New(make([]byte, dekBytes))
36 if err != nil {
37 t.Fatalf("seal.New: %v", err)
38 }
39 return New(Config{DB: d.G, Sealer: s, Admit: func(string) bool { return true }})
40}
41
42// testMailers remembers which recorder belongs to which test App, so a
43// test holding only an *App can still read the mail it sent.
44//
45// A map rather than a field on App: the recorder is a test fixture and
46// App is production code, and threading a mailer through Config purely
47// so tests can reach it would put test scaffolding in the shipped
48// struct.
49var (
50 testMailersMu sync.Mutex
51 testMailers = map[*App]*recorder{}
52)
53
54func rememberMailer(app *App, rec *recorder) {
55 testMailersMu.Lock()
56 defer testMailersMu.Unlock()
57 testMailers[app] = rec
58}
59
60// testMailer returns the recorder behind this App's sign-in mail.
61func (a *App) testMailer(t *testing.T) *recorder {
62 t.Helper()
63 testMailersMu.Lock()
64 defer testMailersMu.Unlock()
65 rec, ok := testMailers[a]
66 if !ok {
67 t.Fatal("this App was not built by appWithOrigin, so no mail was recorded")
68 }
69 return rec
70}
71
72// signedInAddress is who signIn signs in as, and who appWithRecorder's
73// allowlist admits. Named once so a test helper and the fixture cannot
74// drift apart — which they did the moment team roles existed and a
75// helper listing as somebody else started seeing nothing.
76const signedInAddress = "paul@example.com"
77
78// signedInMember is the Member behind signIn's session.
79func signedInMember(t *testing.T, app *App) Member {
80 t.Helper()
81 m, err := app.memberFor(signedInAddress)
82 if err != nil {
83 t.Fatalf("memberFor %s: %v", signedInAddress, err)
84 }
85 return m
86}
87
88// testMember is the actor most service tests write as.
89func testMember(t *testing.T, app *App) Member {
90 t.Helper()
91 m, err := app.memberFor("ada@example.com")
92 if err != nil {
93 t.Fatalf("memberFor: %v", err)
94 }
95 return m
96}
97
98// signIn drives the real magic-link flow — ask for a link, take the one
99// that was actually emailed, open it and press the confirm door's Sign
100// in — and returns the app plus the
101// session cookies it minted.
102//
103// Driven end to end rather than faked, because a hand-minted session
104// cookie proves nothing about the flow a person actually walks, and
105// this helper is what every route behind requireSession is tested
106// through.
107func signIn(t *testing.T) (*App, []*http.Cookie) {
108 t.Helper()
109 app, mail := appWithRecorder(t)
110 return app, sessionFor(t, app, mail)
111}
112
113// signInWithOrigin is signIn against an app whose CSRF check is live.
114func signInWithOrigin(t *testing.T) (*App, []*http.Cookie) {
115 t.Helper()
116 app, mail := appWithOrigin(t, testOrigin)
117 return app, sessionFor(t, app, mail)
118}
119
120func sessionFor(t *testing.T, app *App, mail *recorder) []*http.Cookie {
121 t.Helper()
122 postSignin(t, app, "paul@example.com")
123
124 rec := followLink(t, app.Router(), mail.lastLink(t))
125 cookies := rec.Result().Cookies()
126 if len(cookies) == 0 {
127 t.Fatalf("verify set no cookie (status %d, location %q)",
128 rec.Code, rec.Header().Get("Location"))
129 }
130 return cookies
131}
132
133// as attaches a session to a request.
134func as(req *http.Request, cookies []*http.Cookie) *http.Request {
135 for _, c := range cookies {
136 req.AddCookie(c)
137 }
138 return req
139}
140
141// do runs one request through the app's router and hands back the
142// recorder, so a test reads as one line per request.
143func do(t *testing.T, app *App, req *http.Request) *httptest.ResponseRecorder {
144 t.Helper()
145 rec := httptest.NewRecorder()
146 app.Router().ServeHTTP(rec, req)
147 return rec
148}
149