v3.4.1 de8f68e

service_test.go RawHistoryFolder

1package registry
2
3import (
4	"bytes"
5	"context"
6	"crypto/sha512"
7	"errors"
8	"fmt"
9	"io"
10	"strings"
11	"testing"
12	"time"
13
14	"github.com/bnema/zerowrap"
15	"github.com/stretchr/testify/assert"
16	"github.com/stretchr/testify/mock"
17	"github.com/stretchr/testify/require"
18
19	"github.com/bnema/gordon/internal/boundaries/out/mocks"
20	"github.com/bnema/gordon/internal/domain"
21	"github.com/bnema/gordon/internal/usecase/registrystate"
22)
23
24func testContext() context.Context {
25	return zerowrap.WithCtx(context.Background(), zerowrap.Default())
26}
27
28func TestService_GetManifest_Success(t *testing.T) {
29	blobStorage := mocks.NewMockBlobStorage(t)
30	manifestStorage := mocks.NewMockManifestStorage(t)
31	eventBus := mocks.NewMockEventPublisher(t)
32
33	svc := NewService(blobStorage, manifestStorage, eventBus)
34	ctx := testContext()
35
36	manifestData := []byte(`{"schemaVersion": 2}`)
37	manifestStorage.EXPECT().GetManifest("myapp", "latest").Return(manifestData, "application/vnd.docker.distribution.manifest.v2+json", nil)
38
39	result, err := svc.GetManifest(ctx, "myapp", "latest")
40
41	assert.NoError(t, err)
42	assert.NotNil(t, result)
43	assert.Equal(t, "myapp", result.Name)
44	assert.Equal(t, "latest", result.Reference)
45	assert.Equal(t, "application/vnd.docker.distribution.manifest.v2+json", result.ContentType)
46	assert.Equal(t, manifestData, result.Data)
47}
48
49func TestService_GetManifest_NotFound(t *testing.T) {
50	blobStorage := mocks.NewMockBlobStorage(t)
51	manifestStorage := mocks.NewMockManifestStorage(t)
52	eventBus := mocks.NewMockEventPublisher(t)
53
54	svc := NewService(blobStorage, manifestStorage, eventBus)
55	ctx := testContext()
56
57	manifestStorage.EXPECT().GetManifest("myapp", "notexists").Return(nil, "", errors.New("manifest not found"))
58
59	result, err := svc.GetManifest(ctx, "myapp", "notexists")
60
61	assert.Error(t, err)
62	assert.Nil(t, result)
63	assert.Contains(t, err.Error(), "failed to get manifest")
64}
65
66func TestService_PutManifest_Success(t *testing.T) {
67	blobStorage := mocks.NewMockBlobStorage(t)
68	manifestStorage := mocks.NewMockManifestStorage(t)
69	eventBus := mocks.NewMockEventPublisher(t)
70
71	svc := NewService(blobStorage, manifestStorage, eventBus)
72	ctx := testContext()
73
74	manifest := &domain.Manifest{
75		Name:        "myapp",
76		Reference:   "latest",
77		ContentType: "application/vnd.docker.distribution.manifest.v2+json",
78		Data:        []byte(`{"schemaVersion": 2}`),
79	}
80
81	manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(nil)
82	eventBus.EXPECT().Publish(domain.EventImagePushed, mock.AnythingOfType("domain.ImagePushedPayload")).Return(nil)
83
84	digest, err := svc.PutManifest(ctx, manifest)
85
86	assert.NoError(t, err)
87	assert.NotEmpty(t, digest)
88	assert.True(t, strings.HasPrefix(digest, "sha256:"))
89}
90
91func TestService_PutManifest_RecordsPushMetrics(t *testing.T) {
92	manifestStorage := mocks.NewMockManifestStorage(t)
93	metrics := mocks.NewMockMetrics(t)
94	svc := NewService(mocks.NewMockBlobStorage(t), manifestStorage, nil)
95	svc.SetMetrics(metrics)
96
97	data := []byte(`{"schemaVersion": 2}`)
98	manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.oci.image.manifest.v1+json", data).Return(nil)
99	metrics.EXPECT().RecordImagePush(mock.Anything, "myapp", "latest", int64(len(data))).Return()
100
101	_, err := svc.PutManifest(testContext(), &domain.Manifest{
102		Name:        "myapp",
103		Reference:   "latest",
104		ContentType: "application/vnd.oci.image.manifest.v1+json",
105		Data:        data,
106	})
107
108	require.NoError(t, err)
109}
110
111func TestService_PutManifest_RejectsUnownedBlob(t *testing.T) {
112	const digest = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
113	blobStorage := mocks.NewMockBlobStorage(t)
114	manifestStorage := mocks.NewMockManifestStorage(t)
115	svc := NewService(blobStorage, manifestStorage, nil)
116
117	blobStorage.EXPECT().BlobOwnedByRepository("attacker", digest).Return(false, nil)
118
119	manifest := &domain.Manifest{
120		Name:        "attacker",
121		Reference:   "latest",
122		ContentType: "application/vnd.oci.image.manifest.v1+json",
123		Data:        []byte(`{"schemaVersion":2,"layers":[{"digest":"` + digest + `"}]}`),
124	}
125
126	_, err := svc.PutManifest(testContext(), manifest)
127
128	require.ErrorIs(t, err, domain.ErrManifestBlobUnknown)
129	manifestStorage.AssertNotCalled(t, "PutManifest", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
130}
131
132func TestService_PutManifest_RejectsUnknownChildManifest(t *testing.T) {
133	const child = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
134	blobStorage := mocks.NewMockBlobStorage(t)
135	manifestStorage := mocks.NewMockManifestStorage(t)
136	svc := NewService(blobStorage, manifestStorage, nil)
137
138	manifestStorage.EXPECT().GetManifest("index", child).Return(nil, "", domain.ErrManifestNotFound)
139
140	manifest := &domain.Manifest{
141		Name:        "index",
142		Reference:   "latest",
143		ContentType: "application/vnd.oci.image.index.v1+json",
144		Data:        []byte(`{"schemaVersion":2,"manifests":[{"digest":"` + child + `"}]}`),
145	}
146
147	_, err := svc.PutManifest(testContext(), manifest)
148
149	require.ErrorIs(t, err, domain.ErrManifestBlobUnknown)
150}
151
152func TestService_PutManifest_RejectsMalformedChildDigest(t *testing.T) {
153	blobStorage := mocks.NewMockBlobStorage(t)
154	manifestStorage := mocks.NewMockManifestStorage(t)
155	svc := NewService(blobStorage, manifestStorage, nil)
156
157	manifest := &domain.Manifest{
158		Name:        "index",
159		Reference:   "latest",
160		ContentType: "application/vnd.oci.image.index.v1+json",
161		Data:        []byte(`{"schemaVersion":2,"manifests":[{"digest":"../../tags.json"}]}`),
162	}
163
164	_, err := svc.PutManifest(testContext(), manifest)
165
166	require.ErrorIs(t, err, domain.ErrManifestBlobUnknown)
167	manifestStorage.AssertNotCalled(t, "GetManifest", mock.Anything, mock.Anything)
168}
169
170func TestService_PutManifest_SHA512DigestDoesNotPublishEvent(t *testing.T) {
171	blobStorage := mocks.NewMockBlobStorage(t)
172	manifestStorage := mocks.NewMockManifestStorage(t)
173	eventBus := mocks.NewMockEventPublisher(t)
174	state := registrystate.New()
175	state.AddPending("sha256:config", time.Now().UTC())
176	svc := NewService(blobStorage, manifestStorage, eventBus, state)
177	data := []byte(`{"schemaVersion":2,"config":{"digest":"sha256:config"}}`)
178	reference := fmt.Sprintf("sha512:%x", sha512.Sum512(data))
179	manifest := &domain.Manifest{Name: "myapp", Reference: reference, ContentType: "application/vnd.oci.image.manifest.v1+json", Data: data}
180
181	manifestStorage.EXPECT().PutManifest("myapp", reference, manifest.ContentType, data).Return(nil)
182	blobStorage.EXPECT().BlobOwnedByRepository("myapp", "sha256:config").Return(true, nil)
183
184	digest, err := svc.PutManifest(testContext(), manifest)
185
186	require.NoError(t, err)
187	assert.Equal(t, reference, digest)
188	assert.Empty(t, state.PendingDigests(time.Now().UTC()))
189}
190
191func TestService_PutManifest_RejectsDigestMismatch(t *testing.T) {
192	blobStorage := mocks.NewMockBlobStorage(t)
193	manifestStorage := mocks.NewMockManifestStorage(t)
194	eventBus := mocks.NewMockEventPublisher(t)
195	svc := NewService(blobStorage, manifestStorage, eventBus)
196
197	manifest := &domain.Manifest{
198		Name:        "myapp",
199		Reference:   "sha256:0000000000000000000000000000000000000000000000000000000000000000",
200		ContentType: "application/vnd.oci.image.manifest.v1+json",
201		Data:        []byte(`{"schemaVersion":2}`),
202	}
203
204	_, err := svc.PutManifest(testContext(), manifest)
205
206	require.Error(t, err)
207	assert.ErrorIs(t, err, domain.ErrDigestMismatch)
208}
209
210// TestService_GetBlobPath_RequiresRepositoryOwnership proves a blob is
211// served only to a repository that completed an upload of it.
212func TestService_GetBlobPath_RequiresRepositoryOwnership(t *testing.T) {
213	const digest = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
214	blobStorage := mocks.NewMockBlobStorage(t)
215	manifestStorage := mocks.NewMockManifestStorage(t)
216	svc := NewService(blobStorage, manifestStorage, nil)
217
218	blobStorage.EXPECT().BlobOwnedByRepository("allowed", digest).Return(true, nil)
219	blobStorage.EXPECT().GetBlobPath(digest).Return("/registry/blob", nil)
220
221	path, err := svc.GetBlobPath(testContext(), "allowed", digest)
222	require.NoError(t, err)
223	assert.Equal(t, "/registry/blob", path)
224
225	blobStorage.EXPECT().BlobOwnedByRepository("denied", digest).Return(false, nil)
226
227	_, err = svc.GetBlobPath(testContext(), "denied", digest)
228	assert.ErrorIs(t, err, domain.ErrBlobNotFound)
229}
230
231// TestService_GetBlobPath_IgnoresManifestReferences proves a manifest that
232// merely names a foreign digest does not confer access to it: ownership is
233// never inferred from repository-controlled manifest content.
234func TestService_GetBlobPath_IgnoresManifestReferences(t *testing.T) {
235	const target = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
236	blobStorage := mocks.NewMockBlobStorage(t)
237	manifestStorage := mocks.NewMockManifestStorage(t)
238	svc := NewService(blobStorage, manifestStorage, nil)
239
240	blobStorage.EXPECT().BlobOwnedByRepository("attacker", target).Return(false, nil)
241
242	_, err := svc.GetBlobPath(testContext(), "attacker", target)
243
244	assert.ErrorIs(t, err, domain.ErrBlobNotFound)
245	blobStorage.AssertNotCalled(t, "GetBlobPath", target)
246	manifestStorage.AssertNotCalled(t, "GetManifest", mock.Anything, mock.Anything)
247}
248
249func TestService_GetBlobPath_OwnershipLookupError(t *testing.T) {
250	const target = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
251	blobStorage := mocks.NewMockBlobStorage(t)
252	manifestStorage := mocks.NewMockManifestStorage(t)
253	svc := NewService(blobStorage, manifestStorage, nil)
254
255	blobStorage.EXPECT().BlobOwnedByRepository("busy", target).Return(false, errors.New("ownership lookup failed"))
256
257	_, err := svc.GetBlobPath(testContext(), "busy", target)
258
259	assert.Error(t, err)
260}
261
262func TestService_PutManifest_StorageError(t *testing.T) {
263	blobStorage := mocks.NewMockBlobStorage(t)
264	manifestStorage := mocks.NewMockManifestStorage(t)
265	eventBus := mocks.NewMockEventPublisher(t)
266
267	svc := NewService(blobStorage, manifestStorage, eventBus)
268	ctx := testContext()
269
270	manifest := &domain.Manifest{
271		Name:        "myapp",
272		Reference:   "latest",
273		ContentType: "application/vnd.docker.distribution.manifest.v2+json",
274		Data:        []byte(`{"schemaVersion": 2}`),
275	}
276
277	manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(errors.New("storage full"))
278
279	_, err := svc.PutManifest(ctx, manifest)
280
281	assert.Error(t, err)
282	assert.Contains(t, err.Error(), "failed to store manifest")
283}
284
285func TestService_PutManifest_EventPublishError(t *testing.T) {
286	blobStorage := mocks.NewMockBlobStorage(t)
287	manifestStorage := mocks.NewMockManifestStorage(t)
288	eventBus := mocks.NewMockEventPublisher(t)
289
290	svc := NewService(blobStorage, manifestStorage, eventBus)
291	ctx := testContext()
292
293	manifest := &domain.Manifest{
294		Name:        "myapp",
295		Reference:   "latest",
296		ContentType: "application/vnd.docker.distribution.manifest.v2+json",
297		Data:        []byte(`{"schemaVersion": 2}`),
298	}
299
300	manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(nil)
301	eventBus.EXPECT().Publish(domain.EventImagePushed, mock.Anything).Return(errors.New("event bus error"))
302
303	// Should still succeed - event publish error is logged but doesn't fail the operation
304	_, err := svc.PutManifest(ctx, manifest)
305
306	assert.NoError(t, err)
307}
308
309func TestService_PutManifest_NilEventBus(t *testing.T) {
310	blobStorage := mocks.NewMockBlobStorage(t)
311	manifestStorage := mocks.NewMockManifestStorage(t)
312
313	// Nil event bus
314	svc := NewService(blobStorage, manifestStorage, nil)
315	ctx := testContext()
316
317	manifest := &domain.Manifest{
318		Name:        "myapp",
319		Reference:   "latest",
320		ContentType: "application/vnd.docker.distribution.manifest.v2+json",
321		Data:        []byte(`{"schemaVersion": 2}`),
322	}
323
324	manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(nil)
325
326	_, err := svc.PutManifest(ctx, manifest)
327
328	assert.NoError(t, err)
329}
330
331func TestService_DeleteManifest_Success(t *testing.T) {
332	blobStorage := mocks.NewMockBlobStorage(t)
333	manifestStorage := mocks.NewMockManifestStorage(t)
334	eventBus := mocks.NewMockEventPublisher(t)
335
336	svc := NewService(blobStorage, manifestStorage, eventBus)
337	ctx := testContext()
338
339	manifestStorage.EXPECT().DeleteManifest("myapp", "latest").Return(nil)
340
341	err := svc.DeleteManifest(ctx, "myapp", "latest")
342
343	assert.NoError(t, err)
344}
345
346func TestService_DeleteManifest_Error(t *testing.T) {
347	blobStorage := mocks.NewMockBlobStorage(t)
348	manifestStorage := mocks.NewMockManifestStorage(t)
349	eventBus := mocks.NewMockEventPublisher(t)
350
351	svc := NewService(blobStorage, manifestStorage, eventBus)
352	ctx := testContext()
353
354	manifestStorage.EXPECT().DeleteManifest("myapp", "latest").Return(errors.New("manifest not found"))
355
356	err := svc.DeleteManifest(ctx, "myapp", "latest")
357
358	assert.Error(t, err)
359	assert.Contains(t, err.Error(), "failed to delete manifest")
360}
361
362func TestService_GetBlob_Success(t *testing.T) {
363	blobStorage := mocks.NewMockBlobStorage(t)
364	manifestStorage := mocks.NewMockManifestStorage(t)
365	eventBus := mocks.NewMockEventPublisher(t)
366
367	svc := NewService(blobStorage, manifestStorage, eventBus)
368	ctx := testContext()
369
370	blobData := []byte("blob content")
371	blobStorage.EXPECT().GetBlob("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(io.NopCloser(bytes.NewReader(blobData)), nil)
372
373	reader, err := svc.GetBlob(ctx, "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
374
375	assert.NoError(t, err)
376	assert.NotNil(t, reader)
377
378	data, _ := io.ReadAll(reader)
379	assert.Equal(t, blobData, data)
380}
381
382func TestService_GetBlob_NotFound(t *testing.T) {
383	blobStorage := mocks.NewMockBlobStorage(t)
384	manifestStorage := mocks.NewMockManifestStorage(t)
385	eventBus := mocks.NewMockEventPublisher(t)
386
387	svc := NewService(blobStorage, manifestStorage, eventBus)
388	ctx := testContext()
389
390	blobStorage.EXPECT().GetBlob("sha256:notexists").Return(nil, errors.New("blob not found"))
391
392	reader, err := svc.GetBlob(ctx, "sha256:notexists")
393
394	assert.Error(t, err)
395	assert.Nil(t, reader)
396	assert.Contains(t, err.Error(), "failed to get blob")
397}
398
399func TestService_PutBlob_Success(t *testing.T) {
400	blobStorage := mocks.NewMockBlobStorage(t)
401	manifestStorage := mocks.NewMockManifestStorage(t)
402	eventBus := mocks.NewMockEventPublisher(t)
403
404	svc := NewService(blobStorage, manifestStorage, eventBus)
405	ctx := testContext()
406
407	blobData := []byte("blob content")
408	reader := bytes.NewReader(blobData)
409
410	blobStorage.EXPECT().PutBlob("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", mock.Anything, int64(len(blobData))).Return(nil)
411
412	err := svc.PutBlob(ctx, "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", reader, int64(len(blobData)))
413
414	assert.NoError(t, err)
415}
416
417func TestService_PutBlob_Error(t *testing.T) {
418	blobStorage := mocks.NewMockBlobStorage(t)
419	manifestStorage := mocks.NewMockManifestStorage(t)
420	eventBus := mocks.NewMockEventPublisher(t)
421
422	svc := NewService(blobStorage, manifestStorage, eventBus)
423	ctx := testContext()
424
425	blobData := []byte("blob content")
426	reader := bytes.NewReader(blobData)
427
428	blobStorage.EXPECT().PutBlob("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", mock.Anything, int64(len(blobData))).Return(errors.New("storage full"))
429
430	err := svc.PutBlob(ctx, "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", reader, int64(len(blobData)))
431
432	assert.Error(t, err)
433	assert.Contains(t, err.Error(), "failed to store blob")
434}
435
436func TestService_BlobExists(t *testing.T) {
437	blobStorage := mocks.NewMockBlobStorage(t)
438	manifestStorage := mocks.NewMockManifestStorage(t)
439	eventBus := mocks.NewMockEventPublisher(t)
440
441	svc := NewService(blobStorage, manifestStorage, eventBus)
442	ctx := testContext()
443
444	blobStorage.EXPECT().BlobExists("sha256:exists").Return(true)
445	blobStorage.EXPECT().BlobExists("sha256:notexists").Return(false)
446
447	assert.True(t, svc.BlobExists(ctx, "sha256:exists"))
448	assert.False(t, svc.BlobExists(ctx, "sha256:notexists"))
449}
450
451func TestService_StartUpload_Success(t *testing.T) {
452	blobStorage := mocks.NewMockBlobStorage(t)
453	manifestStorage := mocks.NewMockManifestStorage(t)
454	eventBus := mocks.NewMockEventPublisher(t)
455
456	svc := NewService(blobStorage, manifestStorage, eventBus)
457	ctx := testContext()
458
459	blobStorage.EXPECT().StartBlobUpload("myapp").Return("1234567890-myapp", nil)
460
461	uuid, err := svc.StartUpload(ctx, "myapp")
462
463	assert.NoError(t, err)
464	assert.Equal(t, "1234567890-myapp", uuid)
465}
466
467func TestService_StartUpload_Error(t *testing.T) {
468	blobStorage := mocks.NewMockBlobStorage(t)
469	manifestStorage := mocks.NewMockManifestStorage(t)
470	eventBus := mocks.NewMockEventPublisher(t)
471
472	svc := NewService(blobStorage, manifestStorage, eventBus)
473	ctx := testContext()
474
475	blobStorage.EXPECT().StartBlobUpload("myapp").Return("", errors.New("cannot create upload"))
476
477	uuid, err := svc.StartUpload(ctx, "myapp")
478
479	assert.Error(t, err)
480	assert.Empty(t, uuid)
481	assert.Contains(t, err.Error(), "failed to start blob upload")
482}
483
484func TestService_AppendBlobChunk_PassesMaxBlobSize(t *testing.T) {
485	blobStorage := mocks.NewMockBlobStorage(t)
486	manifestStorage := mocks.NewMockManifestStorage(t)
487	eventBus := mocks.NewMockEventPublisher(t)
488
489	svc := NewService(blobStorage, manifestStorage, eventBus)
490	ctx := testContext()
491
492	blobStorage.EXPECT().AppendBlobChunk("myapp", "upload-id", mock.Anything, int64(4), int64(10)).Return(int64(4), nil)
493
494	length, err := svc.AppendBlobChunk(ctx, "myapp", "upload-id", strings.NewReader("data"), 4, 10)
495
496	assert.NoError(t, err)
497	assert.Equal(t, int64(4), length)
498}
499
500func TestService_AppendBlobChunk_MaxBlobSizeExceeded(t *testing.T) {
501	blobStorage := mocks.NewMockBlobStorage(t)
502	manifestStorage := mocks.NewMockManifestStorage(t)
503	eventBus := mocks.NewMockEventPublisher(t)
504
505	svc := NewService(blobStorage, manifestStorage, eventBus)
506	ctx := testContext()
507
508	blobStorage.EXPECT().AppendBlobChunk("myapp", "upload-id", mock.Anything, int64(11), int64(10)).Return(int64(0), domain.ErrBlobSizeExceeded)
509
510	_, err := svc.AppendBlobChunk(ctx, "myapp", "upload-id", strings.NewReader("too large"), 11, 10)
511
512	require.Error(t, err)
513	assert.ErrorIs(t, err, domain.ErrBlobSizeExceeded)
514}
515
516func TestService_FinishUpload_Success(t *testing.T) {
517	blobStorage := mocks.NewMockBlobStorage(t)
518	manifestStorage := mocks.NewMockManifestStorage(t)
519	eventBus := mocks.NewMockEventPublisher(t)
520
521	svc := NewService(blobStorage, manifestStorage, eventBus)
522	ctx := testContext()
523
524	blobStorage.EXPECT().FinishBlobUpload("myapp", "1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil)
525
526	err := svc.FinishUpload(ctx, "myapp", "1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
527
528	assert.NoError(t, err)
529}
530
531func TestService_FinishUploadMarksBlobPending(t *testing.T) {
532	blobStorage := mocks.NewMockBlobStorage(t)
533	manifestStorage := mocks.NewMockManifestStorage(t)
534	eventBus := mocks.NewMockEventPublisher(t)
535	state := registrystate.New()
536	svc := NewService(blobStorage, manifestStorage, eventBus, state)
537	const digest = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
538
539	blobStorage.EXPECT().FinishBlobUpload("myapp", "1234567890-myapp", digest).Return(nil)
540
541	require.NoError(t, svc.FinishUpload(testContext(), "myapp", "1234567890-myapp", digest))
542	assert.Contains(t, state.PendingDigests(time.Now().UTC()), digest)
543}
544
545func TestService_FinishUploadBlocksRegistryPruning(t *testing.T) {
546	blobStorage := mocks.NewMockBlobStorage(t)
547	manifestStorage := mocks.NewMockManifestStorage(t)
548	eventBus := mocks.NewMockEventPublisher(t)
549	state := registrystate.New()
550	svc := NewService(blobStorage, manifestStorage, eventBus, state)
551	started := make(chan struct{})
552	release := make(chan struct{})
553
554	blobStorage.EXPECT().FinishBlobUpload("myapp", "1234567890-myapp", mock.Anything).RunAndReturn(func(string, string, string) error {
555		close(started)
556		<-release
557		return nil
558	})
559
560	finished := make(chan error, 1)
561	go func() {
562		finished <- svc.FinishUpload(testContext(), "myapp", "1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
563	}()
564	<-started
565
566	pruneLockAcquired := make(chan struct{})
567	go func() {
568		state.MutationMu.Lock()
569		close(pruneLockAcquired)
570		state.MutationMu.Unlock()
571	}()
572	assert.Never(t, func() bool {
573		select {
574		case <-pruneLockAcquired:
575			return true
576		default:
577			return false
578		}
579	}, 100*time.Millisecond, 10*time.Millisecond)
580
581	close(release)
582	require.NoError(t, <-finished)
583	require.Eventually(t, func() bool {
584		select {
585		case <-pruneLockAcquired:
586			return true
587		default:
588			return false
589		}
590	}, time.Second, 10*time.Millisecond)
591}
592
593func TestService_FinishUpload_Error(t *testing.T) {
594	blobStorage := mocks.NewMockBlobStorage(t)
595	manifestStorage := mocks.NewMockManifestStorage(t)
596	eventBus := mocks.NewMockEventPublisher(t)
597
598	svc := NewService(blobStorage, manifestStorage, eventBus)
599	ctx := testContext()
600
601	blobStorage.EXPECT().FinishBlobUpload("myapp", "1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(errors.New("digest mismatch"))
602
603	err := svc.FinishUpload(ctx, "myapp", "1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
604
605	assert.Error(t, err)
606	assert.Contains(t, err.Error(), "failed to finish blob upload")
607}
608
609func TestService_CancelUpload_Success(t *testing.T) {
610	blobStorage := mocks.NewMockBlobStorage(t)
611	manifestStorage := mocks.NewMockManifestStorage(t)
612	eventBus := mocks.NewMockEventPublisher(t)
613
614	svc := NewService(blobStorage, manifestStorage, eventBus)
615	ctx := testContext()
616
617	blobStorage.EXPECT().CancelBlobUpload("myapp", "1234567890-myapp").Return(nil)
618
619	err := svc.CancelUpload(ctx, "myapp", "1234567890-myapp")
620
621	assert.NoError(t, err)
622}
623
624func TestService_CancelUpload_Error(t *testing.T) {
625	blobStorage := mocks.NewMockBlobStorage(t)
626	manifestStorage := mocks.NewMockManifestStorage(t)
627	eventBus := mocks.NewMockEventPublisher(t)
628
629	svc := NewService(blobStorage, manifestStorage, eventBus)
630	ctx := testContext()
631
632	blobStorage.EXPECT().CancelBlobUpload("myapp", "1234567890-myapp").Return(errors.New("upload not found"))
633
634	err := svc.CancelUpload(ctx, "myapp", "1234567890-myapp")
635
636	assert.Error(t, err)
637	assert.Contains(t, err.Error(), "failed to cancel blob upload")
638}
639
640func TestService_ListTags_Success(t *testing.T) {
641	blobStorage := mocks.NewMockBlobStorage(t)
642	manifestStorage := mocks.NewMockManifestStorage(t)
643	eventBus := mocks.NewMockEventPublisher(t)
644
645	svc := NewService(blobStorage, manifestStorage, eventBus)
646	ctx := testContext()
647
648	manifestStorage.EXPECT().ListTags("myapp").Return([]string{
649		"latest",
650		"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
651		"v1.0",
652		"v2.0",
653	}, nil)
654
655	tags, err := svc.ListTags(ctx, "myapp")
656
657	assert.NoError(t, err)
658	assert.Equal(t, []string{"latest", "v1.0", "v2.0"}, tags)
659}
660
661func TestService_ListTags_Error(t *testing.T) {
662	blobStorage := mocks.NewMockBlobStorage(t)
663	manifestStorage := mocks.NewMockManifestStorage(t)
664	eventBus := mocks.NewMockEventPublisher(t)
665
666	svc := NewService(blobStorage, manifestStorage, eventBus)
667	ctx := testContext()
668
669	manifestStorage.EXPECT().ListTags("myapp").Return(nil, errors.New("repository not found"))
670
671	tags, err := svc.ListTags(ctx, "myapp")
672
673	assert.Error(t, err)
674	assert.Nil(t, tags)
675	assert.Contains(t, err.Error(), "failed to list tags")
676}
677
678func TestService_ListRepositories_Success(t *testing.T) {
679	blobStorage := mocks.NewMockBlobStorage(t)
680	manifestStorage := mocks.NewMockManifestStorage(t)
681	eventBus := mocks.NewMockEventPublisher(t)
682
683	svc := NewService(blobStorage, manifestStorage, eventBus)
684	ctx := testContext()
685
686	manifestStorage.EXPECT().ListRepositories().Return([]string{"myapp", "otherapp", "thirdapp"}, nil)
687
688	repos, err := svc.ListRepositories(ctx)
689
690	assert.NoError(t, err)
691	assert.Equal(t, []string{"myapp", "otherapp", "thirdapp"}, repos)
692}
693
694func TestService_ListRepositories_Error(t *testing.T) {
695	blobStorage := mocks.NewMockBlobStorage(t)
696	manifestStorage := mocks.NewMockManifestStorage(t)
697	eventBus := mocks.NewMockEventPublisher(t)
698
699	svc := NewService(blobStorage, manifestStorage, eventBus)
700	ctx := testContext()
701
702	manifestStorage.EXPECT().ListRepositories().Return(nil, errors.New("storage error"))
703
704	repos, err := svc.ListRepositories(ctx)
705
706	assert.Error(t, err)
707	assert.Nil(t, repos)
708	assert.Contains(t, err.Error(), "failed to list repositories")
709}
710
711func TestPutManifest_PublishesEventForTagReference(t *testing.T) {
712	blobStorage := mocks.NewMockBlobStorage(t)
713	manifestStorage := mocks.NewMockManifestStorage(t)
714	eventBus := mocks.NewMockEventPublisher(t)
715
716	svc := NewService(blobStorage, manifestStorage, eventBus)
717
718	manifestStorage.EXPECT().PutManifest("my-app", "latest", "application/vnd.oci.image.manifest.v1+json", mock.Anything).Return(nil)
719	eventBus.EXPECT().Publish(domain.EventImagePushed, mock.Anything).Return(nil).Once()
720
721	manifest := &domain.Manifest{
722		Name:        "my-app",
723		Reference:   "latest",
724		ContentType: "application/vnd.oci.image.manifest.v1+json",
725		Data:        []byte(`{"schemaVersion":2}`),
726	}
727
728	_, err := svc.PutManifest(context.Background(), manifest)
729	require.NoError(t, err)
730}

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