internal/config/config_test.go

0338e6ace3de199d5fc383852649919b68ef3e42
gitbay/internal/config/config_test.go history · blame · raw

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}