detached 973e682

mr_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6	"context"
7	"testing"
8
9	"github.com/stretchr/testify/assert"
10	"github.com/stretchr/testify/mock"
11	"github.com/stretchr/testify/require"
12
13	"bnema.dev/ermite/internal/domain"
14	"bnema.dev/ermite/internal/ports"
15	"bnema.dev/ermite/internal/ports/mocks"
16)
17
18func repoWithModes(modes ...domain.MergeMode) ports.RepoView {
19	var v ports.RepoView
20	v.Settings.AllowedMergeModes = modes
21	return v
22}
23
24func watchOf(ev domain.Event) ports.WatchEvent { return ports.WatchEvent{Event: ev} }
25
26func TestMergeabilityView(t *testing.T) {
27	t.Parallel()
28	for _, tt := range []struct {
29		name          string
30		m             domain.Mergeability
31		label, detail string
32		ffOK, mergeOK bool
33		note          string
34		paths         []string
35	}{
36		{"up to date", domain.Mergeability{State: domain.MergeabilityUpToDate}, "Ready to merge", "", true, true, "", nil},
37		{"behind", domain.Mergeability{State: domain.MergeabilityBehind, Behind: 2}, "Behind main by 2 commits", "merges without conflicts", false, true, "", nil},
38		{"behind by one", domain.Mergeability{State: domain.MergeabilityBehind, Behind: 1}, "Behind main by 1 commit", "merges without conflicts", false, true, "", nil},
39		{"behind, capped", domain.Mergeability{State: domain.MergeabilityBehind, Behind: 1000, BehindCapped: true}, "Behind main by 1000+ commits", "merges without conflicts", false, true, "", nil},
40		{"conflict", domain.Mergeability{State: domain.MergeabilityConflict, ConflictPaths: []string{"a.go"}}, "Conflicts with main", "rebase needed", false, false, "", []string{"a.go"}},
41		{"conflict, truncated", domain.Mergeability{State: domain.MergeabilityConflict, ConflictPaths: []string{"a.go"}, ConflictPathsTruncated: true}, "Conflicts with main", "rebase needed", false, false, "more files conflict than are listed.", []string{"a.go"}},
42		{"no common history", domain.Mergeability{State: domain.MergeabilityConflict}, "Conflicts with main", "rebase needed", false, false, "no common history.", nil},
43		{"checking", domain.Mergeability{State: domain.MergeabilityChecking}, "Checking…", "", false, false, "", nil},
44		{"none", domain.Mergeability{}, "Checking…", "", false, false, "", nil},
45	} {
46		t.Run(tt.name, func(t *testing.T) {
47			t.Parallel()
48			v := mergeabilityView(tt.m, "main")
49			assert.Equal(t, tt.label, v.Label)
50			assert.Equal(t, tt.detail, v.Detail)
51			assert.Equal(t, tt.ffOK, v.FFOk)
52			assert.Equal(t, tt.mergeOK, v.MergeOk)
53			assert.Equal(t, tt.note, v.PathsNote)
54			assert.Equal(t, tt.paths, v.Paths)
55			assert.Equal(t, !tt.ffOK, v.FFReason != "", "a mode that is not possible says why")
56			assert.Equal(t, !tt.mergeOK, v.MergeReason != "")
57		})
58	}
59}
60
61func TestMergeBoxModes(t *testing.T) {
62	t.Parallel()
63	d := &repoData{repo: repoWithModes(domain.MergeModes()...)}
64	s := &Server{}
65
66	behind := s.mergeBox(d, 1, "feat", "abc", true, mergeabilityView(domain.Mergeability{State: domain.MergeabilityBehind, Behind: 1}, "main"))
67	require.Len(t, behind.Modes, 3)
68	assert.True(t, behind.Modes[0].Disabled, "fast-forward is not possible when behind")
69	assert.NotEmpty(t, behind.Modes[0].Reason)
70	assert.False(t, behind.Modes[1].Disabled)
71	assert.False(t, behind.Modes[2].Disabled)
72	assert.Equal(t, 1, behind.Checked(), "the first possible mode is checked")
73	assert.True(t, behind.CanMerge())
74	assert.True(t, behind.Signed)
75
76	conflict := s.mergeBox(d, 1, "feat", "abc", true, mergeabilityView(domain.Mergeability{State: domain.MergeabilityConflict}, "main"))
77	assert.False(t, conflict.CanMerge())
78	assert.NotEmpty(t, conflict.MergeTitle(), "the button says why it is disabled")
79	assert.Equal(t, 0, conflict.Checked())
80	assert.Contains(t, conflict.CLI.Lines[2], "--mode merge")
81	assert.NotContains(t, conflict.CLI.Lines[1], "--squash")
82}
83
84func TestMergeBoxOnlyFastForwardIsNotSigned(t *testing.T) {
85	t.Parallel()
86	d := &repoData{repo: repoWithModes(domain.MergeModeFastForward)}
87	b := (&Server{}).mergeBox(d, 1, "feat", "abc", true, mergeabilityView(domain.Mergeability{State: domain.MergeabilityUpToDate}, "main"))
88	assert.False(t, b.Signed, "no commit is created, nothing to explain")
89	assert.Len(t, b.Modes, 1)
90}
91
92func TestMergeBoxNamesTheSigningKey(t *testing.T) {
93	t.Parallel()
94	key := mocks.NewSigningKeyService(t)
95	key.EXPECT().Info().Return(domain.SigningKeyInfo{Key: domain.SigningPublicKey{Fingerprint: "SHA256:abc"}})
96	d := &repoData{repo: repoWithModes(domain.MergeModes()...)}
97	b := (&Server{cfg: Config{SigningKey: key}}).mergeBox(d, 1, "feat", "abc", true, mergeabilityView(domain.Mergeability{}, "main"))
98	assert.Equal(t, "SHA256:abc", b.Fingerprint)
99}
100
101func TestMergeErrorTravelsInTheContext(t *testing.T) {
102	t.Parallel()
103	assert.Empty(t, mergeErrorOf(context.Background()))
104	assert.Equal(t, "boom", mergeErrorOf(withMergeError(context.Background(), "boom")))
105}
106
107func TestMergeabilityLiveEvents(t *testing.T) {
108	t.Parallel()
109	assert.True(t, liveEvent(watchOf(domain.MergeRequestMergeability{Number: 1})))
110	svc := mocks.NewMergeRequestService(t)
111	svc.EXPECT().Mergeability(mock.Anything, mock.Anything, mock.Anything, int64(1)).
112		Return(domain.Mergeability{State: domain.MergeabilityBehind, Behind: 3}, nil)
113	l := liveStream{s: &Server{cfg: Config{MergeRequests: svc}}, repo: "demo", view: "mr", n: "1", target: "main"}
114	ctx := context.WithValue(context.Background(), signedInKey{}, struct{}{})
115
116	fs, err := l.repoPage(ctx, watchOf(domain.MergeRequestMergeability{Number: 1}))
117	require.NoError(t, err)
118	require.Len(t, fs, 1)
119	assert.Equal(t, "mr-mergeability", fs[0].event)
120	assert.Contains(t, fs[0].html, "Behind main by 3 commits")
121
122	// Another request, or a page that is not one: nothing.
123	fs, err = l.repoPage(ctx, watchOf(domain.MergeRequestMergeability{Number: 2}))
124	require.NoError(t, err)
125	assert.Empty(t, fs)
126	// The box is the owner's.
127	fs, err = l.repoPage(context.Background(), watchOf(domain.MergeRequestMergeability{Number: 1}))
128	require.NoError(t, err)
129	assert.Empty(t, fs)
130}

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