v0.1.0 3f267b5

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, "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}

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 3

Tags 43

↑↓ moveenter openesc close