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: 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 assert.Equal(t, pb.MergeabilityState_MERGEABILITY_STATE_CHECKING, resp.Msg.GetMergeRequest().GetMergeability().GetState())
55 })
56 }
57}
58
59func TestMergeRequestServiceListRejectsUnknownState(t *testing.T) {
60 t.Parallel()
61 _, err := (&mergeRequestService{mrs: mocks.NewMergeRequestService(t)}).ListMergeRequests(ownerCtx(),
62 connect.NewRequest(&pb.ListMergeRequestsRequest{Repo: "demo", State: pb.MergeRequestState(99)}))
63 requireCode(t, connect.CodeInvalidArgument, err)
64}
65
66func TestMergeRequestServiceListFiltersByState(t *testing.T) {
67 t.Parallel()
68 mrs := mocks.NewMergeRequestService(t)
69 mrs.EXPECT().List(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), ports.ListMergeRequestsParams{
70 State: domain.MRStateMerged, PageSize: 3, PageToken: "tok",
71 }).Return(ports.MergeRequestPage{MergeRequests: []domain.MergeRequest{{Number: 1}}, NextPageToken: "n"}, nil).Once()
72 resp, err := (&mergeRequestService{mrs: mrs}).ListMergeRequests(ownerCtx(), connect.NewRequest(&pb.ListMergeRequestsRequest{
73 Repo: "demo", State: pb.MergeRequestState_MERGE_REQUEST_STATE_MERGED, PageSize: 3, PageToken: "tok",
74 }))
75 require.NoError(t, err)
76 assert.Len(t, resp.Msg.GetMergeRequests(), 1)
77 assert.Equal(t, "n", resp.Msg.GetNextPageToken())
78}
79
80func TestMergeRequestServiceAddComment(t *testing.T) {
81 t.Parallel()
82 tests := []struct {
83 name string
84 side pb.DiffSide
85 usecase error
86 wantCode connect.Code
87 wantCall bool
88 }{
89 {name: "ok", side: pb.DiffSide_DIFF_SIDE_NEW, wantCall: true},
90 {name: "unknown side", side: pb.DiffSide(99), wantCode: connect.CodeInvalidArgument},
91 {name: "merge request not open", side: pb.DiffSide_DIFF_SIDE_NEW, usecase: domain.ErrPreconditionFailed, wantCode: connect.CodeFailedPrecondition, wantCall: true},
92 {name: "concurrent update", side: pb.DiffSide_DIFF_SIDE_NEW, usecase: domain.ErrConflict, wantCode: connect.CodeAborted, wantCall: true},
93 {name: "not found", side: pb.DiffSide_DIFF_SIDE_NEW, usecase: domain.ErrNotFound, wantCode: connect.CodeNotFound, wantCall: true},
94 }
95 for _, tt := range tests {
96 t.Run(tt.name, func(t *testing.T) {
97 t.Parallel()
98 mrs := mocks.NewMergeRequestService(t)
99 if tt.wantCall {
100 mrs.EXPECT().AddComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(2), mock.Anything).
101 Return(domain.Comment{ID: 5}, tt.usecase).Once()
102 }
103 resp, err := (&mergeRequestService{mrs: mrs}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{
104 Repo: "demo", Number: 2, Path: "f.go", Line: 3, Side: tt.side, Text: "hi",
105 }))
106 if tt.wantCode != 0 {
107 requireCode(t, tt.wantCode, err)
108 return
109 }
110 require.NoError(t, err)
111 assert.Equal(t, "5", resp.Msg.GetComment().GetId())
112 })
113 }
114}
115
116func TestMergeRequestServiceResolveCommentMapsUnissuedIDsToZero(t *testing.T) {
117 t.Parallel()
118 for _, id := range []string{"", "abc", "-1", "0", "007", "99999999999999999999"} {
119 t.Run(id, func(t *testing.T) {
120 t.Parallel()
121 mrs := mocks.NewMergeRequestService(t)
122 mrs.EXPECT().ResolveComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), int64(0), true).
123 Return(domain.Comment{}, domain.ErrNotFound).Once()
124 _, err := (&mergeRequestService{mrs: mrs}).ResolveComment(ownerCtx(), connect.NewRequest(&pb.ResolveCommentRequest{
125 Repo: "demo", Number: 1, CommentId: id, Resolved: true,
126 }))
127 requireCode(t, connect.CodeNotFound, err)
128 })
129 }
130}
131
132func TestMergeRequestServiceMerge(t *testing.T) {
133 t.Parallel()
134 const head = "0123456789abcdef0123456789abcdef01234567"
135 tests := []struct {
136 name string
137 req *pb.MergeMergeRequestRequest
138 usecase error
139 wantCode connect.Code
140 wantCall bool
141 }{
142 {name: "ok", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: head, DeleteSourceBranch: true}, wantCall: true},
143 {name: "head is not a full id", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: "abc"}, wantCode: connect.CodeInvalidArgument},
144 {name: "invalid repo", req: &pb.MergeMergeRequestRequest{Repo: "../x", Number: 1}, wantCode: connect.CodeInvalidArgument},
145 {name: "head moved", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: head}, usecase: domain.ErrConflict, wantCode: connect.CodeAborted, wantCall: true},
146 {name: "checks failing", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1}, usecase: domain.ErrPreconditionFailed, wantCode: connect.CodeFailedPrecondition, wantCall: true},
147 {name: "forbidden", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1}, usecase: domain.ErrForbidden, wantCode: connect.CodePermissionDenied, wantCall: true},
148 }
149 for _, tt := range tests {
150 t.Run(tt.name, func(t *testing.T) {
151 t.Parallel()
152 mrs := mocks.NewMergeRequestService(t)
153 if tt.wantCall {
154 mrs.EXPECT().Merge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), mock.MatchedBy(func(p ports.MergeParams) bool {
155 return p.DeleteSource == tt.req.GetDeleteSourceBranch() && (tt.req.GetHeadId() == "") == p.HeadID.IsZero()
156 })).Return(ports.MergeResult{MergeRequest: domain.MergeRequest{Number: 1}, SourceDeleted: true}, tt.usecase).Once()
157 }
158 resp, err := (&mergeRequestService{mrs: mrs}).MergeMergeRequest(ownerCtx(), connect.NewRequest(tt.req))
159 if tt.wantCode != 0 {
160 requireCode(t, tt.wantCode, err)
161 return
162 }
163 require.NoError(t, err)
164 assert.True(t, resp.Msg.GetSourceBranchDeleted())
165 })
166 }
167}
168
169func TestMergeRequestServiceCloseAndMarkRead(t *testing.T) {
170 t.Parallel()
171 t.Run("close not found", func(t *testing.T) {
172 t.Parallel()
173 mrs := mocks.NewMergeRequestService(t)
174 mrs.EXPECT().Close(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(9)).Return(domain.MergeRequest{}, domain.ErrNotFound).Once()
175 _, err := (&mergeRequestService{mrs: mrs}).CloseMergeRequest(ownerCtx(), connect.NewRequest(&pb.CloseMergeRequestRequest{Repo: "demo", Number: 9}))
176 requireCode(t, connect.CodeNotFound, err)
177 })
178 t.Run("close ok", func(t *testing.T) {
179 t.Parallel()
180 mrs := mocks.NewMergeRequestService(t)
181 mrs.EXPECT().Close(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(9)).Return(domain.MergeRequest{Number: 9, State: domain.MRStateClosed}, nil).Once()
182 resp, err := (&mergeRequestService{mrs: mrs}).CloseMergeRequest(ownerCtx(), connect.NewRequest(&pb.CloseMergeRequestRequest{Repo: "demo", Number: 9}))
183 require.NoError(t, err)
184 assert.Equal(t, pb.MergeRequestState_MERGE_REQUEST_STATE_CLOSED, resp.Msg.GetMergeRequest().GetState())
185 })
186 t.Run("mark read rejects a short head", func(t *testing.T) {
187 t.Parallel()
188 _, err := (&mergeRequestService{mrs: mocks.NewMergeRequestService(t)}).MarkRead(ownerCtx(),
189 connect.NewRequest(&pb.MarkReadRequest{Repo: "demo", Number: 1, HeadId: "abc"}))
190 requireCode(t, connect.CodeInvalidArgument, err)
191 })
192 t.Run("mark read unauthenticated", func(t *testing.T) {
193 t.Parallel()
194 mrs := mocks.NewMergeRequestService(t)
195 mrs.EXPECT().MarkRead(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), domain.ObjectID{}).Return(domain.ObjectID{}, domain.ErrUnauthenticated).Once()
196 _, err := (&mergeRequestService{mrs: mrs}).MarkRead(ownerCtx(), connect.NewRequest(&pb.MarkReadRequest{Repo: "demo", Number: 1}))
197 requireCode(t, connect.CodeUnauthenticated, err)
198 })
199}
200
201func TestMergeRequestServiceGet(t *testing.T) {
202 t.Parallel()
203 tests := []struct {
204 name string
205 usecase error
206 wantCode connect.Code
207 }{
208 {name: "ok"},
209 {name: "not found", usecase: domain.ErrNotFound, wantCode: connect.CodeNotFound},
210 {name: "unauthenticated", usecase: domain.ErrUnauthenticated, wantCode: connect.CodeUnauthenticated},
211 }
212 for _, tt := range tests {
213 t.Run(tt.name, func(t *testing.T) {
214 t.Parallel()
215 mrs := mocks.NewMergeRequestService(t)
216 mrs.EXPECT().Get(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(4), ports.GetMergeRequestOptions{SinceLastRead: true}).
217 Return(ports.MergeRequestDetail{MergeRequest: domain.MergeRequest{Number: 4}, CommitsTruncated: true}, tt.usecase).Once()
218 resp, err := (&mergeRequestService{mrs: mrs}).GetMergeRequest(ownerCtx(), connect.NewRequest(&pb.GetMergeRequestRequest{Repo: "demo", Number: 4, SinceLastRead: true}))
219 if tt.wantCode != 0 {
220 requireCode(t, tt.wantCode, err)
221 return
222 }
223 require.NoError(t, err)
224 assert.EqualValues(t, 4, resp.Msg.GetMergeRequest().GetNumber())
225 assert.True(t, resp.Msg.GetCommitsTruncated())
226 })
227 }
228}