| @@ -0,0 +1,130 @@ |
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | + | |
| 3 | + | package web |
| 4 | + | |
| 5 | + | import ( |
| 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 | + | |
| 18 | + | func repoWithModes(modes ...domain.MergeMode) ports.RepoView { |
| 19 | + | var v ports.RepoView |
| 20 | + | v.Settings.AllowedMergeModes = modes |
| 21 | + | return v |
| 22 | + | } |
| 23 | + | |
| 24 | + | func watchOf(ev domain.Event) ports.WatchEvent { return ports.WatchEvent{Event: ev} } |
| 25 | + | |
| 26 | + | func 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 | + | |
| 61 | + | func 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", "", 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", "", 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 | + | |
| 84 | + | func TestMergeBoxOnlyFastForwardIsNotSigned(t *testing.T) { |
| 85 | + | t.Parallel() |
| 86 | + | d := &repoData{repo: repoWithModes(domain.MergeModeFastForward)} |
| 87 | + | b := (&Server{}).mergeBox(d, 1, "feat", "abc", "", 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 | + | |
| 92 | + | func 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", "", mergeabilityView(domain.Mergeability{}, "main")) |
| 98 | + | assert.Equal(t, "SHA256:abc", b.Fingerprint) |
| 99 | + | } |
| 100 | + | |
| 101 | + | func 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 | + | |
| 107 | + | func 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 | + | } |