| @@ -0,0 +1,162 @@ |
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | + | |
| 3 | + | package web |
| 4 | + | |
| 5 | + | import ( |
| 6 | + | "bnema.dev/ermite/internal/ports/mocks" |
| 7 | + | "bytes" |
| 8 | + | "context" |
| 9 | + | "github.com/stretchr/testify/mock" |
| 10 | + | "net/http" |
| 11 | + | "net/http/httptest" |
| 12 | + | "net/url" |
| 13 | + | "strings" |
| 14 | + | "testing" |
| 15 | + | "time" |
| 16 | + | |
| 17 | + | "bnema.dev/ermite/internal/domain" |
| 18 | + | "bnema.dev/ermite/internal/ports" |
| 19 | + | "github.com/stretchr/testify/assert" |
| 20 | + | "github.com/stretchr/testify/require" |
| 21 | + | ) |
| 22 | + | |
| 23 | + | func TestParseOnRange(t *testing.T) { |
| 24 | + | t.Parallel() |
| 25 | + | for _, raw := range []string{"new:0-2:a", "new:2-2:a", "new:3-2:a", "new:1-x:a", "new:1-2:", "other:1-2:a"} { |
| 26 | + | assert.Nil(t, parseOn(raw), raw) |
| 27 | + | } |
| 28 | + | assert.Equal(t, &lineRef{side: "new", start: 10, line: 24, path: "dir:a.go"}, parseOn("new:10-24:dir:a.go")) |
| 29 | + | assert.Equal(t, &lineRef{side: "old", line: 10, path: "a"}, parseOn("old:10:a")) |
| 30 | + | } |
| 31 | + | |
| 32 | + | func TestNoteThreads(t *testing.T) { |
| 33 | + | t.Parallel() |
| 34 | + | text, err := domain.ParsePlainText("**hello** ^2") |
| 35 | + | require.NoError(t, err) |
| 36 | + | s := &Server{cfg: Config{Now: time.Now}, md: newMarkdown()} |
| 37 | + | comments := []domain.Comment{ |
| 38 | + | {ID: 1, Author: "alice", Text: text}, |
| 39 | + | {ID: 2, Author: "bob", ReplyTo: 1, Text: text}, |
| 40 | + | {ID: 3, Author: "alice", Deleted: true}, |
| 41 | + | {ID: 4, Author: "alice", Deleted: true}, |
| 42 | + | {ID: 5, Author: "bob", ReplyTo: 4, Text: text}, |
| 43 | + | {ID: 6, Author: "alice", Text: text, Verdict: domain.VerdictApprove, Resolved: true}, |
| 44 | + | } |
| 45 | + | p := MRPage{NoteAuthor: "alice"} |
| 46 | + | s.placeComments(&p, "demo", 1, comments, "", true, true, nil) |
| 47 | + | require.Len(t, p.General, 3) |
| 48 | + | assert.Equal(t, 4, p.NComments) |
| 49 | + | assert.Equal(t, 1, p.NUnresolved) |
| 50 | + | require.Len(t, p.General[0].Replies, 1) |
| 51 | + | assert.NotNil(t, p.General[0].Edit) |
| 52 | + | assert.Nil(t, p.General[0].Replies[0].Edit) |
| 53 | + | assert.Nil(t, p.General[0].Replies[0].Resolve) |
| 54 | + | assert.Contains(t, string(p.General[0].Body), "<strong>hello</strong>") |
| 55 | + | assert.Contains(t, string(p.General[0].Body), `href="#c2"`) |
| 56 | + | assert.True(t, p.General[1].Deleted) |
| 57 | + | var b bytes.Buffer |
| 58 | + | require.NoError(t, commentView(p.General[0], "").Render(context.Background(), &b)) |
| 59 | + | assert.Contains(t, b.String(), `name="reply_to" value="1"`) |
| 60 | + | assert.Contains(t, b.String(), `data-js data-quote=`) |
| 61 | + | assert.Contains(t, b.String(), `/comments/1/edit`) |
| 62 | + | assert.NotContains(t, b.String(), `/comments/2/edit`) |
| 63 | + | } |
| 64 | + | |
| 65 | + | func TestNoteLiveEvents(t *testing.T) { |
| 66 | + | t.Parallel() |
| 67 | + | l := liveStream{view: "mr", n: "1"} |
| 68 | + | events := []domain.Event{domain.CommentEdited{Number: 1}, domain.CommentDeleted{Number: 1}, domain.CommentResolved{Number: 1}} |
| 69 | + | for _, ev := range events { |
| 70 | + | we := ports.WatchEvent{Event: ev} |
| 71 | + | assert.True(t, liveEvent(we)) |
| 72 | + | fs, err := l.repoPage(context.Background(), we) |
| 73 | + | require.NoError(t, err) |
| 74 | + | require.Len(t, fs, 1) |
| 75 | + | assert.Contains(t, fs[0].html, "reload") |
| 76 | + | } |
| 77 | + | } |
| 78 | + | |
| 79 | + | func TestNotePosts(t *testing.T) { |
| 80 | + | t.Parallel() |
| 81 | + | svc := mocks.NewMergeRequestService(t) |
| 82 | + | repo := repoName(t, "demo") |
| 83 | + | s := &Server{cfg: Config{MergeRequests: svc}} |
| 84 | + | ctx := domain.ContextWithPrincipal(context.Background(), domain.Owner()) |
| 85 | + | request := func(values url.Values) *http.Request { |
| 86 | + | r := httptest.NewRequest("POST", "/demo/merge-requests/1/comments", strings.NewReader(values.Encode())).WithContext(ctx) |
| 87 | + | r.Header.Set("Content-Type", "application/x-www-form-urlencoded") |
| 88 | + | r.SetPathValue("repo", "demo") |
| 89 | + | r.SetPathValue("n", "1") |
| 90 | + | r.SetPathValue("id", "2") |
| 91 | + | require.NoError(t, r.ParseForm()) |
| 92 | + | return r |
| 93 | + | } |
| 94 | + | svc.EXPECT().AddComment(mock.Anything, mock.Anything, repo, int64(1), mock.MatchedBy(func(p ports.AddCommentParams) bool { return p.ReplyTo == 2 && p.Text == "reply" })).Return(domain.Comment{ID: 7}, nil).Once() |
| 95 | + | to, _, err := s.mrComment(ctx, request(url.Values{"reply_to": {"2"}, "text": {"reply"}})) |
| 96 | + | require.NoError(t, err) |
| 97 | + | assert.Equal(t, "/demo/merge-requests/1#c7", to) |
| 98 | + | svc.EXPECT().AddComment(mock.Anything, mock.Anything, repo, int64(1), mock.MatchedBy(func(p ports.AddCommentParams) bool { |
| 99 | + | return p.StartLine == 10 && p.Line == 24 && p.Path == "a.go" && p.Verdict == domain.VerdictApprove |
| 100 | + | })).Return(domain.Comment{ID: 8}, nil).Once() |
| 101 | + | to, _, err = s.mrComment(ctx, request(url.Values{"path": {"a.go"}, "line": {"24"}, "start_line": {"10"}, "side": {"new"}, "verdict": {"approve"}})) |
| 102 | + | require.NoError(t, err) |
| 103 | + | assert.Equal(t, "/demo/merge-requests/1#c8", to) |
| 104 | + | svc.EXPECT().EditComment(mock.Anything, mock.Anything, repo, int64(1), int64(2), "new\ntext").Return(domain.Comment{ID: 2}, nil).Once() |
| 105 | + | to, _, err = s.mrEditComment(ctx, request(url.Values{"text": {"new\r\ntext"}})) |
| 106 | + | require.NoError(t, err) |
| 107 | + | assert.Equal(t, "/demo/merge-requests/1#c2", to) |
| 108 | + | svc.EXPECT().DeleteComment(mock.Anything, mock.Anything, repo, int64(1), int64(2)).Return(domain.Comment{ID: 2}, nil).Once() |
| 109 | + | to, _, err = s.mrDeleteComment(ctx, request(nil)) |
| 110 | + | require.NoError(t, err) |
| 111 | + | assert.Equal(t, "/demo/merge-requests/1#c2", to) |
| 112 | + | } |
| 113 | + | |
| 114 | + | func TestDeletedNoteRendering(t *testing.T) { |
| 115 | + | t.Parallel() |
| 116 | + | text, err := domain.ParsePlainText("surviving reply") |
| 117 | + | require.NoError(t, err) |
| 118 | + | s := &Server{cfg: Config{Now: time.Now}, md: newMarkdown()} |
| 119 | + | p := MRPage{} |
| 120 | + | s.placeComments(&p, "demo", 1, []domain.Comment{ |
| 121 | + | {ID: 1, Author: "reviewer", Deleted: true, Verdict: domain.VerdictApprove}, |
| 122 | + | {ID: 2, Deleted: true}, |
| 123 | + | {ID: 3, ReplyTo: 2, Deleted: true}, |
| 124 | + | {ID: 4, Deleted: true}, |
| 125 | + | {ID: 5, ReplyTo: 4, Text: text}, |
| 126 | + | }, "", true, true, nil) |
| 127 | + | require.Len(t, p.General, 2) |
| 128 | + | assert.Equal(t, "1", p.General[0].ID) |
| 129 | + | assert.Equal(t, "4", p.General[1].ID) |
| 130 | + | var b bytes.Buffer |
| 131 | + | require.NoError(t, commentView(p.General[0], "").Render(context.Background(), &b)) |
| 132 | + | assert.Contains(t, b.String(), `<span class="c-who">owner</span>`) |
| 133 | + | assert.Contains(t, b.String(), "approve") |
| 134 | + | assert.Contains(t, b.String(), "text deleted") |
| 135 | + | b.Reset() |
| 136 | + | require.NoError(t, commentView(p.General[1], "").Render(context.Background(), &b)) |
| 137 | + | assert.Contains(t, b.String(), "surviving reply") |
| 138 | + | assert.NotContains(t, b.String(), "Quote") |
| 139 | + | assert.NotContains(t, b.String(), "data-quote") |
| 140 | + | } |
| 141 | + | |
| 142 | + | func TestInlineNoteHeadingIDs(t *testing.T) { |
| 143 | + | t.Parallel() |
| 144 | + | // "S H" would get the id the split copy of "H" gets if the prefixes of the |
| 145 | + | // two copies overlapped. |
| 146 | + | text, err := domain.ParsePlainText("# H\n\n# S H\n\n[jump](#h)") |
| 147 | + | require.NoError(t, err) |
| 148 | + | s := &Server{cfg: Config{Now: time.Now}, md: newMarkdown()} |
| 149 | + | p := MRPage{Files: []DiffFile{{Key: "a", ID: "f1", Hunks: []DiffHunk{{Lines: []DiffLine{{Kind: "add", newN: 1}}}}}}} |
| 150 | + | s.placeComments(&p, "demo", 1, []domain.Comment{{ID: 12, Path: "a", Side: domain.SideNew, Line: 1, Text: text}}, "", true, true, nil) |
| 151 | + | notes := p.Files[0].Hunks[0].Lines[0].Notes |
| 152 | + | require.Len(t, notes, 1) |
| 153 | + | var b bytes.Buffer |
| 154 | + | require.NoError(t, commentView(notes[0], "").Render(context.Background(), &b)) |
| 155 | + | require.NoError(t, commentView(notes[0], "-s").Render(context.Background(), &b)) |
| 156 | + | html := b.String() |
| 157 | + | for _, id := range []string{"c12-h", "c12-s-h", "c12s-h", "c12s-s-h"} { |
| 158 | + | assert.Equal(t, 1, strings.Count(html, `id="`+id+`"`), id) |
| 159 | + | } |
| 160 | + | assert.Contains(t, html, `href="#c12-h"`) |
| 161 | + | assert.Contains(t, html, `href="#c12s-h"`) |
| 162 | + | } |