v2.31.1 f37fe67

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

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