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 "io"
5 "log/slog"
6 "strings"
7 "testing"
8)
9
10func quietLogger() *slog.Logger {
11 return slog.New(slog.NewTextHandler(io.Discard, nil))
12}
13
14// setEnv clears every variable LoadAuthEnv reads, then applies the given
15// ones. Clearing first matters: the process running these tests may have
16// real values in its environment, and a test that passed only because a
17// developer had SHEETS_ORIGIN exported would be worthless.
18func setEnv(t *testing.T, kv map[string]string) {
19 t.Helper()
20 for _, k := range []string{
21 "SHEETS_ORIGIN", "SHEETS_INSTANCE_KEY", "SHEETS_OWNERS",
22 "SHEETS_HOME_ISSUER", "SHEETS_HOME_CONSUMER_ID", "SHEETS_HOME_KEYS_BASE64", "SHEETS_HOME_DEK_BASE64",
23 "CARLOS_SMTP_HOST", "CARLOS_SMTP_FROM",
24 } {
25 t.Setenv(k, "")
26 }
27 for k, v := range kv {
28 t.Setenv(k, v)
29 }
30}
31
32func goodEnv() map[string]string {
33 return map[string]string{
34 "SHEETS_ORIGIN": "https://sheets.example.com",
35 "SHEETS_INSTANCE_KEY": "not-empty",
36 "SHEETS_OWNERS": "paul@example.com",
37 "CARLOS_SMTP_HOST": "smtp.example.com",
38 "CARLOS_SMTP_FROM": "noreply@sheets.example.com",
39 }
40}
41
42func TestLoadAuthEnvAcceptsCompleteConfig(t *testing.T) {
43 setEnv(t, goodEnv())
44 env, err := LoadAuthEnv(quietLogger())
45 if err != nil {
46 t.Fatalf("LoadAuthEnv: %v", err)
47 }
48 if env.Origin != "https://sheets.example.com" {
49 t.Errorf("Origin = %q", env.Origin)
50 }
51 if env.Mailer == nil {
52 t.Error("Mailer is nil")
53 }
54}
55
56// TestLoadAuthEnvRefusesEachMissingVar is the boot gate. Each of these
57// would otherwise produce an instance that serves pages and answers
58// /healthz while being unable to sign anyone in safely.
59func TestLoadAuthEnvRefusesEachMissingVar(t *testing.T) {
60 for _, missing := range []string{"SHEETS_ORIGIN", "SHEETS_INSTANCE_KEY", "SHEETS_OWNERS"} {
61 t.Run(missing, func(t *testing.T) {
62 env := goodEnv()
63 delete(env, missing)
64 setEnv(t, env)
65 _, err := LoadAuthEnv(quietLogger())
66 if err == nil {
67 t.Fatalf("missing %s: accepted, want refusal", missing)
68 }
69 if !strings.Contains(err.Error(), missing) {
70 t.Errorf("error does not name %s: %v", missing, err)
71 }
72 })
73 }
74}
75
76// TestLoadAuthEnvRefusesMissingMail: mail.FromEnv degrades to a logging
77// sender with a warning. The link is a live credential and the log is
78// platform-held storage, so degrading is worse than refusing.
79func TestLoadAuthEnvRefusesMissingMail(t *testing.T) {
80 for _, missing := range []string{"CARLOS_SMTP_HOST", "CARLOS_SMTP_FROM"} {
81 t.Run(missing, func(t *testing.T) {
82 env := goodEnv()
83 delete(env, missing)
84 setEnv(t, env)
85 if _, err := LoadAuthEnv(quietLogger()); err == nil {
86 t.Fatalf("missing %s: accepted, want refusal", missing)
87 }
88 })
89 }
90}
91
92// TestLoadAuthEnvAllowsLoggedMailOnLoopback pins the one relaxation, and
93// pins that it is bound to the origin rather than to a switch. A
94// deployed instance's origin is its public https host, so it cannot
95// reach this branch.
96func TestLoadAuthEnvAllowsLoggedMailOnLoopback(t *testing.T) {
97 for _, origin := range []string{"http://localhost:8080", "http://127.0.0.1:8080"} {
98 t.Run(origin, func(t *testing.T) {
99 env := goodEnv()
100 env["SHEETS_ORIGIN"] = origin
101 delete(env, "CARLOS_SMTP_HOST")
102 delete(env, "CARLOS_SMTP_FROM")
103 setEnv(t, env)
104 if _, err := LoadAuthEnv(quietLogger()); err != nil {
105 t.Fatalf("loopback origin with no SMTP: %v, want accepted", err)
106 }
107 })
108 }
109}
110
111// TestLoopbackIsNotPrefixSpoofable: the relaxation must not be reachable
112// by a hostname that merely starts with the loopback text.
113func TestLoopbackIsNotPrefixSpoofable(t *testing.T) {
114 for _, origin := range []string{
115 "https://localhost.attacker.example",
116 "http://127.0.0.1.attacker.example",
117 "http://localhost.attacker.example",
118 "http://127.0.0.1.example.com",
119 "http://notlocalhost",
120 } {
121 t.Run(origin, func(t *testing.T) {
122 env := goodEnv()
123 env["SHEETS_ORIGIN"] = origin
124 delete(env, "CARLOS_SMTP_HOST")
125 delete(env, "CARLOS_SMTP_FROM")
126 setEnv(t, env)
127 if _, err := LoadAuthEnv(quietLogger()); err == nil {
128 t.Errorf("%s reached the loopback relaxation", origin)
129 }
130 })
131 }
132}
133
134func TestAdmitsIsFailClosed(t *testing.T) {
135 allow := Admits(splitOwners("Paul@Example.com, other@example.com,"))
136 for _, tc := range []struct {
137 address string
138 want bool
139 }{
140 {"paul@example.com", true},
141 {"PAUL@EXAMPLE.COM", true},
142 {" paul@example.com ", true},
143 {"other@example.com", true},
144 {"stranger@example.com", false},
145 {"", false},
146 } {
147 if got := allow(tc.address); got != tc.want {
148 t.Errorf("Admits(%q) = %v, want %v", tc.address, got, tc.want)
149 }
150 }
151}
152
153// TestSplitOwnersDropsBlanks: a trailing comma or a stray space must not
154// put an empty string in the allowlist, which Admits("") would then let
155// through.
156func TestSplitOwnersDropsBlanks(t *testing.T) {
157 for _, raw := range []string{"", " ", ",", " , , "} {
158 if got := splitOwners(raw); len(got) != 0 {
159 t.Errorf("splitOwners(%q) = %v, want empty", raw, got)
160 }
161 }
162}
163