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}