v0.35.1 2aca1df

cli_mirror_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	"github.com/stretchr/testify/assert"
12	"github.com/stretchr/testify/require"
13
14	"bnema.dev/ermite/internal/config"
15	"bnema.dev/ermite/internal/domain"
16	"bnema.dev/ermite/internal/ports"
17	"bnema.dev/ermite/internal/usecase"
18)
19
20func TestMirrorCLIArguments(t *testing.T) {
21	for _, args := range [][]string{
22		{"mirror", "sync"}, {"mirror", "sync", "repo", "--all"}, {"mirror", "sync", "a", "b"},
23		{"mirror", "add", "repo"}, {"mirror", "remove"}, {"mirror", "exclude"}, {"mirror", "include"},
24		{"mirror", "key", "show", "extra"}, {"mirror", "key", "rotate", "extra"}, {"mirror", "status", "a", "b"},
25	} {
26		t.Run(strings.Join(args, " "), func(t *testing.T) {
27			r := runCLI(t, nil, "", args...)
28			require.Error(t, r.err)
29			assert.NotContains(t, r.err.Error(), "ERMITE_URL", "arguments must be rejected before loading credentials")
30		})
31	}
32	r := runCLI(t, nil, "", "mirror", "sync", "--help").ok(t)
33	assert.Contains(t, r.stdout, "never force-pushes")
34	assert.Contains(t, r.stdout, "lifts a pause")
35}
36
37func TestMirrorCLIConfiguration(t *testing.T) {
38	h := newSecretsCLI(t)
39	run := func(args ...string) result { return h.cli(t, adminToken, append([]string{"mirror"}, args...)...) }
40	run("config", "--enabled", "--url", "git@example.test:owner/{repo}.git", "--cron", "*/5 * * * *", "--all-branches").ok(t)
41	r := run("config", "show", "--json").ok(t)
42	g := decode[map[string]any](t, r.stdout)
43	assert.Equal(t, true, g["enabled"])
44	assert.Equal(t, true, g["all_branches"])
45	assert.Equal(t, "git@example.test:owner/{repo}.git", g["url_template"])
46	run("config", "--enabled=false", "--all-branches=false").ok(t)
47	g = decode[map[string]any](t, run("config", "show", "--json").ok(t).stdout)
48	assert.Equal(t, false, g["enabled"])
49	assert.Equal(t, false, g["all_branches"])
50	assert.Equal(t, "*/5 * * * *", g["cron"], "omitted fields are not cleared")
51	run("add", "proj", "git@example.test:owner/proj.git", "--cron", "*/10 * * * *", "--all-branches=false").ok(t)
52	r = run("status", "proj", "--json").ok(t)
53	s := decode[map[string]any](t, r.stdout)
54	assert.Equal(t, "override", s["source"])
55	assert.Equal(t, false, s["all_branches"])
56	assert.Equal(t, "ok", s["state"])
57	assert.Equal(t, "proj", s["repo"])
58	assert.Contains(t, run("status").ok(t).stdout, "FAILURES")
59	assert.Len(t, decode[[]any](t, run("status", "--json").ok(t).stdout), 1)
60	run("remove", "proj").ok(t)
61	run("exclude", "proj").ok(t)
62	s = decode[map[string]any](t, run("status", "proj", "--json").ok(t).stdout)
63	assert.Equal(t, "excluded", s["source"])
64	run("include", "proj").ok(t)
65	first := run("key", "show").ok(t)
66	assert.Empty(t, first.stderr)
67	firstFingerprint := decode[map[string]any](t, run("key", "show", "--json").ok(t).stdout)["fingerprint"].(string)
68	rotated := run("key", "rotate").ok(t)
69	assert.Contains(t, rotated.stderr, "replaced key "+firstFingerprint)
70	assert.NotEqual(t, first.stdout, rotated.stdout)
71	assert.Contains(t, rotated.stderr, "remotes refuse pushes until this key is added")
72	assert.Equal(t, rotated.stdout, run("key", "show").ok(t).stdout)
73	assert.Equal(t, []string{"authorized_key", "created_at", "fingerprint"}, keys(t, run("key", "show", "--json").ok(t).stdout))
74}
75
76func TestMirrorCLISyncHelpDocumentsBackgroundSync(t *testing.T) {
77	r := runCLI(t, nil, "", "mirror", "sync", "--help").ok(t)
78	assert.Contains(t, r.stdout, "queued")
79	assert.Contains(t, r.stdout, "ermite mirror status")
80}
81
82type blockingPusher struct{ entered, release chan struct{} }
83
84func (p blockingPusher) Push(ctx context.Context, _ ports.MirrorPush) (ports.MirrorPushResult, error) {
85	p.entered <- struct{}{}
86	select {
87	case <-p.release:
88	case <-ctx.Done():
89	}
90	return ports.MirrorPushResult{}, nil
91}
92
93// A push that outlasts the server's wait bound answers "queued"; the CLI says it continues.
94func TestMirrorCLISyncReportsBackgroundSync(t *testing.T) {
95	p := blockingPusher{entered: make(chan struct{}, 4), release: make(chan struct{})}
96	h := newHarnessWith(t, harnessOpts{env: map[string]string{config.EnvSecretsKey: secretsKeyA}}, func(o *options) {
97		o.mirrors = func(c *usecase.MirrorsConfig) {
98			c.ValidateURL = func(string) error { return nil }
99			c.Pusher = p
100			c.SyncWait = 50 * time.Millisecond
101		}
102	})
103	h.createRepo("proj", domain.VisibilityPrivate)
104	c := &cliHarness{harness: h, url: h.srv.URL}
105	wt := h.workTree("main", "README.md")
106	h.mustGit(wt, "push", h.urlAuth(adminToken, "/proj.git"), "main")
107	c.cli(t, adminToken, "mirror", "add", "proj", "file:///nowhere", "--cron", "*/5 * * * *").ok(t)
108	for _, args := range [][]string{{"sync", "proj", "--force"}, {"sync", "--all", "--force"}} {
109		r := c.cli(t, adminToken, append([]string{"mirror"}, args...)...).ok(t)
110		assert.Contains(t, r.stdout, "proj")
111		assert.Contains(t, r.stdout, "true", "QUEUED column")
112		assert.Contains(t, r.stderr, "continue in the background")
113		assert.Contains(t, r.stderr, "ermite mirror status")
114	}
115	close(p.release)
116	h.forge.Mirrors.Wait(h.ctx)
117}

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