main 2aca1df

cli_issue_e2e_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package app
4
5import (
6	"encoding/json"
7	"strings"
8	"testing"
9
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/require"
12
13	"bnema.dev/ermite-api/gen/ermite"
14)
15
16type issueOut struct {
17	Repo         string  `json:"repo"`
18	Number       int64   `json:"number"`
19	Title        string  `json:"title"`
20	Body         string  `json:"body"`
21	State        string  `json:"state"`
22	Author       string  `json:"author"`
23	CreatedAt    *string `json:"created_at"`
24	UpdatedAt    *string `json:"updated_at"`
25	CommentCount int     `json:"comment_count"`
26}
27
28var issueKeys = []string{"author", "body", "closed_by_merge_request", "comment_count", "created_at", "number", "repo", "state", "title", "updated_at"}
29
30type issueCommentOut struct {
31	ID     string `json:"id"`
32	Author string `json:"author"`
33	Text   string `json:"text"`
34}
35
36// The issue commands against a real forge: issues share the numbering of the
37// merge requests.
38func TestIssuesCLIEndToEnd(t *testing.T) {
39	c := newCLIHarness(t)
40	agent := strings.TrimSpace(func() string {
41		out, errOut, code := c.bin(t, adminToken, "token", "create", "agent-7", "--preset", "agent", "--scope", "issue:write", "--repo", "proj")
42		require.Equal(t, 0, code, errOut)
43		return out
44	}())
45	c.cli(t, adminToken, "repo", "create", "proj").ok(t)
46
47	// Issues are off by default.
48	res := c.cli(t, adminToken, "repo", "show", "proj", "--json").ok(t)
49	assert.Equal(t, "off", decode[struct {
50		Issues string `json:"issues"`
51	}](t, res.stdout).Issues)
52	res = c.cli(t, adminToken, "issue", "create", "proj", "--title", "too early")
53	require.Error(t, res.err)
54
55	// repo edit --issues.
56	res = c.cli(t, adminToken, "repo", "edit", "proj", "--issues", "owner", "--json").ok(t)
57	assert.Equal(t, "owner", decode[struct {
58		Issues string `json:"issues"`
59	}](t, res.stdout).Issues)
60	res = c.cli(t, adminToken, "repo", "show", "proj").ok(t)
61	assert.Regexp(t, `issues\s+owner`, res.stdout)
62	require.Error(t, c.cli(t, adminToken, "repo", "edit", "proj", "--issues", "everyone").err)
63	res = c.cli(t, adminToken, "repo", "edit", "proj", "--issues", "public", "--json").ok(t)
64	assert.Equal(t, "public", decode[struct {
65		Issues string `json:"issues"`
66	}](t, res.stdout).Issues)
67	c.cli(t, adminToken, "repo", "edit", "proj", "--issues", "owner").ok(t)
68
69	// A merge request first: the issue takes the next number.
70	wt := c.workTree("main", "README.md")
71	url := c.urlAuth(adminToken, "/proj.git")
72	c.mustGit(wt, "push", "-q", url, "main")
73	c.mustGit(wt, "checkout", "-q", "-b", "feat/x")
74	c.writeFile(wt, "x.txt", []byte("x\n"))
75	c.mustGit(wt, "push", "-q", url, "feat/x")
76	c.cli(t, adminToken, "mr", "create", "proj", "--source", "feat/x", "--title", "First").ok(t)
77
78	res = c.cli(t, agent, "issue", "create", "proj", "--title", "Crash on start", "--body", "It crashes.\nSecond line.", "--json").ok(t)
79	assert.Equal(t, issueKeys, keys(t, res.stdout))
80	issue := decode[issueOut](t, res.stdout)
81	assert.EqualValues(t, 2, issue.Number, "shared numbering with merge requests")
82	assert.Equal(t, "open", issue.State)
83	assert.Equal(t, "agent-7", issue.Author)
84	assert.Equal(t, "It crashes.\nSecond line.", issue.Body)
85
86	res = runCLI(t, c.vars(agent), "from stdin\n", "issue", "create", "proj", "--title", "Second", "--body-file", "-").ok(t)
87	assert.Equal(t, "opened proj#3 (Second)\n", res.stdout)
88
89	// list.
90	res = c.cli(t, adminToken, "issue", "list", "proj", "--json").ok(t)
91	list := decode[[]issueOut](t, res.stdout)
92	require.Len(t, list, 2)
93	assert.EqualValues(t, 3, list[0].Number, "highest number first")
94	res = c.cli(t, adminToken, "issue", "list", "proj").ok(t)
95	lines := strings.Split(strings.TrimSpace(res.stdout), "\n")
96	require.Len(t, lines, 3)
97	assert.Regexp(t, `^NUMBER\s+STATE\s+AUTHOR\s+COMMENTS\s+UPDATED\s+TITLE$`, lines[0])
98	assert.Regexp(t, `^#3\s+open\s+agent-7\s+0\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2}\s+Second$`, lines[1])
99
100	// comment, then show with it.
101	res = c.cli(t, agent, "issue", "comment", "proj", "2", "-m", "Reproduced.", "--json").ok(t)
102	cm := decode[issueCommentOut](t, res.stdout)
103	assert.Equal(t, "agent-7", cm.Author)
104	assert.Equal(t, "Reproduced.", cm.Text)
105	res = c.cli(t, adminToken, "issue", "comment", "proj", "#2", "-m", "Thanks.").ok(t)
106	assert.Regexp(t, `^comment \S+ added to proj#2\n$`, res.stdout)
107	res = c.cli(t, adminToken, "issue", "show", "proj", "2", "--json").ok(t)
108	type showOut struct {
109		issueOut
110		Comments   []issueCommentOut `json:"comments"`
111		References []json.RawMessage `json:"references"`
112	}
113	show := decode[showOut](t, res.stdout)
114	assert.Equal(t, 2, show.CommentCount)
115	require.Len(t, show.Comments, 2)
116	assert.Equal(t, "Reproduced.", show.Comments[0].Text, "oldest first")
117	res = c.cli(t, adminToken, "issue", "show", "proj", "2").ok(t)
118	assert.Contains(t, res.stdout, "Crash on start")
119	assert.Contains(t, res.stdout, "comments (2)")
120	assert.NotContains(t, res.stdout, "merge requests", "no merge request names the issue")
121	assert.NotNil(t, show.References, "references is always present")
122	assert.Empty(t, show.References)
123
124	// close and reopen.
125	res = c.cli(t, agent, "issue", "close", "proj", "2", "--json").ok(t)
126	assert.Equal(t, "closed", decode[issueOut](t, res.stdout).State)
127	res = c.cli(t, adminToken, "issue", "list", "proj", "--json").ok(t)
128	assert.Len(t, decode[[]issueOut](t, res.stdout), 1)
129	res = c.cli(t, adminToken, "issue", "list", "proj", "--state", "closed", "--json").ok(t)
130	assert.Len(t, decode[[]issueOut](t, res.stdout), 1)
131	res = c.cli(t, adminToken, "issue", "list", "proj", "--state", "all", "--json").ok(t)
132	assert.Len(t, decode[[]issueOut](t, res.stdout), 2)
133	res = c.cli(t, agent, "issue", "reopen", "proj", "2").ok(t)
134	assert.Equal(t, "reopened proj#2 (Crash on start)\n", res.stdout)
135
136	// An issue is not a merge request and the other way round.
137	_, stderr, code := c.bin(t, adminToken, "issue", "show", "proj", "1")
138	requireOneLineError(t, stderr, code, "not found")
139	_, stderr, code = c.bin(t, adminToken, "issue", "show", "proj", "9")
140	requireOneLineError(t, stderr, code, "not found")
141}
142
143// A server without the issues feature is refused before anything is created.
144func TestIssueWriteTokenRefusedWithoutTheFeature(t *testing.T) {
145	f := &fakeAPI{
146		getVersion: func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error) {
147			return &ermite.GetVersionResponse{Version: "v0.4.0", Features: []string{"merge_requests"}}, nil
148		},
149		createToken: func(*ermite.CreateTokenRequest) (*ermite.CreateTokenResponse, error) {
150			return &ermite.CreateTokenResponse{Token: &ermite.Token{Id: "t"}, Secret: "ermite_S"}, nil
151		},
152	}
153	res := runCLI(t, envFor(f.serve(t)), "", "token", "create", "bot", "--preset", "agent", "--scope", "issue:write")
154	require.Error(t, res.err)
155	assert.Contains(t, res.err.Error(), "does not support issues")
156	assert.Empty(t, res.stdout)
157	calls, _ := f.snapshot()
158	assert.NotContains(t, calls, "CreateToken")
159}

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 43

↑↓ moveenter openesc close