1package container
2
3import (
4 "context"
5 "encoding/json"
6 "io"
7 "strings"
8 "testing"
9
10 "github.com/bnema/zerowrap"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/mock"
13 "github.com/stretchr/testify/require"
14
15 inmocks "gordon/internal/boundaries/in/mocks"
16 "gordon/internal/boundaries/out/mocks"
17 "gordon/internal/domain"
18)
19
20// domainToEnvFileName tests
21
22func TestDomainToEnvFileName(t *testing.T) {
23 tests := []struct {
24 name string
25 domain string
26 expected string
27 }{
28 {
29 name: "simple domain",
30 domain: "app.example.com",
31 expected: "app_example_com.env",
32 },
33 {
34 name: "domain with port",
35 domain: "app.example.com:8080",
36 expected: "app_example_com_8080.env",
37 },
38 {
39 name: "domain with path",
40 domain: "app.example.com/api",
41 expected: "app_example_com_api.env",
42 },
43 {
44 name: "complex domain",
45 domain: "sub.app.example.com:443/v1",
46 expected: "sub_app_example_com_443_v1.env",
47 },
48 {
49 name: "single word",
50 domain: "localhost",
51 expected: "localhost.env",
52 },
53 }
54
55 for _, tt := range tests {
56 t.Run(tt.name, func(t *testing.T) {
57 result := domainToEnvFileName(tt.domain)
58 assert.Equal(t, tt.expected, result)
59 })
60 }
61}
62
63// parseEnvFile tests
64
65func TestParseEnvFile(t *testing.T) {
66 tests := []struct {
67 name string
68 input string
69 expected map[string]string
70 wantErr bool
71 }{
72 {
73 name: "simple key-value pairs",
74 input: "FOO=bar\nBAZ=qux",
75 expected: map[string]string{
76 "FOO": "bar",
77 "BAZ": "qux",
78 },
79 },
80 {
81 name: "with comments and empty lines",
82 input: "# This is a comment\nFOO=bar\n\n# Another comment\nBAZ=qux\n",
83 expected: map[string]string{
84 "FOO": "bar",
85 "BAZ": "qux",
86 },
87 },
88 {
89 name: "with quoted values - double quotes",
90 input: `FOO="bar baz"`,
91 expected: map[string]string{
92 "FOO": "bar baz",
93 },
94 },
95 {
96 name: "with quoted values - single quotes",
97 input: `FOO='bar baz'`,
98 expected: map[string]string{
99 "FOO": "bar baz",
100 },
101 },
102 {
103 name: "value with equals sign",
104 input: "DATABASE_URL=postgres://user:pass@host/db?option=value",
105 expected: map[string]string{
106 "DATABASE_URL": "postgres://user:pass@host/db?option=value",
107 },
108 },
109 {
110 name: "empty value",
111 input: "EMPTY=",
112 expected: map[string]string{
113 "EMPTY": "",
114 },
115 },
116 {
117 name: "empty file",
118 input: "",
119 expected: map[string]string{},
120 },
121 {
122 name: "only comments",
123 input: "# comment 1\n# comment 2",
124 expected: map[string]string{},
125 },
126 {
127 name: "malformed line without equals",
128 input: "MALFORMED\nGOOD=value",
129 expected: map[string]string{"GOOD": "value"},
130 },
131 {
132 name: "whitespace trimming",
133 input: " FOO = bar \n BAZ=qux",
134 expected: map[string]string{
135 "FOO": "bar",
136 "BAZ": "qux",
137 },
138 },
139 }
140
141 for _, tt := range tests {
142 t.Run(tt.name, func(t *testing.T) {
143 result, err := parseEnvFile([]byte(tt.input))
144
145 if tt.wantErr {
146 assert.Error(t, err)
147 return
148 }
149
150 require.NoError(t, err)
151 assert.Equal(t, tt.expected, result)
152 })
153 }
154}
155
156// parseConfigDigest tests
157
158func TestParseConfigDigest(t *testing.T) {
159 tests := []struct {
160 name string
161 manifest map[string]any
162 expected string
163 wantErr bool
164 }{
165 {
166 name: "valid manifest v2",
167 manifest: map[string]any{
168 "schemaVersion": 2,
169 "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
170 "config": map[string]any{
171 "mediaType": "application/vnd.docker.container.image.v1+json",
172 "digest": "sha256:abc123",
173 "size": 1234,
174 },
175 },
176 expected: "sha256:abc123",
177 },
178 {
179 name: "OCI manifest",
180 manifest: map[string]any{
181 "schemaVersion": 2,
182 "mediaType": "application/vnd.oci.image.manifest.v1+json",
183 "config": map[string]any{
184 "mediaType": "application/vnd.oci.image.config.v1+json",
185 "digest": "sha256:def456",
186 "size": 5678,
187 },
188 },
189 expected: "sha256:def456",
190 },
191 {
192 name: "empty config digest",
193 manifest: map[string]any{
194 "schemaVersion": 2,
195 "config": map[string]any{
196 "digest": "",
197 },
198 },
199 expected: "",
200 },
201 }
202
203 for _, tt := range tests {
204 t.Run(tt.name, func(t *testing.T) {
205 data, err := json.Marshal(tt.manifest)
206 require.NoError(t, err)
207
208 result, err := parseConfigDigest(data)
209
210 if tt.wantErr {
211 assert.Error(t, err)
212 return
213 }
214
215 require.NoError(t, err)
216 assert.Equal(t, tt.expected, result)
217 })
218 }
219}
220
221func TestParseConfigDigest_InvalidJSON(t *testing.T) {
222 _, err := parseConfigDigest([]byte("invalid json"))
223 assert.Error(t, err)
224}
225
226// parseImageLabels tests
227
228func TestParseImageLabels(t *testing.T) {
229 tests := []struct {
230 name string
231 config map[string]any
232 expected *domain.ImageLabels
233 }{
234 {
235 name: "single domain label",
236 config: map[string]any{
237 "config": map[string]any{
238 "Labels": map[string]any{
239 "gordon.domain": "app.example.com",
240 },
241 },
242 },
243 expected: &domain.ImageLabels{
244 Domain: "app.example.com",
245 },
246 },
247 {
248 name: "multiple domains label",
249 config: map[string]any{
250 "config": map[string]any{
251 "Labels": map[string]any{
252 "gordon.domains": "app1.example.com, app2.example.com",
253 },
254 },
255 },
256 expected: &domain.ImageLabels{
257 Domains: []string{"app1.example.com", "app2.example.com"},
258 },
259 },
260 {
261 name: "all gordon labels",
262 config: map[string]any{
263 "config": map[string]any{
264 "Labels": map[string]any{
265 "gordon.domain": "app.example.com",
266 "gordon.health": "/healthz",
267 "gordon.port": "8080",
268 "gordon.env-file": ".env.prod",
269 },
270 },
271 },
272 expected: &domain.ImageLabels{
273 Domain: "app.example.com",
274 Health: "/healthz",
275 Port: "8080",
276 EnvFile: ".env.prod",
277 },
278 },
279 {
280 name: "no labels",
281 config: map[string]any{
282 "config": map[string]any{
283 "Labels": nil,
284 },
285 },
286 expected: &domain.ImageLabels{},
287 },
288 {
289 name: "no gordon labels",
290 config: map[string]any{
291 "config": map[string]any{
292 "Labels": map[string]any{
293 "other.label": "value",
294 },
295 },
296 },
297 expected: &domain.ImageLabels{},
298 },
299 {
300 name: "whitespace in labels",
301 config: map[string]any{
302 "config": map[string]any{
303 "Labels": map[string]any{
304 "gordon.domain": " app.example.com ",
305 },
306 },
307 },
308 expected: &domain.ImageLabels{
309 Domain: "app.example.com",
310 },
311 },
312 }
313
314 for _, tt := range tests {
315 t.Run(tt.name, func(t *testing.T) {
316 data, err := json.Marshal(tt.config)
317 require.NoError(t, err)
318
319 result, err := parseImageLabels(data)
320 require.NoError(t, err)
321
322 assert.Equal(t, tt.expected.Domain, result.Domain)
323 assert.Equal(t, tt.expected.Domains, result.Domains)
324 assert.Equal(t, tt.expected.Health, result.Health)
325 assert.Equal(t, tt.expected.Port, result.Port)
326 assert.Equal(t, tt.expected.EnvFile, result.EnvFile)
327 })
328 }
329}
330
331func TestParseImageLabels_InvalidJSON(t *testing.T) {
332 _, err := parseImageLabels([]byte("invalid json"))
333 assert.Error(t, err)
334}
335
336// AutoRouteHandler tests
337
338func TestAutoRouteHandler_CanHandle(t *testing.T) {
339 ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
340 configSvc := inmocks.NewMockConfigService(t)
341 containerSvc := inmocks.NewMockContainerService(t)
342 blobStorage := mocks.NewMockBlobStorage(t)
343
344 handler := NewAutoRouteHandler(ctx, configSvc, containerSvc, blobStorage, "registry.example.com")
345
346 assert.True(t, handler.CanHandle(domain.EventImagePushed))
347 assert.False(t, handler.CanHandle(domain.EventConfigReload))
348 assert.False(t, handler.CanHandle(domain.EventManualReload))
349}
350
351func TestAutoRouteHandler_Handle_DisabledAutoRoute(t *testing.T) {
352 ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
353 configSvc := inmocks.NewMockConfigService(t)
354 containerSvc := inmocks.NewMockContainerService(t)
355 blobStorage := mocks.NewMockBlobStorage(t)
356
357 handler := NewAutoRouteHandler(ctx, configSvc, containerSvc, blobStorage, "registry.example.com")
358
359 configSvc.EXPECT().IsAutoRouteEnabled().Return(false)
360
361 event := domain.Event{
362 ID: "event-123",
363 Type: domain.EventImagePushed,
364 Data: domain.ImagePushedPayload{
365 Name: "myapp",
366 Reference: "latest",
367 Manifest: []byte(`{"schemaVersion": 2}`),
368 },
369 }
370
371 err := handler.Handle(event)
372
373 assert.NoError(t, err)
374}
375
376func TestAutoRouteHandler_Handle_InvalidPayload(t *testing.T) {
377 ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
378 configSvc := inmocks.NewMockConfigService(t)
379 containerSvc := inmocks.NewMockContainerService(t)
380 blobStorage := mocks.NewMockBlobStorage(t)
381
382 handler := NewAutoRouteHandler(ctx, configSvc, containerSvc, blobStorage, "registry.example.com")
383
384 configSvc.EXPECT().IsAutoRouteEnabled().Return(true)
385
386 event := domain.Event{
387 ID: "event-123",
388 Type: domain.EventImagePushed,
389 Data: "invalid payload type",
390 }
391
392 err := handler.Handle(event)
393
394 // Handler skips invalid payload, doesn't error
395 assert.NoError(t, err)
396}
397
398func TestAutoRouteHandler_Handle_EmptyManifest(t *testing.T) {
399 ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
400 configSvc := inmocks.NewMockConfigService(t)
401 containerSvc := inmocks.NewMockContainerService(t)
402 blobStorage := mocks.NewMockBlobStorage(t)
403
404 handler := NewAutoRouteHandler(ctx, configSvc, containerSvc, blobStorage, "registry.example.com")
405
406 configSvc.EXPECT().IsAutoRouteEnabled().Return(true)
407
408 event := domain.Event{
409 ID: "event-123",
410 Type: domain.EventImagePushed,
411 Data: domain.ImagePushedPayload{
412 Name: "myapp",
413 Reference: "latest",
414 Manifest: []byte{},
415 },
416 }
417
418 err := handler.Handle(event)
419
420 assert.NoError(t, err)
421}
422
423func TestAutoRouteHandler_Handle_CreatesNewRoute(t *testing.T) {
424 ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
425 configSvc := inmocks.NewMockConfigService(t)
426 containerSvc := inmocks.NewMockContainerService(t)
427 blobStorage := mocks.NewMockBlobStorage(t)
428
429 handler := NewAutoRouteHandler(ctx, configSvc, containerSvc, blobStorage, "registry.example.com")
430
431 // Build manifest with config digest
432 manifest := map[string]any{
433 "schemaVersion": 2,
434 "config": map[string]any{
435 "digest": "sha256:configdigest123",
436 },
437 }
438 manifestData, _ := json.Marshal(manifest)
439
440 // Build image config with gordon labels
441 imageConfig := map[string]any{
442 "config": map[string]any{
443 "Labels": map[string]any{
444 "gordon.domain": "app.example.com",
445 },
446 },
447 }
448 configData, _ := json.Marshal(imageConfig)
449
450 configSvc.EXPECT().IsAutoRouteEnabled().Return(true)
451 blobStorage.EXPECT().GetBlob("sha256:configdigest123").Return(io.NopCloser(strings.NewReader(string(configData))), nil)
452
453 // No existing routes
454 configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{})
455
456 // Expect route to be added
457 configSvc.EXPECT().AddRoute(mock.Anything, domain.Route{
458 Domain: "app.example.com",
459 Image: "myapp:latest",
460 }).Return(nil)
461
462 // Expect deploy to be triggered
463 containerSvc.EXPECT().Deploy(mock.Anything, domain.Route{
464 Domain: "app.example.com",
465 Image: "myapp:latest",
466 }).Return(&domain.Container{ID: "container-1"}, nil)
467
468 event := domain.Event{
469 ID: "event-123",
470 Type: domain.EventImagePushed,
471 Data: domain.ImagePushedPayload{
472 Name: "myapp",
473 Reference: "latest",
474 Manifest: manifestData,
475 },
476 }
477
478 err := handler.Handle(event)
479
480 assert.NoError(t, err)
481}
482
483func TestAutoRouteHandler_Handle_UpdatesExistingRoute(t *testing.T) {
484 ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
485 configSvc := inmocks.NewMockConfigService(t)
486 containerSvc := inmocks.NewMockContainerService(t)
487 blobStorage := mocks.NewMockBlobStorage(t)
488
489 handler := NewAutoRouteHandler(ctx, configSvc, containerSvc, blobStorage, "registry.example.com")
490
491 // Build manifest with config digest
492 manifest := map[string]any{
493 "schemaVersion": 2,
494 "config": map[string]any{
495 "digest": "sha256:configdigest123",
496 },
497 }
498 manifestData, _ := json.Marshal(manifest)
499
500 // Build image config with gordon labels
501 imageConfig := map[string]any{
502 "config": map[string]any{
503 "Labels": map[string]any{
504 "gordon.domain": "app.example.com",
505 },
506 },
507 }
508 configData, _ := json.Marshal(imageConfig)
509
510 configSvc.EXPECT().IsAutoRouteEnabled().Return(true)
511 blobStorage.EXPECT().GetBlob("sha256:configdigest123").Return(io.NopCloser(strings.NewReader(string(configData))), nil)
512
513 // Existing route with different image
514 configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{
515 {Domain: "app.example.com", Image: "myapp:v1"},
516 })
517
518 // Expect route to be updated
519 configSvc.EXPECT().UpdateRoute(mock.Anything, domain.Route{
520 Domain: "app.example.com",
521 Image: "myapp:v2",
522 }).Return(nil)
523
524 // Expect deploy to be triggered
525 containerSvc.EXPECT().Deploy(mock.Anything, domain.Route{
526 Domain: "app.example.com",
527 Image: "myapp:v2",
528 }).Return(&domain.Container{ID: "container-1"}, nil)
529
530 event := domain.Event{
531 ID: "event-123",
532 Type: domain.EventImagePushed,
533 Data: domain.ImagePushedPayload{
534 Name: "myapp",
535 Reference: "v2",
536 Manifest: manifestData,
537 },
538 }
539
540 err := handler.Handle(event)
541
542 assert.NoError(t, err)
543}
544
545func TestAutoRouteHandler_Handle_NoUpdateIfSameImage(t *testing.T) {
546 ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
547 configSvc := inmocks.NewMockConfigService(t)
548 containerSvc := inmocks.NewMockContainerService(t)
549 blobStorage := mocks.NewMockBlobStorage(t)
550
551 handler := NewAutoRouteHandler(ctx, configSvc, containerSvc, blobStorage, "registry.example.com")
552
553 // Build manifest with config digest
554 manifest := map[string]any{
555 "schemaVersion": 2,
556 "config": map[string]any{
557 "digest": "sha256:configdigest123",
558 },
559 }
560 manifestData, _ := json.Marshal(manifest)
561
562 // Build image config with gordon labels
563 imageConfig := map[string]any{
564 "config": map[string]any{
565 "Labels": map[string]any{
566 "gordon.domain": "app.example.com",
567 },
568 },
569 }
570 configData, _ := json.Marshal(imageConfig)
571
572 configSvc.EXPECT().IsAutoRouteEnabled().Return(true)
573 blobStorage.EXPECT().GetBlob("sha256:configdigest123").Return(io.NopCloser(strings.NewReader(string(configData))), nil)
574
575 // Existing route with same image - no update needed
576 configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{
577 {Domain: "app.example.com", Image: "myapp:latest"},
578 })
579
580 // No AddRoute, UpdateRoute, or Deploy calls expected
581
582 event := domain.Event{
583 ID: "event-123",
584 Type: domain.EventImagePushed,
585 Data: domain.ImagePushedPayload{
586 Name: "myapp",
587 Reference: "latest",
588 Manifest: manifestData,
589 },
590 }
591
592 err := handler.Handle(event)
593
594 assert.NoError(t, err)
595}
596
597func TestAutoRouteHandler_Handle_NoDomainLabel(t *testing.T) {
598 ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
599 configSvc := inmocks.NewMockConfigService(t)
600 containerSvc := inmocks.NewMockContainerService(t)
601 blobStorage := mocks.NewMockBlobStorage(t)
602
603 handler := NewAutoRouteHandler(ctx, configSvc, containerSvc, blobStorage, "registry.example.com")
604
605 // Build manifest with config digest
606 manifest := map[string]any{
607 "schemaVersion": 2,
608 "config": map[string]any{
609 "digest": "sha256:configdigest123",
610 },
611 }
612 manifestData, _ := json.Marshal(manifest)
613
614 // Build image config with no gordon labels
615 imageConfig := map[string]any{
616 "config": map[string]any{
617 "Labels": map[string]any{
618 "other.label": "value",
619 },
620 },
621 }
622 configData, _ := json.Marshal(imageConfig)
623
624 configSvc.EXPECT().IsAutoRouteEnabled().Return(true)
625 blobStorage.EXPECT().GetBlob("sha256:configdigest123").Return(io.NopCloser(strings.NewReader(string(configData))), nil)
626
627 // No route operations expected
628
629 event := domain.Event{
630 ID: "event-123",
631 Type: domain.EventImagePushed,
632 Data: domain.ImagePushedPayload{
633 Name: "myapp",
634 Reference: "latest",
635 Manifest: manifestData,
636 },
637 }
638
639 err := handler.Handle(event)
640
641 assert.NoError(t, err)
642}
643
644func TestAutoRouteHandler_Handle_MultipleDomains(t *testing.T) {
645 ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
646 configSvc := inmocks.NewMockConfigService(t)
647 containerSvc := inmocks.NewMockContainerService(t)
648 blobStorage := mocks.NewMockBlobStorage(t)
649
650 handler := NewAutoRouteHandler(ctx, configSvc, containerSvc, blobStorage, "registry.example.com")
651
652 // Build manifest with config digest
653 manifest := map[string]any{
654 "schemaVersion": 2,
655 "config": map[string]any{
656 "digest": "sha256:configdigest123",
657 },
658 }
659 manifestData, _ := json.Marshal(manifest)
660
661 // Build image config with multiple domains
662 imageConfig := map[string]any{
663 "config": map[string]any{
664 "Labels": map[string]any{
665 "gordon.domain": "app.example.com",
666 "gordon.domains": "api.example.com, www.example.com",
667 },
668 },
669 }
670 configData, _ := json.Marshal(imageConfig)
671
672 configSvc.EXPECT().IsAutoRouteEnabled().Return(true)
673 blobStorage.EXPECT().GetBlob("sha256:configdigest123").Return(io.NopCloser(strings.NewReader(string(configData))), nil)
674
675 // No existing routes
676 configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{}).Times(3)
677
678 // Expect routes to be added for all three domains
679 configSvc.EXPECT().AddRoute(mock.Anything, domain.Route{
680 Domain: "app.example.com",
681 Image: "myapp:latest",
682 }).Return(nil)
683 configSvc.EXPECT().AddRoute(mock.Anything, domain.Route{
684 Domain: "api.example.com",
685 Image: "myapp:latest",
686 }).Return(nil)
687 configSvc.EXPECT().AddRoute(mock.Anything, domain.Route{
688 Domain: "www.example.com",
689 Image: "myapp:latest",
690 }).Return(nil)
691
692 // Expect deploy for all three
693 containerSvc.EXPECT().Deploy(mock.Anything, mock.AnythingOfType("domain.Route")).Return(&domain.Container{ID: "container-1"}, nil).Times(3)
694
695 event := domain.Event{
696 ID: "event-123",
697 Type: domain.EventImagePushed,
698 Data: domain.ImagePushedPayload{
699 Name: "myapp",
700 Reference: "latest",
701 Manifest: manifestData,
702 },
703 }
704
705 err := handler.Handle(event)
706
707 assert.NoError(t, err)
708}
709
710func TestAutoRouteHandler_TriggerDeploy_NilContainerService(t *testing.T) {
711 ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
712 configSvc := inmocks.NewMockConfigService(t)
713 blobStorage := mocks.NewMockBlobStorage(t)
714
715 // Create handler without container service
716 handler := NewAutoRouteHandler(ctx, configSvc, nil, blobStorage, "registry.example.com")
717
718 route := domain.Route{
719 Domain: "app.example.com",
720 Image: "myapp:latest",
721 }
722
723 // Should not panic, just log and skip
724 handler.triggerDeploy(ctx, route)
725}
726
727// collectDomains tests
728
729func TestAutoRouteHandler_CollectDomains(t *testing.T) {
730 ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
731 configSvc := inmocks.NewMockConfigService(t)
732 containerSvc := inmocks.NewMockContainerService(t)
733 blobStorage := mocks.NewMockBlobStorage(t)
734
735 handler := NewAutoRouteHandler(ctx, configSvc, containerSvc, blobStorage, "registry.example.com")
736
737 tests := []struct {
738 name string
739 labels *domain.ImageLabels
740 expected []string
741 }{
742 {
743 name: "single domain",
744 labels: &domain.ImageLabels{
745 Domain: "app.example.com",
746 },
747 expected: []string{"app.example.com"},
748 },
749 {
750 name: "domains list only",
751 labels: &domain.ImageLabels{
752 Domains: []string{"api.example.com", "www.example.com"},
753 },
754 expected: []string{"api.example.com", "www.example.com"},
755 },
756 {
757 name: "both domain and domains",
758 labels: &domain.ImageLabels{
759 Domain: "app.example.com",
760 Domains: []string{"api.example.com"},
761 },
762 expected: []string{"app.example.com", "api.example.com"},
763 },
764 {
765 name: "empty labels",
766 labels: &domain.ImageLabels{},
767 expected: nil,
768 },
769 }
770
771 for _, tt := range tests {
772 t.Run(tt.name, func(t *testing.T) {
773 result := handler.collectDomains(tt.labels)
774 assert.Equal(t, tt.expected, result)
775 })
776 }
777}
778
779// buildImageName tests
780
781func TestAutoRouteHandler_BuildImageName(t *testing.T) {
782 ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
783 configSvc := inmocks.NewMockConfigService(t)
784 containerSvc := inmocks.NewMockContainerService(t)
785 blobStorage := mocks.NewMockBlobStorage(t)
786
787 handler := NewAutoRouteHandler(ctx, configSvc, containerSvc, blobStorage, "registry.example.com")
788
789 tests := []struct {
790 name string
791 imageName string
792 reference string
793 expected string
794 }{
795 {
796 name: "with tag",
797 imageName: "myapp",
798 reference: "latest",
799 expected: "myapp:latest",
800 },
801 {
802 name: "with version tag",
803 imageName: "myapp",
804 reference: "v1.2.3",
805 expected: "myapp:v1.2.3",
806 },
807 {
808 name: "empty reference",
809 imageName: "myapp",
810 reference: "",
811 expected: "myapp",
812 },
813 {
814 name: "with digest",
815 imageName: "myapp",
816 reference: "sha256:abc123",
817 expected: "myapp@sha256:abc123",
818 },
819 }
820
821 for _, tt := range tests {
822 t.Run(tt.name, func(t *testing.T) {
823 result := handler.buildImageName(tt.imageName, tt.reference)
824 assert.Equal(t, tt.expected, result)
825 })
826 }
827}
828
829// buildFullImageRef tests
830
831func TestAutoRouteHandler_BuildFullImageRef(t *testing.T) {
832 ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
833 configSvc := inmocks.NewMockConfigService(t)
834 containerSvc := inmocks.NewMockContainerService(t)
835 blobStorage := mocks.NewMockBlobStorage(t)
836
837 tests := []struct {
838 name string
839 registryDomain string
840 imageName string
841 expected string
842 }{
843 {
844 name: "simple image with registry",
845 registryDomain: "registry.example.com",
846 imageName: "myapp:latest",
847 expected: "registry.example.com/myapp:latest",
848 },
849 {
850 name: "already has registry prefix",
851 registryDomain: "registry.example.com",
852 imageName: "registry.example.com/myapp:latest",
853 expected: "registry.example.com/myapp:latest",
854 },
855 {
856 name: "external registry reference",
857 registryDomain: "registry.example.com",
858 imageName: "docker.io/library/nginx:latest",
859 expected: "docker.io/library/nginx:latest",
860 },
861 {
862 name: "digest reference",
863 registryDomain: "registry.example.com",
864 imageName: "myapp@sha256:abc123def456",
865 expected: "registry.example.com/myapp@sha256:abc123def456",
866 },
867 {
868 name: "empty registry domain",
869 registryDomain: "",
870 imageName: "myapp:latest",
871 expected: "myapp:latest",
872 },
873 {
874 name: "image without tag",
875 registryDomain: "registry.example.com",
876 imageName: "myapp",
877 expected: "registry.example.com/myapp",
878 },
879 }
880
881 for _, tt := range tests {
882 t.Run(tt.name, func(t *testing.T) {
883 handler := NewAutoRouteHandler(ctx, configSvc, containerSvc, blobStorage, tt.registryDomain)
884 result := handler.buildFullImageRef(tt.imageName)
885 assert.Equal(t, tt.expected, result)
886 })
887 }
888}