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}