v0.3.0 9fdf386

config_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 45

↑↓ moveenter openesc close