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}