internal/config/config_test.go
277 lines · 7314 bytes
1package config
2
3import (
4 "crypto/ecdsa"
5 "crypto/elliptic"
6 "crypto/rand"
7 "crypto/x509"
8 "encoding/pem"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13)
14
15func writeConfig(t *testing.T, body string) string {
16 t.Helper()
17 p := filepath.Join(t.TempDir(), "config.toml")
18 if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
19 t.Fatal(err)
20 }
21 return p
22}
23
24const minimal = `
25[server]
26root = "/var/lib/gitbay"
27site_url = "https://gitbay.example"
28`
29
30func TestLoadMinimal(t *testing.T) {
31 cfg, err := Load(writeConfig(t, minimal))
32 if err != nil {
33 t.Fatal(err)
34 }
35 // Defaults applied.
36 if cfg.SSH.Mode != "embedded" || cfg.SSH.Port != 22 {
37 t.Errorf("ssh defaults wrong: %+v", cfg.SSH)
38 }
39 if cfg.Web.Mode != "view_only" {
40 t.Errorf("web default wrong: %+v", cfg.Web)
41 }
42 if cfg.Registration.Mode != "closed" {
43 t.Errorf("registration default wrong: %+v", cfg.Registration)
44 }
45}
46
47func TestContradictions(t *testing.T) {
48 cases := []struct {
49 name string
50 body string
51 wantErr string
52 }{
53 {
54 "registration open without smtp",
55 minimal + "\n[registration]\nmode = \"open\"\n",
56 "requires [mail] smtp_host",
57 },
58 {
59 "notify_admin without smtp",
60 minimal + "\n[registration]\nnotify_admin = true\n",
61 "notify_admin = true requires [mail] smtp_host",
62 },
63 {
64 "system ssh with open registration",
65 minimal + "\n[ssh]\nmode = \"system\"\n[registration]\nmode = \"open\"\n[mail]\nsmtp_host = \"mx.example\"\nfrom = \"gitbay@example\"\n",
66 "requires registration.mode = \"closed\"",
67 },
68 {
69 "unknown mail.tls",
70 minimal + "\n[mail]\nsmtp_host = \"mx.example\"\nfrom = \"gitbay@example\"\ntls = \"ssl\"\n",
71 "mail.tls must be starttls or implicit",
72 },
73 {
74 "password auth in view_only",
75 minimal + "\n[web]\nmode = \"view_only\"\npassword_auth = true\n",
76 "password_auth",
77 },
78 {
79 "password auth not implemented",
80 minimal + "\n[web]\nmode = \"accounts\"\npassword_auth = true\n",
81 "not implemented",
82 },
83 {
84 "bad ssh mode",
85 minimal + "\n[ssh]\nmode = \"tcp\"\n",
86 "ssh.mode",
87 },
88 {
89 "unknown key",
90 "[server]\nroot = \"/var/lib/gitbay\"\nsite_url = \"https://gitbay.example\"\nbogus = 1\n",
91 "unknown config key",
92 },
93 {
94 "missing site_url",
95 "[server]\nroot = \"/var/lib/gitbay\"\n",
96 "site_url",
97 },
98 {
99 "negative repo limit",
100 minimal + "\n[limits]\nmax_repos_per_user = -1\n",
101 "must not be negative",
102 },
103 {
104 "negative snippet limit",
105 minimal + "\n[limits]\nmax_snippets_per_user = -1\n",
106 "max_snippets_per_user",
107 },
108 }
109 for _, tc := range cases {
110 t.Run(tc.name, func(t *testing.T) {
111 _, err := Load(writeConfig(t, tc.body))
112 if err == nil {
113 t.Fatalf("expected error containing %q, got nil", tc.wantErr)
114 }
115 if !strings.Contains(err.Error(), tc.wantErr) {
116 t.Fatalf("error %q does not contain %q", err, tc.wantErr)
117 }
118 })
119 }
120}
121
122func TestValidCombinations(t *testing.T) {
123 cases := []struct {
124 name string
125 body string
126 }{
127 {
128 "invite with smtp",
129 minimal + "\n[registration]\nmode = \"invite\"\n[mail]\nsmtp_host = \"mx.example\"\nfrom = \"gitbay@example\"\n",
130 },
131 {
132 "system ssh closed registration",
133 minimal + "\n[ssh]\nmode = \"system\"\n",
134 },
135 {
136 "accounts web without password auth",
137 minimal + "\n[web]\nmode = \"accounts\"\n",
138 },
139 {
140 "closed registration, no smtp at all",
141 minimal,
142 },
143 {
144 "acme with public https host",
145 "[server]\nroot = \"/var/lib/gitbay\"\nsite_url = \"https://gitbay.org\"\n[http]\ntls = \"acme\"\nacme_email = \"noreply@gitbay.org\"\n",
146 },
147 }
148 for _, tc := range cases {
149 t.Run(tc.name, func(t *testing.T) {
150 if _, err := Load(writeConfig(t, tc.body)); err != nil {
151 t.Fatal(err)
152 }
153 })
154 }
155}
156
157// writeP8 writes a PEM-wrapped PKCS#8 P-256 key, the shape of Apple's
158// .p8 provider key, and returns its path.
159func writeP8(t *testing.T) string {
160 t.Helper()
161 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
162 if err != nil {
163 t.Fatal(err)
164 }
165 der, err := x509.MarshalPKCS8PrivateKey(key)
166 if err != nil {
167 t.Fatal(err)
168 }
169 p := filepath.Join(t.TempDir(), "apns.p8")
170 f, err := os.Create(p)
171 if err != nil {
172 t.Fatal(err)
173 }
174 defer f.Close()
175 if err := pem.Encode(f, &pem.Block{Type: "PRIVATE KEY", Bytes: der}); err != nil {
176 t.Fatal(err)
177 }
178 return p
179}
180
181func TestPushConfigValidation(t *testing.T) {
182 keyPath := writeP8(t)
183 full := `
184[push]
185enabled = true
186key_file = "` + keyPath + `"
187key_id = "KEYID"
188team_id = "TEAMID"
189topic = "org.gitbay.gitbay"
190environment = "production"
191`
192 cases := []struct {
193 name string
194 body string
195 want string // substring of the expected error; "" means valid
196 }{
197 {"disabled needs nothing", "\n[push]\nenabled = false\n", ""},
198 {"complete is valid", full, ""},
199 {"key_id required", strings.Replace(full, `key_id = "KEYID"`, "", 1), "push.key_id"},
200 {"team_id required", strings.Replace(full, `team_id = "TEAMID"`, "", 1), "push.team_id"},
201 {"topic required", strings.Replace(full, `topic = "org.gitbay.gitbay"`, "", 1), "push.topic"},
202 {"environment must be a known name",
203 strings.Replace(full, `environment = "production"`, `environment = "staging"`, 1),
204 "push.environment"},
205 }
206 for _, tc := range cases {
207 t.Run(tc.name, func(t *testing.T) {
208 _, err := Load(writeConfig(t, minimal+tc.body))
209 if tc.want == "" {
210 if err != nil {
211 t.Fatalf("want valid, got %v", err)
212 }
213 return
214 }
215 if err == nil || !strings.Contains(err.Error(), tc.want) {
216 t.Fatalf("want an error mentioning %q, got %v", tc.want, err)
217 }
218 })
219 }
220}
221
222// A key_file that exists but is not a PKCS#8 EC key is refused at load,
223// not at the first notice: the failure mode otherwise is a queue that
224// fills and dead-letters with nobody watching.
225func TestPushConfigRejectsAnUnparseableKey(t *testing.T) {
226 p := filepath.Join(t.TempDir(), "junk.p8")
227 if err := os.WriteFile(p, []byte("not a key\n"), 0o600); err != nil {
228 t.Fatal(err)
229 }
230 body := `
231[push]
232enabled = true
233key_file = "` + p + `"
234key_id = "K"
235team_id = "T"
236topic = "org.gitbay.gitbay"
237environment = "production"
238`
239 _, err := Load(writeConfig(t, minimal+body))
240 if err == nil || !strings.Contains(err.Error(), "push.key_file") {
241 t.Fatalf("want a push.key_file error, got %v", err)
242 }
243}
244
245func TestPushHost(t *testing.T) {
246 if got := (Push{Environment: "production"}).Host(); got != "api.push.apple.com" {
247 t.Fatalf("production host = %q", got)
248 }
249 if got := (Push{Environment: "sandbox"}).Host(); got != "api.sandbox.push.apple.com" {
250 t.Fatalf("sandbox host = %q", got)
251 }
252 t.Setenv("GITBAY_APNS_HOST", "127.0.0.1:1234")
253 if got := (Push{Environment: "production"}).Host(); got != "127.0.0.1:1234" {
254 t.Fatalf("GITBAY_APNS_HOST ignored: %q", got)
255 }
256}
257
258func TestMailTLSRequired(t *testing.T) {
259 off, on := false, true
260 for _, tc := range []struct {
261 m Mail
262 want bool
263 }{
264 {Mail{SMTPHost: "smtp.example.com:587"}, true},
265 {Mail{SMTPHost: "smtp.example.com"}, true},
266 {Mail{SMTPHost: "localhost:25"}, false},
267 {Mail{SMTPHost: "localhost"}, false},
268 {Mail{SMTPHost: "127.0.0.1:25"}, false},
269 {Mail{SMTPHost: "[::1]:25"}, false},
270 {Mail{SMTPHost: "smtp.example.com:587", RequireTLS: &off}, false},
271 {Mail{SMTPHost: "127.0.0.1:25", RequireTLS: &on}, true},
272 } {
273 if got := tc.m.TLSRequired(); got != tc.want {
274 t.Errorf("%+v: TLSRequired = %v, want %v", tc.m, got, tc.want)
275 }
276 }
277}