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, "ermite-git", c.GitReader, "ermite-git reads by default")
48 assert.Equal(t, "https://git.example.com/ermite", c.SourceURL)
49 assert.Equal(t, "info", c.LogLevel)
50 assert.Equal(t, "json", c.LogFormat)
51 assert.Empty(t, c.TrustedProxies)
52 assert.Equal(t, sha256.Sum256([]byte(token)), c.AdminTokenHash)
53}
54
55func TestLoadAllValues(t *testing.T) {
56 c, err := config.Load(lookup(valid(map[string]string{
57 config.EnvAddr: "127.0.0.1:8080",
58 config.EnvDataDir: "/srv/../srv/data/",
59 config.EnvPublicURL: "http://localhost:7341/",
60 config.EnvTrustedProxies: "10.1.2.3/8, fd00::1/8",
61 config.EnvGit: "/usr/local/bin/git",
62 config.EnvGitReader: " CLI ",
63 config.EnvSourceURL: "https://forge.example.org/src/",
64 config.EnvLogLevel: "DEBUG",
65 config.EnvLogFormat: "Console",
66 })))
67 require.NoError(t, err)
68 assert.Equal(t, "127.0.0.1:8080", c.Addr)
69 assert.Equal(t, "/srv/data", c.DataDir)
70 assert.Equal(t, "http://localhost:7341", c.PublicURL)
71 assert.Equal(t, []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8"), netip.MustParsePrefix("fd00::/8")}, c.TrustedProxies)
72 assert.Equal(t, "/usr/local/bin/git", c.Git)
73 assert.Equal(t, "cli", c.GitReader)
74 assert.Equal(t, "https://forge.example.org/src", c.SourceURL)
75 assert.Equal(t, "debug", c.LogLevel)
76 assert.Equal(t, "console", c.LogFormat)
77}
78
79func TestLoadSourceURLFollowsPublicURL(t *testing.T) {
80 c, err := config.Load(lookup(valid(map[string]string{config.EnvPublicURL: "http://127.0.0.1:7341/"})))
81 require.NoError(t, err)
82 assert.Equal(t, "http://127.0.0.1:7341/ermite", c.SourceURL)
83}
84
85func TestLoadProblems(t *testing.T) {
86 tests := []struct {
87 name string
88 env map[string]string
89 want string // variable named in the error
90 msg string // fragment of its message
91 }{
92 {"missing data dir", map[string]string{config.EnvDataDir: "\x00unset"}, config.EnvDataDir, "required"},
93 {"empty data dir", map[string]string{config.EnvDataDir: ""}, config.EnvDataDir, "required"},
94 {"relative data dir", map[string]string{config.EnvDataDir: "data"}, config.EnvDataDir, "absolute"},
95 {"missing public url", map[string]string{config.EnvPublicURL: "\x00unset"}, config.EnvPublicURL, "required"},
96 {"public url scheme", map[string]string{config.EnvPublicURL: "ftp://x.org"}, config.EnvPublicURL, "scheme"},
97 {"public url no scheme", map[string]string{config.EnvPublicURL: "git.example.com"}, config.EnvPublicURL, "scheme"},
98 {"public url no host", map[string]string{config.EnvPublicURL: "https://"}, config.EnvPublicURL, "host"},
99 {"public url path", map[string]string{config.EnvPublicURL: "https://x.org/forge"}, config.EnvPublicURL, "path"},
100 {"public url query", map[string]string{config.EnvPublicURL: "https://x.org?a=b"}, config.EnvPublicURL, "query"},
101 {"public url fragment", map[string]string{config.EnvPublicURL: "https://x.org#top"}, config.EnvPublicURL, "fragment"},
102 {"public url credentials", map[string]string{config.EnvPublicURL: "https://u:p@x.org"}, config.EnvPublicURL, "credentials"},
103 {"missing token", map[string]string{config.EnvAdminToken: "\x00unset"}, config.EnvAdminToken, "required"},
104 {"short token", map[string]string{config.EnvAdminToken: strings.Repeat("a", 31)}, config.EnvAdminToken, "at least 32"},
105 {"bad addr", map[string]string{config.EnvAddr: "7341"}, config.EnvAddr, "host:port"},
106 {"bad port", map[string]string{config.EnvAddr: ":http"}, config.EnvAddr, "port"},
107 {"port zero", map[string]string{config.EnvAddr: ":0"}, config.EnvAddr, "port"},
108 {"port too big", map[string]string{config.EnvAddr: ":70000"}, config.EnvAddr, "port"},
109 {"bad cidr", map[string]string{config.EnvTrustedProxies: "10.0.0.1"}, config.EnvTrustedProxies, "CIDR"},
110 {"empty cidr entry", map[string]string{config.EnvTrustedProxies: "10.0.0.0/8,,"}, config.EnvTrustedProxies, "empty"},
111 {"bad git reader", map[string]string{config.EnvGitReader: "libgit2"}, config.EnvGitReader, "cli or ermite-git"},
112 {"git reader typo", map[string]string{config.EnvGitReader: "ermite_git"}, config.EnvGitReader, `"ermite_git"`},
113 {"bad source url", map[string]string{config.EnvSourceURL: "nope"}, config.EnvSourceURL, "scheme"},
114 {"bad level", map[string]string{config.EnvLogLevel: "loud"}, config.EnvLogLevel, "trace, debug"},
115 {"bad format", map[string]string{config.EnvLogFormat: "xml"}, config.EnvLogFormat, "json or console"},
116 }
117 for _, tt := range tests {
118 t.Run(tt.name, func(t *testing.T) {
119 _, err := config.Load(lookup(valid(tt.env)))
120 require.Error(t, err)
121 var ce *config.Error
122 require.True(t, errors.As(err, &ce))
123 require.Len(t, ce.Problems, 1, err.Error())
124 assert.True(t, strings.HasPrefix(ce.Problems[0], tt.want+":"), ce.Problems[0])
125 assert.Contains(t, ce.Problems[0], tt.msg)
126 })
127 }
128}
129
130func TestLoadReportsEveryProblemTogether(t *testing.T) {
131 _, err := config.Load(lookup(map[string]string{
132 config.EnvAddr: "bad",
133 config.EnvLogLevel: "loud",
134 config.EnvLogFormat: "xml",
135 }))
136 require.Error(t, err)
137 msg := err.Error()
138 for _, name := range []string{
139 config.EnvAddr, config.EnvDataDir, config.EnvPublicURL, config.EnvAdminToken, config.EnvLogLevel, config.EnvLogFormat,
140 } {
141 assert.Contains(t, msg, name)
142 }
143 var ce *config.Error
144 require.True(t, errors.As(err, &ce))
145 assert.Len(t, ce.Problems, 6)
146}
147
148func TestErrorNeverContainsToken(t *testing.T) {
149 short := "secret-but-too-short"
150 _, err := config.Load(lookup(valid(map[string]string{config.EnvAdminToken: short, config.EnvAddr: "bad"})))
151 require.Error(t, err)
152 assert.NotContains(t, err.Error(), short)
153}
154
155func TestTokenLengthCountsCharacters(t *testing.T) {
156 _, err := config.Load(lookup(valid(map[string]string{config.EnvAdminToken: strings.Repeat("é", 32)})))
157 assert.NoError(t, err)
158 _, err = config.Load(lookup(valid(map[string]string{config.EnvAdminToken: strings.Repeat("é", 31)})))
159 assert.Error(t, err)
160}
161
162func TestLoadCommitterDefaults(t *testing.T) {
163 c, err := config.Load(lookup(valid(nil)))
164 require.NoError(t, err)
165 assert.Equal(t, "ermite", c.CommitterName)
166 assert.Equal(t, "ermite@git.example.com", c.CommitterEmail, "the host of the public URL, without port or path")
167
168 c, err = config.Load(lookup(valid(map[string]string{config.EnvPublicURL: "http://localhost:7341/"})))
169 require.NoError(t, err)
170 assert.Equal(t, "ermite@localhost", c.CommitterEmail)
171}
172
173func TestLoadCommitterFromTheEnvironment(t *testing.T) {
174 c, err := config.Load(lookup(valid(map[string]string{
175 config.EnvCommitterName: " Forge Bot ",
176 config.EnvCommitterEmail: "bot@example.org",
177 })))
178 require.NoError(t, err)
179 assert.Equal(t, "Forge Bot", c.CommitterName)
180 assert.Equal(t, "bot@example.org", c.CommitterEmail)
181}
182
183func TestLoadRejectsBadCommitter(t *testing.T) {
184 for name, env := range map[string]map[string]string{
185 "name with an angle bracket": {config.EnvCommitterName: "Bot <x>"},
186 "name with a line break": {config.EnvCommitterName: "Bot\nCo-authored-by: x"},
187 "name not utf-8": {config.EnvCommitterName: "\xff"},
188 "email without an @": {config.EnvCommitterEmail: "bot"},
189 "email with a space": {config.EnvCommitterEmail: "a b@example.org"},
190 "email with an angle bracket": {config.EnvCommitterEmail: "<bot@example.org>"},
191 } {
192 t.Run(name, func(t *testing.T) {
193 _, err := config.Load(lookup(valid(env)))
194 var ce *config.Error
195 require.ErrorAs(t, err, &ce)
196 assert.Contains(t, ce.Problems[0], "ERMITE_COMMITTER_")
197 })
198 }
199}
200
201func TestLoadGitReader(t *testing.T) {
202 for in, want := range map[string]string{
203 "": config.GitReaderErmiteGit, "ermite-git": config.GitReaderErmiteGit, "ERMITE-GIT": config.GitReaderErmiteGit,
204 "cli": config.GitReaderCLI, "Cli": config.GitReaderCLI,
205 } {
206 c, err := config.Load(lookup(valid(map[string]string{config.EnvGitReader: in})))
207 require.NoError(t, err, in)
208 assert.Equal(t, want, c.GitReader, in)
209 }
210}