1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package config_test
4
5import (
6 "crypto/sha256"
7 "errors"
8 "net/netip"
9 "strings"
10 "testing"
11
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"
14
15 "bnema.dev/ermite/internal/config"
16)
17
18const token = "0123456789abcdef0123456789abcdef" // 32 chars
19
20func lookup(env map[string]string) func(string) string {
21 return func(k string) string { return env[k] }
22}
23
24func valid(overrides map[string]string) map[string]string {
25 env := map[string]string{
26 config.EnvDataDir: "/var/lib/ermite",
27 config.EnvPublicURL: "https://git.example.com",
28 config.EnvAdminToken: token,
29 }
30 for k, v := range overrides {
31 if v == "\x00unset" {
32 delete(env, k)
33 continue
34 }
35 env[k] = v
36 }
37 return env
38}
39
40func TestLoadDefaults(t *testing.T) {
41 c, err := config.Load(lookup(valid(nil)))
42 require.NoError(t, err)
43 assert.Equal(t, ":7341", c.Addr)
44 assert.Equal(t, "/var/lib/ermite", c.DataDir)
45 assert.Equal(t, "https://git.example.com", c.PublicURL)
46 assert.Equal(t, "git", c.Git)
47 assert.Equal(t, "https://git.example.com/ermite", c.SourceURL)
48 assert.Equal(t, "info", c.LogLevel)
49 assert.Equal(t, "json", c.LogFormat)
50 assert.Empty(t, c.TrustedProxies)
51 assert.Equal(t, sha256.Sum256([]byte(token)), c.AdminTokenHash)
52}
53
54func TestLoadAllValues(t *testing.T) {
55 c, err := config.Load(lookup(valid(map[string]string{
56 config.EnvAddr: "127.0.0.1:8080",
57 config.EnvDataDir: "/srv/../srv/data/",
58 config.EnvPublicURL: "http://localhost:7341/",
59 config.EnvTrustedProxies: "10.1.2.3/8, fd00::1/8",
60 config.EnvGit: "/usr/local/bin/git",
61 config.EnvSourceURL: "https://forge.example.org/src/",
62 config.EnvLogLevel: "DEBUG",
63 config.EnvLogFormat: "Console",
64 })))
65 require.NoError(t, err)
66 assert.Equal(t, "127.0.0.1:8080", c.Addr)
67 assert.Equal(t, "/srv/data", c.DataDir)
68 assert.Equal(t, "http://localhost:7341", c.PublicURL)
69 assert.Equal(t, []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8"), netip.MustParsePrefix("fd00::/8")}, c.TrustedProxies)
70 assert.Equal(t, "/usr/local/bin/git", c.Git)
71 assert.Equal(t, "https://forge.example.org/src", c.SourceURL)
72 assert.Equal(t, "debug", c.LogLevel)
73 assert.Equal(t, "console", c.LogFormat)
74}
75
76func TestLoadSourceURLFollowsPublicURL(t *testing.T) {
77 c, err := config.Load(lookup(valid(map[string]string{config.EnvPublicURL: "http://127.0.0.1:7341/"})))
78 require.NoError(t, err)
79 assert.Equal(t, "http://127.0.0.1:7341/ermite", c.SourceURL)
80}
81
82func TestLoadProblems(t *testing.T) {
83 tests := []struct {
84 name string
85 env map[string]string
86 want string // variable named in the error
87 msg string // fragment of its message
88 }{
89 {"missing data dir", map[string]string{config.EnvDataDir: "\x00unset"}, config.EnvDataDir, "required"},
90 {"empty data dir", map[string]string{config.EnvDataDir: ""}, config.EnvDataDir, "required"},
91 {"relative data dir", map[string]string{config.EnvDataDir: "data"}, config.EnvDataDir, "absolute"},
92 {"missing public url", map[string]string{config.EnvPublicURL: "\x00unset"}, config.EnvPublicURL, "required"},
93 {"public url scheme", map[string]string{config.EnvPublicURL: "ftp://x.org"}, config.EnvPublicURL, "scheme"},
94 {"public url no scheme", map[string]string{config.EnvPublicURL: "git.example.com"}, config.EnvPublicURL, "scheme"},
95 {"public url no host", map[string]string{config.EnvPublicURL: "https://"}, config.EnvPublicURL, "host"},
96 {"public url path", map[string]string{config.EnvPublicURL: "https://x.org/forge"}, config.EnvPublicURL, "path"},
97 {"public url query", map[string]string{config.EnvPublicURL: "https://x.org?a=b"}, config.EnvPublicURL, "query"},
98 {"public url fragment", map[string]string{config.EnvPublicURL: "https://x.org#top"}, config.EnvPublicURL, "fragment"},
99 {"public url credentials", map[string]string{config.EnvPublicURL: "https://u:p@x.org"}, config.EnvPublicURL, "credentials"},
100 {"missing token", map[string]string{config.EnvAdminToken: "\x00unset"}, config.EnvAdminToken, "required"},
101 {"short token", map[string]string{config.EnvAdminToken: strings.Repeat("a", 31)}, config.EnvAdminToken, "at least 32"},
102 {"bad addr", map[string]string{config.EnvAddr: "7341"}, config.EnvAddr, "host:port"},
103 {"bad port", map[string]string{config.EnvAddr: ":http"}, config.EnvAddr, "port"},
104 {"port zero", map[string]string{config.EnvAddr: ":0"}, config.EnvAddr, "port"},
105 {"port too big", map[string]string{config.EnvAddr: ":70000"}, config.EnvAddr, "port"},
106 {"bad cidr", map[string]string{config.EnvTrustedProxies: "10.0.0.1"}, config.EnvTrustedProxies, "CIDR"},
107 {"empty cidr entry", map[string]string{config.EnvTrustedProxies: "10.0.0.0/8,,"}, config.EnvTrustedProxies, "empty"},
108 {"bad source url", map[string]string{config.EnvSourceURL: "nope"}, config.EnvSourceURL, "scheme"},
109 {"bad level", map[string]string{config.EnvLogLevel: "loud"}, config.EnvLogLevel, "trace, debug"},
110 {"bad format", map[string]string{config.EnvLogFormat: "xml"}, config.EnvLogFormat, "json or console"},
111 }
112 for _, tt := range tests {
113 t.Run(tt.name, func(t *testing.T) {
114 _, err := config.Load(lookup(valid(tt.env)))
115 require.Error(t, err)
116 var ce *config.Error
117 require.True(t, errors.As(err, &ce))
118 require.Len(t, ce.Problems, 1, err.Error())
119 assert.True(t, strings.HasPrefix(ce.Problems[0], tt.want+":"), ce.Problems[0])
120 assert.Contains(t, ce.Problems[0], tt.msg)
121 })
122 }
123}
124
125func TestLoadReportsEveryProblemTogether(t *testing.T) {
126 _, err := config.Load(lookup(map[string]string{
127 config.EnvAddr: "bad",
128 config.EnvLogLevel: "loud",
129 config.EnvLogFormat: "xml",
130 }))
131 require.Error(t, err)
132 msg := err.Error()
133 for _, name := range []string{
134 config.EnvAddr, config.EnvDataDir, config.EnvPublicURL, config.EnvAdminToken, config.EnvLogLevel, config.EnvLogFormat,
135 } {
136 assert.Contains(t, msg, name)
137 }
138 var ce *config.Error
139 require.True(t, errors.As(err, &ce))
140 assert.Len(t, ce.Problems, 6)
141}
142
143func TestErrorNeverContainsToken(t *testing.T) {
144 short := "secret-but-too-short"
145 _, err := config.Load(lookup(valid(map[string]string{config.EnvAdminToken: short, config.EnvAddr: "bad"})))
146 require.Error(t, err)
147 assert.NotContains(t, err.Error(), short)
148}
149
150func TestTokenLengthCountsCharacters(t *testing.T) {
151 _, err := config.Load(lookup(valid(map[string]string{config.EnvAdminToken: strings.Repeat("é", 32)})))
152 assert.NoError(t, err)
153 _, err = config.Load(lookup(valid(map[string]string{config.EnvAdminToken: strings.Repeat("é", 31)})))
154 assert.Error(t, err)
155}
156
157func TestLoadCommitterDefaults(t *testing.T) {
158 c, err := config.Load(lookup(valid(nil)))
159 require.NoError(t, err)
160 assert.Equal(t, "ermite", c.CommitterName)
161 assert.Equal(t, "ermite@git.example.com", c.CommitterEmail, "the host of the public URL, without port or path")
162
163 c, err = config.Load(lookup(valid(map[string]string{config.EnvPublicURL: "http://localhost:7341/"})))
164 require.NoError(t, err)
165 assert.Equal(t, "ermite@localhost", c.CommitterEmail)
166}
167
168func TestLoadCommitterFromTheEnvironment(t *testing.T) {
169 c, err := config.Load(lookup(valid(map[string]string{
170 config.EnvCommitterName: " Forge Bot ",
171 config.EnvCommitterEmail: "bot@example.org",
172 })))
173 require.NoError(t, err)
174 assert.Equal(t, "Forge Bot", c.CommitterName)
175 assert.Equal(t, "bot@example.org", c.CommitterEmail)
176}
177
178func TestLoadRejectsBadCommitter(t *testing.T) {
179 for name, env := range map[string]map[string]string{
180 "name with an angle bracket": {config.EnvCommitterName: "Bot <x>"},
181 "name with a line break": {config.EnvCommitterName: "Bot\nCo-authored-by: x"},
182 "name not utf-8": {config.EnvCommitterName: "\xff"},
183 "email without an @": {config.EnvCommitterEmail: "bot"},
184 "email with a space": {config.EnvCommitterEmail: "a b@example.org"},
185 "email with an angle bracket": {config.EnvCommitterEmail: "<bot@example.org>"},
186 } {
187 t.Run(name, func(t *testing.T) {
188 _, err := config.Load(lookup(valid(env)))
189 var ce *config.Error
190 require.ErrorAs(t, err, &ce)
191 assert.Contains(t, ce.Problems[0], "ERMITE_COMMITTER_")
192 })
193 }
194}