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}