v0.11.0 0633e44

cli_config_ci_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package app
4
5import (
6	"context"
7	"testing"
8
9	"connectrpc.com/connect"
10	"github.com/stretchr/testify/assert"
11
12	"bnema.dev/ermite-api/gen/ermite"
13)
14
15// The RunService calls of the `run` and `runner` commands answer with empty
16// results: these tests look at which address and token reach the server, not
17// at the output.
18func (f *fakeAPI) ListRuns(_ context.Context, r *connect.Request[ermite.ListRunsRequest]) (*connect.Response[ermite.ListRunsResponse], error) {
19	return respond(f, "ListRuns", r, func(*ermite.ListRunsRequest) (*ermite.ListRunsResponse, error) {
20		return &ermite.ListRunsResponse{}, nil
21	})
22}
23
24func (f *fakeAPI) CancelRun(_ context.Context, r *connect.Request[ermite.CancelRunRequest]) (*connect.Response[ermite.CancelRunResponse], error) {
25	return respond(f, "CancelRun", r, func(*ermite.CancelRunRequest) (*ermite.CancelRunResponse, error) {
26		return &ermite.CancelRunResponse{}, nil
27	})
28}
29
30func (f *fakeAPI) RerunRun(_ context.Context, r *connect.Request[ermite.RerunRunRequest]) (*connect.Response[ermite.RerunRunResponse], error) {
31	return respond(f, "RerunRun", r, func(*ermite.RerunRunRequest) (*ermite.RerunRunResponse, error) {
32		return &ermite.RerunRunResponse{}, nil
33	})
34}
35
36func (f *fakeAPI) ListRunners(_ context.Context, r *connect.Request[ermite.ListRunnersRequest]) (*connect.Response[ermite.ListRunnersResponse], error) {
37	return respond(f, "ListRunners", r, func(*ermite.ListRunnersRequest) (*ermite.ListRunnersResponse, error) {
38		return &ermite.ListRunnersResponse{Runners: []*ermite.Runner{{Id: "r1", Name: "alpha"}}}, nil
39	})
40}
41
42func (f *fakeAPI) RemoveRunner(_ context.Context, r *connect.Request[ermite.RemoveRunnerRequest]) (*connect.Response[ermite.RemoveRunnerResponse], error) {
43	return respond(f, "RemoveRunner", r, func(*ermite.RemoveRunnerRequest) (*ermite.RemoveRunnerResponse, error) {
44		return &ermite.RemoveRunnerResponse{}, nil
45	})
46}
47
48// The `run` and `runner` commands read the server address and the token like
49// every other remote command: the config file (url, token.cmd), with
50// ERMITE_URL / ERMITE_TOKEN winning, and token.cmd run once and only for the
51// file's own url.
52func TestCIcommandsUseTheConfigFile(t *testing.T) {
53	commands := map[string][]string{
54		"run list":      {"run", "list", "proj"},
55		"run cancel":    {"run", "cancel", "proj", "1"},
56		"run rerun":     {"run", "rerun", "proj", "1"},
57		"runner list":   {"runner", "list"},
58		"runner remove": {"runner", "remove", "r1"},
59	}
60	for name, args := range commands {
61		t.Run(name, func(t *testing.T) {
62			f := &fakeAPI{}
63			url := f.serve(t)
64
65			// url and token.cmd from the file
66			env := testEnv(t, nil)
67			writeConfigFile(t, env, "url = "+url+"\ntoken.cmd = get-token\n", 0o600)
68			ran := tokenCmdStub(env, "ermite_cmd\n", nil)
69			runCLIWith(t, env, "", args...).ok(t)
70			assert.Equal(t, "Bearer ermite_cmd", lastAuth(f))
71			assert.Len(t, *ran, 1)
72
73			// ERMITE_TOKEN wins and token.cmd stays idle
74			env = testEnv(t, map[string]string{"ERMITE_TOKEN": "ermite_env"})
75			writeConfigFile(t, env, "url = "+url+"\ntoken.cmd = get-token\n", 0o600)
76			ran = tokenCmdStub(env, "ermite_cmd", nil)
77			runCLIWith(t, env, "", args...).ok(t)
78			assert.Equal(t, "Bearer ermite_env", lastAuth(f))
79			assert.Empty(t, *ran)
80
81			// another ERMITE_URL never receives the file's token
82			other := &fakeAPI{}
83			env = testEnv(t, map[string]string{"ERMITE_URL": other.serve(t)})
84			writeConfigFile(t, env, "url = "+url+"\ntoken.cmd = get-token\n", 0o600)
85			ran = tokenCmdStub(env, "ermite_cmd", nil)
86			runCLIWith(t, env, "", args...).ok(t)
87			assert.Empty(t, lastAuth(other))
88			assert.Empty(t, *ran)
89		})
90	}
91}
92
93// Completing a repository name for `run` or a runner id for `runner remove`
94// must not run token.cmd: it may prompt, and a completion script cannot answer.
95func TestCICompletionNeverRunsTokenCmd(t *testing.T) {
96	f := &fakeAPI{listRepos: func(*ermite.ListReposRequest) (*ermite.ListReposResponse, error) {
97		return &ermite.ListReposResponse{Repos: []*ermite.Repo{repoAlpha}}, nil
98	}}
99	url := f.serve(t)
100	for name, args := range map[string][]string{
101		"run list":      {"__complete", "run", "list", ""},
102		"run cancel":    {"__complete", "run", "cancel", ""},
103		"runner remove": {"__complete", "runner", "remove", ""},
104	} {
105		t.Run(name, func(t *testing.T) {
106			env := testEnv(t, nil)
107			writeConfigFile(t, env, "url = "+url+"\ntoken.cmd = get-token\n", 0o600)
108			ran := tokenCmdStub(env, "ermite_cmd", nil)
109			runCLIWith(t, env, "", args...).ok(t)
110			assert.Empty(t, *ran)
111			assert.Empty(t, lastAuth(f))
112		})
113	}
114}

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