oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1package slides
2
3import (
4 "io"
5 "log/slog"
6 "path/filepath"
7 "testing"
8
9 "amadan.net/rastrillo/rastrillo/db"
10 "amadan.net/rastrillo/rastrillo/mail"
11)
12
13// quietLogger keeps the suite's output pristine: newApp logs one
14// migrate line per call, which is right in a real boot and noise here.
15func quietLogger() *slog.Logger {
16 return slog.New(slog.NewTextHandler(io.Discard, nil))
17}
18
19func testDB(t *testing.T) *db.DB {
20 t.Helper()
21 d, err := db.Open(filepath.Join(t.TempDir(), "app.db"), quietLogger())
22 if err != nil {
23 t.Fatalf("db.Open: %v", err)
24 }
25 t.Cleanup(func() { d.Close() })
26 return d
27}
28
29// TestDevelopmentStoresTheResolvedConfig is the regression test for a
30// real bug: New resolved Origin/InstanceKey/Mailer into locals, handed
31// those to the auth plugin, and then stored the caller's RAW Config on
32// the app. So in development a.cfg.Origin was "" while the app was
33// protecting http://localhost:8091, and a.cfg.Mailer was nil while
34// mail was going to the log — the app's own description of itself was
35// false, and the first later task to read it would have been misled.
36//
37// Package-internal because a.cfg is unexported and this is precisely a
38// question about what New stored, which the *http.ServeMux New returns
39// cannot answer.
40func TestDevelopmentStoresTheResolvedConfig(t *testing.T) {
41 a, _, err := newApp(testDB(t), Config{Env: EnvDevelopment, Logger: quietLogger()})
42 if err != nil {
43 t.Fatalf("newApp: %v", err)
44 }
45 t.Cleanup(func() { a.Close() })
46 if a.cfg.Origin != DefaultOrigin {
47 t.Errorf("a.cfg.Origin = %q, want the resolved %q", a.cfg.Origin, DefaultOrigin)
48 }
49 if a.cfg.InstanceKey != devInstanceKey {
50 t.Errorf("a.cfg.InstanceKey = %q, want the resolved %q", a.cfg.InstanceKey, devInstanceKey)
51 }
52 if a.cfg.Mailer == nil {
53 t.Error("a.cfg.Mailer is nil; want the resolved development sender")
54 } else if mail.IsConfigured(a.cfg.Mailer) {
55 t.Error("a.cfg.Mailer delivers for real in development; want mail.Logged")
56 }
57 if a.cfg.Env != EnvDevelopment {
58 t.Errorf("a.cfg.Env = %q, want %q", a.cfg.Env, EnvDevelopment)
59 }
60 if a.cfg.Logger == nil {
61 t.Error("a.cfg.Logger is nil; resolve must never leave it unset")
62 }
63 // The allowlist that actually admits, beside the raw list that was
64 // configured: development's empty list means "admit everyone".
65 if !a.allowed.allows("anyone@example.test") {
66 t.Error("development allowlist refused an address; want admit-everyone")
67 }
68}
69
70// TestSuppliedConfigSurvivesResolution is the other direction: what a
71// caller did set must come back unchanged, trimmed but not replaced.
72func TestSuppliedConfigSurvivesResolution(t *testing.T) {
73 given := Config{
74 Origin: " https://slides.example.test ",
75 Env: EnvProduction,
76 InstanceKey: "a-real-key",
77 Mailer: mail.SMTP("smtp.example.test", "", "slides@example.test", "", ""),
78 AllowedAddresses: []string{"Paul@Example.Test "},
79 Logger: quietLogger(),
80 }
81 got, list, err := resolve(given)
82 if err != nil {
83 t.Fatalf("resolve: %v", err)
84 }
85 if got.Origin != "https://slides.example.test" {
86 t.Errorf("Origin = %q, want it trimmed and kept", got.Origin)
87 }
88 if got.InstanceKey != given.InstanceKey {
89 t.Errorf("InstanceKey = %q, want %q", got.InstanceKey, given.InstanceKey)
90 }
91 if got.Mailer == nil {
92 t.Error("Mailer was dropped")
93 }
94 // The allowlist normalizes case and whitespace on both sides, so a
95 // config file with spaces after its commas and a person typing
96 // capitals both work.
97 if !list.allows("paul@example.test") {
98 t.Error("allowlist refused the normalized form of a configured address")
99 }
100 if list.allows("someone@else.test") {
101 t.Error("allowlist admitted an address nobody configured")
102 }
103}
104
105// TestProductionRefusesEachMissingSetting pins the fail-closed rule at
106// the resolve level, one setting at a time, so a regression names which
107// one stopped refusing. The HTTP-level counterpart lives in
108// internal/slidestest.
109func TestProductionRefusesEachMissingSetting(t *testing.T) {
110 full := func() Config {
111 return Config{
112 Origin: "https://slides.example.test",
113 Env: EnvProduction,
114 InstanceKey: "a-real-key",
115 Mailer: mail.SMTP("smtp.example.test", "", "slides@example.test", "", ""),
116 AllowedAddresses: []string{"paul@example.test"},
117 Logger: quietLogger(),
118 }
119 }
120 for _, tc := range []struct {
121 name string
122 bend func(*Config)
123 }{
124 {"Origin", func(c *Config) { c.Origin = "" }},
125 {"Origin whitespace only", func(c *Config) { c.Origin = " " }},
126 {"Origin with a path", func(c *Config) { c.Origin = "https://slides.example.test/app" }},
127 {"Origin with no host", func(c *Config) { c.Origin = "http://:8091" }},
128 {"InstanceKey", func(c *Config) { c.InstanceKey = "" }},
129 {"Mailer", func(c *Config) { c.Mailer = nil }},
130 {"AllowedAddresses", func(c *Config) { c.AllowedAddresses = nil }},
131 {"AllowedAddresses all blank", func(c *Config) { c.AllowedAddresses = []string{"", " "} }},
132 } {
133 t.Run(tc.name, func(t *testing.T) {
134 cfg := full()
135 tc.bend(&cfg)
136 if _, _, err := resolve(cfg); err == nil {
137 t.Fatalf("resolve with %s bent returned no error; want a startup refusal", tc.name)
138 }
139 })
140 }
141}
142