v2.31.0-dev16 8d0a092

service_test.go RawHistoryFolder

1package registry
2
3import (
4	"bytes"
5	"context"
6	"errors"
7	"io"
8	"strings"
9	"testing"
10
11	"github.com/bnema/zerowrap"
12	"github.com/stretchr/testify/assert"
13	"github.com/stretchr/testify/mock"
14	"github.com/stretchr/testify/require"
15
16	"github.com/bnema/gordon/internal/boundaries/out/mocks"
17	"github.com/bnema/gordon/internal/domain"
18)
19
20func testContext() context.Context {
21	return zerowrap.WithCtx(context.Background(), zerowrap.Default())
22}
23
24func TestService_GetManifest_Success(t *testing.T) {
25	blobStorage := mocks.NewMockBlobStorage(t)
26	manifestStorage := mocks.NewMockManifestStorage(t)
27	eventBus := mocks.NewMockEventPublisher(t)
28
29	svc := NewService(blobStorage, manifestStorage, eventBus)
30	ctx := testContext()
31
32	manifestData := []byte(`{"schemaVersion": 2}`)
33	manifestStorage.EXPECT().GetManifest("myapp", "latest").Return(manifestData, "application/vnd.docker.distribution.manifest.v2+json", nil)
34
35	result, err := svc.GetManifest(ctx, "myapp", "latest")
36
37	assert.NoError(t, err)
38	assert.NotNil(t, result)
39	assert.Equal(t, "myapp", result.Name)
40	assert.Equal(t, "latest", result.Reference)
41	assert.Equal(t, "application/vnd.docker.distribution.manifest.v2+json", result.ContentType)
42	assert.Equal(t, manifestData, result.Data)
43}
44
45func TestService_GetManifest_NotFound(t *testing.T) {
46	blobStorage := mocks.NewMockBlobStorage(t)
47	manifestStorage := mocks.NewMockManifestStorage(t)
48	eventBus := mocks.NewMockEventPublisher(t)
49
50	svc := NewService(blobStorage, manifestStorage, eventBus)
51	ctx := testContext()
52
53	manifestStorage.EXPECT().GetManifest("myapp", "notexists").Return(nil, "", errors.New("manifest not found"))
54
55	result, err := svc.GetManifest(ctx, "myapp", "notexists")
56
57	assert.Error(t, err)
58	assert.Nil(t, result)
59	assert.Contains(t, err.Error(), "failed to get manifest")
60}
61
62func TestService_PutManifest_Success(t *testing.T) {
63	blobStorage := mocks.NewMockBlobStorage(t)
64	manifestStorage := mocks.NewMockManifestStorage(t)
65	eventBus := mocks.NewMockEventPublisher(t)
66
67	svc := NewService(blobStorage, manifestStorage, eventBus)
68	ctx := testContext()
69
70	manifest := &domain.Manifest{
71		Name:        "myapp",
72		Reference:   "latest",
73		ContentType: "application/vnd.docker.distribution.manifest.v2+json",
74		Data:        []byte(`{"schemaVersion": 2}`),
75	}
76
77	manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(nil)
78	eventBus.EXPECT().Publish(domain.EventImagePushed, mock.AnythingOfType("domain.ImagePushedPayload")).Return(nil)
79
80	digest, err := svc.PutManifest(ctx, manifest)
81
82	assert.NoError(t, err)
83	assert.NotEmpty(t, digest)
84	assert.True(t, strings.HasPrefix(digest, "sha256:"))
85}
86
87func TestService_PutManifest_StorageError(t *testing.T) {
88	blobStorage := mocks.NewMockBlobStorage(t)
89	manifestStorage := mocks.NewMockManifestStorage(t)
90	eventBus := mocks.NewMockEventPublisher(t)
91
92	svc := NewService(blobStorage, manifestStorage, eventBus)
93	ctx := testContext()
94
95	manifest := &domain.Manifest{
96		Name:        "myapp",
97		Reference:   "latest",
98		ContentType: "application/vnd.docker.distribution.manifest.v2+json",
99		Data:        []byte(`{"schemaVersion": 2}`),
100	}
101
102	manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(errors.New("storage full"))
103
104	_, err := svc.PutManifest(ctx, manifest)
105
106	assert.Error(t, err)
107	assert.Contains(t, err.Error(), "failed to store manifest")
108}
109
110func TestService_PutManifest_EventPublishError(t *testing.T) {
111	blobStorage := mocks.NewMockBlobStorage(t)
112	manifestStorage := mocks.NewMockManifestStorage(t)
113	eventBus := mocks.NewMockEventPublisher(t)
114
115	svc := NewService(blobStorage, manifestStorage, eventBus)
116	ctx := testContext()
117
118	manifest := &domain.Manifest{
119		Name:        "myapp",
120		Reference:   "latest",
121		ContentType: "application/vnd.docker.distribution.manifest.v2+json",
122		Data:        []byte(`{"schemaVersion": 2}`),
123	}
124
125	manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(nil)
126	eventBus.EXPECT().Publish(domain.EventImagePushed, mock.Anything).Return(errors.New("event bus error"))
127
128	// Should still succeed - event publish error is logged but doesn't fail the operation
129	_, err := svc.PutManifest(ctx, manifest)
130
131	assert.NoError(t, err)
132}
133
134func TestService_PutManifest_NilEventBus(t *testing.T) {
135	blobStorage := mocks.NewMockBlobStorage(t)
136	manifestStorage := mocks.NewMockManifestStorage(t)
137
138	// Nil event bus
139	svc := NewService(blobStorage, manifestStorage, nil)
140	ctx := testContext()
141
142	manifest := &domain.Manifest{
143		Name:        "myapp",
144		Reference:   "latest",
145		ContentType: "application/vnd.docker.distribution.manifest.v2+json",
146		Data:        []byte(`{"schemaVersion": 2}`),
147	}
148
149	manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(nil)
150
151	_, err := svc.PutManifest(ctx, manifest)
152
153	assert.NoError(t, err)
154}
155
156func TestService_DeleteManifest_Success(t *testing.T) {
157	blobStorage := mocks.NewMockBlobStorage(t)
158	manifestStorage := mocks.NewMockManifestStorage(t)
159	eventBus := mocks.NewMockEventPublisher(t)
160
161	svc := NewService(blobStorage, manifestStorage, eventBus)
162	ctx := testContext()
163
164	manifestStorage.EXPECT().DeleteManifest("myapp", "latest").Return(nil)
165
166	err := svc.DeleteManifest(ctx, "myapp", "latest")
167
168	assert.NoError(t, err)
169}
170
171func TestService_DeleteManifest_Error(t *testing.T) {
172	blobStorage := mocks.NewMockBlobStorage(t)
173	manifestStorage := mocks.NewMockManifestStorage(t)
174	eventBus := mocks.NewMockEventPublisher(t)
175
176	svc := NewService(blobStorage, manifestStorage, eventBus)
177	ctx := testContext()
178
179	manifestStorage.EXPECT().DeleteManifest("myapp", "latest").Return(errors.New("manifest not found"))
180
181	err := svc.DeleteManifest(ctx, "myapp", "latest")
182
183	assert.Error(t, err)
184	assert.Contains(t, err.Error(), "failed to delete manifest")
185}
186
187func TestService_GetBlob_Success(t *testing.T) {
188	blobStorage := mocks.NewMockBlobStorage(t)
189	manifestStorage := mocks.NewMockManifestStorage(t)
190	eventBus := mocks.NewMockEventPublisher(t)
191
192	svc := NewService(blobStorage, manifestStorage, eventBus)
193	ctx := testContext()
194
195	blobData := []byte("blob content")
196	blobStorage.EXPECT().GetBlob("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(io.NopCloser(bytes.NewReader(blobData)), nil)
197
198	reader, err := svc.GetBlob(ctx, "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
199
200	assert.NoError(t, err)
201	assert.NotNil(t, reader)
202
203	data, _ := io.ReadAll(reader)
204	assert.Equal(t, blobData, data)
205}
206
207func TestService_GetBlob_NotFound(t *testing.T) {
208	blobStorage := mocks.NewMockBlobStorage(t)
209	manifestStorage := mocks.NewMockManifestStorage(t)
210	eventBus := mocks.NewMockEventPublisher(t)
211
212	svc := NewService(blobStorage, manifestStorage, eventBus)
213	ctx := testContext()
214
215	blobStorage.EXPECT().GetBlob("sha256:notexists").Return(nil, errors.New("blob not found"))
216
217	reader, err := svc.GetBlob(ctx, "sha256:notexists")
218
219	assert.Error(t, err)
220	assert.Nil(t, reader)
221	assert.Contains(t, err.Error(), "failed to get blob")
222}
223
224func TestService_PutBlob_Success(t *testing.T) {
225	blobStorage := mocks.NewMockBlobStorage(t)
226	manifestStorage := mocks.NewMockManifestStorage(t)
227	eventBus := mocks.NewMockEventPublisher(t)
228
229	svc := NewService(blobStorage, manifestStorage, eventBus)
230	ctx := testContext()
231
232	blobData := []byte("blob content")
233	reader := bytes.NewReader(blobData)
234
235	blobStorage.EXPECT().PutBlob("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", mock.Anything, int64(len(blobData))).Return(nil)
236
237	err := svc.PutBlob(ctx, "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", reader, int64(len(blobData)))
238
239	assert.NoError(t, err)
240}
241
242func TestService_PutBlob_Error(t *testing.T) {
243	blobStorage := mocks.NewMockBlobStorage(t)
244	manifestStorage := mocks.NewMockManifestStorage(t)
245	eventBus := mocks.NewMockEventPublisher(t)
246
247	svc := NewService(blobStorage, manifestStorage, eventBus)
248	ctx := testContext()
249
250	blobData := []byte("blob content")
251	reader := bytes.NewReader(blobData)
252
253	blobStorage.EXPECT().PutBlob("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", mock.Anything, int64(len(blobData))).Return(errors.New("storage full"))
254
255	err := svc.PutBlob(ctx, "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", reader, int64(len(blobData)))
256
257	assert.Error(t, err)
258	assert.Contains(t, err.Error(), "failed to store blob")
259}
260
261func TestService_BlobExists(t *testing.T) {
262	blobStorage := mocks.NewMockBlobStorage(t)
263	manifestStorage := mocks.NewMockManifestStorage(t)
264	eventBus := mocks.NewMockEventPublisher(t)
265
266	svc := NewService(blobStorage, manifestStorage, eventBus)
267	ctx := testContext()
268
269	blobStorage.EXPECT().BlobExists("sha256:exists").Return(true)
270	blobStorage.EXPECT().BlobExists("sha256:notexists").Return(false)
271
272	assert.True(t, svc.BlobExists(ctx, "sha256:exists"))
273	assert.False(t, svc.BlobExists(ctx, "sha256:notexists"))
274}
275
276func TestService_StartUpload_Success(t *testing.T) {
277	blobStorage := mocks.NewMockBlobStorage(t)
278	manifestStorage := mocks.NewMockManifestStorage(t)
279	eventBus := mocks.NewMockEventPublisher(t)
280
281	svc := NewService(blobStorage, manifestStorage, eventBus)
282	ctx := testContext()
283
284	blobStorage.EXPECT().StartBlobUpload("myapp").Return("1234567890-myapp", nil)
285
286	uuid, err := svc.StartUpload(ctx, "myapp")
287
288	assert.NoError(t, err)
289	assert.Equal(t, "1234567890-myapp", uuid)
290}
291
292func TestService_StartUpload_Error(t *testing.T) {
293	blobStorage := mocks.NewMockBlobStorage(t)
294	manifestStorage := mocks.NewMockManifestStorage(t)
295	eventBus := mocks.NewMockEventPublisher(t)
296
297	svc := NewService(blobStorage, manifestStorage, eventBus)
298	ctx := testContext()
299
300	blobStorage.EXPECT().StartBlobUpload("myapp").Return("", errors.New("cannot create upload"))
301
302	uuid, err := svc.StartUpload(ctx, "myapp")
303
304	assert.Error(t, err)
305	assert.Empty(t, uuid)
306	assert.Contains(t, err.Error(), "failed to start blob upload")
307}
308
309func TestService_AppendBlobChunk_PassesMaxBlobSize(t *testing.T) {
310	blobStorage := mocks.NewMockBlobStorage(t)
311	manifestStorage := mocks.NewMockManifestStorage(t)
312	eventBus := mocks.NewMockEventPublisher(t)
313
314	svc := NewService(blobStorage, manifestStorage, eventBus)
315	ctx := testContext()
316
317	blobStorage.EXPECT().AppendBlobChunk("myapp", "upload-id", mock.Anything, int64(4), int64(10)).Return(int64(4), nil)
318
319	length, err := svc.AppendBlobChunk(ctx, "myapp", "upload-id", strings.NewReader("data"), 4, 10)
320
321	assert.NoError(t, err)
322	assert.Equal(t, int64(4), length)
323}
324
325func TestService_AppendBlobChunk_MaxBlobSizeExceeded(t *testing.T) {
326	blobStorage := mocks.NewMockBlobStorage(t)
327	manifestStorage := mocks.NewMockManifestStorage(t)
328	eventBus := mocks.NewMockEventPublisher(t)
329
330	svc := NewService(blobStorage, manifestStorage, eventBus)
331	ctx := testContext()
332
333	blobStorage.EXPECT().AppendBlobChunk("myapp", "upload-id", mock.Anything, int64(11), int64(10)).Return(int64(0), domain.ErrBlobSizeExceeded)
334
335	_, err := svc.AppendBlobChunk(ctx, "myapp", "upload-id", strings.NewReader("too large"), 11, 10)
336
337	require.Error(t, err)
338	assert.ErrorIs(t, err, domain.ErrBlobSizeExceeded)
339}
340
341func TestService_FinishUpload_Success(t *testing.T) {
342	blobStorage := mocks.NewMockBlobStorage(t)
343	manifestStorage := mocks.NewMockManifestStorage(t)
344	eventBus := mocks.NewMockEventPublisher(t)
345
346	svc := NewService(blobStorage, manifestStorage, eventBus)
347	ctx := testContext()
348
349	blobStorage.EXPECT().FinishBlobUpload("1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil)
350
351	err := svc.FinishUpload(ctx, "1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
352
353	assert.NoError(t, err)
354}
355
356func TestService_FinishUpload_Error(t *testing.T) {
357	blobStorage := mocks.NewMockBlobStorage(t)
358	manifestStorage := mocks.NewMockManifestStorage(t)
359	eventBus := mocks.NewMockEventPublisher(t)
360
361	svc := NewService(blobStorage, manifestStorage, eventBus)
362	ctx := testContext()
363
364	blobStorage.EXPECT().FinishBlobUpload("1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(errors.New("digest mismatch"))
365
366	err := svc.FinishUpload(ctx, "1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
367
368	assert.Error(t, err)
369	assert.Contains(t, err.Error(), "failed to finish blob upload")
370}
371
372func TestService_CancelUpload_Success(t *testing.T) {
373	blobStorage := mocks.NewMockBlobStorage(t)
374	manifestStorage := mocks.NewMockManifestStorage(t)
375	eventBus := mocks.NewMockEventPublisher(t)
376
377	svc := NewService(blobStorage, manifestStorage, eventBus)
378	ctx := testContext()
379
380	blobStorage.EXPECT().CancelBlobUpload("1234567890-myapp").Return(nil)
381
382	err := svc.CancelUpload(ctx, "1234567890-myapp")
383
384	assert.NoError(t, err)
385}
386
387func TestService_CancelUpload_Error(t *testing.T) {
388	blobStorage := mocks.NewMockBlobStorage(t)
389	manifestStorage := mocks.NewMockManifestStorage(t)
390	eventBus := mocks.NewMockEventPublisher(t)
391
392	svc := NewService(blobStorage, manifestStorage, eventBus)
393	ctx := testContext()
394
395	blobStorage.EXPECT().CancelBlobUpload("1234567890-myapp").Return(errors.New("upload not found"))
396
397	err := svc.CancelUpload(ctx, "1234567890-myapp")
398
399	assert.Error(t, err)
400	assert.Contains(t, err.Error(), "failed to cancel blob upload")
401}
402
403func TestService_ListTags_Success(t *testing.T) {
404	blobStorage := mocks.NewMockBlobStorage(t)
405	manifestStorage := mocks.NewMockManifestStorage(t)
406	eventBus := mocks.NewMockEventPublisher(t)
407
408	svc := NewService(blobStorage, manifestStorage, eventBus)
409	ctx := testContext()
410
411	manifestStorage.EXPECT().ListTags("myapp").Return([]string{
412		"latest",
413		"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
414		"v1.0",
415		"v2.0",
416	}, nil)
417
418	tags, err := svc.ListTags(ctx, "myapp")
419
420	assert.NoError(t, err)
421	assert.Equal(t, []string{"latest", "v1.0", "v2.0"}, tags)
422}
423
424func TestService_ListTags_Error(t *testing.T) {
425	blobStorage := mocks.NewMockBlobStorage(t)
426	manifestStorage := mocks.NewMockManifestStorage(t)
427	eventBus := mocks.NewMockEventPublisher(t)
428
429	svc := NewService(blobStorage, manifestStorage, eventBus)
430	ctx := testContext()
431
432	manifestStorage.EXPECT().ListTags("myapp").Return(nil, errors.New("repository not found"))
433
434	tags, err := svc.ListTags(ctx, "myapp")
435
436	assert.Error(t, err)
437	assert.Nil(t, tags)
438	assert.Contains(t, err.Error(), "failed to list tags")
439}
440
441func TestService_ListRepositories_Success(t *testing.T) {
442	blobStorage := mocks.NewMockBlobStorage(t)
443	manifestStorage := mocks.NewMockManifestStorage(t)
444	eventBus := mocks.NewMockEventPublisher(t)
445
446	svc := NewService(blobStorage, manifestStorage, eventBus)
447	ctx := testContext()
448
449	manifestStorage.EXPECT().ListRepositories().Return([]string{"myapp", "otherapp", "thirdapp"}, nil)
450
451	repos, err := svc.ListRepositories(ctx)
452
453	assert.NoError(t, err)
454	assert.Equal(t, []string{"myapp", "otherapp", "thirdapp"}, repos)
455}
456
457func TestService_ListRepositories_Error(t *testing.T) {
458	blobStorage := mocks.NewMockBlobStorage(t)
459	manifestStorage := mocks.NewMockManifestStorage(t)
460	eventBus := mocks.NewMockEventPublisher(t)
461
462	svc := NewService(blobStorage, manifestStorage, eventBus)
463	ctx := testContext()
464
465	manifestStorage.EXPECT().ListRepositories().Return(nil, errors.New("storage error"))
466
467	repos, err := svc.ListRepositories(ctx)
468
469	assert.Error(t, err)
470	assert.Nil(t, repos)
471	assert.Contains(t, err.Error(), "failed to list repositories")
472}
473
474func TestPutManifest_SkipsEventWhenDeployIntentSuppressed(t *testing.T) {
475	blobStorage := mocks.NewMockBlobStorage(t)
476	manifestStorage := mocks.NewMockManifestStorage(t)
477	eventBus := mocks.NewMockEventPublisher(t)
478
479	svc := NewService(blobStorage, manifestStorage, eventBus)
480
481	svc.SuppressDeployEvent("my-app")
482
483	manifestStorage.EXPECT().PutManifest("my-app", "latest", "application/vnd.oci.image.manifest.v1+json", mock.Anything).Return(nil)
484
485	manifest := &domain.Manifest{
486		Name:        "my-app",
487		Reference:   "latest",
488		ContentType: "application/vnd.oci.image.manifest.v1+json",
489		Data:        []byte(`{"schemaVersion":2}`),
490	}
491
492	_, err := svc.PutManifest(context.Background(), manifest)
493	require.NoError(t, err)
494
495	eventBus.AssertNotCalled(t, "Publish", mock.Anything, mock.Anything)
496}
497
498func TestSuppressDeployEvent_ClearsCorrectly(t *testing.T) {
499	blobStorage := mocks.NewMockBlobStorage(t)
500	manifestStorage := mocks.NewMockManifestStorage(t)
501	eventBus := mocks.NewMockEventPublisher(t)
502
503	svc := NewService(blobStorage, manifestStorage, eventBus)
504
505	svc.SuppressDeployEvent("my-app")
506	assert.True(t, svc.IsDeployEventSuppressed("my-app"))
507
508	svc.ClearDeployEventSuppression("my-app")
509	assert.False(t, svc.IsDeployEventSuppressed("my-app"))
510}

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 7

Tags 106

↑↓ moveenter openesc close