1// SPDX-License-Identifier: MIT
2
3package api_test
4
5import (
6 "errors"
7 "testing"
8
9 "connectrpc.com/connect"
10
11 "google.golang.org/protobuf/proto"
12 "google.golang.org/protobuf/reflect/protoreflect"
13 "google.golang.org/protobuf/types/known/fieldmaskpb"
14
15 "bnema.dev/ermite-api/gen/ermite"
16 "bnema.dev/ermite-api/gen/ermite/ermiteconnect"
17)
18
19// TestProcedures pins the procedure paths: they are the wire contract.
20func TestProcedures(t *testing.T) {
21 tests := []struct{ got, want string }{
22 {ermiteconnect.ReleaseServiceCreateReleaseProcedure, "/ermite.ReleaseService/CreateRelease"},
23 {ermiteconnect.ReleaseServiceGetReleaseProcedure, "/ermite.ReleaseService/GetRelease"},
24 {ermiteconnect.ReleaseServiceListReleasesProcedure, "/ermite.ReleaseService/ListReleases"},
25 {ermiteconnect.ReleaseServiceUpdateReleaseProcedure, "/ermite.ReleaseService/UpdateRelease"},
26 {ermiteconnect.ReleaseServiceDeleteReleaseProcedure, "/ermite.ReleaseService/DeleteRelease"},
27 {ermiteconnect.ReleaseServiceUploadReleaseAssetProcedure, "/ermite.ReleaseService/UploadReleaseAsset"},
28 {ermiteconnect.ReleaseServiceUpdateReleaseAssetProcedure, "/ermite.ReleaseService/UpdateReleaseAsset"},
29 {ermiteconnect.ReleaseServiceDeleteReleaseAssetProcedure, "/ermite.ReleaseService/DeleteReleaseAsset"},
30 {ermiteconnect.ReleaseServiceDownloadReleaseAssetProcedure, "/ermite.ReleaseService/DownloadReleaseAsset"},
31 {ermiteconnect.ReleaseServiceListReleaseAssetsProcedure, "/ermite.ReleaseService/ListReleaseAssets"},
32 {ermiteconnect.ReleaseServiceGetReleaseUsageProcedure, "/ermite.ReleaseService/GetReleaseUsage"},
33
34 {ermiteconnect.MirrorServiceGetMirrorKeyProcedure, "/ermite.MirrorService/GetMirrorKey"},
35 {ermiteconnect.MirrorServiceRotateMirrorKeyProcedure, "/ermite.MirrorService/RotateMirrorKey"},
36 {ermiteconnect.MirrorServiceGetMirrorConfigProcedure, "/ermite.MirrorService/GetMirrorConfig"},
37 {ermiteconnect.MirrorServiceSetMirrorConfigProcedure, "/ermite.MirrorService/SetMirrorConfig"},
38 {ermiteconnect.MirrorServiceSetRepoMirrorProcedure, "/ermite.MirrorService/SetRepoMirror"},
39 {ermiteconnect.MirrorServiceRemoveRepoMirrorProcedure, "/ermite.MirrorService/RemoveRepoMirror"},
40 {ermiteconnect.MirrorServiceSetRepoMirrorExcludedProcedure, "/ermite.MirrorService/SetRepoMirrorExcluded"},
41 {ermiteconnect.MirrorServiceGetMirrorStatusProcedure, "/ermite.MirrorService/GetMirrorStatus"},
42 {ermiteconnect.MirrorServiceListMirrorStatusProcedure, "/ermite.MirrorService/ListMirrorStatus"},
43 {ermiteconnect.MirrorServiceSyncMirrorProcedure, "/ermite.MirrorService/SyncMirror"},
44
45 {ermiteconnect.RepoServiceListReposProcedure, "/ermite.RepoService/ListRepos"},
46 {ermiteconnect.RepoServiceGetRepoProcedure, "/ermite.RepoService/GetRepo"},
47 {ermiteconnect.RepoServiceCreateRepoProcedure, "/ermite.RepoService/CreateRepo"},
48 {ermiteconnect.RepoServiceUpdateRepoProcedure, "/ermite.RepoService/UpdateRepo"},
49 {ermiteconnect.RepoServiceDeleteRepoProcedure, "/ermite.RepoService/DeleteRepo"},
50 {ermiteconnect.GitServiceListRefsProcedure, "/ermite.GitService/ListRefs"},
51 {ermiteconnect.GitServiceGetTreeProcedure, "/ermite.GitService/GetTree"},
52 {ermiteconnect.GitServiceListPathsProcedure, "/ermite.GitService/ListPaths"},
53 {ermiteconnect.GitServiceGetBlobProcedure, "/ermite.GitService/GetBlob"},
54 {ermiteconnect.GitServiceListCommitsProcedure, "/ermite.GitService/ListCommits"},
55 {ermiteconnect.GitServiceGetCommitProcedure, "/ermite.GitService/GetCommit"},
56 {ermiteconnect.GitServiceResolveRevisionProcedure, "/ermite.GitService/ResolveRevision"},
57 {ermiteconnect.TokenServiceCreateTokenProcedure, "/ermite.TokenService/CreateToken"},
58 {ermiteconnect.TokenServiceListTokensProcedure, "/ermite.TokenService/ListTokens"},
59 {ermiteconnect.TokenServiceRevokeTokenProcedure, "/ermite.TokenService/RevokeToken"},
60 {ermiteconnect.EventServiceWatchProcedure, "/ermite.EventService/Watch"},
61 {ermiteconnect.MergeRequestServiceCreateMergeRequestProcedure, "/ermite.MergeRequestService/CreateMergeRequest"},
62 {ermiteconnect.MergeRequestServiceListMergeRequestsProcedure, "/ermite.MergeRequestService/ListMergeRequests"},
63 {ermiteconnect.MergeRequestServiceGetMergeRequestProcedure, "/ermite.MergeRequestService/GetMergeRequest"},
64 {ermiteconnect.MergeRequestServiceAddCommentProcedure, "/ermite.MergeRequestService/AddComment"},
65 {ermiteconnect.MergeRequestServiceResolveCommentProcedure, "/ermite.MergeRequestService/ResolveComment"},
66 {ermiteconnect.MergeRequestServiceCloseMergeRequestProcedure, "/ermite.MergeRequestService/CloseMergeRequest"},
67 {ermiteconnect.MergeRequestServiceMergeMergeRequestProcedure, "/ermite.MergeRequestService/MergeMergeRequest"},
68 {ermiteconnect.MergeRequestServiceMarkReadProcedure, "/ermite.MergeRequestService/MarkRead"},
69 {ermiteconnect.MergeRequestServiceEnableAutoMergeProcedure, "/ermite.MergeRequestService/EnableAutoMerge"},
70 {ermiteconnect.MergeRequestServiceCancelAutoMergeProcedure, "/ermite.MergeRequestService/CancelAutoMerge"},
71 {ermiteconnect.SessionServiceCreateLoginLinkProcedure, "/ermite.SessionService/CreateLoginLink"},
72 {ermiteconnect.SessionServiceSignOutEverywhereProcedure, "/ermite.SessionService/SignOutEverywhere"},
73 {ermiteconnect.ServerServiceGetVersionProcedure, "/ermite.ServerService/GetVersion"},
74 {ermiteconnect.ServerServiceGetSigningKeyProcedure, "/ermite.ServerService/GetSigningKey"},
75 {ermiteconnect.ServerServiceRotateOIDCKeyProcedure, "/ermite.ServerService/RotateOIDCKey"},
76 {ermiteconnect.RunnerServiceRegisterRunnerProcedure, "/ermite.RunnerService/RegisterRunner"},
77 {ermiteconnect.RunnerServiceFetchJobProcedure, "/ermite.RunnerService/FetchJob"},
78 {ermiteconnect.RunnerServiceUpdateJobProcedure, "/ermite.RunnerService/UpdateJob"},
79 {ermiteconnect.RunnerServiceAppendLogProcedure, "/ermite.RunnerService/AppendLog"},
80 {ermiteconnect.RunnerServiceHeartbeatProcedure, "/ermite.RunnerService/Heartbeat"},
81 {ermiteconnect.RunnerServiceWatchJobProcedure, "/ermite.RunnerService/WatchJob"},
82 {ermiteconnect.RunnerServiceCompleteJobProcedure, "/ermite.RunnerService/CompleteJob"},
83 {ermiteconnect.RunnerServiceResolveActionsProcedure, "/ermite.RunnerService/ResolveActions"},
84 {ermiteconnect.RunnerServiceUploadSummaryProcedure, "/ermite.RunnerService/UploadSummary"},
85 {ermiteconnect.RunServiceListRunsProcedure, "/ermite.RunService/ListRuns"},
86 {ermiteconnect.RunServiceGetRunProcedure, "/ermite.RunService/GetRun"},
87 {ermiteconnect.RunServiceGetJobProcedure, "/ermite.RunService/GetJob"},
88 {ermiteconnect.RunServiceGetJobLogProcedure, "/ermite.RunService/GetJobLog"},
89 {ermiteconnect.RunServiceListAnnotationsProcedure, "/ermite.RunService/ListAnnotations"},
90 {ermiteconnect.RunServiceGetStepSummariesProcedure, "/ermite.RunService/GetStepSummaries"},
91 {ermiteconnect.RunServiceCancelRunProcedure, "/ermite.RunService/CancelRun"},
92 {ermiteconnect.RunServiceRerunRunProcedure, "/ermite.RunService/RerunRun"},
93 {ermiteconnect.RunServiceDispatchWorkflowProcedure, "/ermite.RunService/DispatchWorkflow"},
94 {ermiteconnect.RunServiceListWorkflowsProcedure, "/ermite.RunService/ListWorkflows"},
95 {ermiteconnect.RunServiceDisableWorkflowProcedure, "/ermite.RunService/DisableWorkflow"},
96 {ermiteconnect.RunServiceEnableWorkflowProcedure, "/ermite.RunService/EnableWorkflow"},
97 {ermiteconnect.RunServiceListChecksProcedure, "/ermite.RunService/ListChecks"},
98 {ermiteconnect.RunServiceListRunnersProcedure, "/ermite.RunService/ListRunners"},
99 {ermiteconnect.RunServiceRemoveRunnerProcedure, "/ermite.RunService/RemoveRunner"},
100 {ermiteconnect.SecretServiceSetSecretProcedure, "/ermite.SecretService/SetSecret"},
101 {ermiteconnect.SecretServiceListSecretsProcedure, "/ermite.SecretService/ListSecrets"},
102 {ermiteconnect.SecretServiceDeleteSecretProcedure, "/ermite.SecretService/DeleteSecret"},
103 {ermiteconnect.VariableServiceSetVariableProcedure, "/ermite.VariableService/SetVariable"},
104 {ermiteconnect.VariableServiceGetVariableProcedure, "/ermite.VariableService/GetVariable"},
105 {ermiteconnect.VariableServiceListVariablesProcedure, "/ermite.VariableService/ListVariables"},
106 {ermiteconnect.VariableServiceDeleteVariableProcedure, "/ermite.VariableService/DeleteVariable"},
107 {ermiteconnect.EnvironmentServiceCreateEnvironmentProcedure, "/ermite.EnvironmentService/CreateEnvironment"},
108 {ermiteconnect.EnvironmentServiceListEnvironmentsProcedure, "/ermite.EnvironmentService/ListEnvironments"},
109 {ermiteconnect.EnvironmentServiceDeleteEnvironmentProcedure, "/ermite.EnvironmentService/DeleteEnvironment"},
110 {ermiteconnect.JobArtifactServiceCreateJobArtifactProcedure, "/ermite.JobArtifactService/CreateJobArtifact"},
111 {ermiteconnect.JobArtifactServiceUploadJobArtifactBlockProcedure, "/ermite.JobArtifactService/UploadJobArtifactBlock"},
112 {ermiteconnect.JobArtifactServiceCommitJobArtifactProcedure, "/ermite.JobArtifactService/CommitJobArtifact"},
113 {ermiteconnect.JobArtifactServiceFinalizeJobArtifactProcedure, "/ermite.JobArtifactService/FinalizeJobArtifact"},
114 {ermiteconnect.JobArtifactServiceListJobArtifactsProcedure, "/ermite.JobArtifactService/ListJobArtifacts"},
115 {ermiteconnect.JobArtifactServiceDownloadJobArtifactProcedure, "/ermite.JobArtifactService/DownloadJobArtifact"},
116 {ermiteconnect.JobArtifactServiceDeleteJobArtifactProcedure, "/ermite.JobArtifactService/DeleteJobArtifact"},
117 {ermiteconnect.ArtifactServiceListArtifactsProcedure, "/ermite.ArtifactService/ListArtifacts"},
118 {ermiteconnect.ArtifactServiceDownloadArtifactProcedure, "/ermite.ArtifactService/DownloadArtifact"},
119 {ermiteconnect.ArtifactServiceDeleteArtifactProcedure, "/ermite.ArtifactService/DeleteArtifact"},
120 }
121 for _, tt := range tests {
122 if tt.got != tt.want {
123 t.Errorf("procedure = %q, want %q", tt.got, tt.want)
124 }
125 }
126}
127
128// TestEnumNumbers pins enum numbers: they are part of the wire contract.
129func TestEnumNumbers(t *testing.T) {
130 if got := ermite.Scope_SCOPE_TOKEN_ADMIN; got != 9 {
131 t.Errorf("SCOPE_TOKEN_ADMIN = %d, want 9", got)
132 }
133 if got := ermite.FileStatus_FILE_STATUS_TYPE_CHANGED; got != 6 {
134 t.Errorf("FILE_STATUS_TYPE_CHANGED = %d, want 6", got)
135 }
136 if got := ermite.Visibility_VISIBILITY_PRIVATE; got != 2 {
137 t.Errorf("VISIBILITY_PRIVATE = %d, want 2", got)
138 }
139 if got := ermite.MergeMode_MERGE_MODE_SQUASH; got != 2 {
140 t.Errorf("MERGE_MODE_SQUASH = %d, want 2", got)
141 }
142 if got := ermite.MergeMode_MERGE_MODE_MERGE_COMMIT; got != 3 {
143 t.Errorf("MERGE_MODE_MERGE_COMMIT = %d, want 3", got)
144 }
145 if got := ermite.MergeabilityState_MERGEABILITY_STATE_CONFLICT; got != 4 {
146 t.Errorf("MERGEABILITY_STATE_CONFLICT = %d, want 4", got)
147 }
148 if got := ermite.MergeRequestState_MERGE_REQUEST_STATE_CLOSED; got != 3 {
149 t.Errorf("MERGE_REQUEST_STATE_CLOSED = %d, want 3", got)
150 }
151 if got := ermite.DiffSide_DIFF_SIDE_NEW; got != 2 {
152 t.Errorf("DIFF_SIDE_NEW = %d, want 2", got)
153 }
154 enums := []struct {
155 name string
156 got int32
157 want int32
158 }{
159 {"CI_STATUS_UNSPECIFIED", int32(ermite.CiStatus_CI_STATUS_UNSPECIFIED), 0},
160 {"CI_STATUS_QUEUED", int32(ermite.CiStatus_CI_STATUS_QUEUED), 1},
161 {"CI_STATUS_IN_PROGRESS", int32(ermite.CiStatus_CI_STATUS_IN_PROGRESS), 2},
162 {"CI_STATUS_COMPLETED", int32(ermite.CiStatus_CI_STATUS_COMPLETED), 3},
163 {"CI_STATUS_PENDING", int32(ermite.CiStatus_CI_STATUS_PENDING), 4},
164 {"CI_CONCLUSION_UNSPECIFIED", int32(ermite.CiConclusion_CI_CONCLUSION_UNSPECIFIED), 0},
165 {"CI_CONCLUSION_SUCCESS", int32(ermite.CiConclusion_CI_CONCLUSION_SUCCESS), 1},
166 {"CI_CONCLUSION_FAILURE", int32(ermite.CiConclusion_CI_CONCLUSION_FAILURE), 2},
167 {"CI_CONCLUSION_CANCELLED", int32(ermite.CiConclusion_CI_CONCLUSION_CANCELLED), 3},
168 {"CI_CONCLUSION_SKIPPED", int32(ermite.CiConclusion_CI_CONCLUSION_SKIPPED), 4},
169 {"CI_CONCLUSION_TIMED_OUT", int32(ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT), 5},
170 {"CI_CONCLUSION_NEUTRAL", int32(ermite.CiConclusion_CI_CONCLUSION_NEUTRAL), 6},
171 {"CI_CONCLUSION_ACTION_REQUIRED", int32(ermite.CiConclusion_CI_CONCLUSION_ACTION_REQUIRED), 7},
172 {"STEP_STATE_UNSPECIFIED", int32(ermite.StepState_STEP_STATE_UNSPECIFIED), 0},
173 {"STEP_STATE_PENDING", int32(ermite.StepState_STEP_STATE_PENDING), 1},
174 {"STEP_STATE_IN_PROGRESS", int32(ermite.StepState_STEP_STATE_IN_PROGRESS), 2},
175 {"STEP_STATE_COMPLETED", int32(ermite.StepState_STEP_STATE_COMPLETED), 3},
176 {"RUNNER_ISOLATION_UNSPECIFIED", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_UNSPECIFIED), 0},
177 {"RUNNER_ISOLATION_VM", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_VM), 1},
178 {"RUNNER_ISOLATION_CONTAINER_SYSBOX", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_SYSBOX), 2},
179 {"RUNNER_ISOLATION_CONTAINER_PRIVILEGED", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_PRIVILEGED), 3},
180 {"CANCEL_REASON_UNSPECIFIED", int32(ermite.CancelReason_CANCEL_REASON_UNSPECIFIED), 0},
181 {"CANCEL_REASON_USER", int32(ermite.CancelReason_CANCEL_REASON_USER), 1},
182 {"CANCEL_REASON_FAIL_FAST", int32(ermite.CancelReason_CANCEL_REASON_FAIL_FAST), 2},
183 {"CANCEL_REASON_TIMEOUT", int32(ermite.CancelReason_CANCEL_REASON_TIMEOUT), 3},
184 {"ANNOTATION_LEVEL_UNSPECIFIED", int32(ermite.AnnotationLevel_ANNOTATION_LEVEL_UNSPECIFIED), 0},
185 {"ANNOTATION_LEVEL_NOTICE", int32(ermite.AnnotationLevel_ANNOTATION_LEVEL_NOTICE), 1},
186 {"ANNOTATION_LEVEL_WARNING", int32(ermite.AnnotationLevel_ANNOTATION_LEVEL_WARNING), 2},
187 {"ANNOTATION_LEVEL_FAILURE", int32(ermite.AnnotationLevel_ANNOTATION_LEVEL_FAILURE), 3},
188 {"ACTION_SOURCE_UNSPECIFIED", int32(ermite.ActionSource_ACTION_SOURCE_UNSPECIFIED), 0},
189 {"ACTION_SOURCE_GITHUB", int32(ermite.ActionSource_ACTION_SOURCE_GITHUB), 1},
190 {"ACTION_SOURCE_ERMITE", int32(ermite.ActionSource_ACTION_SOURCE_ERMITE), 2},
191 }
192 for _, e := range enums {
193 if e.got != e.want {
194 t.Errorf("%s = %d, want %d", e.name, e.got, e.want)
195 }
196 }
197}
198
199// TestEventRoundTrip checks that an event with a oneof kind survives the wire.
200func TestEventRoundTrip(t *testing.T) {
201 in := &ermite.WatchResponse{Event: &ermite.Event{
202 Id: "42",
203 Kind: &ermite.Event_Pushed{Pushed: &ermite.Pushed{
204 Repo: "ermite",
205 RefUpdates: []*ermite.RefUpdate{
206 {Ref: "refs/heads/main", OldId: "a", NewId: "b"},
207 },
208 }},
209 }}
210 b, err := proto.Marshal(in)
211 if err != nil {
212 t.Fatal(err)
213 }
214 var out ermite.WatchResponse
215 if err := proto.Unmarshal(b, &out); err != nil {
216 t.Fatal(err)
217 }
218 if !proto.Equal(in, &out) {
219 t.Errorf("round trip mismatch: %v != %v", &out, in)
220 }
221 if out.GetEvent().GetPushed().GetRefUpdates()[0].GetNewId() != "b" {
222 t.Errorf("unexpected ref update: %v", &out)
223 }
224}
225
226// TestUpdateRepoMask checks that the request carries a field mask.
227func TestUpdateRepoMask(t *testing.T) {
228 req := &ermite.UpdateRepoRequest{
229 Repo: "ermite",
230 Visibility: ermite.Visibility_VISIBILITY_PUBLIC,
231 UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"visibility"}},
232 }
233 if got := req.GetUpdateMask().GetPaths(); len(got) != 1 || got[0] != "visibility" {
234 t.Errorf("paths = %v", got)
235 }
236}
237
238// TestMergeRequestEventRoundTrip checks the merge request event kinds and the
239// repository settings survive the wire.
240func TestMergeRequestEventRoundTrip(t *testing.T) {
241 in := &ermite.WatchResponse{Event: &ermite.Event{
242 Id: "43",
243 Kind: &ermite.Event_CommentAdded{CommentAdded: &ermite.CommentAdded{
244 Repo: "ermite",
245 Number: 7,
246 Comment: &ermite.Comment{
247 Id: "c1", Path: "a.go", Line: 3, Side: ermite.DiffSide_DIFF_SIDE_OLD, Text: "hi",
248 },
249 }},
250 }}
251 b, err := proto.Marshal(in)
252 if err != nil {
253 t.Fatal(err)
254 }
255 var out ermite.WatchResponse
256 if err := proto.Unmarshal(b, &out); err != nil {
257 t.Fatal(err)
258 }
259 if !proto.Equal(in, &out) {
260 t.Errorf("round trip mismatch: %v != %v", &out, in)
261 }
262 if got := out.GetEvent().GetCommentAdded().GetNumber(); got != 7 {
263 t.Errorf("number = %d, want 7", got)
264 }
265
266 req := &ermite.UpdateRepoRequest{
267 Repo: "ermite",
268 Settings: &ermite.RepoSettings{
269 AllowedMergeModes: []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_SQUASH},
270 },
271 UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"settings.allowed_merge_modes"}},
272 }
273 if got := req.GetSettings().GetAllowedMergeModes(); len(got) != 1 || got[0] != ermite.MergeMode_MERGE_MODE_SQUASH {
274 t.Errorf("modes = %v", got)
275 }
276}
277
278// TestCIEventRoundTrip checks that the CI event kinds keep their field numbers
279// and survive the wire.
280func TestCIEventRoundTrip(t *testing.T) {
281 in := &ermite.WatchResponse{Event: &ermite.Event{
282 Id: "44",
283 Kind: &ermite.Event_JobUpdated{JobUpdated: &ermite.JobUpdated{Job: &ermite.RunJob{
284 Id: "j1",
285 RunId: 9,
286 Attempt: 2,
287 Status: ermite.CiStatus_CI_STATUS_COMPLETED,
288 Conclusion: ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT,
289 Isolation: ermite.RunnerIsolation_RUNNER_ISOLATION_VM,
290 Steps: []*ermite.Step{{Number: 1, Name: "Set up job", LogStart: 0, LogEnd: 3}},
291 }}},
292 }}
293 b, err := proto.Marshal(in)
294 if err != nil {
295 t.Fatal(err)
296 }
297 var out ermite.WatchResponse
298 if err := proto.Unmarshal(b, &out); err != nil {
299 t.Fatal(err)
300 }
301 if !proto.Equal(in, &out) {
302 t.Errorf("round trip mismatch: %v != %v", &out, in)
303 }
304 f := (&ermite.Event{}).ProtoReflect().Descriptor().Fields()
305 for name, num := range map[string]int{"run_updated": 12, "job_updated": 13} {
306 if fd := f.ByName(protoreflect.Name(name)); fd == nil || int(fd.Number()) != num {
307 t.Errorf("event field %s: %v, want number %d", name, fd, num)
308 }
309 }
310}
311
312// TestJobRoundTrip checks the job handed to a runner survives the wire.
313func TestJobRoundTrip(t *testing.T) {
314 in := &ermite.FetchJobResponse{Job: &ermite.Job{
315 JobId: "j1", RunId: 9, Attempt: 1, JobToken: "tok", WorkflowPath: ".github/workflows/ci.yml",
316 WorkflowYaml: "on: push", JobKey: "build", Matrix: "{}", Contexts: "{}",
317 Labels: []string{"self-hosted"}, TimeoutMinutes: 360, Repo: "ermite",
318 Sha: "0123456789012345678901234567890123456789", Ref: "refs/heads/main",
319 }}
320 b, err := proto.Marshal(in)
321 if err != nil {
322 t.Fatal(err)
323 }
324 var out ermite.FetchJobResponse
325 if err := proto.Unmarshal(b, &out); err != nil {
326 t.Fatal(err)
327 }
328 if !proto.Equal(in, &out) {
329 t.Errorf("round trip mismatch: %v != %v", &out, in)
330 }
331}
332
333// TestWatchJobRoundTrip checks the three kinds of WatchJob message and the
334// JobEnded detail.
335func TestWatchJobRoundTrip(t *testing.T) {
336 msgs := []*ermite.WatchJobResponse{
337 {Msg: &ermite.WatchJobResponse_Keepalive{Keepalive: &ermite.Keepalive{}}},
338 {Msg: &ermite.WatchJobResponse_Cancel{Cancel: &ermite.CancelRequest{
339 Reason: ermite.CancelReason_CANCEL_REASON_FAIL_FAST, Message: "m",
340 }}},
341 {Msg: &ermite.WatchJobResponse_Ended{Ended: &ermite.JobEnded{
342 Conclusion: ermite.CiConclusion_CI_CONCLUSION_CANCELLED, Error: "e",
343 }}},
344 }
345 for _, in := range msgs {
346 b, err := proto.Marshal(in)
347 if err != nil {
348 t.Fatal(err)
349 }
350 var out ermite.WatchJobResponse
351 if err := proto.Unmarshal(b, &out); err != nil {
352 t.Fatal(err)
353 }
354 if !proto.Equal(in, &out) {
355 t.Errorf("round trip mismatch: %v != %v", &out, in)
356 }
357 }
358 // The detail travels in a Connect error.
359 d, err := connect.NewErrorDetail(&ermite.JobEnded{Conclusion: ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT})
360 if err != nil {
361 t.Fatal(err)
362 }
363 cerr := connect.NewError(connect.CodeFailedPrecondition, errors.New("job ended"))
364 cerr.AddDetail(d)
365 v, err := cerr.Details()[0].Value()
366 if err != nil {
367 t.Fatal(err)
368 }
369 if got := v.(*ermite.JobEnded).GetConclusion(); got != ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT {
370 t.Errorf("detail conclusion = %v", got)
371 }
372}
373
374// TestCompleteJobRoundTrip checks the final report of a job and its answer.
375func TestCompleteJobRoundTrip(t *testing.T) {
376 in := &ermite.CompleteJobRequest{
377 JobId: "j1",
378 Conclusion: ermite.CiConclusion_CI_CONCLUSION_CANCELLED,
379 Outputs: map[string]string{"a": "b"},
380 RunnerShutdown: true,
381 Steps: []*ermite.StepUpdate{{Number: 2, State: ermite.StepState_STEP_STATE_COMPLETED}},
382 }
383 b, err := proto.Marshal(in)
384 if err != nil {
385 t.Fatal(err)
386 }
387 var out ermite.CompleteJobRequest
388 if err := proto.Unmarshal(b, &out); err != nil {
389 t.Fatal(err)
390 }
391 if !proto.Equal(in, &out) {
392 t.Errorf("round trip mismatch: %v != %v", &out, in)
393 }
394 r := &ermite.CompleteJobResponse{Applied: false, Conclusion: ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT}
395 if r.GetApplied() || r.GetConclusion() != ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT {
396 t.Errorf("unexpected response %v", r)
397 }
398}
399
400// TestRunNameField pins the number of Run.name, the GitHub name of the run
401// that sits next to workflow_name, and that it survives the wire.
402func TestRunNameField(t *testing.T) {
403 fd := (&ermite.Run{}).ProtoReflect().Descriptor().Fields().ByName("name")
404 if fd == nil || fd.Number() != 19 {
405 t.Fatalf("Run.name = %v, want field 19", fd)
406 }
407 in := &ermite.Run{WorkflowName: "Names Workflow", Name: "run push on branch by someone", Title: "run push on branch by someone"}
408 b, err := proto.Marshal(in)
409 if err != nil {
410 t.Fatal(err)
411 }
412 var out ermite.Run
413 if err := proto.Unmarshal(b, &out); err != nil {
414 t.Fatal(err)
415 }
416 if out.GetName() != in.GetName() || out.GetWorkflowName() != "Names Workflow" {
417 t.Errorf("round trip: name %q, workflow_name %q", out.GetName(), out.GetWorkflowName())
418 }
419}
420
421// TestCI2FieldNumbers pins the numbers of the fields added for CI-2.
422func TestCI2FieldNumbers(t *testing.T) {
423 tests := []struct {
424 msg proto.Message
425 fields map[string]int
426 }{
427 {&ermite.Job{}, map[string]int{"github_token": 15}},
428 {&ermite.UpdateJobRequest{}, map[string]int{"annotations": 3}},
429 {&ermite.CompleteJobRequest{}, map[string]int{"annotations": 6}},
430 {&ermite.StepUpdate{}, map[string]int{"outcome": 7}},
431 {&ermite.Step{}, map[string]int{"outcome": 9}},
432 {&ermite.Annotation{}, map[string]int{
433 "level": 1, "title": 2, "message": 3, "file": 4, "start_line": 5,
434 "end_line": 6, "start_column": 7, "end_column": 8, "step_number": 9,
435 }},
436 {&ermite.ActionRef{}, map[string]int{"uses": 1}},
437 {&ermite.ResolveActionsRequest{}, map[string]int{"job_id": 1, "refs": 2}},
438 {&ermite.ResolveActionsResponse{}, map[string]int{"actions": 1}},
439 {&ermite.ResolvedAction{}, map[string]int{
440 "ref": 1, "name_with_owner": 2, "path": 3, "resolved_sha": 4,
441 "tarball_url": 5, "token": 6, "source": 7, "error": 8,
442 }},
443 {&ermite.UploadSummaryRequest{}, map[string]int{"job_id": 1, "step_number": 2, "markdown": 3}},
444 {&ermite.ListAnnotationsRequest{}, map[string]int{"repo": 1, "run_id": 2, "attempt": 3, "job_id": 4}},
445 {&ermite.ListAnnotationsResponse{}, map[string]int{"annotations": 1, "truncated": 2}},
446 {&ermite.JobAnnotation{}, map[string]int{"job_id": 1, "annotation": 2}},
447 {&ermite.GetStepSummariesRequest{}, map[string]int{"repo": 1, "job_id": 2, "attempt": 3}},
448 {&ermite.GetStepSummariesResponse{}, map[string]int{"summaries": 1, "truncated": 2}},
449 {&ermite.StepSummary{}, map[string]int{"step_number": 1, "markdown": 2}},
450 }
451 for _, tt := range tests {
452 md := tt.msg.ProtoReflect().Descriptor()
453 for name, num := range tt.fields {
454 if fd := md.Fields().ByName(protoreflect.Name(name)); fd == nil || int(fd.Number()) != num {
455 t.Errorf("%s.%s: %v, want number %d", md.Name(), name, fd, num)
456 }
457 }
458 }
459}
460
461// TestCI2RoundTrip checks that the CI-2 messages survive the wire.
462func TestCI2RoundTrip(t *testing.T) {
463 msgs := []proto.Message{
464 &ermite.Job{JobId: "j1", GithubToken: "ermitegh_x"},
465 &ermite.UpdateJobRequest{
466 JobId: "j1",
467 Steps: []*ermite.StepUpdate{{
468 Number: 2, State: ermite.StepState_STEP_STATE_COMPLETED,
469 Conclusion: ermite.CiConclusion_CI_CONCLUSION_SUCCESS,
470 Outcome: ermite.CiConclusion_CI_CONCLUSION_FAILURE,
471 }},
472 Annotations: []*ermite.Annotation{{
473 Level: ermite.AnnotationLevel_ANNOTATION_LEVEL_WARNING, Title: "t", Message: "m",
474 File: "a.go", StartLine: 1, EndLine: 2, StartColumn: 3, EndColumn: 4, StepNumber: 2,
475 }},
476 },
477 &ermite.CompleteJobRequest{
478 JobId: "j1",
479 Conclusion: ermite.CiConclusion_CI_CONCLUSION_SUCCESS,
480 Annotations: []*ermite.Annotation{{Level: ermite.AnnotationLevel_ANNOTATION_LEVEL_FAILURE, Message: "m", StepNumber: 3}},
481 },
482 &ermite.ResolveActionsRequest{JobId: "j1", Refs: []*ermite.ActionRef{{Uses: "actions/checkout@v4"}, {Uses: "o/r/p@main"}}},
483 &ermite.ResolveActionsResponse{Actions: []*ermite.ResolvedAction{
484 {
485 Ref: "o/r/p@main", NameWithOwner: "o/r", Path: "p", ResolvedSha: "0123456789012345678901234567890123456789",
486 TarballUrl: "https://example.test/o/r.tar.gz", Token: "t", Source: ermite.ActionSource_ACTION_SOURCE_ERMITE,
487 },
488 {Ref: "x/y@z", Error: "Unable to resolve action `x/y@z`, unable to find version `z`"},
489 }},
490 &ermite.UploadSummaryRequest{JobId: "j1", StepNumber: 2, Markdown: "# hi"},
491 &ermite.ListAnnotationsResponse{Annotations: []*ermite.JobAnnotation{{JobId: "j1", Annotation: &ermite.Annotation{Message: "m"}}}, Truncated: true},
492 &ermite.GetStepSummariesResponse{Summaries: []*ermite.StepSummary{{StepNumber: 2, Markdown: "# hi"}}, Truncated: true},
493 &ermite.RunJob{Id: "j1", Steps: []*ermite.Step{{Number: 2, Outcome: ermite.CiConclusion_CI_CONCLUSION_FAILURE}}},
494 }
495 for _, in := range msgs {
496 b, err := proto.Marshal(in)
497 if err != nil {
498 t.Fatal(err)
499 }
500 out := in.ProtoReflect().New().Interface()
501 if err := proto.Unmarshal(b, out); err != nil {
502 t.Fatal(err)
503 }
504 if !proto.Equal(in, out) {
505 t.Errorf("%T: round trip mismatch: %v != %v", in, out, in)
506 }
507 }
508}
509
510// TestCI3FieldNumbers pins the numbers of the fields added for CI-3.
511func TestCI3FieldNumbers(t *testing.T) {
512 tests := []struct {
513 msg proto.Message
514 fields map[string]int
515 }{
516 {&ermite.Job{}, map[string]int{
517 "secrets": 16, "vars": 17, "environment": 18, "permissions": 19, "cache": 20, "trusted": 21,
518 }},
519 {&ermite.JobPermissions{}, map[string]int{"levels": 1}},
520 {&ermite.JobCache{}, map[string]int{"repository": 1, "write_ref": 2, "read_refs": 3}},
521 {&ermite.Run{}, map[string]int{"trusted": 20}},
522 {&ermite.RunJob{}, map[string]int{"concurrency_group": 20}},
523 {&ermite.DispatchWorkflowRequest{}, map[string]int{"repo": 1, "workflow_path": 2, "ref": 3, "inputs": 4}},
524 {&ermite.DispatchWorkflowResponse{}, map[string]int{"run": 1}},
525 {&ermite.Workflow{}, map[string]int{"path": 1, "name": 2, "state": 3}},
526 {&ermite.ListWorkflowsRequest{}, map[string]int{"repo": 1}},
527 {&ermite.ListWorkflowsResponse{}, map[string]int{"workflows": 1}},
528 {&ermite.DisableWorkflowRequest{}, map[string]int{"repo": 1, "workflow": 2}},
529 {&ermite.DisableWorkflowResponse{}, map[string]int{"workflow": 1}},
530 {&ermite.EnableWorkflowRequest{}, map[string]int{"repo": 1, "workflow": 2}},
531 {&ermite.EnableWorkflowResponse{}, map[string]int{"workflow": 1}},
532 {&ermite.SettingsScope{}, map[string]int{"instance": 1, "repo": 2, "environment": 3}},
533 {&ermite.EnvironmentRef{}, map[string]int{"repo": 1, "name": 2}},
534 {&ermite.Secret{}, map[string]int{"name": 1, "updated_at": 2}},
535 {&ermite.Variable{}, map[string]int{"name": 1, "value": 2, "updated_at": 3}},
536 {&ermite.Environment{}, map[string]int{"repo": 1, "name": 2, "created_at": 3}},
537 {&ermite.SetSecretRequest{}, map[string]int{"scope": 1, "name": 2, "value": 3}},
538 {&ermite.ListSecretsRequest{}, map[string]int{"scope": 1, "page_size": 2, "page_token": 3}},
539 {&ermite.ListSecretsResponse{}, map[string]int{"secrets": 1, "next_page_token": 2}},
540 {&ermite.ListVariablesResponse{}, map[string]int{"variables": 1, "next_page_token": 2}},
541 {&ermite.ListEnvironmentsRequest{}, map[string]int{"repo": 1, "page_size": 2, "page_token": 3}},
542 {&ermite.ListEnvironmentsResponse{}, map[string]int{"environments": 1, "next_page_token": 2}},
543 }
544 for _, tt := range tests {
545 md := tt.msg.ProtoReflect().Descriptor()
546 for name, num := range tt.fields {
547 if fd := md.Fields().ByName(protoreflect.Name(name)); fd == nil || int(fd.Number()) != num {
548 t.Errorf("%s.%s: %v, want number %d", md.Name(), name, fd, num)
549 }
550 }
551 }
552}
553
554// TestCI3RoundTrip checks that the CI-3 messages survive the wire.
555func TestCI3RoundTrip(t *testing.T) {
556 msgs := []proto.Message{
557 &ermite.Job{
558 JobId: "j1", Secrets: map[string]string{"TOKEN": "s3cret"}, Vars: map[string]string{"REGION": "eu"},
559 Environment: "staging", Trusted: true,
560 Permissions: &ermite.JobPermissions{Levels: map[string]string{"contents": "write", "packages": "none"}},
561 Cache: &ermite.JobCache{
562 Repository: "ermite", WriteRef: "refs/pull/7/merge",
563 ReadRefs: []string{"refs/pull/7/merge", "refs/heads/main"},
564 },
565 },
566 &ermite.Run{Id: 9, Event: "workflow_dispatch", Trusted: true},
567 &ermite.RunJob{Id: "j1", Status: ermite.CiStatus_CI_STATUS_PENDING, ConcurrencyGroup: "deploy"},
568 &ermite.DispatchWorkflowRequest{Repo: "ermite", WorkflowPath: "ci.yml", Ref: "main", Inputs: map[string]string{"debug": "true"}},
569 &ermite.DispatchWorkflowResponse{Run: &ermite.Run{Id: 9, Event: "workflow_dispatch"}},
570 &ermite.ListWorkflowsResponse{Workflows: []*ermite.Workflow{{Path: ".github/workflows/release.yml", Name: "Release", State: ermite.WorkflowState_WORKFLOW_STATE_DISABLED}}},
571 &ermite.DisableWorkflowRequest{Repo: "ermite", Workflow: "release.yml"},
572 &ermite.EnableWorkflowResponse{Workflow: &ermite.Workflow{Path: ".github/workflows/release.yml", State: ermite.WorkflowState_WORKFLOW_STATE_ENABLED}},
573 &ermite.SetSecretRequest{Scope: &ermite.SettingsScope{Where: &ermite.SettingsScope_Instance{Instance: true}}, Name: "A", Value: "v"},
574 &ermite.SetSecretRequest{Scope: &ermite.SettingsScope{Where: &ermite.SettingsScope_Repo{Repo: "ermite"}}, Name: "A", Value: "v"},
575 &ermite.SetVariableRequest{
576 Scope: &ermite.SettingsScope{Where: &ermite.SettingsScope_Environment{Environment: &ermite.EnvironmentRef{Repo: "ermite", Name: "prod"}}},
577 Name: "A", Value: "v",
578 },
579 &ermite.ListSecretsResponse{Secrets: []*ermite.Secret{{Name: "A"}}, NextPageToken: "t"},
580 &ermite.ListEnvironmentsResponse{Environments: []*ermite.Environment{{Repo: "ermite", Name: "prod"}}},
581 }
582 for _, in := range msgs {
583 b, err := proto.Marshal(in)
584 if err != nil {
585 t.Fatal(err)
586 }
587 out := in.ProtoReflect().New().Interface()
588 if err := proto.Unmarshal(b, out); err != nil {
589 t.Fatal(err)
590 }
591 if !proto.Equal(in, out) {
592 t.Errorf("%T: round trip mismatch: %v != %v", in, out, in)
593 }
594 }
595}
596
597// TestMirrorWire pins event numbers and verifies presence of explicit zero patches.
598func TestMirrorWire(t *testing.T) {
599 fields := (&ermite.Event{}).ProtoReflect().Descriptor().Fields()
600 for name, number := range map[protoreflect.Name]protoreflect.FieldNumber{"mirror_failed": 23, "mirror_recovered": 24} {
601 if fd := fields.ByName(name); fd == nil || fd.Number() != number || fd.ContainingOneof().Name() != "kind" {
602 t.Fatalf("Event.%s: %v, want kind field %d", name, fd, number)
603 }
604 }
605 for _, msg := range []proto.Message{&ermite.MirrorFailed{}, &ermite.MirrorRecovered{}} {
606 fields := msg.ProtoReflect().Descriptor().Fields()
607 if fields.ByName("url") != nil || fields.ByName("detail") != nil {
608 t.Fatalf("%T leaks URL or detail", msg)
609 }
610 }
611 for _, in := range []proto.Message{
612 &ermite.SetMirrorConfigRequest{Enabled: proto.Bool(false), UrlTemplate: proto.String(""), Cron: proto.String(""), AllBranches: proto.Bool(false)},
613 &ermite.SetRepoMirrorRequest{Repo: "repo", AllBranches: proto.Bool(false)},
614 &ermite.Event{Kind: &ermite.Event_MirrorFailed{MirrorFailed: &ermite.MirrorFailed{Repo: "repo", State: ermite.MirrorState_MIRROR_STATE_PAUSED, FailureClass: "diverged"}}},
615 &ermite.Event{Kind: &ermite.Event_MirrorRecovered{MirrorRecovered: &ermite.MirrorRecovered{Repo: "repo"}}},
616 } {
617 b, err := proto.Marshal(in)
618 if err != nil {
619 t.Fatal(err)
620 }
621 out := in.ProtoReflect().New().Interface()
622 if err := proto.Unmarshal(b, out); err != nil {
623 t.Fatal(err)
624 }
625 if !proto.Equal(in, out) {
626 t.Errorf("%T: round trip mismatch: %v != %v", in, out, in)
627 }
628 }
629}
630
631// TestReleaseSymbols pins the scope and event wrapper exports used by callers.
632func TestReleaseSymbols(t *testing.T) {
633 if ermite.Scope_SCOPE_RELEASE_WRITE != 13 {
634 t.Fatal("release scope wire value changed")
635 }
636 var _ proto.Message = &ermite.Event{Kind: &ermite.Event_ReleasePublished{ReleasePublished: &ermite.ReleasePublished{}}}
637}