v0.29.0 55fcc8f

cli_config_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package app
4
5import (
6	"context"
7	"errors"
8	"os"
9	"path/filepath"
10	"runtime"
11	"strconv"
12	"strings"
13	"testing"
14
15	"github.com/stretchr/testify/assert"
16	"github.com/stretchr/testify/require"
17)
18
19func TestConfigShowWithoutFile(t *testing.T) {
20	env := testEnv(t, nil)
21	path, _ := env.configPath()
22	res := runCLIWith(t, env, "", "config", "show").ok(t)
23	assert.Equal(t, "file       "+path+"\nurl        (not set)\ntoken.cmd  (not set)\n", res.stdout)
24	_, err := os.Stat(path)
25	assert.ErrorIs(t, err, os.ErrNotExist, "show does not create the file")
26}
27
28func TestConfigSetUnsetShowRoundTrip(t *testing.T) {
29	env := testEnv(t, nil)
30	path, _ := env.configPath()
31	writeConfigFile(t, env, "# my forge\nurl = https://old.example.com\n\n# secrets stay in pass\n", 0o600)
32
33	runCLIWith(t, env, "", "config", "set", "url", "https://git.example.com/").ok(t)
34	runCLIWith(t, env, "", "config", "set", "token.cmd", "pass show ermite/token").ok(t)
35	// a fresh process sees the new content
36	res := runCLIWith(t, testEnvAt(t, path), "", "config", "show").ok(t)
37	assert.Contains(t, res.stdout, "url        https://git.example.com\n")
38	assert.Contains(t, res.stdout, "token.cmd  pass show ermite/token\n")
39
40	data, err := os.ReadFile(path)
41	require.NoError(t, err)
42	assert.Contains(t, string(data), "# my forge\n")
43	assert.Contains(t, string(data), "# secrets stay in pass\n")
44
45	runCLIWith(t, testEnvAt(t, path), "", "config", "unset", "token.cmd").ok(t)
46	runCLIWith(t, testEnvAt(t, path), "", "config", "unset", "url").ok(t)
47	data, err = os.ReadFile(path)
48	require.NoError(t, err)
49	assert.NotContains(t, string(data), "token.cmd")
50	assert.NotContains(t, string(data), "url")
51	assert.Contains(t, string(data), "# my forge\n", "comments survive")
52
53	res = runCLIWith(t, testEnvAt(t, path), "", "config", "show").ok(t)
54	assert.Contains(t, res.stdout, "url        (not set)\n")
55	assert.Contains(t, res.stdout, "token.cmd  (not set)\n")
56}
57
58// testEnvAt is testEnv with the config file at path: a new process.
59func testEnvAt(t *testing.T, path string) *cliEnv {
60	t.Helper()
61	env := testEnv(t, nil)
62	env.configPath = func() (string, error) { return path, nil }
63	return env
64}
65
66func TestConfigSetCreatesPrivateFile(t *testing.T) {
67	env := testEnv(t, nil)
68	path, _ := env.configPath()
69	res := runCLIWith(t, env, "", "config", "set", "token.cmd", "gopass show -o ermite").ok(t)
70	assert.Contains(t, res.stderr, path)
71	info, err := os.Stat(path)
72	require.NoError(t, err)
73	if runtime.GOOS != "windows" {
74		assert.Equal(t, os.FileMode(0o600), info.Mode().Perm())
75	}
76	data, _ := os.ReadFile(path)
77	assert.Equal(t, "token.cmd = gopass show -o ermite\n", string(data))
78}
79
80func TestConfigSetRefusesBadInput(t *testing.T) {
81	tests := []struct {
82		name string
83		args []string
84		want string
85	}{
86		{"unknown key", []string{"set", "token", "x"}, `unknown key "token": the keys are url and token.cmd`},
87		{"unknown key unset", []string{"unset", "colour"}, `unknown key "colour": the keys are url and token.cmd`},
88		{"no scheme", []string{"set", "url", "git.example.com"}, "invalid url"},
89		{"credentials", []string{"set", "url", "https://u:s3cret@git.example.com"}, "must not contain credentials"},
90		{"ftp", []string{"set", "url", "ftp://git.example.com"}, "invalid url"},
91		{"empty token.cmd", []string{"set", "token.cmd", "  "}, "token.cmd is empty"},
92		{"multiline token.cmd", []string{"set", "token.cmd", "a\nb"}, "single line"},
93		{"missing value", []string{"set", "url"}, "accepts 2 arg"},
94	}
95	for _, tt := range tests {
96		t.Run(tt.name, func(t *testing.T) {
97			env := testEnv(t, nil)
98			path, _ := env.configPath()
99			res := runCLIWith(t, env, "", append([]string{"config"}, tt.args...)...)
100			require.Error(t, res.err)
101			assert.Contains(t, res.err.Error(), tt.want)
102			assert.NotContains(t, res.err.Error(), "s3cret")
103			_, err := os.Stat(path)
104			assert.ErrorIs(t, err, os.ErrNotExist, "nothing is written")
105		})
106	}
107}
108
109func TestConfigUnsetWithoutFile(t *testing.T) {
110	env := testEnv(t, nil)
111	path, _ := env.configPath()
112	res := runCLIWith(t, env, "", "config", "unset", "url").ok(t)
113	assert.Contains(t, res.stderr, "does not exist")
114	_, err := os.Stat(path)
115	assert.ErrorIs(t, err, os.ErrNotExist)
116}
117
118func TestConfigShowNeverRunsTokenCmdAndNotesPrecedence(t *testing.T) {
119	env := testEnv(t, map[string]string{"ERMITE_URL": "https://env.example.com", "ERMITE_TOKEN": "ermite_x"})
120	writeConfigFile(t, env, "url = https://file.example.com\ntoken.cmd = get-token\n", 0o666)
121	res := runCLIWith(t, env, "", "config", "show").ok(t) // testEnv fails the test if token.cmd runs
122	assert.Contains(t, res.stdout, "url        https://file.example.com\n", "show prints what is stored")
123	assert.Contains(t, res.stderr, "ERMITE_URL is set and takes precedence")
124	assert.Contains(t, res.stderr, "ERMITE_TOKEN is set: token.cmd is not used")
125	assert.False(t, strings.Contains(res.stdout+res.stderr, "ermite_x"), "the token is never printed")
126}
127
128func TestHelpDocumentsTheConfigFileContract(t *testing.T) {
129	root := runCLI(t, nil, "", "--help").ok(t).stdout
130	cfg := runCLI(t, nil, "", "config", "--help").ok(t).stdout
131	for name, help := range map[string]string{"root": root, "config": cfg} {
132		assert.Contains(t, help, "ermite/config", "%s help names the file", name)
133		assert.Contains(t, help, "ERMITE_CONFIG", "%s help names the override", name)
134		assert.Contains(t, help, "ERMITE_URL", "%s help states the precedence", name)
135		assert.Contains(t, help, "ERMITE_TOKEN", "%s help states the precedence", name)
136	}
137	assert.Contains(t, cfg, "token.cmd = ", "the config help has an example")
138	assert.Contains(t, cfg, "Windows", "the config help states the Windows limit")
139}
140
141func TestConfigShowNamesTheFileItCannotRead(t *testing.T) {
142	env := testEnv(t, nil)
143	path := writeConfigFile(t, env, "url = https://git.example.com\n", 0o600)
144	require.NoError(t, os.Remove(path))
145	require.NoError(t, os.Mkdir(path, 0o700)) // a directory is not a config file
146	res := runCLIWith(t, env, "", "config", "show")
147	require.Error(t, res.err)
148	assert.Contains(t, res.stdout, path)
149	assert.Contains(t, res.err.Error(), "config:")
150}
151
152func TestConfigUnsetOfAnAbsentKey(t *testing.T) {
153	env := testEnv(t, nil)
154	path := writeConfigFile(t, env, "# only a comment\nurl = https://git.example.com\n", 0o600)
155	res := runCLIWith(t, env, "", "config", "unset", "token.cmd").ok(t)
156	assert.Equal(t, "ermite: token.cmd is not set\n", res.stderr)
157	data, err := os.ReadFile(path)
158	require.NoError(t, err)
159	assert.Equal(t, "# only a comment\nurl = https://git.example.com\n", string(data), "the file is untouched")
160}
161
162func TestConfigShowJSON(t *testing.T) {
163	env := testEnv(t, nil)
164	path, _ := env.configPath()
165	res := runCLIWith(t, env, "", "config", "show", "--json").ok(t)
166	assert.JSONEq(t, `{"file":`+strconv.Quote(path)+`,"url":null,"token_cmd":null}`, res.stdout)
167
168	env = testEnv(t, nil)
169	path = writeConfigFile(t, env, "url = https://git.example.com\ntoken.cmd = pass show ermite/token\n", 0o600)
170	res = runCLIWith(t, env, "", "config", "show", "--json").ok(t) // testEnv fails the test if token.cmd runs
171	assert.JSONEq(t, `{"file":`+strconv.Quote(path)+`,"url":"https://git.example.com","token_cmd":"pass show ermite/token"}`, res.stdout)
172}
173
174func TestConfigShowAndSetWarnWhenTokenCmdWouldBeRefused(t *testing.T) {
175	if runtime.GOOS == "windows" {
176		t.Skip("no Unix permission bits")
177	}
178	env := testEnv(t, nil)
179	path := writeConfigFile(t, env, "url = https://git.example.com\ntoken.cmd = get-token\n", 0o666)
180
181	res := runCLIWith(t, env, "", "config", "show").ok(t)
182	assert.Contains(t, res.stderr, "ermite: warning: config file "+path+" is writable by others: token.cmd not run")
183
184	// a fresh process; set keeps the mode of an existing file
185	res = runCLIWith(t, testEnvAt(t, path), "", "config", "set", "url", "https://git2.example.com").ok(t)
186	assert.Contains(t, res.stderr, "writable by others")
187
188	// no warning without a token.cmd, or with a private file
189	env = testEnv(t, nil)
190	writeConfigFile(t, env, "url = https://git.example.com\n", 0o666)
191	assert.NotContains(t, runCLIWith(t, env, "", "config", "show").ok(t).stderr, "warning")
192	res = runCLIWith(t, testEnv(t, nil), "", "config", "set", "token.cmd", "get-token").ok(t)
193	assert.NotContains(t, res.stderr, "warning")
194}
195
196func TestDefaultConfigPathUsesXDGConfigHome(t *testing.T) {
197	xdg := t.TempDir()
198	t.Setenv("XDG_CONFIG_HOME", xdg)
199	t.Setenv("ERMITE_CONFIG", "")
200	f := listFake()
201
202	env := defaultCLIEnv() // the real path resolution, the real environment
203	res := runCLIWith(t, env, "", "config", "set", "url", f.serve(t)).ok(t)
204	want := filepath.Join(xdg, "ermite", "config")
205	assert.Contains(t, res.stderr, want)
206	info, err := os.Stat(want)
207	require.NoError(t, err)
208	if runtime.GOOS != "windows" {
209		assert.Equal(t, os.FileMode(0o600), info.Mode().Perm())
210	}
211
212	env = defaultCLIEnv()
213	env.runTokenCmd = func(context.Context, string) (string, error) { return "", errors.New("must not run") }
214	t.Setenv("ERMITE_URL", "")
215	t.Setenv("ERMITE_TOKEN", "")
216	runCLIWith(t, env, "", "repo", "list").ok(t)
217	assert.Equal(t, 1, callCount(f, "ListRepos"))
218
219	// ERMITE_CONFIG wins over the default
220	other := filepath.Join(t.TempDir(), "other")
221	t.Setenv("ERMITE_CONFIG", other)
222	res = runCLIWith(t, defaultCLIEnv(), "", "config", "show").ok(t)
223	assert.Contains(t, res.stdout, other)
224}
225
226func callCount(f *fakeAPI, name string) int {
227	calls, _ := f.snapshot()
228	n := 0
229	for _, c := range calls {
230		if c == name {
231			n++
232		}
233	}
234	return n
235}

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