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

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