detached 973e682

mr_api_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package rpc
4
5import (
6	"testing"
7	"time"
8
9	pb "bnema.dev/ermite-api/gen/ermite"
10	"bnema.dev/ermite/internal/domain"
11	"bnema.dev/ermite/internal/ports"
12	"bnema.dev/ermite/internal/ports/mocks"
13	"connectrpc.com/connect"
14	"github.com/stretchr/testify/assert"
15	"github.com/stretchr/testify/mock"
16	"github.com/stretchr/testify/require"
17)
18
19func TestNoteRPCFields(t *testing.T) {
20	now := time.Now().UTC()
21	note := domain.Comment{ID: 4, ReplyTo: 2, StartLine: 10, Line: 24, Verdict: domain.VerdictApprove, EditedAt: now, Deleted: true}
22	got := commentToPB(note)
23	assert.Equal(t, "2", got.GetReplyTo())
24	assert.EqualValues(t, 10, got.GetStartLine())
25	assert.Equal(t, pb.Verdict_VERDICT_APPROVE, got.GetVerdict())
26	assert.Equal(t, now, got.GetEditedAt().AsTime())
27	assert.True(t, got.GetDeleted())
28	assert.Empty(t, commentToPB(domain.Comment{}).GetReplyTo())
29	assert.Nil(t, commentToPB(domain.Comment{}).GetEditedAt())
30	for _, v := range []domain.Verdict{domain.VerdictNone, domain.VerdictApprove, domain.VerdictChangesRequested} {
31		back, err := verdictFromPB(verdictToPB(v))
32		require.NoError(t, err)
33		assert.Equal(t, v, back)
34	}
35	m := mocks.NewMergeRequestService(t)
36	m.EXPECT().AddComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), ports.AddCommentParams{Text: "note", StartLine: 10, Line: 24, Verdict: domain.VerdictApprove}).Return(note, nil).Once()
37	res, err := (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, Text: "note", StartLine: 10, Line: 24, Verdict: pb.Verdict_VERDICT_APPROVE}))
38	require.NoError(t, err)
39	assert.Equal(t, got, res.Msg.GetComment())
40	_, err = (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Verdict: pb.Verdict(99)}))
41	requireCode(t, connect.CodeInvalidArgument, err)
42}
43
44func TestNoteMutationRPCCodes(t *testing.T) {
45	for _, edit := range []bool{false, true} {
46		for _, failure := range []error{nil, domain.ErrForbidden, domain.ErrPreconditionFailed, domain.ErrNotFound} {
47			m := mocks.NewMergeRequestService(t)
48			note := domain.Comment{ID: 2, Deleted: !edit}
49			var err error
50			if edit {
51				m.EXPECT().EditComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), int64(2), "new").Return(note, failure).Once()
52				_, err = (&mergeRequestService{m}).EditComment(ownerCtx(), connect.NewRequest(&pb.EditCommentRequest{Repo: "demo", Number: 1, CommentId: "2", Text: "new"}))
53			} else {
54				m.EXPECT().DeleteComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), int64(2)).Return(note, failure).Once()
55				_, err = (&mergeRequestService{m}).DeleteComment(ownerCtx(), connect.NewRequest(&pb.DeleteCommentRequest{Repo: "demo", Number: 1, CommentId: "2"}))
56			}
57			switch failure {
58			case nil:
59				require.NoError(t, err)
60			case domain.ErrForbidden:
61				requireCode(t, connect.CodePermissionDenied, err)
62			case domain.ErrPreconditionFailed:
63				requireCode(t, connect.CodeFailedPrecondition, err)
64			case domain.ErrNotFound:
65				requireCode(t, connect.CodeNotFound, err)
66			}
67		}
68	}
69}
70
71func TestNoteEventsRPC(t *testing.T) {
72	repo := mustRepoName(t, "demo")
73	note := domain.Comment{ID: 3, Verdict: domain.VerdictApprove, Deleted: true}
74	at := time.Now().UTC()
75	events := []domain.Event{
76		domain.CommentEdited{Repo: repo, Number: 1, Comment: note, At: at},
77		domain.CommentDeleted{Repo: repo, Number: 1, Comment: note, At: at},
78		domain.CommentResolved{Repo: repo, Number: 1, Comment: note, At: at},
79	}
80	for i, ev := range events {
81		got := watchEventToPB(ports.WatchEvent{ID: "e", Event: ev})
82		require.NotNil(t, got)
83		assert.Equal(t, at, got.GetTime().AsTime())
84		var c *pb.Comment
85		switch i {
86		case 0:
87			c = got.GetCommentEdited().GetComment()
88		case 1:
89			c = got.GetCommentDeleted().GetComment()
90		case 2:
91			c = got.GetCommentResolved().GetComment()
92		}
93		assert.Equal(t, "3", c.GetId())
94		assert.True(t, c.GetDeleted())
95		assert.Equal(t, pb.Verdict_VERDICT_APPROVE, c.GetVerdict())
96	}
97}
98
99func TestNoteReplyIDValidation(t *testing.T) {
100	for _, id := range []string{"abc", "0", "-1", "01", "9223372036854775808"} {
101		_, err := (&mergeRequestService{mocks.NewMergeRequestService(t)}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, ReplyTo: id, Text: "reply"}))
102		requireCode(t, connect.CodeInvalidArgument, err)
103		assert.Contains(t, err.Error(), "reply_to")
104		assert.Contains(t, err.Error(), "is not a note id")
105	}
106	m := mocks.NewMergeRequestService(t)
107	m.EXPECT().AddComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), ports.AddCommentParams{ReplyTo: 2, Text: "reply"}).Return(domain.Comment{ID: 3, ReplyTo: 2}, nil).Once()
108	res, err := (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, ReplyTo: "2", Text: "reply"}))
109	require.NoError(t, err)
110	assert.Equal(t, "2", res.Msg.GetComment().GetReplyTo())
111}
112
113func TestNoteVerdictsDetailAndScope(t *testing.T) {
114	m := mocks.NewMergeRequestService(t)
115	m.EXPECT().Get(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), ports.GetMergeRequestOptions{}).Return(ports.MergeRequestDetail{Verdicts: []domain.AuthorVerdict{{Author: "owner", Verdict: domain.VerdictApprove, CommentID: 4, Stale: true}}}, nil).Once()
116	res, err := (&mergeRequestService{m}).GetMergeRequest(ownerCtx(), connect.NewRequest(&pb.GetMergeRequestRequest{Repo: "demo", Number: 1}))
117	require.NoError(t, err)
118	require.Len(t, res.Msg.GetVerdicts(), 1)
119	assert.Equal(t, "4", res.Msg.GetVerdicts()[0].GetCommentId())
120	assert.True(t, res.Msg.GetVerdicts()[0].GetStale())
121	m.EXPECT().AddComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), ports.AddCommentParams{Verdict: domain.VerdictApprove}).Return(domain.Comment{}, domain.ErrForbidden).Once()
122	_, err = (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, Verdict: pb.Verdict_VERDICT_APPROVE}))
123	requireCode(t, connect.CodePermissionDenied, err)
124}

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