internal/config/config_test.go

32a5f76e5b270097b63a5bba9a43557cf50ad63d
gitbay/internal/config/config_test.go history · blame · raw

251 lines · 6498 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			"password auth in view_only",
 70			minimal + "\n[web]\nmode = \"view_only\"\npassword_auth = true\n",
 71			"password_auth",
 72		},
 73		{
 74			"password auth not implemented",
 75			minimal + "\n[web]\nmode = \"accounts\"\npassword_auth = true\n",
 76			"not implemented",
 77		},
 78		{
 79			"bad ssh mode",
 80			minimal + "\n[ssh]\nmode = \"tcp\"\n",
 81			"ssh.mode",
 82		},
 83		{
 84			"unknown key",
 85			"[server]\nroot = \"/var/lib/gitbay\"\nsite_url = \"https://gitbay.example\"\nbogus = 1\n",
 86			"unknown config key",
 87		},
 88		{
 89			"missing site_url",
 90			"[server]\nroot = \"/var/lib/gitbay\"\n",
 91			"site_url",
 92		},
 93		{
 94			"negative repo limit",
 95			minimal + "\n[limits]\nmax_repos_per_user = -1\n",
 96			"must not be negative",
 97		},
 98		{
 99			"negative snippet limit",
100			minimal + "\n[limits]\nmax_snippets_per_user = -1\n",
101			"max_snippets_per_user",
102		},
103	}
104	for _, tc := range cases {
105		t.Run(tc.name, func(t *testing.T) {
106			_, err := Load(writeConfig(t, tc.body))
107			if err == nil {
108				t.Fatalf("expected error containing %q, got nil", tc.wantErr)
109			}
110			if !strings.Contains(err.Error(), tc.wantErr) {
111				t.Fatalf("error %q does not contain %q", err, tc.wantErr)
112			}
113		})
114	}
115}
116
117func TestValidCombinations(t *testing.T) {
118	cases := []struct {
119		name string
120		body string
121	}{
122		{
123			"invite with smtp",
124			minimal + "\n[registration]\nmode = \"invite\"\n[mail]\nsmtp_host = \"mx.example\"\nfrom = \"gitbay@example\"\n",
125		},
126		{
127			"system ssh closed registration",
128			minimal + "\n[ssh]\nmode = \"system\"\n",
129		},
130		{
131			"accounts web without password auth",
132			minimal + "\n[web]\nmode = \"accounts\"\n",
133		},
134		{
135			"closed registration, no smtp at all",
136			minimal,
137		},
138		{
139			"acme with public https host",
140			"[server]\nroot = \"/var/lib/gitbay\"\nsite_url = \"https://gitbay.org\"\n[http]\ntls = \"acme\"\nacme_email = \"noreply@gitbay.org\"\n",
141		},
142	}
143	for _, tc := range cases {
144		t.Run(tc.name, func(t *testing.T) {
145			if _, err := Load(writeConfig(t, tc.body)); err != nil {
146				t.Fatal(err)
147			}
148		})
149	}
150}
151
152// writeP8 writes a PEM-wrapped PKCS#8 P-256 key, the shape of Apple's
153// .p8 provider key, and returns its path.
154func writeP8(t *testing.T) string {
155	t.Helper()
156	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
157	if err != nil {
158		t.Fatal(err)
159	}
160	der, err := x509.MarshalPKCS8PrivateKey(key)
161	if err != nil {
162		t.Fatal(err)
163	}
164	p := filepath.Join(t.TempDir(), "apns.p8")
165	f, err := os.Create(p)
166	if err != nil {
167		t.Fatal(err)
168	}
169	defer f.Close()
170	if err := pem.Encode(f, &pem.Block{Type: "PRIVATE KEY", Bytes: der}); err != nil {
171		t.Fatal(err)
172	}
173	return p
174}
175
176func TestPushConfigValidation(t *testing.T) {
177	keyPath := writeP8(t)
178	full := `
179[push]
180enabled = true
181key_file = "` + keyPath + `"
182key_id = "KEYID"
183team_id = "TEAMID"
184topic = "org.gitbay.gitbay"
185environment = "production"
186`
187	cases := []struct {
188		name string
189		body string
190		want string // substring of the expected error; "" means valid
191	}{
192		{"disabled needs nothing", "\n[push]\nenabled = false\n", ""},
193		{"complete is valid", full, ""},
194		{"key_id required", strings.Replace(full, `key_id = "KEYID"`, "", 1), "push.key_id"},
195		{"team_id required", strings.Replace(full, `team_id = "TEAMID"`, "", 1), "push.team_id"},
196		{"topic required", strings.Replace(full, `topic = "org.gitbay.gitbay"`, "", 1), "push.topic"},
197		{"environment must be a known name",
198			strings.Replace(full, `environment = "production"`, `environment = "staging"`, 1),
199			"push.environment"},
200	}
201	for _, tc := range cases {
202		t.Run(tc.name, func(t *testing.T) {
203			_, err := Load(writeConfig(t, minimal+tc.body))
204			if tc.want == "" {
205				if err != nil {
206					t.Fatalf("want valid, got %v", err)
207				}
208				return
209			}
210			if err == nil || !strings.Contains(err.Error(), tc.want) {
211				t.Fatalf("want an error mentioning %q, got %v", tc.want, err)
212			}
213		})
214	}
215}
216
217// A key_file that exists but is not a PKCS#8 EC key is refused at load,
218// not at the first notice: the failure mode otherwise is a queue that
219// fills and dead-letters with nobody watching.
220func TestPushConfigRejectsAnUnparseableKey(t *testing.T) {
221	p := filepath.Join(t.TempDir(), "junk.p8")
222	if err := os.WriteFile(p, []byte("not a key\n"), 0o600); err != nil {
223		t.Fatal(err)
224	}
225	body := `
226[push]
227enabled = true
228key_file = "` + p + `"
229key_id = "K"
230team_id = "T"
231topic = "org.gitbay.gitbay"
232environment = "production"
233`
234	_, err := Load(writeConfig(t, minimal+body))
235	if err == nil || !strings.Contains(err.Error(), "push.key_file") {
236		t.Fatalf("want a push.key_file error, got %v", err)
237	}
238}
239
240func TestPushHost(t *testing.T) {
241	if got := (Push{Environment: "production"}).Host(); got != "api.push.apple.com" {
242		t.Fatalf("production host = %q", got)
243	}
244	if got := (Push{Environment: "sandbox"}).Host(); got != "api.sandbox.push.apple.com" {
245		t.Fatalf("sandbox host = %q", got)
246	}
247	t.Setenv("GITBAY_APNS_HOST", "127.0.0.1:1234")
248	if got := (Push{Environment: "production"}).Host(); got != "127.0.0.1:1234" {
249		t.Fatalf("GITBAY_APNS_HOST ignored: %q", got)
250	}
251}