aboutsummaryrefslogtreecommitdiff
path: root/config_test.go
diff options
context:
space:
mode:
authorLena <lena@omega>2026-08-01 00:00:00 +0000
committerLena <lena@omega>2026-08-01 00:00:00 +0000
commitbb6bc2387d58c737dbf92e73f8544bf39319dbd8 (patch)
tree6d5eaf5dac213074cd832da50f64260fce728fed /config_test.go
downloadgit-forge-master.tar.gz
Enter git-forgeHEADmaster
A small self-hosted git forge: HTTP push and pull, cgit browsing, local and OIDC accounts, TOTP, scoped push tokens, per-repo permissions, issues, a wiki, and outbound mail. It does not reimplement git. git-http-backend and cgit run as CGI behind its own listener, which authorises every request before exec'ing either. One binary, one data directory, one listener: no database, no JavaScript, and state is JSON in a git repo with a working tree.
Diffstat (limited to 'config_test.go')
-rw-r--r--config_test.go120
1 files changed, 120 insertions, 0 deletions
diff --git a/config_test.go b/config_test.go
new file mode 100644
index 0000000..fe7406d
--- /dev/null
+++ b/config_test.go
@@ -0,0 +1,120 @@
+package main
+
+import (
+ "strings"
+ "testing"
+)
+
+// Startup refuses an incoherent file rather than guessing, and reports
+// everything wrong at once. What each rule catches matters most for the ones
+// that would otherwise fail silently much later: an allow entry that can never
+// match is a lockout, and a bad issuer is a login that never works.
+func TestConfigValidate(t *testing.T) {
+ provider := OIDCProvider{Key: "sso", Label: "SSO", Issuer: "https://id.example", ClientID: "forge"}
+ cases := []struct {
+ what string
+ spoil func(*Config)
+ want string
+ }{
+ {"a good file", func(*Config) {}, ""},
+ {"no listener", func(c *Config) { c.Listen = "" }, "listen"},
+ {"no name", func(c *Config) { c.SiteName = "" }, "site_name"},
+ {"a quote in the name", func(c *Config) { c.SiteName = `a"b` }, "site_name"},
+ {"a header name with a colon", func(c *Config) { c.ClientIPHeader = "X-Real-IP:" }, "client_ip_header"},
+ {"a base url with a path", func(c *Config) { c.BaseURL = "https://git.example/forge" }, "base_url"},
+ {"a base url with no scheme", func(c *Config) { c.BaseURL = "git.example" }, "base_url"},
+ {"an oidc key that prefixes nothing usable", func(c *Config) {
+ p := provider
+ p.Key = "SSO"
+ c.OIDC = []OIDCProvider{p}
+ }, "key"},
+ {"two providers under one key", func(c *Config) { c.OIDC = []OIDCProvider{provider, provider} }, "duplicate"},
+ {"a provider with no label", func(c *Config) {
+ p := provider
+ p.Label = ""
+ c.OIDC = []OIDCProvider{p}
+ }, "label"},
+ {"an issuer that is not https", func(c *Config) {
+ p := provider
+ p.Issuer = "http://id.example"
+ c.OIDC = []OIDCProvider{p}
+ }, "issuer"},
+ {"an issuer with a trailing slash", func(c *Config) {
+ p := provider
+ p.Issuer = "https://id.example/"
+ c.OIDC = []OIDCProvider{p}
+ }, "trailing slash"},
+ {"an allow entry that is neither", func(c *Config) {
+ p := provider
+ p.Allow = []string{"everyone"}
+ c.OIDC = []OIDCProvider{p}
+ }, "allow[0]"},
+ {"an allow entry that is a bare at", func(c *Config) {
+ p := provider
+ p.Allow = []string{"@"}
+ c.OIDC = []OIDCProvider{p}
+ }, "allow[0]"},
+ {"an allow entry padded with spaces", func(c *Config) {
+ p := provider
+ p.Allow = []string{" carol@example.com"}
+ c.OIDC = []OIDCProvider{p}
+ }, "allow[0]"},
+ {"a good provider", func(c *Config) {
+ p := provider
+ p.Allow = []string{"carol@example.com", "@example.com"}
+ c.OIDC = []OIDCProvider{p}
+ }, ""},
+ {"no sender", func(c *Config) { c.Mail.From = "" }, "mail.from"},
+ {"a driver that is neither", func(c *Config) { c.Mail.Driver = "sendmail" }, "mail.driver"},
+ {"mailgun with no domain", func(c *Config) { c.Mail.Driver = "mailgun" }, "mailgun.domain"},
+ {"mailgun with no key in the environment", func(c *Config) {
+ c.Mail.Driver = "mailgun"
+ c.Mail.Mailgun.Domain = "mg.example"
+ }, "MAILGUN_API_KEY"},
+ }
+ for _, c := range cases {
+ cfg := defaultConfig()
+ c.spoil(cfg)
+ err := cfg.validate()
+ switch {
+ case c.want == "" && err != nil:
+ t.Errorf("%s was refused: %v", c.what, err)
+ case c.want != "" && err == nil:
+ t.Errorf("%s was accepted", c.what)
+ case c.want != "" && !strings.Contains(err.Error(), c.want):
+ t.Errorf("%s was refused for the wrong reason: %v", c.what, err)
+ }
+ }
+}
+
+// Everything wrong at once, because fixing a config one restart at a time is
+// miserable.
+func TestConfigReportsEveryFault(t *testing.T) {
+ cfg := defaultConfig()
+ cfg.Listen, cfg.SiteName, cfg.BaseURL, cfg.Mail.From = "", "", "ftp://x", ""
+ err := cfg.validate()
+ if err == nil {
+ t.Fatal("a file with four faults in it was accepted")
+ }
+ for _, want := range []string{"listen", "site_name", "base_url", "mail.from"} {
+ if !strings.Contains(err.Error(), want) {
+ t.Errorf("%q is not in the report:\n%v", want, err)
+ }
+ }
+}
+
+// The host is what generated addresses are built from, so the port has to come
+// off and a trailing slash must not survive into it.
+func TestConfigBaseURLIsNormalised(t *testing.T) {
+ cfg := defaultConfig()
+ cfg.BaseURL = "https://git.example.com:8443/"
+ if err := cfg.validate(); err != nil {
+ t.Fatal(err)
+ }
+ if cfg.BaseURL != "https://git.example.com:8443" {
+ t.Errorf("base_url is %q", cfg.BaseURL)
+ }
+ if cfg.host != "git.example.com" {
+ t.Errorf("addresses would be built from %q", cfg.host)
+ }
+}