accounts/helpers_test.go (2881 bytes)
1 package main 2 3 import ( 4 "errors" 5 "io" 6 "log" 7 "os" 8 "path/filepath" 9 "strings" 10 "sync" 11 "testing" 12 "time" 13 ) 14 15 // captureMailer keeps each message. If fail is true, Send returns an error 16 // and keeps nothing. 17 type captureMailer struct { 18 mu sync.Mutex 19 sent []message 20 fail bool 21 } 22 23 func (c *captureMailer) Send(m message) error { 24 c.mu.Lock() 25 defer c.mu.Unlock() 26 if c.fail { 27 return errors.New("smtp is down") 28 } 29 c.sent = append(c.sent, m) 30 return nil 31 } 32 33 // clock is a time source that a test moves forward. 34 type clock struct{ t time.Time } 35 36 func (c *clock) now() time.Time { return c.t } 37 38 type testEnv struct { 39 app *app 40 mail *captureMailer 41 clock *clock 42 } 43 44 // fataler is the part of testing.TB that the helpers use. *rapid.T has 45 // these methods too. 46 type fataler interface { 47 Helper() 48 Fatal(args ...any) 49 Fatalf(format string, args ...any) 50 } 51 52 // newTestEnv makes an app with a :memory: database and a site directory 53 // that has the posts in index. 54 func newTestEnv(t *testing.T, index []Post) *testEnv { 55 t.Helper() 56 db, err := openDB(":memory:") 57 if err != nil { 58 t.Fatal(err) 59 } 60 t.Cleanup(func() { db.Close() }) 61 site := t.TempDir() 62 if err := os.MkdirAll(filepath.Join(site, "postdata"), 0o755); err != nil { 63 t.Fatal(err) 64 } 65 e := &testEnv{ 66 mail: &captureMailer{}, 67 clock: &clock{t: time.Date(2026, 9, 1, 12, 0, 0, 0, time.UTC)}, 68 } 69 e.app = &app{ 70 cfg: config{BaseURL: "https://example.com", SiteDir: site}, 71 db: db, 72 mail: e.mail, 73 log: log.New(io.Discard, "", 0), 74 } 75 e.app.now = e.clock.now 76 e.writeIndex(t, index) 77 return e 78 } 79 80 // writeIndex writes postdata/index.tsv and a body for each post, as 81 // build.sh does. 82 func (e *testEnv) writeIndex(t fataler, posts []Post) { 83 t.Helper() 84 var b strings.Builder 85 for _, p := range posts { 86 b.WriteString(p.Date + "\t" + p.Slug + "\t" + strings.Join(p.Tags, " ") + "\t" + p.Title + "\n") 87 body := "<p>Body of " + p.Slug + " & more.</p>\n" 88 if err := os.WriteFile(filepath.Join(e.app.cfg.SiteDir, "postdata", p.Slug+".html"), []byte(body), 0o644); err != nil { 89 t.Fatal(err) 90 } 91 } 92 if err := os.WriteFile(filepath.Join(e.app.cfg.SiteDir, "postdata", "index.tsv"), []byte(b.String()), 0o644); err != nil { 93 t.Fatal(err) 94 } 95 } 96 97 // signUp makes a verified user for email and returns it. 98 func (e *testEnv) signUp(t fataler, email string) user { 99 t.Helper() 100 tok, err := createLoginToken(e.app.db, email, e.clock.now()) 101 if err != nil || tok == "" { 102 t.Fatalf("createLoginToken: %q, %v", tok, err) 103 } 104 u, err := consumeLoginToken(e.app.db, tok, e.clock.now()) 105 if err != nil { 106 t.Fatal(err) 107 } 108 return u 109 } 110 111 func (e *testEnv) advance(d time.Duration) { e.clock.t = e.clock.t.Add(d) } 112 113 var oldPosts = []Post{ 114 {Date: "2025-09-11", Slug: "old-article", Tags: []string{"articles"}, Title: "Old article"}, 115 {Date: "2025-09-06", Slug: "old-project", Tags: []string{"projects"}, Title: "Old project"}, 116 }