v0.1.0 4db3b8b

gen_test.go RawHistoryFolder

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.RepoServiceListReposProcedure, "/ermite.RepoService/ListRepos"},
23		{ermiteconnect.RepoServiceGetRepoProcedure, "/ermite.RepoService/GetRepo"},
24		{ermiteconnect.RepoServiceCreateRepoProcedure, "/ermite.RepoService/CreateRepo"},
25		{ermiteconnect.RepoServiceUpdateRepoProcedure, "/ermite.RepoService/UpdateRepo"},
26		{ermiteconnect.RepoServiceDeleteRepoProcedure, "/ermite.RepoService/DeleteRepo"},
27		{ermiteconnect.GitServiceListRefsProcedure, "/ermite.GitService/ListRefs"},
28		{ermiteconnect.GitServiceGetTreeProcedure, "/ermite.GitService/GetTree"},
29		{ermiteconnect.GitServiceListPathsProcedure, "/ermite.GitService/ListPaths"},
30		{ermiteconnect.GitServiceGetBlobProcedure, "/ermite.GitService/GetBlob"},
31		{ermiteconnect.GitServiceListCommitsProcedure, "/ermite.GitService/ListCommits"},
32		{ermiteconnect.GitServiceGetCommitProcedure, "/ermite.GitService/GetCommit"},
33		{ermiteconnect.GitServiceResolveRevisionProcedure, "/ermite.GitService/ResolveRevision"},
34		{ermiteconnect.TokenServiceCreateTokenProcedure, "/ermite.TokenService/CreateToken"},
35		{ermiteconnect.TokenServiceListTokensProcedure, "/ermite.TokenService/ListTokens"},
36		{ermiteconnect.TokenServiceRevokeTokenProcedure, "/ermite.TokenService/RevokeToken"},
37		{ermiteconnect.EventServiceWatchProcedure, "/ermite.EventService/Watch"},
38		{ermiteconnect.MergeRequestServiceCreateMergeRequestProcedure, "/ermite.MergeRequestService/CreateMergeRequest"},
39		{ermiteconnect.MergeRequestServiceListMergeRequestsProcedure, "/ermite.MergeRequestService/ListMergeRequests"},
40		{ermiteconnect.MergeRequestServiceGetMergeRequestProcedure, "/ermite.MergeRequestService/GetMergeRequest"},
41		{ermiteconnect.MergeRequestServiceAddCommentProcedure, "/ermite.MergeRequestService/AddComment"},
42		{ermiteconnect.MergeRequestServiceResolveCommentProcedure, "/ermite.MergeRequestService/ResolveComment"},
43		{ermiteconnect.MergeRequestServiceCloseMergeRequestProcedure, "/ermite.MergeRequestService/CloseMergeRequest"},
44		{ermiteconnect.MergeRequestServiceMergeMergeRequestProcedure, "/ermite.MergeRequestService/MergeMergeRequest"},
45		{ermiteconnect.MergeRequestServiceMarkReadProcedure, "/ermite.MergeRequestService/MarkRead"},
46		{ermiteconnect.SessionServiceCreateLoginLinkProcedure, "/ermite.SessionService/CreateLoginLink"},
47		{ermiteconnect.SessionServiceSignOutEverywhereProcedure, "/ermite.SessionService/SignOutEverywhere"},
48		{ermiteconnect.ServerServiceGetVersionProcedure, "/ermite.ServerService/GetVersion"},
49		{ermiteconnect.ServerServiceGetSigningKeyProcedure, "/ermite.ServerService/GetSigningKey"},
50		{ermiteconnect.RunnerServiceRegisterRunnerProcedure, "/ermite.RunnerService/RegisterRunner"},
51		{ermiteconnect.RunnerServiceFetchJobProcedure, "/ermite.RunnerService/FetchJob"},
52		{ermiteconnect.RunnerServiceUpdateJobProcedure, "/ermite.RunnerService/UpdateJob"},
53		{ermiteconnect.RunnerServiceAppendLogProcedure, "/ermite.RunnerService/AppendLog"},
54		{ermiteconnect.RunnerServiceHeartbeatProcedure, "/ermite.RunnerService/Heartbeat"},
55		{ermiteconnect.RunnerServiceWatchJobProcedure, "/ermite.RunnerService/WatchJob"},
56		{ermiteconnect.RunnerServiceCompleteJobProcedure, "/ermite.RunnerService/CompleteJob"},
57		{ermiteconnect.RunServiceListRunsProcedure, "/ermite.RunService/ListRuns"},
58		{ermiteconnect.RunServiceGetRunProcedure, "/ermite.RunService/GetRun"},
59		{ermiteconnect.RunServiceGetJobProcedure, "/ermite.RunService/GetJob"},
60		{ermiteconnect.RunServiceGetJobLogProcedure, "/ermite.RunService/GetJobLog"},
61		{ermiteconnect.RunServiceCancelRunProcedure, "/ermite.RunService/CancelRun"},
62		{ermiteconnect.RunServiceRerunRunProcedure, "/ermite.RunService/RerunRun"},
63		{ermiteconnect.RunServiceListChecksProcedure, "/ermite.RunService/ListChecks"},
64		{ermiteconnect.RunServiceListRunnersProcedure, "/ermite.RunService/ListRunners"},
65		{ermiteconnect.RunServiceRemoveRunnerProcedure, "/ermite.RunService/RemoveRunner"},
66	}
67	for _, tt := range tests {
68		if tt.got != tt.want {
69			t.Errorf("procedure = %q, want %q", tt.got, tt.want)
70		}
71	}
72}
73
74// TestEnumNumbers pins enum numbers: they are part of the wire contract.
75func TestEnumNumbers(t *testing.T) {
76	if got := ermite.Scope_SCOPE_TOKEN_ADMIN; got != 9 {
77		t.Errorf("SCOPE_TOKEN_ADMIN = %d, want 9", got)
78	}
79	if got := ermite.FileStatus_FILE_STATUS_TYPE_CHANGED; got != 6 {
80		t.Errorf("FILE_STATUS_TYPE_CHANGED = %d, want 6", got)
81	}
82	if got := ermite.Visibility_VISIBILITY_PRIVATE; got != 2 {
83		t.Errorf("VISIBILITY_PRIVATE = %d, want 2", got)
84	}
85	if got := ermite.MergeMode_MERGE_MODE_SQUASH; got != 2 {
86		t.Errorf("MERGE_MODE_SQUASH = %d, want 2", got)
87	}
88	if got := ermite.MergeRequestState_MERGE_REQUEST_STATE_CLOSED; got != 3 {
89		t.Errorf("MERGE_REQUEST_STATE_CLOSED = %d, want 3", got)
90	}
91	if got := ermite.DiffSide_DIFF_SIDE_NEW; got != 2 {
92		t.Errorf("DIFF_SIDE_NEW = %d, want 2", got)
93	}
94	enums := []struct {
95		name string
96		got  int32
97		want int32
98	}{
99		{"CI_STATUS_UNSPECIFIED", int32(ermite.CiStatus_CI_STATUS_UNSPECIFIED), 0},
100		{"CI_STATUS_QUEUED", int32(ermite.CiStatus_CI_STATUS_QUEUED), 1},
101		{"CI_STATUS_IN_PROGRESS", int32(ermite.CiStatus_CI_STATUS_IN_PROGRESS), 2},
102		{"CI_STATUS_COMPLETED", int32(ermite.CiStatus_CI_STATUS_COMPLETED), 3},
103		{"CI_CONCLUSION_UNSPECIFIED", int32(ermite.CiConclusion_CI_CONCLUSION_UNSPECIFIED), 0},
104		{"CI_CONCLUSION_SUCCESS", int32(ermite.CiConclusion_CI_CONCLUSION_SUCCESS), 1},
105		{"CI_CONCLUSION_FAILURE", int32(ermite.CiConclusion_CI_CONCLUSION_FAILURE), 2},
106		{"CI_CONCLUSION_CANCELLED", int32(ermite.CiConclusion_CI_CONCLUSION_CANCELLED), 3},
107		{"CI_CONCLUSION_SKIPPED", int32(ermite.CiConclusion_CI_CONCLUSION_SKIPPED), 4},
108		{"CI_CONCLUSION_TIMED_OUT", int32(ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT), 5},
109		{"CI_CONCLUSION_NEUTRAL", int32(ermite.CiConclusion_CI_CONCLUSION_NEUTRAL), 6},
110		{"CI_CONCLUSION_ACTION_REQUIRED", int32(ermite.CiConclusion_CI_CONCLUSION_ACTION_REQUIRED), 7},
111		{"STEP_STATE_UNSPECIFIED", int32(ermite.StepState_STEP_STATE_UNSPECIFIED), 0},
112		{"STEP_STATE_PENDING", int32(ermite.StepState_STEP_STATE_PENDING), 1},
113		{"STEP_STATE_IN_PROGRESS", int32(ermite.StepState_STEP_STATE_IN_PROGRESS), 2},
114		{"STEP_STATE_COMPLETED", int32(ermite.StepState_STEP_STATE_COMPLETED), 3},
115		{"RUNNER_ISOLATION_UNSPECIFIED", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_UNSPECIFIED), 0},
116		{"RUNNER_ISOLATION_VM", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_VM), 1},
117		{"RUNNER_ISOLATION_CONTAINER_SYSBOX", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_SYSBOX), 2},
118		{"RUNNER_ISOLATION_CONTAINER_PRIVILEGED", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_PRIVILEGED), 3},
119		{"CANCEL_REASON_UNSPECIFIED", int32(ermite.CancelReason_CANCEL_REASON_UNSPECIFIED), 0},
120		{"CANCEL_REASON_USER", int32(ermite.CancelReason_CANCEL_REASON_USER), 1},
121		{"CANCEL_REASON_FAIL_FAST", int32(ermite.CancelReason_CANCEL_REASON_FAIL_FAST), 2},
122		{"CANCEL_REASON_TIMEOUT", int32(ermite.CancelReason_CANCEL_REASON_TIMEOUT), 3},
123	}
124	for _, e := range enums {
125		if e.got != e.want {
126			t.Errorf("%s = %d, want %d", e.name, e.got, e.want)
127		}
128	}
129}
130
131// TestEventRoundTrip checks that an event with a oneof kind survives the wire.
132func TestEventRoundTrip(t *testing.T) {
133	in := &ermite.WatchResponse{Event: &ermite.Event{
134		Id: "42",
135		Kind: &ermite.Event_Pushed{Pushed: &ermite.Pushed{
136			Repo: "ermite",
137			RefUpdates: []*ermite.RefUpdate{
138				{Ref: "refs/heads/main", OldId: "a", NewId: "b"},
139			},
140		}},
141	}}
142	b, err := proto.Marshal(in)
143	if err != nil {
144		t.Fatal(err)
145	}
146	var out ermite.WatchResponse
147	if err := proto.Unmarshal(b, &out); err != nil {
148		t.Fatal(err)
149	}
150	if !proto.Equal(in, &out) {
151		t.Errorf("round trip mismatch: %v != %v", &out, in)
152	}
153	if out.GetEvent().GetPushed().GetRefUpdates()[0].GetNewId() != "b" {
154		t.Errorf("unexpected ref update: %v", &out)
155	}
156}
157
158// TestUpdateRepoMask checks that the request carries a field mask.
159func TestUpdateRepoMask(t *testing.T) {
160	req := &ermite.UpdateRepoRequest{
161		Repo:       "ermite",
162		Visibility: ermite.Visibility_VISIBILITY_PUBLIC,
163		UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"visibility"}},
164	}
165	if got := req.GetUpdateMask().GetPaths(); len(got) != 1 || got[0] != "visibility" {
166		t.Errorf("paths = %v", got)
167	}
168}
169
170// TestMergeRequestEventRoundTrip checks the merge request event kinds and the
171// repository settings survive the wire.
172func TestMergeRequestEventRoundTrip(t *testing.T) {
173	in := &ermite.WatchResponse{Event: &ermite.Event{
174		Id: "43",
175		Kind: &ermite.Event_CommentAdded{CommentAdded: &ermite.CommentAdded{
176			Repo:   "ermite",
177			Number: 7,
178			Comment: &ermite.Comment{
179				Id: "c1", Path: "a.go", Line: 3, Side: ermite.DiffSide_DIFF_SIDE_OLD, Text: "hi",
180			},
181		}},
182	}}
183	b, err := proto.Marshal(in)
184	if err != nil {
185		t.Fatal(err)
186	}
187	var out ermite.WatchResponse
188	if err := proto.Unmarshal(b, &out); err != nil {
189		t.Fatal(err)
190	}
191	if !proto.Equal(in, &out) {
192		t.Errorf("round trip mismatch: %v != %v", &out, in)
193	}
194	if got := out.GetEvent().GetCommentAdded().GetNumber(); got != 7 {
195		t.Errorf("number = %d, want 7", got)
196	}
197
198	req := &ermite.UpdateRepoRequest{
199		Repo: "ermite",
200		Settings: &ermite.RepoSettings{
201			AllowedMergeModes:  []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_SQUASH},
202			FailingChecksBlock: true,
203		},
204		UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"settings.allowed_merge_modes", "settings.failing_checks_block"}},
205	}
206	if got := req.GetSettings().GetAllowedMergeModes(); len(got) != 1 || got[0] != ermite.MergeMode_MERGE_MODE_SQUASH {
207		t.Errorf("modes = %v", got)
208	}
209}
210
211// TestCIEventRoundTrip checks that the CI event kinds keep their field numbers
212// and survive the wire.
213func TestCIEventRoundTrip(t *testing.T) {
214	in := &ermite.WatchResponse{Event: &ermite.Event{
215		Id: "44",
216		Kind: &ermite.Event_JobUpdated{JobUpdated: &ermite.JobUpdated{Job: &ermite.RunJob{
217			Id:         "j1",
218			RunId:      9,
219			Attempt:    2,
220			Status:     ermite.CiStatus_CI_STATUS_COMPLETED,
221			Conclusion: ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT,
222			Isolation:  ermite.RunnerIsolation_RUNNER_ISOLATION_VM,
223			Steps:      []*ermite.Step{{Number: 1, Name: "Set up job", LogStart: 0, LogEnd: 3}},
224		}}},
225	}}
226	b, err := proto.Marshal(in)
227	if err != nil {
228		t.Fatal(err)
229	}
230	var out ermite.WatchResponse
231	if err := proto.Unmarshal(b, &out); err != nil {
232		t.Fatal(err)
233	}
234	if !proto.Equal(in, &out) {
235		t.Errorf("round trip mismatch: %v != %v", &out, in)
236	}
237	f := (&ermite.Event{}).ProtoReflect().Descriptor().Fields()
238	for name, num := range map[string]int{"run_updated": 12, "job_updated": 13} {
239		if fd := f.ByName(protoreflect.Name(name)); fd == nil || int(fd.Number()) != num {
240			t.Errorf("event field %s: %v, want number %d", name, fd, num)
241		}
242	}
243}
244
245// TestJobRoundTrip checks the job handed to a runner survives the wire.
246func TestJobRoundTrip(t *testing.T) {
247	in := &ermite.FetchJobResponse{Job: &ermite.Job{
248		JobId: "j1", RunId: 9, Attempt: 1, JobToken: "tok", WorkflowPath: ".github/workflows/ci.yml",
249		WorkflowYaml: "on: push", JobKey: "build", Matrix: "{}", Contexts: "{}",
250		Labels: []string{"self-hosted"}, TimeoutMinutes: 360, Repo: "ermite",
251		Sha: "0123456789012345678901234567890123456789", Ref: "refs/heads/main",
252	}}
253	b, err := proto.Marshal(in)
254	if err != nil {
255		t.Fatal(err)
256	}
257	var out ermite.FetchJobResponse
258	if err := proto.Unmarshal(b, &out); err != nil {
259		t.Fatal(err)
260	}
261	if !proto.Equal(in, &out) {
262		t.Errorf("round trip mismatch: %v != %v", &out, in)
263	}
264}
265
266// TestWatchJobRoundTrip checks the three kinds of WatchJob message and the
267// JobEnded detail.
268func TestWatchJobRoundTrip(t *testing.T) {
269	msgs := []*ermite.WatchJobResponse{
270		{Msg: &ermite.WatchJobResponse_Keepalive{Keepalive: &ermite.Keepalive{}}},
271		{Msg: &ermite.WatchJobResponse_Cancel{Cancel: &ermite.CancelRequest{
272			Reason: ermite.CancelReason_CANCEL_REASON_FAIL_FAST, Message: "m",
273		}}},
274		{Msg: &ermite.WatchJobResponse_Ended{Ended: &ermite.JobEnded{
275			Conclusion: ermite.CiConclusion_CI_CONCLUSION_CANCELLED, Error: "e",
276		}}},
277	}
278	for _, in := range msgs {
279		b, err := proto.Marshal(in)
280		if err != nil {
281			t.Fatal(err)
282		}
283		var out ermite.WatchJobResponse
284		if err := proto.Unmarshal(b, &out); err != nil {
285			t.Fatal(err)
286		}
287		if !proto.Equal(in, &out) {
288			t.Errorf("round trip mismatch: %v != %v", &out, in)
289		}
290	}
291	// The detail travels in a Connect error.
292	d, err := connect.NewErrorDetail(&ermite.JobEnded{Conclusion: ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT})
293	if err != nil {
294		t.Fatal(err)
295	}
296	cerr := connect.NewError(connect.CodeFailedPrecondition, errors.New("job ended"))
297	cerr.AddDetail(d)
298	v, err := cerr.Details()[0].Value()
299	if err != nil {
300		t.Fatal(err)
301	}
302	if got := v.(*ermite.JobEnded).GetConclusion(); got != ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT {
303		t.Errorf("detail conclusion = %v", got)
304	}
305}
306
307// TestCompleteJobRoundTrip checks the final report of a job and its answer.
308func TestCompleteJobRoundTrip(t *testing.T) {
309	in := &ermite.CompleteJobRequest{
310		JobId:          "j1",
311		Conclusion:     ermite.CiConclusion_CI_CONCLUSION_CANCELLED,
312		Outputs:        map[string]string{"a": "b"},
313		RunnerShutdown: true,
314		Steps:          []*ermite.StepUpdate{{Number: 2, State: ermite.StepState_STEP_STATE_COMPLETED}},
315	}
316	b, err := proto.Marshal(in)
317	if err != nil {
318		t.Fatal(err)
319	}
320	var out ermite.CompleteJobRequest
321	if err := proto.Unmarshal(b, &out); err != nil {
322		t.Fatal(err)
323	}
324	if !proto.Equal(in, &out) {
325		t.Errorf("round trip mismatch: %v != %v", &out, in)
326	}
327	r := &ermite.CompleteJobResponse{Applied: false, Conclusion: ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT}
328	if r.GetApplied() || r.GetConclusion() != ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT {
329		t.Errorf("unexpected response %v", r)
330	}
331}
332
333// TestRunNameField pins the number of Run.name, the GitHub name of the run
334// that sits next to workflow_name, and that it survives the wire.
335func TestRunNameField(t *testing.T) {
336	fd := (&ermite.Run{}).ProtoReflect().Descriptor().Fields().ByName("name")
337	if fd == nil || fd.Number() != 19 {
338		t.Fatalf("Run.name = %v, want field 19", fd)
339	}
340	in := &ermite.Run{WorkflowName: "Names Workflow", Name: "run push on branch by someone", Title: "run push on branch by someone"}
341	b, err := proto.Marshal(in)
342	if err != nil {
343		t.Fatal(err)
344	}
345	var out ermite.Run
346	if err := proto.Unmarshal(b, &out); err != nil {
347		t.Fatal(err)
348	}
349	if out.GetName() != in.GetName() || out.GetWorkflowName() != "Names Workflow" {
350		t.Errorf("round trip: name %q, workflow_name %q", out.GetName(), out.GetWorkflowName())
351	}
352}

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 2

Tags 18

↑↓ moveenter openesc close