1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package rpc
4
5import (
6 "testing"
7
8 "connectrpc.com/connect"
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/mock"
11 "github.com/stretchr/testify/require"
12
13 pb "bnema.dev/ermite-api/gen/ermite"
14 "bnema.dev/ermite/internal/domain"
15 "bnema.dev/ermite/internal/ports"
16 "bnema.dev/ermite/internal/ports/mocks"
17)
18
19func TestMergeRequestServiceCreate(t *testing.T) {
20 t.Parallel()
21 tests := []struct {
22 name string
23 repo string
24 usecase error
25 wantCode connect.Code
26 wantCall bool
27 }{
28 {name: "ok", repo: "demo", wantCall: true},
29 {name: "invalid repo", repo: "../x", wantCode: connect.CodeInvalidArgument},
30 {name: "not found", repo: "demo", usecase: domain.ErrNotFound, wantCode: connect.CodeNotFound, wantCall: true},
31 {name: "forbidden", repo: "demo", usecase: domain.ErrForbidden, wantCode: connect.CodePermissionDenied, wantCall: true},
32 {name: "already open", repo: "demo", usecase: domain.ErrAlreadyExists, wantCode: connect.CodeAlreadyExists, wantCall: true},
33 {name: "invalid fields", repo: "demo", usecase: domain.ErrInvalid, wantCode: connect.CodeInvalidArgument, wantCall: true},
34 }
35 for _, tt := range tests {
36 t.Run(tt.name, func(t *testing.T) {
37 t.Parallel()
38 mrs := mocks.NewMergeRequestService(t)
39 if tt.wantCall {
40 mrs.EXPECT().Create(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), ports.CreateMergeRequestParams{
41 Title: "t", Description: "d", Source: "feat", Target: "main",
42 }).Return(domain.MergeRequest{Number: 7, Source: "feat", Target: "main", State: domain.MRStateOpen}, tt.usecase).Once()
43 }
44 resp, err := (&mergeRequestService{mrs}).CreateMergeRequest(ownerCtx(), connect.NewRequest(&pb.CreateMergeRequestRequest{
45 Repo: tt.repo, Title: "t", Description: "d", SourceBranch: "feat", TargetBranch: "main",
46 }))
47 if tt.wantCode != 0 {
48 requireCode(t, tt.wantCode, err)
49 return
50 }
51 require.NoError(t, err)
52 assert.EqualValues(t, 7, resp.Msg.GetMergeRequest().GetNumber())
53 assert.Equal(t, pb.MergeRequestState_MERGE_REQUEST_STATE_OPEN, resp.Msg.GetMergeRequest().GetState())
54 })
55 }
56}
57
58func TestMergeRequestServiceListRejectsUnknownState(t *testing.T) {
59 t.Parallel()
60 _, err := (&mergeRequestService{mocks.NewMergeRequestService(t)}).ListMergeRequests(ownerCtx(),
61 connect.NewRequest(&pb.ListMergeRequestsRequest{Repo: "demo", State: pb.MergeRequestState(99)}))
62 requireCode(t, connect.CodeInvalidArgument, err)
63}
64
65func TestMergeRequestServiceListFiltersByState(t *testing.T) {
66 t.Parallel()
67 mrs := mocks.NewMergeRequestService(t)
68 mrs.EXPECT().List(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), ports.ListMergeRequestsParams{
69 State: domain.MRStateMerged, PageSize: 3, PageToken: "tok",
70 }).Return(ports.MergeRequestPage{MergeRequests: []domain.MergeRequest{{Number: 1}}, NextPageToken: "n"}, nil).Once()
71 resp, err := (&mergeRequestService{mrs}).ListMergeRequests(ownerCtx(), connect.NewRequest(&pb.ListMergeRequestsRequest{
72 Repo: "demo", State: pb.MergeRequestState_MERGE_REQUEST_STATE_MERGED, PageSize: 3, PageToken: "tok",
73 }))
74 require.NoError(t, err)
75 assert.Len(t, resp.Msg.GetMergeRequests(), 1)
76 assert.Equal(t, "n", resp.Msg.GetNextPageToken())
77}
78
79func TestMergeRequestServiceAddComment(t *testing.T) {
80 t.Parallel()
81 tests := []struct {
82 name string
83 side pb.DiffSide
84 usecase error
85 wantCode connect.Code
86 wantCall bool
87 }{
88 {name: "ok", side: pb.DiffSide_DIFF_SIDE_NEW, wantCall: true},
89 {name: "unknown side", side: pb.DiffSide(99), wantCode: connect.CodeInvalidArgument},
90 {name: "merge request not open", side: pb.DiffSide_DIFF_SIDE_NEW, usecase: domain.ErrPreconditionFailed, wantCode: connect.CodeFailedPrecondition, wantCall: true},
91 {name: "concurrent update", side: pb.DiffSide_DIFF_SIDE_NEW, usecase: domain.ErrConflict, wantCode: connect.CodeAborted, wantCall: true},
92 {name: "not found", side: pb.DiffSide_DIFF_SIDE_NEW, usecase: domain.ErrNotFound, wantCode: connect.CodeNotFound, wantCall: true},
93 }
94 for _, tt := range tests {
95 t.Run(tt.name, func(t *testing.T) {
96 t.Parallel()
97 mrs := mocks.NewMergeRequestService(t)
98 if tt.wantCall {
99 mrs.EXPECT().AddComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(2), mock.Anything).
100 Return(domain.Comment{ID: 5}, tt.usecase).Once()
101 }
102 resp, err := (&mergeRequestService{mrs}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{
103 Repo: "demo", Number: 2, Path: "f.go", Line: 3, Side: tt.side, Text: "hi",
104 }))
105 if tt.wantCode != 0 {
106 requireCode(t, tt.wantCode, err)
107 return
108 }
109 require.NoError(t, err)
110 assert.Equal(t, "5", resp.Msg.GetComment().GetId())
111 })
112 }
113}
114
115func TestMergeRequestServiceResolveCommentMapsUnissuedIDsToZero(t *testing.T) {
116 t.Parallel()
117 for _, id := range []string{"", "abc", "-1", "0", "007", "99999999999999999999"} {
118 t.Run(id, func(t *testing.T) {
119 t.Parallel()
120 mrs := mocks.NewMergeRequestService(t)
121 mrs.EXPECT().ResolveComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), int64(0), true).
122 Return(domain.Comment{}, domain.ErrNotFound).Once()
123 _, err := (&mergeRequestService{mrs}).ResolveComment(ownerCtx(), connect.NewRequest(&pb.ResolveCommentRequest{
124 Repo: "demo", Number: 1, CommentId: id, Resolved: true,
125 }))
126 requireCode(t, connect.CodeNotFound, err)
127 })
128 }
129}
130
131func TestMergeRequestServiceMerge(t *testing.T) {
132 t.Parallel()
133 const head = "0123456789abcdef0123456789abcdef01234567"
134 tests := []struct {
135 name string
136 req *pb.MergeMergeRequestRequest
137 usecase error
138 wantCode connect.Code
139 wantCall bool
140 }{
141 {name: "ok", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: head, DeleteSourceBranch: true}, wantCall: true},
142 {name: "head is not a full id", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: "abc"}, wantCode: connect.CodeInvalidArgument},
143 {name: "invalid repo", req: &pb.MergeMergeRequestRequest{Repo: "../x", Number: 1}, wantCode: connect.CodeInvalidArgument},
144 {name: "head moved", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: head}, usecase: domain.ErrConflict, wantCode: connect.CodeAborted, wantCall: true},
145 {name: "checks failing", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1}, usecase: domain.ErrPreconditionFailed, wantCode: connect.CodeFailedPrecondition, wantCall: true},
146 {name: "forbidden", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1}, usecase: domain.ErrForbidden, wantCode: connect.CodePermissionDenied, wantCall: true},
147 }
148 for _, tt := range tests {
149 t.Run(tt.name, func(t *testing.T) {
150 t.Parallel()
151 mrs := mocks.NewMergeRequestService(t)
152 if tt.wantCall {
153 mrs.EXPECT().Merge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), mock.MatchedBy(func(p ports.MergeParams) bool {
154 return p.DeleteSource == tt.req.GetDeleteSourceBranch() && (tt.req.GetHeadId() == "") == p.HeadID.IsZero()
155 })).Return(ports.MergeResult{MergeRequest: domain.MergeRequest{Number: 1}, SourceDeleted: true}, tt.usecase).Once()
156 }
157 resp, err := (&mergeRequestService{mrs}).MergeMergeRequest(ownerCtx(), connect.NewRequest(tt.req))
158 if tt.wantCode != 0 {
159 requireCode(t, tt.wantCode, err)
160 return
161 }
162 require.NoError(t, err)
163 assert.True(t, resp.Msg.GetSourceBranchDeleted())
164 })
165 }
166}
167
168func TestMergeRequestServiceCloseAndMarkRead(t *testing.T) {
169 t.Parallel()
170 t.Run("close not found", func(t *testing.T) {
171 t.Parallel()
172 mrs := mocks.NewMergeRequestService(t)
173 mrs.EXPECT().Close(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(9)).Return(domain.MergeRequest{}, domain.ErrNotFound).Once()
174 _, err := (&mergeRequestService{mrs}).CloseMergeRequest(ownerCtx(), connect.NewRequest(&pb.CloseMergeRequestRequest{Repo: "demo", Number: 9}))
175 requireCode(t, connect.CodeNotFound, err)
176 })
177 t.Run("close ok", func(t *testing.T) {
178 t.Parallel()
179 mrs := mocks.NewMergeRequestService(t)
180 mrs.EXPECT().Close(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(9)).Return(domain.MergeRequest{Number: 9, State: domain.MRStateClosed}, nil).Once()
181 resp, err := (&mergeRequestService{mrs}).CloseMergeRequest(ownerCtx(), connect.NewRequest(&pb.CloseMergeRequestRequest{Repo: "demo", Number: 9}))
182 require.NoError(t, err)
183 assert.Equal(t, pb.MergeRequestState_MERGE_REQUEST_STATE_CLOSED, resp.Msg.GetMergeRequest().GetState())
184 })
185 t.Run("mark read rejects a short head", func(t *testing.T) {
186 t.Parallel()
187 _, err := (&mergeRequestService{mocks.NewMergeRequestService(t)}).MarkRead(ownerCtx(),
188 connect.NewRequest(&pb.MarkReadRequest{Repo: "demo", Number: 1, HeadId: "abc"}))
189 requireCode(t, connect.CodeInvalidArgument, err)
190 })
191 t.Run("mark read unauthenticated", func(t *testing.T) {
192 t.Parallel()
193 mrs := mocks.NewMergeRequestService(t)
194 mrs.EXPECT().MarkRead(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), domain.ObjectID{}).Return(domain.ObjectID{}, domain.ErrUnauthenticated).Once()
195 _, err := (&mergeRequestService{mrs}).MarkRead(ownerCtx(), connect.NewRequest(&pb.MarkReadRequest{Repo: "demo", Number: 1}))
196 requireCode(t, connect.CodeUnauthenticated, err)
197 })
198}
199
200func TestMergeRequestServiceGet(t *testing.T) {
201 t.Parallel()
202 tests := []struct {
203 name string
204 usecase error
205 wantCode connect.Code
206 }{
207 {name: "ok"},
208 {name: "not found", usecase: domain.ErrNotFound, wantCode: connect.CodeNotFound},
209 {name: "unauthenticated", usecase: domain.ErrUnauthenticated, wantCode: connect.CodeUnauthenticated},
210 }
211 for _, tt := range tests {
212 t.Run(tt.name, func(t *testing.T) {
213 t.Parallel()
214 mrs := mocks.NewMergeRequestService(t)
215 mrs.EXPECT().Get(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(4), true).
216 Return(ports.MergeRequestDetail{MergeRequest: domain.MergeRequest{Number: 4}, CommitsTruncated: true}, tt.usecase).Once()
217 resp, err := (&mergeRequestService{mrs}).GetMergeRequest(ownerCtx(), connect.NewRequest(&pb.GetMergeRequestRequest{Repo: "demo", Number: 4, SinceLastRead: true}))
218 if tt.wantCode != 0 {
219 requireCode(t, tt.wantCode, err)
220 return
221 }
222 require.NoError(t, err)
223 assert.EqualValues(t, 4, resp.Msg.GetMergeRequest().GetNumber())
224 assert.True(t, resp.Msg.GetCommitsTruncated())
225 })
226 }
227}