v3.0.0-alpha.1 6fdddb8

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

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