v0.23.0 333c710

cli_config_ci_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package app
4
5import (
6	"context"
7	"strings"
8	"testing"
9	"time"
10
11	"connectrpc.com/connect"
12	"github.com/stretchr/testify/assert"
13
14	"bnema.dev/ermite-api/gen/ermite"
15)
16
17// The RunService calls of the `run` and `runner` commands answer with empty
18// results: these tests look at which address and token reach the server, not
19// at the output.
20func (f *fakeAPI) ListRuns(_ context.Context, r *connect.Request[ermite.ListRunsRequest]) (*connect.Response[ermite.ListRunsResponse], error) {
21	return respond(f, "ListRuns", r, func(*ermite.ListRunsRequest) (*ermite.ListRunsResponse, error) {
22		return &ermite.ListRunsResponse{}, nil
23	})
24}
25
26func (f *fakeAPI) CancelRun(_ context.Context, r *connect.Request[ermite.CancelRunRequest]) (*connect.Response[ermite.CancelRunResponse], error) {
27	return respond(f, "CancelRun", r, func(*ermite.CancelRunRequest) (*ermite.CancelRunResponse, error) {
28		return &ermite.CancelRunResponse{}, nil
29	})
30}
31
32func (f *fakeAPI) RerunRun(_ context.Context, r *connect.Request[ermite.RerunRunRequest]) (*connect.Response[ermite.RerunRunResponse], error) {
33	return respond(f, "RerunRun", r, func(*ermite.RerunRunRequest) (*ermite.RerunRunResponse, error) {
34		return &ermite.RerunRunResponse{}, nil
35	})
36}
37
38func (f *fakeAPI) ListRunners(_ context.Context, r *connect.Request[ermite.ListRunnersRequest]) (*connect.Response[ermite.ListRunnersResponse], error) {
39	return respond(f, "ListRunners", r, func(*ermite.ListRunnersRequest) (*ermite.ListRunnersResponse, error) {
40		if f.runners != nil {
41			return &ermite.ListRunnersResponse{Runners: f.runners}, nil
42		}
43		return &ermite.ListRunnersResponse{Runners: []*ermite.Runner{{Id: "r1", Name: "alpha"}}}, nil
44	})
45}
46
47func (f *fakeAPI) RemoveRunner(_ context.Context, r *connect.Request[ermite.RemoveRunnerRequest]) (*connect.Response[ermite.RemoveRunnerResponse], error) {
48	return respond(f, "RemoveRunner", r, func(*ermite.RemoveRunnerRequest) (*ermite.RemoveRunnerResponse, error) {
49		return &ermite.RemoveRunnerResponse{}, nil
50	})
51}
52
53// The `run` and `runner` commands read the server address and the token like
54// every other remote command: the config file (url, token.cmd), with
55// ERMITE_URL / ERMITE_TOKEN winning, and token.cmd run once and only for the
56// file's own url.
57func TestCIcommandsUseTheConfigFile(t *testing.T) {
58	commands := map[string][]string{
59		"run list":      {"run", "list", "proj"},
60		"run cancel":    {"run", "cancel", "proj", "1"},
61		"run rerun":     {"run", "rerun", "proj", "1"},
62		"runner list":   {"runner", "list"},
63		"runner remove": {"runner", "remove", "r1"},
64	}
65	for name, args := range commands {
66		t.Run(name, func(t *testing.T) {
67			f := &fakeAPI{}
68			url := f.serve(t)
69
70			// url and token.cmd from the file
71			env := testEnv(t, nil)
72			writeConfigFile(t, env, "url = "+url+"\ntoken.cmd = get-token\n", 0o600)
73			ran := tokenCmdStub(env, "ermite_cmd\n", nil)
74			runCLIWith(t, env, "", args...).ok(t)
75			assert.Equal(t, "Bearer ermite_cmd", lastAuth(f))
76			assert.Len(t, *ran, 1)
77
78			// ERMITE_TOKEN wins and token.cmd stays idle
79			env = testEnv(t, map[string]string{"ERMITE_TOKEN": "ermite_env"})
80			writeConfigFile(t, env, "url = "+url+"\ntoken.cmd = get-token\n", 0o600)
81			ran = tokenCmdStub(env, "ermite_cmd", nil)
82			runCLIWith(t, env, "", args...).ok(t)
83			assert.Equal(t, "Bearer ermite_env", lastAuth(f))
84			assert.Empty(t, *ran)
85
86			// another ERMITE_URL never receives the file's token
87			other := &fakeAPI{}
88			env = testEnv(t, map[string]string{"ERMITE_URL": other.serve(t)})
89			writeConfigFile(t, env, "url = "+url+"\ntoken.cmd = get-token\n", 0o600)
90			ran = tokenCmdStub(env, "ermite_cmd", nil)
91			runCLIWith(t, env, "", args...).ok(t)
92			assert.Empty(t, lastAuth(other))
93			assert.Empty(t, *ran)
94		})
95	}
96}
97
98// Completing a repository name for `run` or a runner id for `runner remove`
99// must not run token.cmd: it may prompt, and a completion script cannot answer.
100func TestCICompletionNeverRunsTokenCmd(t *testing.T) {
101	f := &fakeAPI{listRepos: func(*ermite.ListReposRequest) (*ermite.ListReposResponse, error) {
102		return &ermite.ListReposResponse{Repos: []*ermite.Repo{repoAlpha}}, nil
103	}}
104	url := f.serve(t)
105	for name, args := range map[string][]string{
106		"run list":      {"__complete", "run", "list", ""},
107		"run cancel":    {"__complete", "run", "cancel", ""},
108		"runner remove": {"__complete", "runner", "remove", ""},
109	} {
110		t.Run(name, func(t *testing.T) {
111			env := testEnv(t, nil)
112			writeConfigFile(t, env, "url = "+url+"\ntoken.cmd = get-token\n", 0o600)
113			ran := tokenCmdStub(env, "ermite_cmd", nil)
114			runCLIWith(t, env, "", args...).ok(t)
115			assert.Empty(t, *ran)
116			assert.Empty(t, lastAuth(f))
117		})
118	}
119}
120
121// The CLI shows what the server decided: it never compares last_seen with its
122// own clock.
123func TestRunnerListShowsTheServersVerdict(t *testing.T) {
124	f := &fakeAPI{}
125	f.runners = []*ermite.Runner{
126		{Id: "r1", Name: "up", Version: "v2", Online: true},
127		{Id: "r2", Name: "busy", Online: true, Busy: true},
128		{Id: "r3", Name: "down"},
129	}
130	env := testEnv(t, map[string]string{"ERMITE_URL": f.serve(t), "ERMITE_TOKEN": "ermite_x"})
131	env.now = func() time.Time { return time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) } // a clock that is far off
132	out := runCLIWith(t, env, "", "runner", "list").ok(t).stdout
133	state := map[string]string{}
134	for _, line := range strings.Split(strings.TrimSpace(out), "\n")[1:] {
135		fields := strings.Fields(line)
136		state[fields[1]] = fields[2]
137	}
138	assert.Equal(t, map[string]string{"up": "online", "busy": "busy", "down": "offline"}, state, out)
139
140	js := runCLIWith(t, env, "", "runner", "list", "--json").ok(t).stdout
141	assert.Contains(t, js, `"online": true`)
142}

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 2

Tags 45

↑↓ moveenter openesc close