detached a43bd67

test: add comprehensive tests for attachment CRUD methods

Add tests for:
- GetAllAttachments: returns all attachments, empty map, copy semantics
- GetAttachmentsFor: existing domain/group, not found, copy semantics
- AddAttachment: new target, existing target, duplicate, validation, network groups
- RemoveAttachment: single/multiple attachments, not found, validation, network groups

These tests mirror the existing route CRUD test patterns.
commit
a43bd676f047ac04bdcd46a40b635d6dcc0f3e12
author
brice · · 2026-01-17 15:32 UTC
parent
6070679
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +376 -2
M internal/usecase/config/service_test.go +376 -2
@@ -332,7 +332,7 @@ func TestService_GetNetworkGroups(t *testing.T) {
332332 func TestService_GetAttachments(t *testing.T) {
333333 v := viper.New()
334334 v.Set("attachments", map[string]interface{}{
335- "app.example.com": []interface{}{"redis:latest", "postgres:15"},
335+ "app.example.com": []interface{}{"redis:latest", "postgres:18"},
336336 })
337337
338338 eventBus := mocks.NewMockEventPublisher(t)
@@ -344,7 +344,381 @@ func TestService_GetAttachments(t *testing.T) {
344344 attachments := svc.GetAttachments()
345345
346346 assert.Len(t, attachments, 1)
347- assert.ElementsMatch(t, []string{"redis:latest", "postgres:15"}, attachments["app.example.com"])
347+ assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, attachments["app.example.com"])
348+}
349+
350+func TestService_GetAllAttachments(t *testing.T) {
351+ t.Run("returns all attachments", func(t *testing.T) {
352+ v := viper.New()
353+ v.Set("attachments", map[string]interface{}{
354+ "app.example.com": []interface{}{"redis:latest", "postgres:18"},
355+ "api.example.com": []interface{}{"rabbitmq:3"},
356+ })
357+
358+ eventBus := mocks.NewMockEventPublisher(t)
359+ svc := NewService(v, eventBus)
360+ ctx := testContext()
361+
362+ _ = svc.Load(ctx)
363+
364+ attachments := svc.GetAllAttachments(ctx)
365+
366+ assert.Len(t, attachments, 2)
367+ assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, attachments["app.example.com"])
368+ assert.ElementsMatch(t, []string{"rabbitmq:3"}, attachments["api.example.com"])
369+ })
370+
371+ t.Run("returns empty map when no attachments", func(t *testing.T) {
372+ v := viper.New()
373+ eventBus := mocks.NewMockEventPublisher(t)
374+ svc := NewService(v, eventBus)
375+ ctx := testContext()
376+
377+ _ = svc.Load(ctx)
378+
379+ attachments := svc.GetAllAttachments(ctx)
380+
381+ assert.Empty(t, attachments)
382+ })
383+
384+ t.Run("returns copy not reference", func(t *testing.T) {
385+ v := viper.New()
386+ v.Set("attachments", map[string]interface{}{
387+ "app.example.com": []interface{}{"redis:latest"},
388+ })
389+
390+ eventBus := mocks.NewMockEventPublisher(t)
391+ svc := NewService(v, eventBus)
392+ ctx := testContext()
393+
394+ _ = svc.Load(ctx)
395+
396+ attachments := svc.GetAllAttachments(ctx)
397+ // Modify the returned map
398+ attachments["app.example.com"] = append(attachments["app.example.com"], "postgres:18")
399+
400+ // Original should be unchanged
401+ original := svc.GetAllAttachments(ctx)
402+ assert.Len(t, original["app.example.com"], 1)
403+ })
404+}
405+
406+func TestService_GetAttachmentsFor(t *testing.T) {
407+ t.Run("existing domain", func(t *testing.T) {
408+ v := viper.New()
409+ v.Set("attachments", map[string]interface{}{
410+ "app.example.com": []interface{}{"redis:latest", "postgres:18"},
411+ })
412+
413+ eventBus := mocks.NewMockEventPublisher(t)
414+ svc := NewService(v, eventBus)
415+ ctx := testContext()
416+
417+ _ = svc.Load(ctx)
418+
419+ images, err := svc.GetAttachmentsFor(ctx, "app.example.com")
420+
421+ require.NoError(t, err)
422+ assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, images)
423+ })
424+
425+ t.Run("existing network group", func(t *testing.T) {
426+ v := viper.New()
427+ v.Set("attachments", map[string]interface{}{
428+ "backend": []interface{}{"rabbitmq:3", "redis:latest"},
429+ })
430+
431+ eventBus := mocks.NewMockEventPublisher(t)
432+ svc := NewService(v, eventBus)
433+ ctx := testContext()
434+
435+ _ = svc.Load(ctx)
436+
437+ images, err := svc.GetAttachmentsFor(ctx, "backend")
438+
439+ require.NoError(t, err)
440+ assert.ElementsMatch(t, []string{"rabbitmq:3", "redis:latest"}, images)
441+ })
442+
443+ t.Run("non-existent target", func(t *testing.T) {
444+ v := viper.New()
445+ eventBus := mocks.NewMockEventPublisher(t)
446+ svc := NewService(v, eventBus)
447+ ctx := testContext()
448+
449+ _ = svc.Load(ctx)
450+
451+ images, err := svc.GetAttachmentsFor(ctx, "notfound.example.com")
452+
453+ assert.ErrorIs(t, err, domain.ErrAttachmentNotFound)
454+ assert.Nil(t, images)
455+ })
456+
457+ t.Run("returns copy not reference", func(t *testing.T) {
458+ v := viper.New()
459+ v.Set("attachments", map[string]interface{}{
460+ "app.example.com": []interface{}{"redis:latest"},
461+ })
462+
463+ eventBus := mocks.NewMockEventPublisher(t)
464+ svc := NewService(v, eventBus)
465+ ctx := testContext()
466+
467+ _ = svc.Load(ctx)
468+
469+ images, err := svc.GetAttachmentsFor(ctx, "app.example.com")
470+ require.NoError(t, err)
471+
472+ // Modify the returned slice (use _ to satisfy linter)
473+ _ = append(images, "postgres:18")
474+
475+ // Original should be unchanged
476+ original, _ := svc.GetAttachmentsFor(ctx, "app.example.com")
477+ assert.Len(t, original, 1)
478+ })
479+}
480+
481+func TestService_AddAttachment(t *testing.T) {
482+ t.Run("success - new target", func(t *testing.T) {
483+ tmpDir := t.TempDir()
484+ configFile := filepath.Join(tmpDir, "gordon.toml")
485+ err := os.WriteFile(configFile, []byte("[attachments]\n"), 0600)
486+ require.NoError(t, err)
487+
488+ v := viper.New()
489+ v.SetConfigFile(configFile)
490+ eventBus := mocks.NewMockEventPublisher(t)
491+ svc := NewService(v, eventBus)
492+ ctx := testContext()
493+
494+ _ = svc.Load(ctx)
495+
496+ err = svc.AddAttachment(ctx, "app.example.com", "postgres:18")
497+
498+ assert.NoError(t, err)
499+
500+ // Verify attachment was added
501+ config := svc.GetConfig()
502+ assert.Contains(t, config.Attachments["app.example.com"], "postgres:18")
503+ })
504+
505+ t.Run("success - existing target", func(t *testing.T) {
506+ tmpDir := t.TempDir()
507+ configFile := filepath.Join(tmpDir, "gordon.toml")
508+ err := os.WriteFile(configFile, []byte("[attachments]\n\"app.example.com\" = [\"redis:latest\"]\n"), 0600)
509+ require.NoError(t, err)
510+
511+ v := viper.New()
512+ v.SetConfigFile(configFile)
513+ v.Set("attachments", map[string]interface{}{
514+ "app.example.com": []interface{}{"redis:latest"},
515+ })
516+ eventBus := mocks.NewMockEventPublisher(t)
517+ svc := NewService(v, eventBus)
518+ ctx := testContext()
519+
520+ _ = svc.Load(ctx)
521+
522+ err = svc.AddAttachment(ctx, "app.example.com", "postgres:18")
523+
524+ assert.NoError(t, err)
525+
526+ // Verify attachment was added alongside existing
527+ config := svc.GetConfig()
528+ assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, config.Attachments["app.example.com"])
529+ })
530+
531+ t.Run("duplicate attachment", func(t *testing.T) {
532+ v := viper.New()
533+ v.Set("attachments", map[string]interface{}{
534+ "app.example.com": []interface{}{"redis:latest"},
535+ })
536+ eventBus := mocks.NewMockEventPublisher(t)
537+ svc := NewService(v, eventBus)
538+ ctx := testContext()
539+
540+ _ = svc.Load(ctx)
541+
542+ err := svc.AddAttachment(ctx, "app.example.com", "redis:latest")
543+
544+ assert.ErrorIs(t, err, domain.ErrAttachmentExists)
545+ })
546+
547+ t.Run("empty target", func(t *testing.T) {
548+ v := viper.New()
549+ eventBus := mocks.NewMockEventPublisher(t)
550+ svc := NewService(v, eventBus)
551+ ctx := testContext()
552+
553+ err := svc.AddAttachment(ctx, "", "postgres:18")
554+
555+ assert.ErrorIs(t, err, domain.ErrAttachmentTargetEmpty)
556+ })
557+
558+ t.Run("empty image", func(t *testing.T) {
559+ v := viper.New()
560+ eventBus := mocks.NewMockEventPublisher(t)
561+ svc := NewService(v, eventBus)
562+ ctx := testContext()
563+
564+ err := svc.AddAttachment(ctx, "app.example.com", "")
565+
566+ assert.ErrorIs(t, err, domain.ErrAttachmentImageEmpty)
567+ })
568+
569+ t.Run("network group target", func(t *testing.T) {
570+ tmpDir := t.TempDir()
571+ configFile := filepath.Join(tmpDir, "gordon.toml")
572+ err := os.WriteFile(configFile, []byte("[attachments]\n"), 0600)
573+ require.NoError(t, err)
574+
575+ v := viper.New()
576+ v.SetConfigFile(configFile)
577+ eventBus := mocks.NewMockEventPublisher(t)
578+ svc := NewService(v, eventBus)
579+ ctx := testContext()
580+
581+ _ = svc.Load(ctx)
582+
583+ err = svc.AddAttachment(ctx, "backend", "rabbitmq:3")
584+
585+ assert.NoError(t, err)
586+
587+ config := svc.GetConfig()
588+ assert.Contains(t, config.Attachments["backend"], "rabbitmq:3")
589+ })
590+}
591+
592+func TestService_RemoveAttachment(t *testing.T) {
593+ t.Run("success - single attachment", func(t *testing.T) {
594+ tmpDir := t.TempDir()
595+ configFile := filepath.Join(tmpDir, "gordon.toml")
596+ err := os.WriteFile(configFile, []byte("[attachments]\n\"app.example.com\" = [\"redis:latest\"]\n"), 0600)
597+ require.NoError(t, err)
598+
599+ v := viper.New()
600+ v.SetConfigFile(configFile)
601+ v.Set("attachments", map[string]interface{}{
602+ "app.example.com": []interface{}{"redis:latest"},
603+ })
604+ eventBus := mocks.NewMockEventPublisher(t)
605+ svc := NewService(v, eventBus)
606+ ctx := testContext()
607+
608+ _ = svc.Load(ctx)
609+
610+ err = svc.RemoveAttachment(ctx, "app.example.com", "redis:latest")
611+
612+ assert.NoError(t, err)
613+
614+ // Verify target was removed (no attachments left)
615+ config := svc.GetConfig()
616+ _, exists := config.Attachments["app.example.com"]
617+ assert.False(t, exists)
618+ })
619+
620+ t.Run("success - multiple attachments", func(t *testing.T) {
621+ tmpDir := t.TempDir()
622+ configFile := filepath.Join(tmpDir, "gordon.toml")
623+ err := os.WriteFile(configFile, []byte("[attachments]\n\"app.example.com\" = [\"redis:latest\", \"postgres:18\"]\n"), 0600)
624+ require.NoError(t, err)
625+
626+ v := viper.New()
627+ v.SetConfigFile(configFile)
628+ v.Set("attachments", map[string]interface{}{
629+ "app.example.com": []interface{}{"redis:latest", "postgres:18"},
630+ })
631+ eventBus := mocks.NewMockEventPublisher(t)
632+ svc := NewService(v, eventBus)
633+ ctx := testContext()
634+
635+ _ = svc.Load(ctx)
636+
637+ err = svc.RemoveAttachment(ctx, "app.example.com", "redis:latest")
638+
639+ assert.NoError(t, err)
640+
641+ // Verify only redis was removed
642+ config := svc.GetConfig()
643+ assert.Equal(t, []string{"postgres:18"}, config.Attachments["app.example.com"])
644+ })
645+
646+ t.Run("target not found", func(t *testing.T) {
647+ v := viper.New()
648+ eventBus := mocks.NewMockEventPublisher(t)
649+ svc := NewService(v, eventBus)
650+ ctx := testContext()
651+
652+ _ = svc.Load(ctx)
653+
654+ err := svc.RemoveAttachment(ctx, "notfound.example.com", "redis:latest")
655+
656+ assert.ErrorIs(t, err, domain.ErrAttachmentNotFound)
657+ })
658+
659+ t.Run("image not found in target", func(t *testing.T) {
660+ v := viper.New()
661+ v.Set("attachments", map[string]interface{}{
662+ "app.example.com": []interface{}{"redis:latest"},
663+ })
664+ eventBus := mocks.NewMockEventPublisher(t)
665+ svc := NewService(v, eventBus)
666+ ctx := testContext()
667+
668+ _ = svc.Load(ctx)
669+
670+ err := svc.RemoveAttachment(ctx, "app.example.com", "postgres:18")
671+
672+ assert.ErrorIs(t, err, domain.ErrAttachmentNotFound)
673+ })
674+
675+ t.Run("empty target", func(t *testing.T) {
676+ v := viper.New()
677+ eventBus := mocks.NewMockEventPublisher(t)
678+ svc := NewService(v, eventBus)
679+ ctx := testContext()
680+
681+ err := svc.RemoveAttachment(ctx, "", "redis:latest")
682+
683+ assert.ErrorIs(t, err, domain.ErrAttachmentTargetEmpty)
684+ })
685+
686+ t.Run("empty image", func(t *testing.T) {
687+ v := viper.New()
688+ eventBus := mocks.NewMockEventPublisher(t)
689+ svc := NewService(v, eventBus)
690+ ctx := testContext()
691+
692+ err := svc.RemoveAttachment(ctx, "app.example.com", "")
693+
694+ assert.ErrorIs(t, err, domain.ErrAttachmentImageEmpty)
695+ })
696+
697+ t.Run("network group target", func(t *testing.T) {
698+ tmpDir := t.TempDir()
699+ configFile := filepath.Join(tmpDir, "gordon.toml")
700+ err := os.WriteFile(configFile, []byte("[attachments]\n\"backend\" = [\"rabbitmq:3\"]\n"), 0600)
701+ require.NoError(t, err)
702+
703+ v := viper.New()
704+ v.SetConfigFile(configFile)
705+ v.Set("attachments", map[string]interface{}{
706+ "backend": []interface{}{"rabbitmq:3"},
707+ })
708+ eventBus := mocks.NewMockEventPublisher(t)
709+ svc := NewService(v, eventBus)
710+ ctx := testContext()
711+
712+ _ = svc.Load(ctx)
713+
714+ err = svc.RemoveAttachment(ctx, "backend", "rabbitmq:3")
715+
716+ assert.NoError(t, err)
717+
718+ config := svc.GetConfig()
719+ _, exists := config.Attachments["backend"]
720+ assert.False(t, exists)
721+ })
348722 }
349723
350724 func TestService_GetExternalRoutes(t *testing.T) {
@@ -332,7 +332,7 @@ func TestService_GetNetworkGroups(t *testing.T) {
332 func TestService_GetAttachments(t *testing.T) {332 func TestService_GetAttachments(t *testing.T) {
333 v := viper.New()333 v := viper.New()
334 v.Set("attachments", map[string]interface{}{334 v.Set("attachments", map[string]interface{}{
335- "app.example.com": []interface{}{"redis:latest", "postgres:15"},335+ "app.example.com": []interface{}{"redis:latest", "postgres:18"},
336 })336 })
337 337
338 eventBus := mocks.NewMockEventPublisher(t)338 eventBus := mocks.NewMockEventPublisher(t)
@@ -344,7 +344,381 @@ func TestService_GetAttachments(t *testing.T) {
344 attachments := svc.GetAttachments()344 attachments := svc.GetAttachments()
345 345
346 assert.Len(t, attachments, 1)346 assert.Len(t, attachments, 1)
347- assert.ElementsMatch(t, []string{"redis:latest", "postgres:15"}, attachments["app.example.com"])347+ assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, attachments["app.example.com"])
348+}
349+
350+func TestService_GetAllAttachments(t *testing.T) {
351+ t.Run("returns all attachments", func(t *testing.T) {
352+ v := viper.New()
353+ v.Set("attachments", map[string]interface{}{
354+ "app.example.com": []interface{}{"redis:latest", "postgres:18"},
355+ "api.example.com": []interface{}{"rabbitmq:3"},
356+ })
357+
358+ eventBus := mocks.NewMockEventPublisher(t)
359+ svc := NewService(v, eventBus)
360+ ctx := testContext()
361+
362+ _ = svc.Load(ctx)
363+
364+ attachments := svc.GetAllAttachments(ctx)
365+
366+ assert.Len(t, attachments, 2)
367+ assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, attachments["app.example.com"])
368+ assert.ElementsMatch(t, []string{"rabbitmq:3"}, attachments["api.example.com"])
369+ })
370+
371+ t.Run("returns empty map when no attachments", func(t *testing.T) {
372+ v := viper.New()
373+ eventBus := mocks.NewMockEventPublisher(t)
374+ svc := NewService(v, eventBus)
375+ ctx := testContext()
376+
377+ _ = svc.Load(ctx)
378+
379+ attachments := svc.GetAllAttachments(ctx)
380+
381+ assert.Empty(t, attachments)
382+ })
383+
384+ t.Run("returns copy not reference", func(t *testing.T) {
385+ v := viper.New()
386+ v.Set("attachments", map[string]interface{}{
387+ "app.example.com": []interface{}{"redis:latest"},
388+ })
389+
390+ eventBus := mocks.NewMockEventPublisher(t)
391+ svc := NewService(v, eventBus)
392+ ctx := testContext()
393+
394+ _ = svc.Load(ctx)
395+
396+ attachments := svc.GetAllAttachments(ctx)
397+ // Modify the returned map
398+ attachments["app.example.com"] = append(attachments["app.example.com"], "postgres:18")
399+
400+ // Original should be unchanged
401+ original := svc.GetAllAttachments(ctx)
402+ assert.Len(t, original["app.example.com"], 1)
403+ })
404+}
405+
406+func TestService_GetAttachmentsFor(t *testing.T) {
407+ t.Run("existing domain", func(t *testing.T) {
408+ v := viper.New()
409+ v.Set("attachments", map[string]interface{}{
410+ "app.example.com": []interface{}{"redis:latest", "postgres:18"},
411+ })
412+
413+ eventBus := mocks.NewMockEventPublisher(t)
414+ svc := NewService(v, eventBus)
415+ ctx := testContext()
416+
417+ _ = svc.Load(ctx)
418+
419+ images, err := svc.GetAttachmentsFor(ctx, "app.example.com")
420+
421+ require.NoError(t, err)
422+ assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, images)
423+ })
424+
425+ t.Run("existing network group", func(t *testing.T) {
426+ v := viper.New()
427+ v.Set("attachments", map[string]interface{}{
428+ "backend": []interface{}{"rabbitmq:3", "redis:latest"},
429+ })
430+
431+ eventBus := mocks.NewMockEventPublisher(t)
432+ svc := NewService(v, eventBus)
433+ ctx := testContext()
434+
435+ _ = svc.Load(ctx)
436+
437+ images, err := svc.GetAttachmentsFor(ctx, "backend")
438+
439+ require.NoError(t, err)
440+ assert.ElementsMatch(t, []string{"rabbitmq:3", "redis:latest"}, images)
441+ })
442+
443+ t.Run("non-existent target", func(t *testing.T) {
444+ v := viper.New()
445+ eventBus := mocks.NewMockEventPublisher(t)
446+ svc := NewService(v, eventBus)
447+ ctx := testContext()
448+
449+ _ = svc.Load(ctx)
450+
451+ images, err := svc.GetAttachmentsFor(ctx, "notfound.example.com")
452+
453+ assert.ErrorIs(t, err, domain.ErrAttachmentNotFound)
454+ assert.Nil(t, images)
455+ })
456+
457+ t.Run("returns copy not reference", func(t *testing.T) {
458+ v := viper.New()
459+ v.Set("attachments", map[string]interface{}{
460+ "app.example.com": []interface{}{"redis:latest"},
461+ })
462+
463+ eventBus := mocks.NewMockEventPublisher(t)
464+ svc := NewService(v, eventBus)
465+ ctx := testContext()
466+
467+ _ = svc.Load(ctx)
468+
469+ images, err := svc.GetAttachmentsFor(ctx, "app.example.com")
470+ require.NoError(t, err)
471+
472+ // Modify the returned slice (use _ to satisfy linter)
473+ _ = append(images, "postgres:18")
474+
475+ // Original should be unchanged
476+ original, _ := svc.GetAttachmentsFor(ctx, "app.example.com")
477+ assert.Len(t, original, 1)
478+ })
479+}
480+
481+func TestService_AddAttachment(t *testing.T) {
482+ t.Run("success - new target", func(t *testing.T) {
483+ tmpDir := t.TempDir()
484+ configFile := filepath.Join(tmpDir, "gordon.toml")
485+ err := os.WriteFile(configFile, []byte("[attachments]\n"), 0600)
486+ require.NoError(t, err)
487+
488+ v := viper.New()
489+ v.SetConfigFile(configFile)
490+ eventBus := mocks.NewMockEventPublisher(t)
491+ svc := NewService(v, eventBus)
492+ ctx := testContext()
493+
494+ _ = svc.Load(ctx)
495+
496+ err = svc.AddAttachment(ctx, "app.example.com", "postgres:18")
497+
498+ assert.NoError(t, err)
499+
500+ // Verify attachment was added
501+ config := svc.GetConfig()
502+ assert.Contains(t, config.Attachments["app.example.com"], "postgres:18")
503+ })
504+
505+ t.Run("success - existing target", func(t *testing.T) {
506+ tmpDir := t.TempDir()
507+ configFile := filepath.Join(tmpDir, "gordon.toml")
508+ err := os.WriteFile(configFile, []byte("[attachments]\n\"app.example.com\" = [\"redis:latest\"]\n"), 0600)
509+ require.NoError(t, err)
510+
511+ v := viper.New()
512+ v.SetConfigFile(configFile)
513+ v.Set("attachments", map[string]interface{}{
514+ "app.example.com": []interface{}{"redis:latest"},
515+ })
516+ eventBus := mocks.NewMockEventPublisher(t)
517+ svc := NewService(v, eventBus)
518+ ctx := testContext()
519+
520+ _ = svc.Load(ctx)
521+
522+ err = svc.AddAttachment(ctx, "app.example.com", "postgres:18")
523+
524+ assert.NoError(t, err)
525+
526+ // Verify attachment was added alongside existing
527+ config := svc.GetConfig()
528+ assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, config.Attachments["app.example.com"])
529+ })
530+
531+ t.Run("duplicate attachment", func(t *testing.T) {
532+ v := viper.New()
533+ v.Set("attachments", map[string]interface{}{
534+ "app.example.com": []interface{}{"redis:latest"},
535+ })
536+ eventBus := mocks.NewMockEventPublisher(t)
537+ svc := NewService(v, eventBus)
538+ ctx := testContext()
539+
540+ _ = svc.Load(ctx)
541+
542+ err := svc.AddAttachment(ctx, "app.example.com", "redis:latest")
543+
544+ assert.ErrorIs(t, err, domain.ErrAttachmentExists)
545+ })
546+
547+ t.Run("empty target", func(t *testing.T) {
548+ v := viper.New()
549+ eventBus := mocks.NewMockEventPublisher(t)
550+ svc := NewService(v, eventBus)
551+ ctx := testContext()
552+
553+ err := svc.AddAttachment(ctx, "", "postgres:18")
554+
555+ assert.ErrorIs(t, err, domain.ErrAttachmentTargetEmpty)
556+ })
557+
558+ t.Run("empty image", func(t *testing.T) {
559+ v := viper.New()
560+ eventBus := mocks.NewMockEventPublisher(t)
561+ svc := NewService(v, eventBus)
562+ ctx := testContext()
563+
564+ err := svc.AddAttachment(ctx, "app.example.com", "")
565+
566+ assert.ErrorIs(t, err, domain.ErrAttachmentImageEmpty)
567+ })
568+
569+ t.Run("network group target", func(t *testing.T) {
570+ tmpDir := t.TempDir()
571+ configFile := filepath.Join(tmpDir, "gordon.toml")
572+ err := os.WriteFile(configFile, []byte("[attachments]\n"), 0600)
573+ require.NoError(t, err)
574+
575+ v := viper.New()
576+ v.SetConfigFile(configFile)
577+ eventBus := mocks.NewMockEventPublisher(t)
578+ svc := NewService(v, eventBus)
579+ ctx := testContext()
580+
581+ _ = svc.Load(ctx)
582+
583+ err = svc.AddAttachment(ctx, "backend", "rabbitmq:3")
584+
585+ assert.NoError(t, err)
586+
587+ config := svc.GetConfig()
588+ assert.Contains(t, config.Attachments["backend"], "rabbitmq:3")
589+ })
590+}
591+
592+func TestService_RemoveAttachment(t *testing.T) {
593+ t.Run("success - single attachment", func(t *testing.T) {
594+ tmpDir := t.TempDir()
595+ configFile := filepath.Join(tmpDir, "gordon.toml")
596+ err := os.WriteFile(configFile, []byte("[attachments]\n\"app.example.com\" = [\"redis:latest\"]\n"), 0600)
597+ require.NoError(t, err)
598+
599+ v := viper.New()
600+ v.SetConfigFile(configFile)
601+ v.Set("attachments", map[string]interface{}{
602+ "app.example.com": []interface{}{"redis:latest"},
603+ })
604+ eventBus := mocks.NewMockEventPublisher(t)
605+ svc := NewService(v, eventBus)
606+ ctx := testContext()
607+
608+ _ = svc.Load(ctx)
609+
610+ err = svc.RemoveAttachment(ctx, "app.example.com", "redis:latest")
611+
612+ assert.NoError(t, err)
613+
614+ // Verify target was removed (no attachments left)
615+ config := svc.GetConfig()
616+ _, exists := config.Attachments["app.example.com"]
617+ assert.False(t, exists)
618+ })
619+
620+ t.Run("success - multiple attachments", func(t *testing.T) {
621+ tmpDir := t.TempDir()
622+ configFile := filepath.Join(tmpDir, "gordon.toml")
623+ err := os.WriteFile(configFile, []byte("[attachments]\n\"app.example.com\" = [\"redis:latest\", \"postgres:18\"]\n"), 0600)
624+ require.NoError(t, err)
625+
626+ v := viper.New()
627+ v.SetConfigFile(configFile)
628+ v.Set("attachments", map[string]interface{}{
629+ "app.example.com": []interface{}{"redis:latest", "postgres:18"},
630+ })
631+ eventBus := mocks.NewMockEventPublisher(t)
632+ svc := NewService(v, eventBus)
633+ ctx := testContext()
634+
635+ _ = svc.Load(ctx)
636+
637+ err = svc.RemoveAttachment(ctx, "app.example.com", "redis:latest")
638+
639+ assert.NoError(t, err)
640+
641+ // Verify only redis was removed
642+ config := svc.GetConfig()
643+ assert.Equal(t, []string{"postgres:18"}, config.Attachments["app.example.com"])
644+ })
645+
646+ t.Run("target not found", func(t *testing.T) {
647+ v := viper.New()
648+ eventBus := mocks.NewMockEventPublisher(t)
649+ svc := NewService(v, eventBus)
650+ ctx := testContext()
651+
652+ _ = svc.Load(ctx)
653+
654+ err := svc.RemoveAttachment(ctx, "notfound.example.com", "redis:latest")
655+
656+ assert.ErrorIs(t, err, domain.ErrAttachmentNotFound)
657+ })
658+
659+ t.Run("image not found in target", func(t *testing.T) {
660+ v := viper.New()
661+ v.Set("attachments", map[string]interface{}{
662+ "app.example.com": []interface{}{"redis:latest"},
663+ })
664+ eventBus := mocks.NewMockEventPublisher(t)
665+ svc := NewService(v, eventBus)
666+ ctx := testContext()
667+
668+ _ = svc.Load(ctx)
669+
670+ err := svc.RemoveAttachment(ctx, "app.example.com", "postgres:18")
671+
672+ assert.ErrorIs(t, err, domain.ErrAttachmentNotFound)
673+ })
674+
675+ t.Run("empty target", func(t *testing.T) {
676+ v := viper.New()
677+ eventBus := mocks.NewMockEventPublisher(t)
678+ svc := NewService(v, eventBus)
679+ ctx := testContext()
680+
681+ err := svc.RemoveAttachment(ctx, "", "redis:latest")
682+
683+ assert.ErrorIs(t, err, domain.ErrAttachmentTargetEmpty)
684+ })
685+
686+ t.Run("empty image", func(t *testing.T) {
687+ v := viper.New()
688+ eventBus := mocks.NewMockEventPublisher(t)
689+ svc := NewService(v, eventBus)
690+ ctx := testContext()
691+
692+ err := svc.RemoveAttachment(ctx, "app.example.com", "")
693+
694+ assert.ErrorIs(t, err, domain.ErrAttachmentImageEmpty)
695+ })
696+
697+ t.Run("network group target", func(t *testing.T) {
698+ tmpDir := t.TempDir()
699+ configFile := filepath.Join(tmpDir, "gordon.toml")
700+ err := os.WriteFile(configFile, []byte("[attachments]\n\"backend\" = [\"rabbitmq:3\"]\n"), 0600)
701+ require.NoError(t, err)
702+
703+ v := viper.New()
704+ v.SetConfigFile(configFile)
705+ v.Set("attachments", map[string]interface{}{
706+ "backend": []interface{}{"rabbitmq:3"},
707+ })
708+ eventBus := mocks.NewMockEventPublisher(t)
709+ svc := NewService(v, eventBus)
710+ ctx := testContext()
711+
712+ _ = svc.Load(ctx)
713+
714+ err = svc.RemoveAttachment(ctx, "backend", "rabbitmq:3")
715+
716+ assert.NoError(t, err)
717+
718+ config := svc.GetConfig()
719+ _, exists := config.Attachments["backend"]
720+ assert.False(t, exists)
721+ })
348 }722 }
349 723
350 func TestService_GetExternalRoutes(t *testing.T) {724 func TestService_GetExternalRoutes(t *testing.T) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close