detached 7b9923f

Merge pull request #201 from bnema/chore/cli-mocks-diagnostics

test(cli): replace hand-written fakes with mocks
commit
7b9923f2c83a702f25a7d92c5f0a94f482f3848c
author
bnema · · 2026-05-22 04:37 UTC
committer
GitHub · · 2026-05-22 04:37 UTC
merge of
1290c78 + cda2c5a · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v2.31.0-dev18
12 files · +545 -908
M .mockery.yaml +2 -0
@@ -53,3 +53,5 @@ packages:
5353 interfaces:
5454 pushImageOps:
5555 ControlPlane:
56+ imagesClient:
57+ volumesClient:
@@ -53,3 +53,5 @@ packages:
53 interfaces:53 interfaces:
54 pushImageOps:54 pushImageOps:
55 ControlPlane:55 ControlPlane:
56+ imagesClient:
57+ volumesClient:
M internal/adapters/in/cli/attachments_push_test.go +19 -225
@@ -5,207 +5,19 @@ import (
55 "context"
66 "testing"
77
8- "github.com/bnema/gordon/internal/adapters/dto"
98 climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
109 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
11- "github.com/bnema/gordon/internal/domain"
1210 "github.com/stretchr/testify/assert"
1311 "github.com/stretchr/testify/mock"
1412 "github.com/stretchr/testify/require"
1513 )
1614
17-type attachmentPushTestControlPlane struct {
18- findAttachmentTargets func(context.Context, string) ([]string, error)
19- getStatus func(context.Context) (*remote.Status, error)
20-}
21-
22-var _ ControlPlane = (*attachmentPushTestControlPlane)(nil)
23-
24-func (c *attachmentPushTestControlPlane) ListRoutesWithDetails(context.Context) ([]remote.RouteInfo, error) {
25- panic("unexpected call")
26-}
27-
28-func (c *attachmentPushTestControlPlane) GetHealth(context.Context) (map[string]*remote.RouteHealth, error) {
29- panic("unexpected call")
30-}
31-
32-func (c *attachmentPushTestControlPlane) GetRoute(context.Context, string) (*domain.Route, error) {
33- panic("unexpected call")
34-}
35-
36-func (c *attachmentPushTestControlPlane) FindRoutesByImage(context.Context, string) ([]domain.Route, error) {
37- panic("unexpected call")
38-}
39-
40-func (c *attachmentPushTestControlPlane) AddRoute(context.Context, domain.Route) error {
41- panic("unexpected call")
42-}
43-
44-func (c *attachmentPushTestControlPlane) UpdateRoute(context.Context, domain.Route) error {
45- panic("unexpected call")
46-}
47-
48-func (c *attachmentPushTestControlPlane) RemoveRoute(context.Context, string) error {
49- panic("unexpected call")
50-}
51-
52-func (c *attachmentPushTestControlPlane) Bootstrap(context.Context, dto.BootstrapRequest) (*dto.BootstrapResponse, error) {
53- panic("unexpected call")
54-}
55-
56-func (c *attachmentPushTestControlPlane) ListSecretsWithAttachments(context.Context, string) (*remote.SecretsListResult, error) {
57- panic("unexpected call")
58-}
59-
60-func (c *attachmentPushTestControlPlane) SetSecrets(context.Context, string, map[string]string) error {
61- panic("unexpected call")
62-}
63-
64-func (c *attachmentPushTestControlPlane) DeleteSecret(context.Context, string, string) error {
65- panic("unexpected call")
66-}
67-
68-func (c *attachmentPushTestControlPlane) SetAttachmentSecrets(context.Context, string, string, map[string]string) error {
69- panic("unexpected call")
70-}
71-
72-func (c *attachmentPushTestControlPlane) DeleteAttachmentSecret(context.Context, string, string, string) error {
73- panic("unexpected call")
74-}
75-
76-func (c *attachmentPushTestControlPlane) GetAllAttachmentsConfig(context.Context) (map[string][]string, error) {
77- panic("unexpected call")
78-}
79-
80-func (c *attachmentPushTestControlPlane) GetAttachmentsConfig(context.Context, string) ([]string, error) {
81- panic("unexpected call")
82-}
83-
84-func (c *attachmentPushTestControlPlane) ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error) {
85- panic("unexpected call")
86-}
87-
88-func (c *attachmentPushTestControlPlane) CleanupOrphanedAttachments(context.Context, string, bool) (*domain.CleanupReport, error) {
89- panic("unexpected call")
90-}
91-
92-func (c *attachmentPushTestControlPlane) FindAttachmentTargetsByImage(ctx context.Context, imageName string) ([]string, error) {
93- if c.findAttachmentTargets != nil {
94- return c.findAttachmentTargets(ctx, imageName)
95- }
96- return nil, nil
97-}
98-
99-func (c *attachmentPushTestControlPlane) AddAttachment(context.Context, string, string) error {
100- panic("unexpected call")
101-}
102-
103-func (c *attachmentPushTestControlPlane) RemoveAttachment(context.Context, string, string) error {
104- panic("unexpected call")
105-}
106-
107-func (c *attachmentPushTestControlPlane) GetAutoRouteAllowedDomains(context.Context) ([]string, error) {
108- panic("unexpected call")
109-}
110-
111-func (c *attachmentPushTestControlPlane) AddAutoRouteAllowedDomain(context.Context, string) error {
112- panic("unexpected call")
113-}
114-
115-func (c *attachmentPushTestControlPlane) RemoveAutoRouteAllowedDomain(context.Context, string) error {
116- panic("unexpected call")
117-}
118-
119-func (c *attachmentPushTestControlPlane) GetTLSStatus(context.Context) (*dto.TLSStatusResponse, error) {
120- return &dto.TLSStatusResponse{}, nil
121-}
122-
123-func (c *attachmentPushTestControlPlane) GetStatus(ctx context.Context) (*remote.Status, error) {
124- if c.getStatus != nil {
125- return c.getStatus(ctx)
126- }
127- panic("unexpected call")
128-}
129-
130-func (c *attachmentPushTestControlPlane) Reload(context.Context) error {
131- panic("unexpected call")
132-}
133-
134-func (c *attachmentPushTestControlPlane) ListNetworks(context.Context) ([]*domain.NetworkInfo, error) {
135- panic("unexpected call")
136-}
137-
138-func (c *attachmentPushTestControlPlane) GetConfig(context.Context) (*remote.Config, error) {
139- panic("unexpected call")
140-}
141-
142-func (c *attachmentPushTestControlPlane) DeployIntent(context.Context, string) error {
143- panic("unexpected call")
144-}
145-
146-func (c *attachmentPushTestControlPlane) Deploy(context.Context, string) (*remote.DeployResult, error) {
147- panic("unexpected call")
148-}
149-
150-func (c *attachmentPushTestControlPlane) Restart(context.Context, string, bool) (*remote.RestartResult, error) {
151- panic("unexpected call")
152-}
153-
154-func (c *attachmentPushTestControlPlane) ListTags(context.Context, string) ([]string, error) {
155- panic("unexpected call")
156-}
157-
158-func (c *attachmentPushTestControlPlane) ListBackups(context.Context, string) ([]dto.BackupJob, error) {
159- panic("unexpected call")
160-}
161-
162-func (c *attachmentPushTestControlPlane) BackupStatus(context.Context) ([]dto.BackupJob, error) {
163- panic("unexpected call")
164-}
165-
166-func (c *attachmentPushTestControlPlane) RunBackup(context.Context, string, string) (*dto.BackupRunResponse, error) {
167- panic("unexpected call")
168-}
169-
170-func (c *attachmentPushTestControlPlane) DetectDatabases(context.Context, string) ([]dto.DatabaseInfo, error) {
171- panic("unexpected call")
172-}
173-
174-func (c *attachmentPushTestControlPlane) GetProcessLogs(context.Context, int) ([]string, error) {
175- panic("unexpected call")
176-}
177-
178-func (c *attachmentPushTestControlPlane) GetContainerLogs(context.Context, string, int) ([]string, error) {
179- panic("unexpected call")
180-}
181-
182-func (c *attachmentPushTestControlPlane) StreamProcessLogs(context.Context, int) (<-chan string, error) {
183- panic("unexpected call")
184-}
185-
186-func (c *attachmentPushTestControlPlane) StreamContainerLogs(context.Context, string, int) (<-chan string, error) {
187- panic("unexpected call")
188-}
189-
190-func (c *attachmentPushTestControlPlane) ListVolumes(context.Context) ([]dto.Volume, error) {
191- panic("unexpected call")
192-}
193-
194-func (c *attachmentPushTestControlPlane) PruneVolumes(context.Context, dto.VolumePruneRequest) (*dto.VolumePruneResponse, error) {
195- panic("unexpected call")
196-}
197-
19815 func TestResolveAttachmentImage_OneTarget(t *testing.T) {
199- cp := &attachmentPushTestControlPlane{
200- findAttachmentTargets: func(context.Context, string) ([]string, error) {
201- return []string{"app.example.com"}, nil
202- },
203- getStatus: func(context.Context) (*remote.Status, error) {
204- return &remote.Status{RegistryDomain: "registry.example.com"}, nil
205- },
206- }
16+ cpMock := climocks.NewMockControlPlane(t)
17+ cpMock.EXPECT().FindAttachmentTargetsByImage(context.Background(), "postgres").Return([]string{"app.example.com"}, nil).Once()
18+ cpMock.EXPECT().GetStatus(context.Background()).Return(&remote.Status{RegistryDomain: "registry.example.com"}, nil).Once()
20719
208- registry, imageName, targets, err := resolveAttachmentImage(context.Background(), cp, "postgres")
20+ registry, imageName, targets, err := resolveAttachmentImage(context.Background(), cpMock, "postgres")
20921
21022 require.NoError(t, err)
21123 assert.Equal(t, "registry.example.com", registry)
@@ -214,16 +26,11 @@ func TestResolveAttachmentImage_OneTarget(t *testing.T) {
21426 }
21527
21628 func TestResolveAttachmentImage_MultipleTargets(t *testing.T) {
217- cp := &attachmentPushTestControlPlane{
218- findAttachmentTargets: func(context.Context, string) ([]string, error) {
219- return []string{"app.example.com", "backend"}, nil
220- },
221- getStatus: func(context.Context) (*remote.Status, error) {
222- return &remote.Status{RegistryDomain: "registry.example.com"}, nil
223- },
224- }
29+ cpMock := climocks.NewMockControlPlane(t)
30+ cpMock.EXPECT().FindAttachmentTargetsByImage(context.Background(), "redis").Return([]string{"app.example.com", "backend"}, nil).Once()
31+ cpMock.EXPECT().GetStatus(context.Background()).Return(&remote.Status{RegistryDomain: "registry.example.com"}, nil).Once()
22532
226- registry, imageName, targets, err := resolveAttachmentImage(context.Background(), cp, "redis")
33+ registry, imageName, targets, err := resolveAttachmentImage(context.Background(), cpMock, "redis")
22734
22835 require.NoError(t, err)
22936 assert.Equal(t, "registry.example.com", registry)
@@ -232,13 +39,10 @@ func TestResolveAttachmentImage_MultipleTargets(t *testing.T) {
23239 }
23340
23441 func TestResolveAttachmentImage_NotConfigured(t *testing.T) {
235- cp := &attachmentPushTestControlPlane{
236- findAttachmentTargets: func(context.Context, string) ([]string, error) {
237- return nil, nil
238- },
239- }
42+ cpMock := climocks.NewMockControlPlane(t)
43+ cpMock.EXPECT().FindAttachmentTargetsByImage(context.Background(), "postgres").Return(nil, nil).Once()
24044
241- _, _, _, err := resolveAttachmentImage(context.Background(), cp, "postgres")
45+ _, _, _, err := resolveAttachmentImage(context.Background(), cpMock, "postgres")
24246
24347 require.Error(t, err)
24448 assert.Contains(t, err.Error(), "not configured as an attachment")
@@ -254,17 +58,12 @@ func TestAttachmentPushCmd_NoDeploy(t *testing.T) {
25458 newImageOpsFn = origNewImageOps
25559 })
25660
257- cp := &attachmentPushTestControlPlane{
258- findAttachmentTargets: func(context.Context, string) ([]string, error) {
259- return []string{"app.example.com"}, nil
260- },
261- getStatus: func(context.Context) (*remote.Status, error) {
262- return &remote.Status{RegistryDomain: "registry.example.com"}, nil
263- },
264- }
61+ cpMock := climocks.NewMockControlPlane(t)
62+ cpMock.EXPECT().FindAttachmentTargetsByImage(context.Background(), "postgres").Return([]string{"app.example.com"}, nil).Once()
63+ cpMock.EXPECT().GetStatus(context.Background()).Return(&remote.Status{RegistryDomain: "registry.example.com"}, nil).Once()
26564
26665 resolveControlPlaneFn = func(string) (*controlPlaneHandle, error) {
267- return &controlPlaneHandle{plane: cp}, nil
66+ return &controlPlaneHandle{plane: cpMock}, nil
26867 }
26968 determineVersionFn = func(context.Context, string) string { return "v1.2.3" }
27069 pushCalled := false
@@ -301,17 +100,12 @@ func TestAttachmentPushCmd_TaggedImageInputBuildsValidRefs(t *testing.T) {
301100 newImageOpsFn = origNewImageOps
302101 })
303102
304- cp := &attachmentPushTestControlPlane{
305- findAttachmentTargets: func(context.Context, string) ([]string, error) {
306- return []string{"app.example.com"}, nil
307- },
308- getStatus: func(context.Context) (*remote.Status, error) {
309- return &remote.Status{RegistryDomain: "registry.example.com"}, nil
310- },
311- }
103+ cpMock := climocks.NewMockControlPlane(t)
104+ cpMock.EXPECT().FindAttachmentTargetsByImage(context.Background(), "postgres:18").Return([]string{"app.example.com"}, nil).Once()
105+ cpMock.EXPECT().GetStatus(context.Background()).Return(&remote.Status{RegistryDomain: "registry.example.com"}, nil).Once()
312106
313107 resolveControlPlaneFn = func(string) (*controlPlaneHandle, error) {
314- return &controlPlaneHandle{plane: cp}, nil
108+ return &controlPlaneHandle{plane: cpMock}, nil
315109 }
316110 determineVersionFn = func(context.Context, string) string { return "v1.2.3" }
317111
@@ -5,207 +5,19 @@ import (
5 "context"5 "context"
6 "testing"6 "testing"
7 7
8- "github.com/bnema/gordon/internal/adapters/dto"
9 climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"8 climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
10 "github.com/bnema/gordon/internal/adapters/in/cli/remote"9 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
11- "github.com/bnema/gordon/internal/domain"
12 "github.com/stretchr/testify/assert"10 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/mock"11 "github.com/stretchr/testify/mock"
14 "github.com/stretchr/testify/require"12 "github.com/stretchr/testify/require"
15 )13 )
16 14
17-type attachmentPushTestControlPlane struct {
18- findAttachmentTargets func(context.Context, string) ([]string, error)
19- getStatus func(context.Context) (*remote.Status, error)
20-}
21-
22-var _ ControlPlane = (*attachmentPushTestControlPlane)(nil)
23-
24-func (c *attachmentPushTestControlPlane) ListRoutesWithDetails(context.Context) ([]remote.RouteInfo, error) {
25- panic("unexpected call")
26-}
27-
28-func (c *attachmentPushTestControlPlane) GetHealth(context.Context) (map[string]*remote.RouteHealth, error) {
29- panic("unexpected call")
30-}
31-
32-func (c *attachmentPushTestControlPlane) GetRoute(context.Context, string) (*domain.Route, error) {
33- panic("unexpected call")
34-}
35-
36-func (c *attachmentPushTestControlPlane) FindRoutesByImage(context.Context, string) ([]domain.Route, error) {
37- panic("unexpected call")
38-}
39-
40-func (c *attachmentPushTestControlPlane) AddRoute(context.Context, domain.Route) error {
41- panic("unexpected call")
42-}
43-
44-func (c *attachmentPushTestControlPlane) UpdateRoute(context.Context, domain.Route) error {
45- panic("unexpected call")
46-}
47-
48-func (c *attachmentPushTestControlPlane) RemoveRoute(context.Context, string) error {
49- panic("unexpected call")
50-}
51-
52-func (c *attachmentPushTestControlPlane) Bootstrap(context.Context, dto.BootstrapRequest) (*dto.BootstrapResponse, error) {
53- panic("unexpected call")
54-}
55-
56-func (c *attachmentPushTestControlPlane) ListSecretsWithAttachments(context.Context, string) (*remote.SecretsListResult, error) {
57- panic("unexpected call")
58-}
59-
60-func (c *attachmentPushTestControlPlane) SetSecrets(context.Context, string, map[string]string) error {
61- panic("unexpected call")
62-}
63-
64-func (c *attachmentPushTestControlPlane) DeleteSecret(context.Context, string, string) error {
65- panic("unexpected call")
66-}
67-
68-func (c *attachmentPushTestControlPlane) SetAttachmentSecrets(context.Context, string, string, map[string]string) error {
69- panic("unexpected call")
70-}
71-
72-func (c *attachmentPushTestControlPlane) DeleteAttachmentSecret(context.Context, string, string, string) error {
73- panic("unexpected call")
74-}
75-
76-func (c *attachmentPushTestControlPlane) GetAllAttachmentsConfig(context.Context) (map[string][]string, error) {
77- panic("unexpected call")
78-}
79-
80-func (c *attachmentPushTestControlPlane) GetAttachmentsConfig(context.Context, string) ([]string, error) {
81- panic("unexpected call")
82-}
83-
84-func (c *attachmentPushTestControlPlane) ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error) {
85- panic("unexpected call")
86-}
87-
88-func (c *attachmentPushTestControlPlane) CleanupOrphanedAttachments(context.Context, string, bool) (*domain.CleanupReport, error) {
89- panic("unexpected call")
90-}
91-
92-func (c *attachmentPushTestControlPlane) FindAttachmentTargetsByImage(ctx context.Context, imageName string) ([]string, error) {
93- if c.findAttachmentTargets != nil {
94- return c.findAttachmentTargets(ctx, imageName)
95- }
96- return nil, nil
97-}
98-
99-func (c *attachmentPushTestControlPlane) AddAttachment(context.Context, string, string) error {
100- panic("unexpected call")
101-}
102-
103-func (c *attachmentPushTestControlPlane) RemoveAttachment(context.Context, string, string) error {
104- panic("unexpected call")
105-}
106-
107-func (c *attachmentPushTestControlPlane) GetAutoRouteAllowedDomains(context.Context) ([]string, error) {
108- panic("unexpected call")
109-}
110-
111-func (c *attachmentPushTestControlPlane) AddAutoRouteAllowedDomain(context.Context, string) error {
112- panic("unexpected call")
113-}
114-
115-func (c *attachmentPushTestControlPlane) RemoveAutoRouteAllowedDomain(context.Context, string) error {
116- panic("unexpected call")
117-}
118-
119-func (c *attachmentPushTestControlPlane) GetTLSStatus(context.Context) (*dto.TLSStatusResponse, error) {
120- return &dto.TLSStatusResponse{}, nil
121-}
122-
123-func (c *attachmentPushTestControlPlane) GetStatus(ctx context.Context) (*remote.Status, error) {
124- if c.getStatus != nil {
125- return c.getStatus(ctx)
126- }
127- panic("unexpected call")
128-}
129-
130-func (c *attachmentPushTestControlPlane) Reload(context.Context) error {
131- panic("unexpected call")
132-}
133-
134-func (c *attachmentPushTestControlPlane) ListNetworks(context.Context) ([]*domain.NetworkInfo, error) {
135- panic("unexpected call")
136-}
137-
138-func (c *attachmentPushTestControlPlane) GetConfig(context.Context) (*remote.Config, error) {
139- panic("unexpected call")
140-}
141-
142-func (c *attachmentPushTestControlPlane) DeployIntent(context.Context, string) error {
143- panic("unexpected call")
144-}
145-
146-func (c *attachmentPushTestControlPlane) Deploy(context.Context, string) (*remote.DeployResult, error) {
147- panic("unexpected call")
148-}
149-
150-func (c *attachmentPushTestControlPlane) Restart(context.Context, string, bool) (*remote.RestartResult, error) {
151- panic("unexpected call")
152-}
153-
154-func (c *attachmentPushTestControlPlane) ListTags(context.Context, string) ([]string, error) {
155- panic("unexpected call")
156-}
157-
158-func (c *attachmentPushTestControlPlane) ListBackups(context.Context, string) ([]dto.BackupJob, error) {
159- panic("unexpected call")
160-}
161-
162-func (c *attachmentPushTestControlPlane) BackupStatus(context.Context) ([]dto.BackupJob, error) {
163- panic("unexpected call")
164-}
165-
166-func (c *attachmentPushTestControlPlane) RunBackup(context.Context, string, string) (*dto.BackupRunResponse, error) {
167- panic("unexpected call")
168-}
169-
170-func (c *attachmentPushTestControlPlane) DetectDatabases(context.Context, string) ([]dto.DatabaseInfo, error) {
171- panic("unexpected call")
172-}
173-
174-func (c *attachmentPushTestControlPlane) GetProcessLogs(context.Context, int) ([]string, error) {
175- panic("unexpected call")
176-}
177-
178-func (c *attachmentPushTestControlPlane) GetContainerLogs(context.Context, string, int) ([]string, error) {
179- panic("unexpected call")
180-}
181-
182-func (c *attachmentPushTestControlPlane) StreamProcessLogs(context.Context, int) (<-chan string, error) {
183- panic("unexpected call")
184-}
185-
186-func (c *attachmentPushTestControlPlane) StreamContainerLogs(context.Context, string, int) (<-chan string, error) {
187- panic("unexpected call")
188-}
189-
190-func (c *attachmentPushTestControlPlane) ListVolumes(context.Context) ([]dto.Volume, error) {
191- panic("unexpected call")
192-}
193-
194-func (c *attachmentPushTestControlPlane) PruneVolumes(context.Context, dto.VolumePruneRequest) (*dto.VolumePruneResponse, error) {
195- panic("unexpected call")
196-}
197-
198 func TestResolveAttachmentImage_OneTarget(t *testing.T) {15 func TestResolveAttachmentImage_OneTarget(t *testing.T) {
199- cp := &attachmentPushTestControlPlane{16+ cpMock := climocks.NewMockControlPlane(t)
200- findAttachmentTargets: func(context.Context, string) ([]string, error) {17+ cpMock.EXPECT().FindAttachmentTargetsByImage(context.Background(), "postgres").Return([]string{"app.example.com"}, nil).Once()
201- return []string{"app.example.com"}, nil18+ cpMock.EXPECT().GetStatus(context.Background()).Return(&remote.Status{RegistryDomain: "registry.example.com"}, nil).Once()
202- },
203- getStatus: func(context.Context) (*remote.Status, error) {
204- return &remote.Status{RegistryDomain: "registry.example.com"}, nil
205- },
206- }
207 19
208- registry, imageName, targets, err := resolveAttachmentImage(context.Background(), cp, "postgres")20+ registry, imageName, targets, err := resolveAttachmentImage(context.Background(), cpMock, "postgres")
209 21
210 require.NoError(t, err)22 require.NoError(t, err)
211 assert.Equal(t, "registry.example.com", registry)23 assert.Equal(t, "registry.example.com", registry)
@@ -214,16 +26,11 @@ func TestResolveAttachmentImage_OneTarget(t *testing.T) {
214 }26 }
215 27
216 func TestResolveAttachmentImage_MultipleTargets(t *testing.T) {28 func TestResolveAttachmentImage_MultipleTargets(t *testing.T) {
217- cp := &attachmentPushTestControlPlane{29+ cpMock := climocks.NewMockControlPlane(t)
218- findAttachmentTargets: func(context.Context, string) ([]string, error) {30+ cpMock.EXPECT().FindAttachmentTargetsByImage(context.Background(), "redis").Return([]string{"app.example.com", "backend"}, nil).Once()
219- return []string{"app.example.com", "backend"}, nil31+ cpMock.EXPECT().GetStatus(context.Background()).Return(&remote.Status{RegistryDomain: "registry.example.com"}, nil).Once()
220- },
221- getStatus: func(context.Context) (*remote.Status, error) {
222- return &remote.Status{RegistryDomain: "registry.example.com"}, nil
223- },
224- }
225 32
226- registry, imageName, targets, err := resolveAttachmentImage(context.Background(), cp, "redis")33+ registry, imageName, targets, err := resolveAttachmentImage(context.Background(), cpMock, "redis")
227 34
228 require.NoError(t, err)35 require.NoError(t, err)
229 assert.Equal(t, "registry.example.com", registry)36 assert.Equal(t, "registry.example.com", registry)
@@ -232,13 +39,10 @@ func TestResolveAttachmentImage_MultipleTargets(t *testing.T) {
232 }39 }
233 40
234 func TestResolveAttachmentImage_NotConfigured(t *testing.T) {41 func TestResolveAttachmentImage_NotConfigured(t *testing.T) {
235- cp := &attachmentPushTestControlPlane{42+ cpMock := climocks.NewMockControlPlane(t)
236- findAttachmentTargets: func(context.Context, string) ([]string, error) {43+ cpMock.EXPECT().FindAttachmentTargetsByImage(context.Background(), "postgres").Return(nil, nil).Once()
237- return nil, nil
238- },
239- }
240 44
241- _, _, _, err := resolveAttachmentImage(context.Background(), cp, "postgres")45+ _, _, _, err := resolveAttachmentImage(context.Background(), cpMock, "postgres")
242 46
243 require.Error(t, err)47 require.Error(t, err)
244 assert.Contains(t, err.Error(), "not configured as an attachment")48 assert.Contains(t, err.Error(), "not configured as an attachment")
@@ -254,17 +58,12 @@ func TestAttachmentPushCmd_NoDeploy(t *testing.T) {
254 newImageOpsFn = origNewImageOps58 newImageOpsFn = origNewImageOps
255 })59 })
256 60
257- cp := &attachmentPushTestControlPlane{61+ cpMock := climocks.NewMockControlPlane(t)
258- findAttachmentTargets: func(context.Context, string) ([]string, error) {62+ cpMock.EXPECT().FindAttachmentTargetsByImage(context.Background(), "postgres").Return([]string{"app.example.com"}, nil).Once()
259- return []string{"app.example.com"}, nil63+ cpMock.EXPECT().GetStatus(context.Background()).Return(&remote.Status{RegistryDomain: "registry.example.com"}, nil).Once()
260- },
261- getStatus: func(context.Context) (*remote.Status, error) {
262- return &remote.Status{RegistryDomain: "registry.example.com"}, nil
263- },
264- }
265 64
266 resolveControlPlaneFn = func(string) (*controlPlaneHandle, error) {65 resolveControlPlaneFn = func(string) (*controlPlaneHandle, error) {
267- return &controlPlaneHandle{plane: cp}, nil66+ return &controlPlaneHandle{plane: cpMock}, nil
268 }67 }
269 determineVersionFn = func(context.Context, string) string { return "v1.2.3" }68 determineVersionFn = func(context.Context, string) string { return "v1.2.3" }
270 pushCalled := false69 pushCalled := false
@@ -301,17 +100,12 @@ func TestAttachmentPushCmd_TaggedImageInputBuildsValidRefs(t *testing.T) {
301 newImageOpsFn = origNewImageOps100 newImageOpsFn = origNewImageOps
302 })101 })
303 102
304- cp := &attachmentPushTestControlPlane{103+ cpMock := climocks.NewMockControlPlane(t)
305- findAttachmentTargets: func(context.Context, string) ([]string, error) {104+ cpMock.EXPECT().FindAttachmentTargetsByImage(context.Background(), "postgres:18").Return([]string{"app.example.com"}, nil).Once()
306- return []string{"app.example.com"}, nil105+ cpMock.EXPECT().GetStatus(context.Background()).Return(&remote.Status{RegistryDomain: "registry.example.com"}, nil).Once()
307- },
308- getStatus: func(context.Context) (*remote.Status, error) {
309- return &remote.Status{RegistryDomain: "registry.example.com"}, nil
310- },
311- }
312 106
313 resolveControlPlaneFn = func(string) (*controlPlaneHandle, error) {107 resolveControlPlaneFn = func(string) (*controlPlaneHandle, error) {
314- return &controlPlaneHandle{plane: cp}, nil108+ return &controlPlaneHandle{plane: cpMock}, nil
315 }109 }
316 determineVersionFn = func(context.Context, string) string { return "v1.2.3" }110 determineVersionFn = func(context.Context, string) string { return "v1.2.3" }
317 111
M internal/adapters/in/cli/images_test.go +27 -27
@@ -16,7 +16,7 @@ import (
1616 "github.com/bnema/gordon/internal/domain"
1717 )
1818
19-type fakeImagesClient struct {
19+type imagesClientMock struct {
2020 listImagesResp []dto.Image
2121 listImagesErr error
2222 pruneResp *dto.ImagePruneResponse
@@ -27,7 +27,7 @@ type fakeImagesClient struct {
2727 lastPruneOpts dto.ImagePruneRequest
2828 }
2929
30-func (f *fakeImagesClient) ListImages(_ context.Context) ([]dto.Image, error) {
30+func (f *imagesClientMock) ListImages(_ context.Context) ([]dto.Image, error) {
3131 f.listImagesCalls++
3232 if f.listImagesErr != nil {
3333 return nil, f.listImagesErr
@@ -35,7 +35,7 @@ func (f *fakeImagesClient) ListImages(_ context.Context) ([]dto.Image, error) {
3535 return f.listImagesResp, nil
3636 }
3737
38-func (f *fakeImagesClient) PruneImages(_ context.Context, req dto.ImagePruneRequest) (*dto.ImagePruneResponse, error) {
38+func (f *imagesClientMock) PruneImages(_ context.Context, req dto.ImagePruneRequest) (*dto.ImagePruneResponse, error) {
3939 f.pruneCalls++
4040 f.lastPruneOpts = req
4141 if f.pruneErr != nil {
@@ -50,7 +50,7 @@ func (f *fakeImagesClient) PruneImages(_ context.Context, req dto.ImagePruneRequ
5050
5151 func TestRunImagesList_PrintsRowsAndSummary(t *testing.T) {
5252 createdAt := time.Date(2026, 2, 8, 12, 0, 0, 0, time.UTC)
53- client := &fakeImagesClient{
53+ client := &imagesClientMock{
5454 listImagesResp: []dto.Image{
5555 {Repository: "registry.example.com/app", Tag: "latest", Size: 12_000_000, Created: createdAt, ID: "sha256:1111", Dangling: false},
5656 {Repository: "<none>", Tag: "<none>", Size: 512_000, Created: createdAt, ID: "sha256:2222", Dangling: true},
@@ -71,7 +71,7 @@ func TestRunImagesList_PrintsRowsAndSummary(t *testing.T) {
7171
7272 func TestRunImagesList_HeaderOrderAndSummaryLine(t *testing.T) {
7373 createdAt := time.Date(2026, 2, 8, 12, 0, 0, 0, time.UTC)
74- client := &fakeImagesClient{
74+ client := &imagesClientMock{
7575 listImagesResp: []dto.Image{
7676 {Repository: "registry.example.com/app", Tag: "latest", Size: 12_000_000, Created: createdAt, ID: "sha256:1111", Dangling: false},
7777 },
@@ -96,7 +96,7 @@ func TestRunImagesList_TruncatesLongValuesAndKeepsBoundedRowShape(t *testing.T)
9696 createdAt := time.Date(2026, 2, 8, 12, 0, 0, 0, time.UTC)
9797 longRepository := "registry.example.com/team/this-is-a-very-long-service-name-with-extra-segments/and/more/segments/than/anyone/should/reasonably/use"
9898 longImageID := "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef-extra-suffix-for-overflow"
99- client := &fakeImagesClient{
99+ client := &imagesClientMock{
100100 listImagesResp: []dto.Image{
101101 {Repository: longRepository, Tag: "latest", Size: 12_000_000, Created: createdAt, ID: longImageID, Dangling: false},
102102 },
@@ -138,7 +138,7 @@ func TestRunImagesList_LongDigestLikeValuesAndEmptyFormattedFields(t *testing.T)
138138 longRepository := "registry.example.com/very/long/repository/path/with/deep/nesting/and-digest-like-ref@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef-extra"
139139 longTag := "release-2026-02-12-build-metadata-with-a-very-very-long-tag-name"
140140 longImageID := "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
141- client := &fakeImagesClient{
141+ client := &imagesClientMock{
142142 listImagesResp: []dto.Image{
143143 {Repository: longRepository, Tag: longTag, Size: 0, Created: time.Time{}, ID: longImageID, Dangling: true},
144144 },
@@ -174,7 +174,7 @@ func TestRunImagesList_LongDigestLikeValuesAndEmptyFormattedFields(t *testing.T)
174174 }
175175
176176 func TestRunImagesList_EmptyOutput(t *testing.T) {
177- client := &fakeImagesClient{listImagesResp: []dto.Image{}}
177+ client := &imagesClientMock{listImagesResp: []dto.Image{}}
178178
179179 var out bytes.Buffer
180180 err := runImagesList(context.Background(), client, &out, false)
@@ -192,7 +192,7 @@ func TestRunImagesList_EmptyOutput(t *testing.T) {
192192
193193 func TestRunImagesPrune_DefaultScopeIsBothDanglingAndRegistry(t *testing.T) {
194194 // No scope flags → both scopes enabled, keep-releases defaults to domain default.
195- client := &fakeImagesClient{
195+ client := &imagesClientMock{
196196 pruneResp: &dto.ImagePruneResponse{
197197 Runtime: dto.RuntimePruneResult{DeletedCount: 1, SpaceReclaimed: 512},
198198 Registry: dto.RegistryPruneResult{TagsRemoved: 2, BlobsRemoved: 1, SpaceReclaimed: 1024},
@@ -218,7 +218,7 @@ func TestRunImagesPrune_DefaultScopeIsBothDanglingAndRegistry(t *testing.T) {
218218 }
219219
220220 func TestRunImagesPrune_DanglingOnlyScopeSkipsRegistry(t *testing.T) {
221- client := &fakeImagesClient{
221+ client := &imagesClientMock{
222222 pruneResp: &dto.ImagePruneResponse{
223223 Runtime: dto.RuntimePruneResult{DeletedCount: 3, SpaceReclaimed: 2048},
224224 },
@@ -241,7 +241,7 @@ func TestRunImagesPrune_DanglingOnlyScopeSkipsRegistry(t *testing.T) {
241241 }
242242
243243 func TestRunImagesPrune_RegistryOnlyScopeSkipsRuntime(t *testing.T) {
244- client := &fakeImagesClient{
244+ client := &imagesClientMock{
245245 pruneResp: &dto.ImagePruneResponse{
246246 Registry: dto.RegistryPruneResult{TagsRemoved: 4, BlobsRemoved: 2, SpaceReclaimed: 4096},
247247 },
@@ -266,7 +266,7 @@ func TestRunImagesPrune_RegistryOnlyScopeSkipsRuntime(t *testing.T) {
266266 }
267267
268268 func TestRunImagesPrune_BothScopeFlagsExplicit(t *testing.T) {
269- client := &fakeImagesClient{
269+ client := &imagesClientMock{
270270 pruneResp: &dto.ImagePruneResponse{
271271 Runtime: dto.RuntimePruneResult{DeletedCount: 1, SpaceReclaimed: 100},
272272 Registry: dto.RegistryPruneResult{TagsRemoved: 1, BlobsRemoved: 0, SpaceReclaimed: 200},
@@ -296,7 +296,7 @@ func TestRunImagesPrune_BothScopeFlagsExplicit(t *testing.T) {
296296 // ---------------------------------------------------------------------------
297297
298298 func TestRunImagesPrune_DryRunBothScopes(t *testing.T) {
299- client := &fakeImagesClient{
299+ client := &imagesClientMock{
300300 listImagesResp: []dto.Image{{Dangling: true}, {Dangling: false}, {Dangling: true}},
301301 }
302302
@@ -314,7 +314,7 @@ func TestRunImagesPrune_DryRunBothScopes(t *testing.T) {
314314 }
315315
316316 func TestRunImagesPrune_DryRunDanglingOnlyScope(t *testing.T) {
317- client := &fakeImagesClient{
317+ client := &imagesClientMock{
318318 listImagesResp: []dto.Image{{Dangling: true}},
319319 }
320320
@@ -330,7 +330,7 @@ func TestRunImagesPrune_DryRunDanglingOnlyScope(t *testing.T) {
330330 }
331331
332332 func TestRunImagesPrune_DryRunRegistryOnlyScope(t *testing.T) {
333- client := &fakeImagesClient{
333+ client := &imagesClientMock{
334334 listImagesResp: []dto.Image{{Dangling: true}, {Dangling: false}},
335335 }
336336
@@ -350,7 +350,7 @@ func TestRunImagesPrune_DryRunRegistryOnlyScope(t *testing.T) {
350350 // ---------------------------------------------------------------------------
351351
352352 func TestRunImagesPrune_KeepReleasesControlsRetention(t *testing.T) {
353- client := &fakeImagesClient{
353+ client := &imagesClientMock{
354354 pruneResp: &dto.ImagePruneResponse{
355355 Runtime: dto.RuntimePruneResult{DeletedCount: 0},
356356 Registry: dto.RegistryPruneResult{TagsRemoved: 7, BlobsRemoved: 3, SpaceReclaimed: 9000},
@@ -367,7 +367,7 @@ func TestRunImagesPrune_KeepReleasesControlsRetention(t *testing.T) {
367367 }
368368
369369 func TestRunImagesPrune_RejectsNegativeKeepReleases(t *testing.T) {
370- err := runImagesPrune(context.Background(), &fakeImagesClient{}, imagesPruneOptions{KeepReleases: -1}, &bytes.Buffer{})
370+ err := runImagesPrune(context.Background(), &imagesClientMock{}, imagesPruneOptions{KeepReleases: -1}, &bytes.Buffer{})
371371 require.Error(t, err)
372372 assert.Contains(t, err.Error(), "--keep-releases must be >= 0")
373373 }
@@ -385,7 +385,7 @@ func TestRunImagesPrune_NonDryRunCallsConfirm(t *testing.T) {
385385 }
386386 t.Cleanup(func() { pruneConfirmFunc = origConfirm })
387387
388- client := &fakeImagesClient{
388+ client := &imagesClientMock{
389389 pruneResp: &dto.ImagePruneResponse{
390390 Runtime: dto.RuntimePruneResult{DeletedCount: 1},
391391 Registry: dto.RegistryPruneResult{TagsRemoved: 1},
@@ -411,7 +411,7 @@ func TestRunImagesPrune_ConfirmPromptIncludesScopePreviewCounts(t *testing.T) {
411411 }
412412 t.Cleanup(func() { pruneConfirmFunc = origConfirm })
413413
414- client := &fakeImagesClient{
414+ client := &imagesClientMock{
415415 listImagesResp: []dto.Image{
416416 {Repository: "registry.example.com/app", Tag: "latest", Created: created},
417417 {Repository: "registry.example.com/app", Tag: "v3", Created: created.Add(-1 * time.Hour)},
@@ -439,7 +439,7 @@ func TestRunImagesPrune_ConfirmRejectCancelsOperation(t *testing.T) {
439439 }
440440 t.Cleanup(func() { pruneConfirmFunc = origConfirm })
441441
442- client := &fakeImagesClient{pruneResp: &dto.ImagePruneResponse{}}
442+ client := &imagesClientMock{pruneResp: &dto.ImagePruneResponse{}}
443443
444444 var out bytes.Buffer
445445 opts := imagesPruneOptions{KeepReleases: domain.DefaultImagePruneKeepLast}
@@ -459,7 +459,7 @@ func TestRunImagesPrune_NoConfirmSkipsPrompt(t *testing.T) {
459459 }
460460 t.Cleanup(func() { pruneConfirmFunc = origConfirm })
461461
462- client := &fakeImagesClient{
462+ client := &imagesClientMock{
463463 pruneResp: &dto.ImagePruneResponse{
464464 Runtime: dto.RuntimePruneResult{DeletedCount: 1},
465465 },
@@ -483,7 +483,7 @@ func TestRunImagesPrune_DryRunNeverPrompts(t *testing.T) {
483483 }
484484 t.Cleanup(func() { pruneConfirmFunc = origConfirm })
485485
486- client := &fakeImagesClient{
486+ client := &imagesClientMock{
487487 listImagesResp: []dto.Image{{Dangling: true}},
488488 }
489489
@@ -504,7 +504,7 @@ func TestRunImagesPrune_ReturnsRemoteErrors(t *testing.T) {
504504 pruneConfirmFunc = func(_ string) (bool, error) { return true, nil }
505505 t.Cleanup(func() { pruneConfirmFunc = origConfirm })
506506
507- client := &fakeImagesClient{pruneErr: errors.New("request failed")}
507+ client := &imagesClientMock{pruneErr: errors.New("request failed")}
508508
509509 err := runImagesPrune(context.Background(), client, imagesPruneOptions{KeepReleases: 2}, &bytes.Buffer{})
510510 require.Error(t, err)
@@ -516,7 +516,7 @@ func TestRunImagesPrune_RejectsEmptyResponse(t *testing.T) {
516516 pruneConfirmFunc = func(_ string) (bool, error) { return true, nil }
517517 t.Cleanup(func() { pruneConfirmFunc = origConfirm })
518518
519- client := &fakeImagesClient{pruneResp: nil}
519+ client := &imagesClientMock{pruneResp: nil}
520520 err := runImagesPrune(context.Background(), client, imagesPruneOptions{KeepReleases: 1}, &bytes.Buffer{})
521521 require.Error(t, err)
522522 assert.Contains(t, err.Error(), "empty response")
@@ -532,7 +532,7 @@ func stripANSI(value string) string {
532532 }
533533
534534 func findHeaderLine(text string) string {
535- for _, line := range strings.Split(text, "\n") {
535+ for line := range strings.SplitSeq(text, "\n") {
536536 if strings.Contains(line, "REPOSITORY") && strings.Contains(line, "DANGLING") {
537537 return line
538538 }
@@ -542,7 +542,7 @@ func findHeaderLine(text string) string {
542542 }
543543
544544 func findImageListRowLine(text string, tag string) string {
545- for _, line := range strings.Split(text, "\n") {
545+ for line := range strings.SplitSeq(text, "\n") {
546546 trimmed := strings.TrimSpace(line)
547547 if trimmed == "" {
548548 continue
@@ -562,7 +562,7 @@ func findImageListRowLine(text string, tag string) string {
562562
563563 func findImageListRows(text string) [][]string {
564564 rows := make([][]string, 0)
565- for _, line := range strings.Split(text, "\n") {
565+ for line := range strings.SplitSeq(text, "\n") {
566566 trimmed := strings.TrimSpace(line)
567567 if trimmed == "" {
568568 continue
@@ -16,7 +16,7 @@ import (
16 "github.com/bnema/gordon/internal/domain"16 "github.com/bnema/gordon/internal/domain"
17 )17 )
18 18
19-type fakeImagesClient struct {19+type imagesClientMock struct {
20 listImagesResp []dto.Image20 listImagesResp []dto.Image
21 listImagesErr error21 listImagesErr error
22 pruneResp *dto.ImagePruneResponse22 pruneResp *dto.ImagePruneResponse
@@ -27,7 +27,7 @@ type fakeImagesClient struct {
27 lastPruneOpts dto.ImagePruneRequest27 lastPruneOpts dto.ImagePruneRequest
28 }28 }
29 29
30-func (f *fakeImagesClient) ListImages(_ context.Context) ([]dto.Image, error) {30+func (f *imagesClientMock) ListImages(_ context.Context) ([]dto.Image, error) {
31 f.listImagesCalls++31 f.listImagesCalls++
32 if f.listImagesErr != nil {32 if f.listImagesErr != nil {
33 return nil, f.listImagesErr33 return nil, f.listImagesErr
@@ -35,7 +35,7 @@ func (f *fakeImagesClient) ListImages(_ context.Context) ([]dto.Image, error) {
35 return f.listImagesResp, nil35 return f.listImagesResp, nil
36 }36 }
37 37
38-func (f *fakeImagesClient) PruneImages(_ context.Context, req dto.ImagePruneRequest) (*dto.ImagePruneResponse, error) {38+func (f *imagesClientMock) PruneImages(_ context.Context, req dto.ImagePruneRequest) (*dto.ImagePruneResponse, error) {
39 f.pruneCalls++39 f.pruneCalls++
40 f.lastPruneOpts = req40 f.lastPruneOpts = req
41 if f.pruneErr != nil {41 if f.pruneErr != nil {
@@ -50,7 +50,7 @@ func (f *fakeImagesClient) PruneImages(_ context.Context, req dto.ImagePruneRequ
50 50
51 func TestRunImagesList_PrintsRowsAndSummary(t *testing.T) {51 func TestRunImagesList_PrintsRowsAndSummary(t *testing.T) {
52 createdAt := time.Date(2026, 2, 8, 12, 0, 0, 0, time.UTC)52 createdAt := time.Date(2026, 2, 8, 12, 0, 0, 0, time.UTC)
53- client := &fakeImagesClient{53+ client := &imagesClientMock{
54 listImagesResp: []dto.Image{54 listImagesResp: []dto.Image{
55 {Repository: "registry.example.com/app", Tag: "latest", Size: 12_000_000, Created: createdAt, ID: "sha256:1111", Dangling: false},55 {Repository: "registry.example.com/app", Tag: "latest", Size: 12_000_000, Created: createdAt, ID: "sha256:1111", Dangling: false},
56 {Repository: "<none>", Tag: "<none>", Size: 512_000, Created: createdAt, ID: "sha256:2222", Dangling: true},56 {Repository: "<none>", Tag: "<none>", Size: 512_000, Created: createdAt, ID: "sha256:2222", Dangling: true},
@@ -71,7 +71,7 @@ func TestRunImagesList_PrintsRowsAndSummary(t *testing.T) {
71 71
72 func TestRunImagesList_HeaderOrderAndSummaryLine(t *testing.T) {72 func TestRunImagesList_HeaderOrderAndSummaryLine(t *testing.T) {
73 createdAt := time.Date(2026, 2, 8, 12, 0, 0, 0, time.UTC)73 createdAt := time.Date(2026, 2, 8, 12, 0, 0, 0, time.UTC)
74- client := &fakeImagesClient{74+ client := &imagesClientMock{
75 listImagesResp: []dto.Image{75 listImagesResp: []dto.Image{
76 {Repository: "registry.example.com/app", Tag: "latest", Size: 12_000_000, Created: createdAt, ID: "sha256:1111", Dangling: false},76 {Repository: "registry.example.com/app", Tag: "latest", Size: 12_000_000, Created: createdAt, ID: "sha256:1111", Dangling: false},
77 },77 },
@@ -96,7 +96,7 @@ func TestRunImagesList_TruncatesLongValuesAndKeepsBoundedRowShape(t *testing.T)
96 createdAt := time.Date(2026, 2, 8, 12, 0, 0, 0, time.UTC)96 createdAt := time.Date(2026, 2, 8, 12, 0, 0, 0, time.UTC)
97 longRepository := "registry.example.com/team/this-is-a-very-long-service-name-with-extra-segments/and/more/segments/than/anyone/should/reasonably/use"97 longRepository := "registry.example.com/team/this-is-a-very-long-service-name-with-extra-segments/and/more/segments/than/anyone/should/reasonably/use"
98 longImageID := "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef-extra-suffix-for-overflow"98 longImageID := "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef-extra-suffix-for-overflow"
99- client := &fakeImagesClient{99+ client := &imagesClientMock{
100 listImagesResp: []dto.Image{100 listImagesResp: []dto.Image{
101 {Repository: longRepository, Tag: "latest", Size: 12_000_000, Created: createdAt, ID: longImageID, Dangling: false},101 {Repository: longRepository, Tag: "latest", Size: 12_000_000, Created: createdAt, ID: longImageID, Dangling: false},
102 },102 },
@@ -138,7 +138,7 @@ func TestRunImagesList_LongDigestLikeValuesAndEmptyFormattedFields(t *testing.T)
138 longRepository := "registry.example.com/very/long/repository/path/with/deep/nesting/and-digest-like-ref@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef-extra"138 longRepository := "registry.example.com/very/long/repository/path/with/deep/nesting/and-digest-like-ref@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef-extra"
139 longTag := "release-2026-02-12-build-metadata-with-a-very-very-long-tag-name"139 longTag := "release-2026-02-12-build-metadata-with-a-very-very-long-tag-name"
140 longImageID := "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"140 longImageID := "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
141- client := &fakeImagesClient{141+ client := &imagesClientMock{
142 listImagesResp: []dto.Image{142 listImagesResp: []dto.Image{
143 {Repository: longRepository, Tag: longTag, Size: 0, Created: time.Time{}, ID: longImageID, Dangling: true},143 {Repository: longRepository, Tag: longTag, Size: 0, Created: time.Time{}, ID: longImageID, Dangling: true},
144 },144 },
@@ -174,7 +174,7 @@ func TestRunImagesList_LongDigestLikeValuesAndEmptyFormattedFields(t *testing.T)
174 }174 }
175 175
176 func TestRunImagesList_EmptyOutput(t *testing.T) {176 func TestRunImagesList_EmptyOutput(t *testing.T) {
177- client := &fakeImagesClient{listImagesResp: []dto.Image{}}177+ client := &imagesClientMock{listImagesResp: []dto.Image{}}
178 178
179 var out bytes.Buffer179 var out bytes.Buffer
180 err := runImagesList(context.Background(), client, &out, false)180 err := runImagesList(context.Background(), client, &out, false)
@@ -192,7 +192,7 @@ func TestRunImagesList_EmptyOutput(t *testing.T) {
192 192
193 func TestRunImagesPrune_DefaultScopeIsBothDanglingAndRegistry(t *testing.T) {193 func TestRunImagesPrune_DefaultScopeIsBothDanglingAndRegistry(t *testing.T) {
194 // No scope flags → both scopes enabled, keep-releases defaults to domain default.194 // No scope flags → both scopes enabled, keep-releases defaults to domain default.
195- client := &fakeImagesClient{195+ client := &imagesClientMock{
196 pruneResp: &dto.ImagePruneResponse{196 pruneResp: &dto.ImagePruneResponse{
197 Runtime: dto.RuntimePruneResult{DeletedCount: 1, SpaceReclaimed: 512},197 Runtime: dto.RuntimePruneResult{DeletedCount: 1, SpaceReclaimed: 512},
198 Registry: dto.RegistryPruneResult{TagsRemoved: 2, BlobsRemoved: 1, SpaceReclaimed: 1024},198 Registry: dto.RegistryPruneResult{TagsRemoved: 2, BlobsRemoved: 1, SpaceReclaimed: 1024},
@@ -218,7 +218,7 @@ func TestRunImagesPrune_DefaultScopeIsBothDanglingAndRegistry(t *testing.T) {
218 }218 }
219 219
220 func TestRunImagesPrune_DanglingOnlyScopeSkipsRegistry(t *testing.T) {220 func TestRunImagesPrune_DanglingOnlyScopeSkipsRegistry(t *testing.T) {
221- client := &fakeImagesClient{221+ client := &imagesClientMock{
222 pruneResp: &dto.ImagePruneResponse{222 pruneResp: &dto.ImagePruneResponse{
223 Runtime: dto.RuntimePruneResult{DeletedCount: 3, SpaceReclaimed: 2048},223 Runtime: dto.RuntimePruneResult{DeletedCount: 3, SpaceReclaimed: 2048},
224 },224 },
@@ -241,7 +241,7 @@ func TestRunImagesPrune_DanglingOnlyScopeSkipsRegistry(t *testing.T) {
241 }241 }
242 242
243 func TestRunImagesPrune_RegistryOnlyScopeSkipsRuntime(t *testing.T) {243 func TestRunImagesPrune_RegistryOnlyScopeSkipsRuntime(t *testing.T) {
244- client := &fakeImagesClient{244+ client := &imagesClientMock{
245 pruneResp: &dto.ImagePruneResponse{245 pruneResp: &dto.ImagePruneResponse{
246 Registry: dto.RegistryPruneResult{TagsRemoved: 4, BlobsRemoved: 2, SpaceReclaimed: 4096},246 Registry: dto.RegistryPruneResult{TagsRemoved: 4, BlobsRemoved: 2, SpaceReclaimed: 4096},
247 },247 },
@@ -266,7 +266,7 @@ func TestRunImagesPrune_RegistryOnlyScopeSkipsRuntime(t *testing.T) {
266 }266 }
267 267
268 func TestRunImagesPrune_BothScopeFlagsExplicit(t *testing.T) {268 func TestRunImagesPrune_BothScopeFlagsExplicit(t *testing.T) {
269- client := &fakeImagesClient{269+ client := &imagesClientMock{
270 pruneResp: &dto.ImagePruneResponse{270 pruneResp: &dto.ImagePruneResponse{
271 Runtime: dto.RuntimePruneResult{DeletedCount: 1, SpaceReclaimed: 100},271 Runtime: dto.RuntimePruneResult{DeletedCount: 1, SpaceReclaimed: 100},
272 Registry: dto.RegistryPruneResult{TagsRemoved: 1, BlobsRemoved: 0, SpaceReclaimed: 200},272 Registry: dto.RegistryPruneResult{TagsRemoved: 1, BlobsRemoved: 0, SpaceReclaimed: 200},
@@ -296,7 +296,7 @@ func TestRunImagesPrune_BothScopeFlagsExplicit(t *testing.T) {
296 // ---------------------------------------------------------------------------296 // ---------------------------------------------------------------------------
297 297
298 func TestRunImagesPrune_DryRunBothScopes(t *testing.T) {298 func TestRunImagesPrune_DryRunBothScopes(t *testing.T) {
299- client := &fakeImagesClient{299+ client := &imagesClientMock{
300 listImagesResp: []dto.Image{{Dangling: true}, {Dangling: false}, {Dangling: true}},300 listImagesResp: []dto.Image{{Dangling: true}, {Dangling: false}, {Dangling: true}},
301 }301 }
302 302
@@ -314,7 +314,7 @@ func TestRunImagesPrune_DryRunBothScopes(t *testing.T) {
314 }314 }
315 315
316 func TestRunImagesPrune_DryRunDanglingOnlyScope(t *testing.T) {316 func TestRunImagesPrune_DryRunDanglingOnlyScope(t *testing.T) {
317- client := &fakeImagesClient{317+ client := &imagesClientMock{
318 listImagesResp: []dto.Image{{Dangling: true}},318 listImagesResp: []dto.Image{{Dangling: true}},
319 }319 }
320 320
@@ -330,7 +330,7 @@ func TestRunImagesPrune_DryRunDanglingOnlyScope(t *testing.T) {
330 }330 }
331 331
332 func TestRunImagesPrune_DryRunRegistryOnlyScope(t *testing.T) {332 func TestRunImagesPrune_DryRunRegistryOnlyScope(t *testing.T) {
333- client := &fakeImagesClient{333+ client := &imagesClientMock{
334 listImagesResp: []dto.Image{{Dangling: true}, {Dangling: false}},334 listImagesResp: []dto.Image{{Dangling: true}, {Dangling: false}},
335 }335 }
336 336
@@ -350,7 +350,7 @@ func TestRunImagesPrune_DryRunRegistryOnlyScope(t *testing.T) {
350 // ---------------------------------------------------------------------------350 // ---------------------------------------------------------------------------
351 351
352 func TestRunImagesPrune_KeepReleasesControlsRetention(t *testing.T) {352 func TestRunImagesPrune_KeepReleasesControlsRetention(t *testing.T) {
353- client := &fakeImagesClient{353+ client := &imagesClientMock{
354 pruneResp: &dto.ImagePruneResponse{354 pruneResp: &dto.ImagePruneResponse{
355 Runtime: dto.RuntimePruneResult{DeletedCount: 0},355 Runtime: dto.RuntimePruneResult{DeletedCount: 0},
356 Registry: dto.RegistryPruneResult{TagsRemoved: 7, BlobsRemoved: 3, SpaceReclaimed: 9000},356 Registry: dto.RegistryPruneResult{TagsRemoved: 7, BlobsRemoved: 3, SpaceReclaimed: 9000},
@@ -367,7 +367,7 @@ func TestRunImagesPrune_KeepReleasesControlsRetention(t *testing.T) {
367 }367 }
368 368
369 func TestRunImagesPrune_RejectsNegativeKeepReleases(t *testing.T) {369 func TestRunImagesPrune_RejectsNegativeKeepReleases(t *testing.T) {
370- err := runImagesPrune(context.Background(), &fakeImagesClient{}, imagesPruneOptions{KeepReleases: -1}, &bytes.Buffer{})370+ err := runImagesPrune(context.Background(), &imagesClientMock{}, imagesPruneOptions{KeepReleases: -1}, &bytes.Buffer{})
371 require.Error(t, err)371 require.Error(t, err)
372 assert.Contains(t, err.Error(), "--keep-releases must be >= 0")372 assert.Contains(t, err.Error(), "--keep-releases must be >= 0")
373 }373 }
@@ -385,7 +385,7 @@ func TestRunImagesPrune_NonDryRunCallsConfirm(t *testing.T) {
385 }385 }
386 t.Cleanup(func() { pruneConfirmFunc = origConfirm })386 t.Cleanup(func() { pruneConfirmFunc = origConfirm })
387 387
388- client := &fakeImagesClient{388+ client := &imagesClientMock{
389 pruneResp: &dto.ImagePruneResponse{389 pruneResp: &dto.ImagePruneResponse{
390 Runtime: dto.RuntimePruneResult{DeletedCount: 1},390 Runtime: dto.RuntimePruneResult{DeletedCount: 1},
391 Registry: dto.RegistryPruneResult{TagsRemoved: 1},391 Registry: dto.RegistryPruneResult{TagsRemoved: 1},
@@ -411,7 +411,7 @@ func TestRunImagesPrune_ConfirmPromptIncludesScopePreviewCounts(t *testing.T) {
411 }411 }
412 t.Cleanup(func() { pruneConfirmFunc = origConfirm })412 t.Cleanup(func() { pruneConfirmFunc = origConfirm })
413 413
414- client := &fakeImagesClient{414+ client := &imagesClientMock{
415 listImagesResp: []dto.Image{415 listImagesResp: []dto.Image{
416 {Repository: "registry.example.com/app", Tag: "latest", Created: created},416 {Repository: "registry.example.com/app", Tag: "latest", Created: created},
417 {Repository: "registry.example.com/app", Tag: "v3", Created: created.Add(-1 * time.Hour)},417 {Repository: "registry.example.com/app", Tag: "v3", Created: created.Add(-1 * time.Hour)},
@@ -439,7 +439,7 @@ func TestRunImagesPrune_ConfirmRejectCancelsOperation(t *testing.T) {
439 }439 }
440 t.Cleanup(func() { pruneConfirmFunc = origConfirm })440 t.Cleanup(func() { pruneConfirmFunc = origConfirm })
441 441
442- client := &fakeImagesClient{pruneResp: &dto.ImagePruneResponse{}}442+ client := &imagesClientMock{pruneResp: &dto.ImagePruneResponse{}}
443 443
444 var out bytes.Buffer444 var out bytes.Buffer
445 opts := imagesPruneOptions{KeepReleases: domain.DefaultImagePruneKeepLast}445 opts := imagesPruneOptions{KeepReleases: domain.DefaultImagePruneKeepLast}
@@ -459,7 +459,7 @@ func TestRunImagesPrune_NoConfirmSkipsPrompt(t *testing.T) {
459 }459 }
460 t.Cleanup(func() { pruneConfirmFunc = origConfirm })460 t.Cleanup(func() { pruneConfirmFunc = origConfirm })
461 461
462- client := &fakeImagesClient{462+ client := &imagesClientMock{
463 pruneResp: &dto.ImagePruneResponse{463 pruneResp: &dto.ImagePruneResponse{
464 Runtime: dto.RuntimePruneResult{DeletedCount: 1},464 Runtime: dto.RuntimePruneResult{DeletedCount: 1},
465 },465 },
@@ -483,7 +483,7 @@ func TestRunImagesPrune_DryRunNeverPrompts(t *testing.T) {
483 }483 }
484 t.Cleanup(func() { pruneConfirmFunc = origConfirm })484 t.Cleanup(func() { pruneConfirmFunc = origConfirm })
485 485
486- client := &fakeImagesClient{486+ client := &imagesClientMock{
487 listImagesResp: []dto.Image{{Dangling: true}},487 listImagesResp: []dto.Image{{Dangling: true}},
488 }488 }
489 489
@@ -504,7 +504,7 @@ func TestRunImagesPrune_ReturnsRemoteErrors(t *testing.T) {
504 pruneConfirmFunc = func(_ string) (bool, error) { return true, nil }504 pruneConfirmFunc = func(_ string) (bool, error) { return true, nil }
505 t.Cleanup(func() { pruneConfirmFunc = origConfirm })505 t.Cleanup(func() { pruneConfirmFunc = origConfirm })
506 506
507- client := &fakeImagesClient{pruneErr: errors.New("request failed")}507+ client := &imagesClientMock{pruneErr: errors.New("request failed")}
508 508
509 err := runImagesPrune(context.Background(), client, imagesPruneOptions{KeepReleases: 2}, &bytes.Buffer{})509 err := runImagesPrune(context.Background(), client, imagesPruneOptions{KeepReleases: 2}, &bytes.Buffer{})
510 require.Error(t, err)510 require.Error(t, err)
@@ -516,7 +516,7 @@ func TestRunImagesPrune_RejectsEmptyResponse(t *testing.T) {
516 pruneConfirmFunc = func(_ string) (bool, error) { return true, nil }516 pruneConfirmFunc = func(_ string) (bool, error) { return true, nil }
517 t.Cleanup(func() { pruneConfirmFunc = origConfirm })517 t.Cleanup(func() { pruneConfirmFunc = origConfirm })
518 518
519- client := &fakeImagesClient{pruneResp: nil}519+ client := &imagesClientMock{pruneResp: nil}
520 err := runImagesPrune(context.Background(), client, imagesPruneOptions{KeepReleases: 1}, &bytes.Buffer{})520 err := runImagesPrune(context.Background(), client, imagesPruneOptions{KeepReleases: 1}, &bytes.Buffer{})
521 require.Error(t, err)521 require.Error(t, err)
522 assert.Contains(t, err.Error(), "empty response")522 assert.Contains(t, err.Error(), "empty response")
@@ -532,7 +532,7 @@ func stripANSI(value string) string {
532 }532 }
533 533
534 func findHeaderLine(text string) string {534 func findHeaderLine(text string) string {
535- for _, line := range strings.Split(text, "\n") {535+ for line := range strings.SplitSeq(text, "\n") {
536 if strings.Contains(line, "REPOSITORY") && strings.Contains(line, "DANGLING") {536 if strings.Contains(line, "REPOSITORY") && strings.Contains(line, "DANGLING") {
537 return line537 return line
538 }538 }
@@ -542,7 +542,7 @@ func findHeaderLine(text string) string {
542 }542 }
543 543
544 func findImageListRowLine(text string, tag string) string {544 func findImageListRowLine(text string, tag string) string {
545- for _, line := range strings.Split(text, "\n") {545+ for line := range strings.SplitSeq(text, "\n") {
546 trimmed := strings.TrimSpace(line)546 trimmed := strings.TrimSpace(line)
547 if trimmed == "" {547 if trimmed == "" {
548 continue548 continue
@@ -562,7 +562,7 @@ func findImageListRowLine(text string, tag string) string {
562 562
563 func findImageListRows(text string) [][]string {563 func findImageListRows(text string) [][]string {
564 rows := make([][]string, 0)564 rows := make([][]string, 0)
565- for _, line := range strings.Split(text, "\n") {565+ for line := range strings.SplitSeq(text, "\n") {
566 trimmed := strings.TrimSpace(line)566 trimmed := strings.TrimSpace(line)
567 if trimmed == "" {567 if trimmed == "" {
568 continue568 continue
A internal/adapters/in/cli/mocks/mock_images_client.go +169 -0
@@ -0,0 +1,169 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/gordon/internal/adapters/dto"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockimagesClient creates a new instance of MockimagesClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockimagesClient(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockimagesClient {
20+ mock := &MockimagesClient{}
21+ mock.Mock.Test(t)
22+
23+ t.Cleanup(func() { mock.AssertExpectations(t) })
24+
25+ return mock
26+}
27+
28+// MockimagesClient is an autogenerated mock type for the imagesClient type
29+type MockimagesClient struct {
30+ mock.Mock
31+}
32+
33+type MockimagesClient_Expecter struct {
34+ mock *mock.Mock
35+}
36+
37+func (_m *MockimagesClient) EXPECT() *MockimagesClient_Expecter {
38+ return &MockimagesClient_Expecter{mock: &_m.Mock}
39+}
40+
41+// ListImages provides a mock function for the type MockimagesClient
42+func (_mock *MockimagesClient) ListImages(ctx context.Context) ([]dto.Image, error) {
43+ ret := _mock.Called(ctx)
44+
45+ if len(ret) == 0 {
46+ panic("no return value specified for ListImages")
47+ }
48+
49+ var r0 []dto.Image
50+ var r1 error
51+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]dto.Image, error)); ok {
52+ return returnFunc(ctx)
53+ }
54+ if returnFunc, ok := ret.Get(0).(func(context.Context) []dto.Image); ok {
55+ r0 = returnFunc(ctx)
56+ } else {
57+ if ret.Get(0) != nil {
58+ r0 = ret.Get(0).([]dto.Image)
59+ }
60+ }
61+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
62+ r1 = returnFunc(ctx)
63+ } else {
64+ r1 = ret.Error(1)
65+ }
66+ return r0, r1
67+}
68+
69+// MockimagesClient_ListImages_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListImages'
70+type MockimagesClient_ListImages_Call struct {
71+ *mock.Call
72+}
73+
74+// ListImages is a helper method to define mock.On call
75+// - ctx context.Context
76+func (_e *MockimagesClient_Expecter) ListImages(ctx interface{}) *MockimagesClient_ListImages_Call {
77+ return &MockimagesClient_ListImages_Call{Call: _e.mock.On("ListImages", ctx)}
78+}
79+
80+func (_c *MockimagesClient_ListImages_Call) Run(run func(ctx context.Context)) *MockimagesClient_ListImages_Call {
81+ _c.Call.Run(func(args mock.Arguments) {
82+ var arg0 context.Context
83+ if args[0] != nil {
84+ arg0 = args[0].(context.Context)
85+ }
86+ run(
87+ arg0,
88+ )
89+ })
90+ return _c
91+}
92+
93+func (_c *MockimagesClient_ListImages_Call) Return(images []dto.Image, err error) *MockimagesClient_ListImages_Call {
94+ _c.Call.Return(images, err)
95+ return _c
96+}
97+
98+func (_c *MockimagesClient_ListImages_Call) RunAndReturn(run func(ctx context.Context) ([]dto.Image, error)) *MockimagesClient_ListImages_Call {
99+ _c.Call.Return(run)
100+ return _c
101+}
102+
103+// PruneImages provides a mock function for the type MockimagesClient
104+func (_mock *MockimagesClient) PruneImages(ctx context.Context, req dto.ImagePruneRequest) (*dto.ImagePruneResponse, error) {
105+ ret := _mock.Called(ctx, req)
106+
107+ if len(ret) == 0 {
108+ panic("no return value specified for PruneImages")
109+ }
110+
111+ var r0 *dto.ImagePruneResponse
112+ var r1 error
113+ if returnFunc, ok := ret.Get(0).(func(context.Context, dto.ImagePruneRequest) (*dto.ImagePruneResponse, error)); ok {
114+ return returnFunc(ctx, req)
115+ }
116+ if returnFunc, ok := ret.Get(0).(func(context.Context, dto.ImagePruneRequest) *dto.ImagePruneResponse); ok {
117+ r0 = returnFunc(ctx, req)
118+ } else {
119+ if ret.Get(0) != nil {
120+ r0 = ret.Get(0).(*dto.ImagePruneResponse)
121+ }
122+ }
123+ if returnFunc, ok := ret.Get(1).(func(context.Context, dto.ImagePruneRequest) error); ok {
124+ r1 = returnFunc(ctx, req)
125+ } else {
126+ r1 = ret.Error(1)
127+ }
128+ return r0, r1
129+}
130+
131+// MockimagesClient_PruneImages_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PruneImages'
132+type MockimagesClient_PruneImages_Call struct {
133+ *mock.Call
134+}
135+
136+// PruneImages is a helper method to define mock.On call
137+// - ctx context.Context
138+// - req dto.ImagePruneRequest
139+func (_e *MockimagesClient_Expecter) PruneImages(ctx interface{}, req interface{}) *MockimagesClient_PruneImages_Call {
140+ return &MockimagesClient_PruneImages_Call{Call: _e.mock.On("PruneImages", ctx, req)}
141+}
142+
143+func (_c *MockimagesClient_PruneImages_Call) Run(run func(ctx context.Context, req dto.ImagePruneRequest)) *MockimagesClient_PruneImages_Call {
144+ _c.Call.Run(func(args mock.Arguments) {
145+ var arg0 context.Context
146+ if args[0] != nil {
147+ arg0 = args[0].(context.Context)
148+ }
149+ var arg1 dto.ImagePruneRequest
150+ if args[1] != nil {
151+ arg1 = args[1].(dto.ImagePruneRequest)
152+ }
153+ run(
154+ arg0,
155+ arg1,
156+ )
157+ })
158+ return _c
159+}
160+
161+func (_c *MockimagesClient_PruneImages_Call) Return(imagePruneResponse *dto.ImagePruneResponse, err error) *MockimagesClient_PruneImages_Call {
162+ _c.Call.Return(imagePruneResponse, err)
163+ return _c
164+}
165+
166+func (_c *MockimagesClient_PruneImages_Call) RunAndReturn(run func(ctx context.Context, req dto.ImagePruneRequest) (*dto.ImagePruneResponse, error)) *MockimagesClient_PruneImages_Call {
167+ _c.Call.Return(run)
168+ return _c
169+}
@@ -0,0 +1,169 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/gordon/internal/adapters/dto"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockimagesClient creates a new instance of MockimagesClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockimagesClient(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockimagesClient {
20+ mock := &MockimagesClient{}
21+ mock.Mock.Test(t)
22+
23+ t.Cleanup(func() { mock.AssertExpectations(t) })
24+
25+ return mock
26+}
27+
28+// MockimagesClient is an autogenerated mock type for the imagesClient type
29+type MockimagesClient struct {
30+ mock.Mock
31+}
32+
33+type MockimagesClient_Expecter struct {
34+ mock *mock.Mock
35+}
36+
37+func (_m *MockimagesClient) EXPECT() *MockimagesClient_Expecter {
38+ return &MockimagesClient_Expecter{mock: &_m.Mock}
39+}
40+
41+// ListImages provides a mock function for the type MockimagesClient
42+func (_mock *MockimagesClient) ListImages(ctx context.Context) ([]dto.Image, error) {
43+ ret := _mock.Called(ctx)
44+
45+ if len(ret) == 0 {
46+ panic("no return value specified for ListImages")
47+ }
48+
49+ var r0 []dto.Image
50+ var r1 error
51+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]dto.Image, error)); ok {
52+ return returnFunc(ctx)
53+ }
54+ if returnFunc, ok := ret.Get(0).(func(context.Context) []dto.Image); ok {
55+ r0 = returnFunc(ctx)
56+ } else {
57+ if ret.Get(0) != nil {
58+ r0 = ret.Get(0).([]dto.Image)
59+ }
60+ }
61+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
62+ r1 = returnFunc(ctx)
63+ } else {
64+ r1 = ret.Error(1)
65+ }
66+ return r0, r1
67+}
68+
69+// MockimagesClient_ListImages_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListImages'
70+type MockimagesClient_ListImages_Call struct {
71+ *mock.Call
72+}
73+
74+// ListImages is a helper method to define mock.On call
75+// - ctx context.Context
76+func (_e *MockimagesClient_Expecter) ListImages(ctx interface{}) *MockimagesClient_ListImages_Call {
77+ return &MockimagesClient_ListImages_Call{Call: _e.mock.On("ListImages", ctx)}
78+}
79+
80+func (_c *MockimagesClient_ListImages_Call) Run(run func(ctx context.Context)) *MockimagesClient_ListImages_Call {
81+ _c.Call.Run(func(args mock.Arguments) {
82+ var arg0 context.Context
83+ if args[0] != nil {
84+ arg0 = args[0].(context.Context)
85+ }
86+ run(
87+ arg0,
88+ )
89+ })
90+ return _c
91+}
92+
93+func (_c *MockimagesClient_ListImages_Call) Return(images []dto.Image, err error) *MockimagesClient_ListImages_Call {
94+ _c.Call.Return(images, err)
95+ return _c
96+}
97+
98+func (_c *MockimagesClient_ListImages_Call) RunAndReturn(run func(ctx context.Context) ([]dto.Image, error)) *MockimagesClient_ListImages_Call {
99+ _c.Call.Return(run)
100+ return _c
101+}
102+
103+// PruneImages provides a mock function for the type MockimagesClient
104+func (_mock *MockimagesClient) PruneImages(ctx context.Context, req dto.ImagePruneRequest) (*dto.ImagePruneResponse, error) {
105+ ret := _mock.Called(ctx, req)
106+
107+ if len(ret) == 0 {
108+ panic("no return value specified for PruneImages")
109+ }
110+
111+ var r0 *dto.ImagePruneResponse
112+ var r1 error
113+ if returnFunc, ok := ret.Get(0).(func(context.Context, dto.ImagePruneRequest) (*dto.ImagePruneResponse, error)); ok {
114+ return returnFunc(ctx, req)
115+ }
116+ if returnFunc, ok := ret.Get(0).(func(context.Context, dto.ImagePruneRequest) *dto.ImagePruneResponse); ok {
117+ r0 = returnFunc(ctx, req)
118+ } else {
119+ if ret.Get(0) != nil {
120+ r0 = ret.Get(0).(*dto.ImagePruneResponse)
121+ }
122+ }
123+ if returnFunc, ok := ret.Get(1).(func(context.Context, dto.ImagePruneRequest) error); ok {
124+ r1 = returnFunc(ctx, req)
125+ } else {
126+ r1 = ret.Error(1)
127+ }
128+ return r0, r1
129+}
130+
131+// MockimagesClient_PruneImages_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PruneImages'
132+type MockimagesClient_PruneImages_Call struct {
133+ *mock.Call
134+}
135+
136+// PruneImages is a helper method to define mock.On call
137+// - ctx context.Context
138+// - req dto.ImagePruneRequest
139+func (_e *MockimagesClient_Expecter) PruneImages(ctx interface{}, req interface{}) *MockimagesClient_PruneImages_Call {
140+ return &MockimagesClient_PruneImages_Call{Call: _e.mock.On("PruneImages", ctx, req)}
141+}
142+
143+func (_c *MockimagesClient_PruneImages_Call) Run(run func(ctx context.Context, req dto.ImagePruneRequest)) *MockimagesClient_PruneImages_Call {
144+ _c.Call.Run(func(args mock.Arguments) {
145+ var arg0 context.Context
146+ if args[0] != nil {
147+ arg0 = args[0].(context.Context)
148+ }
149+ var arg1 dto.ImagePruneRequest
150+ if args[1] != nil {
151+ arg1 = args[1].(dto.ImagePruneRequest)
152+ }
153+ run(
154+ arg0,
155+ arg1,
156+ )
157+ })
158+ return _c
159+}
160+
161+func (_c *MockimagesClient_PruneImages_Call) Return(imagePruneResponse *dto.ImagePruneResponse, err error) *MockimagesClient_PruneImages_Call {
162+ _c.Call.Return(imagePruneResponse, err)
163+ return _c
164+}
165+
166+func (_c *MockimagesClient_PruneImages_Call) RunAndReturn(run func(ctx context.Context, req dto.ImagePruneRequest) (*dto.ImagePruneResponse, error)) *MockimagesClient_PruneImages_Call {
167+ _c.Call.Return(run)
168+ return _c
169+}
A internal/adapters/in/cli/mocks/mock_volumes_client.go +169 -0
@@ -0,0 +1,169 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/gordon/internal/adapters/dto"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockvolumesClient creates a new instance of MockvolumesClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockvolumesClient(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockvolumesClient {
20+ mock := &MockvolumesClient{}
21+ mock.Mock.Test(t)
22+
23+ t.Cleanup(func() { mock.AssertExpectations(t) })
24+
25+ return mock
26+}
27+
28+// MockvolumesClient is an autogenerated mock type for the volumesClient type
29+type MockvolumesClient struct {
30+ mock.Mock
31+}
32+
33+type MockvolumesClient_Expecter struct {
34+ mock *mock.Mock
35+}
36+
37+func (_m *MockvolumesClient) EXPECT() *MockvolumesClient_Expecter {
38+ return &MockvolumesClient_Expecter{mock: &_m.Mock}
39+}
40+
41+// ListVolumes provides a mock function for the type MockvolumesClient
42+func (_mock *MockvolumesClient) ListVolumes(ctx context.Context) ([]dto.Volume, error) {
43+ ret := _mock.Called(ctx)
44+
45+ if len(ret) == 0 {
46+ panic("no return value specified for ListVolumes")
47+ }
48+
49+ var r0 []dto.Volume
50+ var r1 error
51+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]dto.Volume, error)); ok {
52+ return returnFunc(ctx)
53+ }
54+ if returnFunc, ok := ret.Get(0).(func(context.Context) []dto.Volume); ok {
55+ r0 = returnFunc(ctx)
56+ } else {
57+ if ret.Get(0) != nil {
58+ r0 = ret.Get(0).([]dto.Volume)
59+ }
60+ }
61+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
62+ r1 = returnFunc(ctx)
63+ } else {
64+ r1 = ret.Error(1)
65+ }
66+ return r0, r1
67+}
68+
69+// MockvolumesClient_ListVolumes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListVolumes'
70+type MockvolumesClient_ListVolumes_Call struct {
71+ *mock.Call
72+}
73+
74+// ListVolumes is a helper method to define mock.On call
75+// - ctx context.Context
76+func (_e *MockvolumesClient_Expecter) ListVolumes(ctx interface{}) *MockvolumesClient_ListVolumes_Call {
77+ return &MockvolumesClient_ListVolumes_Call{Call: _e.mock.On("ListVolumes", ctx)}
78+}
79+
80+func (_c *MockvolumesClient_ListVolumes_Call) Run(run func(ctx context.Context)) *MockvolumesClient_ListVolumes_Call {
81+ _c.Call.Run(func(args mock.Arguments) {
82+ var arg0 context.Context
83+ if args[0] != nil {
84+ arg0 = args[0].(context.Context)
85+ }
86+ run(
87+ arg0,
88+ )
89+ })
90+ return _c
91+}
92+
93+func (_c *MockvolumesClient_ListVolumes_Call) Return(volumes []dto.Volume, err error) *MockvolumesClient_ListVolumes_Call {
94+ _c.Call.Return(volumes, err)
95+ return _c
96+}
97+
98+func (_c *MockvolumesClient_ListVolumes_Call) RunAndReturn(run func(ctx context.Context) ([]dto.Volume, error)) *MockvolumesClient_ListVolumes_Call {
99+ _c.Call.Return(run)
100+ return _c
101+}
102+
103+// PruneVolumes provides a mock function for the type MockvolumesClient
104+func (_mock *MockvolumesClient) PruneVolumes(ctx context.Context, req dto.VolumePruneRequest) (*dto.VolumePruneResponse, error) {
105+ ret := _mock.Called(ctx, req)
106+
107+ if len(ret) == 0 {
108+ panic("no return value specified for PruneVolumes")
109+ }
110+
111+ var r0 *dto.VolumePruneResponse
112+ var r1 error
113+ if returnFunc, ok := ret.Get(0).(func(context.Context, dto.VolumePruneRequest) (*dto.VolumePruneResponse, error)); ok {
114+ return returnFunc(ctx, req)
115+ }
116+ if returnFunc, ok := ret.Get(0).(func(context.Context, dto.VolumePruneRequest) *dto.VolumePruneResponse); ok {
117+ r0 = returnFunc(ctx, req)
118+ } else {
119+ if ret.Get(0) != nil {
120+ r0 = ret.Get(0).(*dto.VolumePruneResponse)
121+ }
122+ }
123+ if returnFunc, ok := ret.Get(1).(func(context.Context, dto.VolumePruneRequest) error); ok {
124+ r1 = returnFunc(ctx, req)
125+ } else {
126+ r1 = ret.Error(1)
127+ }
128+ return r0, r1
129+}
130+
131+// MockvolumesClient_PruneVolumes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PruneVolumes'
132+type MockvolumesClient_PruneVolumes_Call struct {
133+ *mock.Call
134+}
135+
136+// PruneVolumes is a helper method to define mock.On call
137+// - ctx context.Context
138+// - req dto.VolumePruneRequest
139+func (_e *MockvolumesClient_Expecter) PruneVolumes(ctx interface{}, req interface{}) *MockvolumesClient_PruneVolumes_Call {
140+ return &MockvolumesClient_PruneVolumes_Call{Call: _e.mock.On("PruneVolumes", ctx, req)}
141+}
142+
143+func (_c *MockvolumesClient_PruneVolumes_Call) Run(run func(ctx context.Context, req dto.VolumePruneRequest)) *MockvolumesClient_PruneVolumes_Call {
144+ _c.Call.Run(func(args mock.Arguments) {
145+ var arg0 context.Context
146+ if args[0] != nil {
147+ arg0 = args[0].(context.Context)
148+ }
149+ var arg1 dto.VolumePruneRequest
150+ if args[1] != nil {
151+ arg1 = args[1].(dto.VolumePruneRequest)
152+ }
153+ run(
154+ arg0,
155+ arg1,
156+ )
157+ })
158+ return _c
159+}
160+
161+func (_c *MockvolumesClient_PruneVolumes_Call) Return(volumePruneResponse *dto.VolumePruneResponse, err error) *MockvolumesClient_PruneVolumes_Call {
162+ _c.Call.Return(volumePruneResponse, err)
163+ return _c
164+}
165+
166+func (_c *MockvolumesClient_PruneVolumes_Call) RunAndReturn(run func(ctx context.Context, req dto.VolumePruneRequest) (*dto.VolumePruneResponse, error)) *MockvolumesClient_PruneVolumes_Call {
167+ _c.Call.Return(run)
168+ return _c
169+}
@@ -0,0 +1,169 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/gordon/internal/adapters/dto"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockvolumesClient creates a new instance of MockvolumesClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockvolumesClient(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockvolumesClient {
20+ mock := &MockvolumesClient{}
21+ mock.Mock.Test(t)
22+
23+ t.Cleanup(func() { mock.AssertExpectations(t) })
24+
25+ return mock
26+}
27+
28+// MockvolumesClient is an autogenerated mock type for the volumesClient type
29+type MockvolumesClient struct {
30+ mock.Mock
31+}
32+
33+type MockvolumesClient_Expecter struct {
34+ mock *mock.Mock
35+}
36+
37+func (_m *MockvolumesClient) EXPECT() *MockvolumesClient_Expecter {
38+ return &MockvolumesClient_Expecter{mock: &_m.Mock}
39+}
40+
41+// ListVolumes provides a mock function for the type MockvolumesClient
42+func (_mock *MockvolumesClient) ListVolumes(ctx context.Context) ([]dto.Volume, error) {
43+ ret := _mock.Called(ctx)
44+
45+ if len(ret) == 0 {
46+ panic("no return value specified for ListVolumes")
47+ }
48+
49+ var r0 []dto.Volume
50+ var r1 error
51+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]dto.Volume, error)); ok {
52+ return returnFunc(ctx)
53+ }
54+ if returnFunc, ok := ret.Get(0).(func(context.Context) []dto.Volume); ok {
55+ r0 = returnFunc(ctx)
56+ } else {
57+ if ret.Get(0) != nil {
58+ r0 = ret.Get(0).([]dto.Volume)
59+ }
60+ }
61+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
62+ r1 = returnFunc(ctx)
63+ } else {
64+ r1 = ret.Error(1)
65+ }
66+ return r0, r1
67+}
68+
69+// MockvolumesClient_ListVolumes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListVolumes'
70+type MockvolumesClient_ListVolumes_Call struct {
71+ *mock.Call
72+}
73+
74+// ListVolumes is a helper method to define mock.On call
75+// - ctx context.Context
76+func (_e *MockvolumesClient_Expecter) ListVolumes(ctx interface{}) *MockvolumesClient_ListVolumes_Call {
77+ return &MockvolumesClient_ListVolumes_Call{Call: _e.mock.On("ListVolumes", ctx)}
78+}
79+
80+func (_c *MockvolumesClient_ListVolumes_Call) Run(run func(ctx context.Context)) *MockvolumesClient_ListVolumes_Call {
81+ _c.Call.Run(func(args mock.Arguments) {
82+ var arg0 context.Context
83+ if args[0] != nil {
84+ arg0 = args[0].(context.Context)
85+ }
86+ run(
87+ arg0,
88+ )
89+ })
90+ return _c
91+}
92+
93+func (_c *MockvolumesClient_ListVolumes_Call) Return(volumes []dto.Volume, err error) *MockvolumesClient_ListVolumes_Call {
94+ _c.Call.Return(volumes, err)
95+ return _c
96+}
97+
98+func (_c *MockvolumesClient_ListVolumes_Call) RunAndReturn(run func(ctx context.Context) ([]dto.Volume, error)) *MockvolumesClient_ListVolumes_Call {
99+ _c.Call.Return(run)
100+ return _c
101+}
102+
103+// PruneVolumes provides a mock function for the type MockvolumesClient
104+func (_mock *MockvolumesClient) PruneVolumes(ctx context.Context, req dto.VolumePruneRequest) (*dto.VolumePruneResponse, error) {
105+ ret := _mock.Called(ctx, req)
106+
107+ if len(ret) == 0 {
108+ panic("no return value specified for PruneVolumes")
109+ }
110+
111+ var r0 *dto.VolumePruneResponse
112+ var r1 error
113+ if returnFunc, ok := ret.Get(0).(func(context.Context, dto.VolumePruneRequest) (*dto.VolumePruneResponse, error)); ok {
114+ return returnFunc(ctx, req)
115+ }
116+ if returnFunc, ok := ret.Get(0).(func(context.Context, dto.VolumePruneRequest) *dto.VolumePruneResponse); ok {
117+ r0 = returnFunc(ctx, req)
118+ } else {
119+ if ret.Get(0) != nil {
120+ r0 = ret.Get(0).(*dto.VolumePruneResponse)
121+ }
122+ }
123+ if returnFunc, ok := ret.Get(1).(func(context.Context, dto.VolumePruneRequest) error); ok {
124+ r1 = returnFunc(ctx, req)
125+ } else {
126+ r1 = ret.Error(1)
127+ }
128+ return r0, r1
129+}
130+
131+// MockvolumesClient_PruneVolumes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PruneVolumes'
132+type MockvolumesClient_PruneVolumes_Call struct {
133+ *mock.Call
134+}
135+
136+// PruneVolumes is a helper method to define mock.On call
137+// - ctx context.Context
138+// - req dto.VolumePruneRequest
139+func (_e *MockvolumesClient_Expecter) PruneVolumes(ctx interface{}, req interface{}) *MockvolumesClient_PruneVolumes_Call {
140+ return &MockvolumesClient_PruneVolumes_Call{Call: _e.mock.On("PruneVolumes", ctx, req)}
141+}
142+
143+func (_c *MockvolumesClient_PruneVolumes_Call) Run(run func(ctx context.Context, req dto.VolumePruneRequest)) *MockvolumesClient_PruneVolumes_Call {
144+ _c.Call.Run(func(args mock.Arguments) {
145+ var arg0 context.Context
146+ if args[0] != nil {
147+ arg0 = args[0].(context.Context)
148+ }
149+ var arg1 dto.VolumePruneRequest
150+ if args[1] != nil {
151+ arg1 = args[1].(dto.VolumePruneRequest)
152+ }
153+ run(
154+ arg0,
155+ arg1,
156+ )
157+ })
158+ return _c
159+}
160+
161+func (_c *MockvolumesClient_PruneVolumes_Call) Return(volumePruneResponse *dto.VolumePruneResponse, err error) *MockvolumesClient_PruneVolumes_Call {
162+ _c.Call.Return(volumePruneResponse, err)
163+ return _c
164+}
165+
166+func (_c *MockvolumesClient_PruneVolumes_Call) RunAndReturn(run func(ctx context.Context, req dto.VolumePruneRequest) (*dto.VolumePruneResponse, error)) *MockvolumesClient_PruneVolumes_Call {
167+ _c.Call.Return(run)
168+ return _c
169+}
M internal/adapters/in/cli/push_image_ops.go +1 -2
@@ -123,8 +123,7 @@ func isAuthError(err error) bool {
123123 if err == nil {
124124 return false
125125 }
126- var httpErr *remote.HTTPError
127- if errors.As(err, &httpErr) {
126+ if httpErr, ok := errors.AsType[*remote.HTTPError](err); ok {
128127 return httpErr.StatusCode == http.StatusUnauthorized
129128 }
130129 msg := err.Error()
@@ -123,8 +123,7 @@ func isAuthError(err error) bool {
123 if err == nil {123 if err == nil {
124 return false124 return false
125 }125 }
126- var httpErr *remote.HTTPError126+ if httpErr, ok := errors.AsType[*remote.HTTPError](err); ok {
127- if errors.As(err, &httpErr) {
128 return httpErr.StatusCode == http.StatusUnauthorized127 return httpErr.StatusCode == http.StatusUnauthorized
129 }128 }
130 msg := err.Error()129 msg := err.Error()
M internal/adapters/in/cli/push_test.go +24 -241
@@ -11,193 +11,13 @@ import (
1111 "strings"
1212 "testing"
1313
14- "github.com/bnema/gordon/internal/adapters/dto"
14+ climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
1515 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
1616 "github.com/bnema/gordon/internal/domain"
1717 "github.com/stretchr/testify/assert"
18+ "github.com/stretchr/testify/mock"
1819 )
1920
20-type resolveFromImageTestControlPlane struct {
21- getRoute func(context.Context, string) (*domain.Route, error)
22- findRoutesByImage func(context.Context, string) ([]domain.Route, error)
23-}
24-
25-var _ ControlPlane = (*resolveFromImageTestControlPlane)(nil)
26-
27-func (c *resolveFromImageTestControlPlane) ListRoutesWithDetails(context.Context) ([]remote.RouteInfo, error) {
28- panic("unexpected call")
29-}
30-
31-func (c *resolveFromImageTestControlPlane) GetHealth(context.Context) (map[string]*remote.RouteHealth, error) {
32- panic("unexpected call")
33-}
34-
35-func (c *resolveFromImageTestControlPlane) GetRoute(ctx context.Context, domainName string) (*domain.Route, error) {
36- if c.getRoute != nil {
37- return c.getRoute(ctx, domainName)
38- }
39- panic("unexpected call")
40-}
41-
42-func (c *resolveFromImageTestControlPlane) FindRoutesByImage(ctx context.Context, imageName string) ([]domain.Route, error) {
43- if c.findRoutesByImage != nil {
44- return c.findRoutesByImage(ctx, imageName)
45- }
46- return nil, nil
47-}
48-
49-func (c *resolveFromImageTestControlPlane) AddRoute(context.Context, domain.Route) error {
50- panic("unexpected call")
51-}
52-
53-func (c *resolveFromImageTestControlPlane) UpdateRoute(context.Context, domain.Route) error {
54- panic("unexpected call")
55-}
56-
57-func (c *resolveFromImageTestControlPlane) RemoveRoute(context.Context, string) error {
58- panic("unexpected call")
59-}
60-
61-func (c *resolveFromImageTestControlPlane) Bootstrap(context.Context, dto.BootstrapRequest) (*dto.BootstrapResponse, error) {
62- panic("unexpected call")
63-}
64-
65-func (c *resolveFromImageTestControlPlane) ListSecretsWithAttachments(context.Context, string) (*remote.SecretsListResult, error) {
66- panic("unexpected call")
67-}
68-
69-func (c *resolveFromImageTestControlPlane) SetSecrets(context.Context, string, map[string]string) error {
70- panic("unexpected call")
71-}
72-
73-func (c *resolveFromImageTestControlPlane) DeleteSecret(context.Context, string, string) error {
74- panic("unexpected call")
75-}
76-
77-func (c *resolveFromImageTestControlPlane) SetAttachmentSecrets(context.Context, string, string, map[string]string) error {
78- panic("unexpected call")
79-}
80-
81-func (c *resolveFromImageTestControlPlane) DeleteAttachmentSecret(context.Context, string, string, string) error {
82- panic("unexpected call")
83-}
84-
85-func (c *resolveFromImageTestControlPlane) GetAllAttachmentsConfig(context.Context) (map[string][]string, error) {
86- panic("unexpected call")
87-}
88-
89-func (c *resolveFromImageTestControlPlane) GetAttachmentsConfig(context.Context, string) ([]string, error) {
90- panic("unexpected call")
91-}
92-
93-func (c *resolveFromImageTestControlPlane) ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error) {
94- panic("unexpected call")
95-}
96-
97-func (c *resolveFromImageTestControlPlane) CleanupOrphanedAttachments(context.Context, string, bool) (*domain.CleanupReport, error) {
98- panic("unexpected call")
99-}
100-
101-func (c *resolveFromImageTestControlPlane) FindAttachmentTargetsByImage(context.Context, string) ([]string, error) {
102- panic("unexpected call")
103-}
104-
105-func (c *resolveFromImageTestControlPlane) AddAttachment(context.Context, string, string) error {
106- panic("unexpected call")
107-}
108-
109-func (c *resolveFromImageTestControlPlane) RemoveAttachment(context.Context, string, string) error {
110- panic("unexpected call")
111-}
112-
113-func (c *resolveFromImageTestControlPlane) GetAutoRouteAllowedDomains(context.Context) ([]string, error) {
114- panic("unexpected call")
115-}
116-
117-func (c *resolveFromImageTestControlPlane) AddAutoRouteAllowedDomain(context.Context, string) error {
118- panic("unexpected call")
119-}
120-
121-func (c *resolveFromImageTestControlPlane) RemoveAutoRouteAllowedDomain(context.Context, string) error {
122- panic("unexpected call")
123-}
124-
125-func (c *resolveFromImageTestControlPlane) GetTLSStatus(context.Context) (*dto.TLSStatusResponse, error) {
126- return &dto.TLSStatusResponse{}, nil
127-}
128-
129-func (c *resolveFromImageTestControlPlane) GetStatus(context.Context) (*remote.Status, error) {
130- panic("unexpected call")
131-}
132-
133-func (c *resolveFromImageTestControlPlane) Reload(context.Context) error {
134- panic("unexpected call")
135-}
136-
137-func (c *resolveFromImageTestControlPlane) ListNetworks(context.Context) ([]*domain.NetworkInfo, error) {
138- panic("unexpected call")
139-}
140-
141-func (c *resolveFromImageTestControlPlane) GetConfig(context.Context) (*remote.Config, error) {
142- panic("unexpected call")
143-}
144-
145-func (c *resolveFromImageTestControlPlane) DeployIntent(context.Context, string) error {
146- panic("unexpected call")
147-}
148-
149-func (c *resolveFromImageTestControlPlane) Deploy(context.Context, string) (*remote.DeployResult, error) {
150- panic("unexpected call")
151-}
152-
153-func (c *resolveFromImageTestControlPlane) Restart(context.Context, string, bool) (*remote.RestartResult, error) {
154- panic("unexpected call")
155-}
156-
157-func (c *resolveFromImageTestControlPlane) ListTags(context.Context, string) ([]string, error) {
158- panic("unexpected call")
159-}
160-
161-func (c *resolveFromImageTestControlPlane) ListBackups(context.Context, string) ([]dto.BackupJob, error) {
162- panic("unexpected call")
163-}
164-
165-func (c *resolveFromImageTestControlPlane) BackupStatus(context.Context) ([]dto.BackupJob, error) {
166- panic("unexpected call")
167-}
168-
169-func (c *resolveFromImageTestControlPlane) RunBackup(context.Context, string, string) (*dto.BackupRunResponse, error) {
170- panic("unexpected call")
171-}
172-
173-func (c *resolveFromImageTestControlPlane) DetectDatabases(context.Context, string) ([]dto.DatabaseInfo, error) {
174- panic("unexpected call")
175-}
176-
177-func (c *resolveFromImageTestControlPlane) GetProcessLogs(context.Context, int) ([]string, error) {
178- panic("unexpected call")
179-}
180-
181-func (c *resolveFromImageTestControlPlane) GetContainerLogs(context.Context, string, int) ([]string, error) {
182- panic("unexpected call")
183-}
184-
185-func (c *resolveFromImageTestControlPlane) StreamProcessLogs(context.Context, int) (<-chan string, error) {
186- panic("unexpected call")
187-}
188-
189-func (c *resolveFromImageTestControlPlane) StreamContainerLogs(context.Context, string, int) (<-chan string, error) {
190- panic("unexpected call")
191-}
192-
193-func (c *resolveFromImageTestControlPlane) ListVolumes(context.Context) ([]dto.Volume, error) {
194- panic("unexpected call")
195-}
196-
197-func (c *resolveFromImageTestControlPlane) PruneVolumes(context.Context, dto.VolumePruneRequest) (*dto.VolumePruneResponse, error) {
198- panic("unexpected call")
199-}
200-
20121 func TestValidateBuildArg(t *testing.T) {
20222 tests := []struct {
20323 name string
@@ -631,13 +451,10 @@ func TestParseLabelPair(t *testing.T) {
631451 }
632452
633453 func TestResolveFromImage_NoRouteSuggestsBootstrap(t *testing.T) {
634- cp := &resolveFromImageTestControlPlane{
635- findRoutesByImage: func(context.Context, string) ([]domain.Route, error) {
636- return nil, nil
637- },
638- }
454+ cpMock := climocks.NewMockControlPlane(t)
455+ cpMock.EXPECT().FindRoutesByImage(context.Background(), "myapp").Return(nil, nil).Once()
639456
640- _, _, _, err := resolveFromImage(context.Background(), cp, "myapp", "Dockerfile")
457+ _, _, _, err := resolveFromImage(context.Background(), cpMock, "myapp", "Dockerfile")
641458
642459 assert.Error(t, err)
643460 assert.Contains(t, err.Error(), `no route configured for image "myapp"`)
@@ -645,18 +462,10 @@ func TestResolveFromImage_NoRouteSuggestsBootstrap(t *testing.T) {
645462 }
646463
647464 func TestResolveRoute_DottedBareImageUsesImageLookup(t *testing.T) {
648- cp := &resolveFromImageTestControlPlane{
649- findRoutesByImage: func(_ context.Context, imageName string) ([]domain.Route, error) {
650- assert.Equal(t, "my.app", imageName)
651- return []domain.Route{{Domain: "app.example.com", Image: "registry.example.com/my.app:latest"}}, nil
652- },
653- getRoute: func(context.Context, string) (*domain.Route, error) {
654- t.Fatalf("unexpected GetRoute call")
655- return nil, nil
656- },
657- }
465+ cpMock := climocks.NewMockControlPlane(t)
466+ cpMock.EXPECT().FindRoutesByImage(context.Background(), "my.app").Return([]domain.Route{{Domain: "app.example.com", Image: "registry.example.com/my.app:latest"}}, nil).Once()
658467
659- registry, imageName, pushDomain, err := resolveRoute(context.Background(), cp, "my.app", "", "Dockerfile")
468+ registry, imageName, pushDomain, err := resolveRoute(context.Background(), cpMock, "my.app", "", "Dockerfile")
660469
661470 assert.NoError(t, err)
662471 assert.Equal(t, "registry.example.com", registry)
@@ -665,21 +474,12 @@ func TestResolveRoute_DottedBareImageUsesImageLookup(t *testing.T) {
665474 }
666475
667476 func TestResolveRoute_DottedBareImageNoRoutesKeepsBootstrapError(t *testing.T) {
668- sawImageLookup := false
669- cp := &resolveFromImageTestControlPlane{
670- findRoutesByImage: func(_ context.Context, imageName string) ([]domain.Route, error) {
671- sawImageLookup = true
672- assert.Equal(t, "my.app", imageName)
673- return nil, nil
674- },
675- getRoute: func(_ context.Context, domainName string) (*domain.Route, error) {
676- assert.True(t, sawImageLookup, "expected image lookup before legacy domain lookup")
677- assert.Equal(t, "my.app", domainName)
678- return nil, domain.ErrRouteNotFound
679- },
680- }
477+ cpMock := climocks.NewMockControlPlane(t)
478+ imageLookup := cpMock.EXPECT().FindRoutesByImage(context.Background(), "my.app").Return(nil, nil).Once()
479+ routeLookup := cpMock.EXPECT().GetRoute(context.Background(), "my.app").Return(nil, domain.ErrRouteNotFound).Once()
480+ mock.InOrder(imageLookup, routeLookup)
681481
682- _, _, _, err := resolveRoute(context.Background(), cp, "my.app", "", "Dockerfile")
482+ _, _, _, err := resolveRoute(context.Background(), cpMock, "my.app", "", "Dockerfile")
683483
684484 assert.Error(t, err)
685485 assert.True(t, errors.Is(err, domain.ErrNoRouteForImage))
@@ -696,21 +496,12 @@ func TestNoRouteForImageErrorWrapsSentinel(t *testing.T) {
696496 }
697497
698498 func TestResolveRoute_DottedBareDomainFallsBackToLegacyLookup(t *testing.T) {
699- sawImageLookup := false
700- cp := &resolveFromImageTestControlPlane{
701- findRoutesByImage: func(_ context.Context, imageName string) ([]domain.Route, error) {
702- sawImageLookup = true
703- assert.Equal(t, "app.example.com", imageName)
704- return nil, nil
705- },
706- getRoute: func(_ context.Context, domainName string) (*domain.Route, error) {
707- assert.True(t, sawImageLookup, "expected image lookup before legacy domain lookup")
708- assert.Equal(t, "app.example.com", domainName)
709- return &domain.Route{Domain: domainName, Image: "registry.example.com/myapp:latest"}, nil
710- },
711- }
499+ cpMock := climocks.NewMockControlPlane(t)
500+ imageLookup := cpMock.EXPECT().FindRoutesByImage(context.Background(), "app.example.com").Return(nil, nil).Once()
501+ routeLookup := cpMock.EXPECT().GetRoute(context.Background(), "app.example.com").Return(&domain.Route{Domain: "app.example.com", Image: "registry.example.com/myapp:latest"}, nil).Once()
502+ mock.InOrder(imageLookup, routeLookup)
712503
713- registry, imageName, pushDomain, err := resolveRoute(context.Background(), cp, "app.example.com", "", "Dockerfile")
504+ registry, imageName, pushDomain, err := resolveRoute(context.Background(), cpMock, "app.example.com", "", "Dockerfile")
714505
715506 assert.NoError(t, err)
716507 assert.Equal(t, "registry.example.com", registry)
@@ -719,14 +510,10 @@ func TestResolveRoute_DottedBareDomainFallsBackToLegacyLookup(t *testing.T) {
719510 }
720511
721512 func TestResolveRoute_TaggedImageStripsTagBeforeLookup(t *testing.T) {
722- cp := &resolveFromImageTestControlPlane{
723- findRoutesByImage: func(_ context.Context, imageName string) ([]domain.Route, error) {
724- assert.Equal(t, "myapp", imageName)
725- return []domain.Route{{Domain: "app.example.com", Image: "registry.example.com/myapp:latest"}}, nil
726- },
727- }
513+ cpMock := climocks.NewMockControlPlane(t)
514+ cpMock.EXPECT().FindRoutesByImage(context.Background(), "myapp").Return([]domain.Route{{Domain: "app.example.com", Image: "registry.example.com/myapp:latest"}}, nil).Once()
728515
729- registry, imageName, pushDomain, err := resolveRoute(context.Background(), cp, "myapp:v1.2.3", "", "Dockerfile")
516+ registry, imageName, pushDomain, err := resolveRoute(context.Background(), cpMock, "myapp:v1.2.3", "", "Dockerfile")
730517
731518 assert.NoError(t, err)
732519 assert.Equal(t, "registry.example.com", registry)
@@ -735,14 +522,10 @@ func TestResolveRoute_TaggedImageStripsTagBeforeLookup(t *testing.T) {
735522 }
736523
737524 func TestResolveRoute_RegistryQualifiedTaggedImageUsesImageLookup(t *testing.T) {
738- cp := &resolveFromImageTestControlPlane{
739- findRoutesByImage: func(_ context.Context, imageName string) ([]domain.Route, error) {
740- assert.Equal(t, "registry.example.com/myapp", imageName)
741- return []domain.Route{{Domain: "app.example.com", Image: "registry.example.com/myapp:latest"}}, nil
742- },
743- }
525+ cpMock := climocks.NewMockControlPlane(t)
526+ cpMock.EXPECT().FindRoutesByImage(context.Background(), "registry.example.com/myapp").Return([]domain.Route{{Domain: "app.example.com", Image: "registry.example.com/myapp:latest"}}, nil).Once()
744527
745- _, _, _, err := resolveRoute(context.Background(), cp, "registry.example.com/myapp:v1.2.3", "", "Dockerfile")
528+ _, _, _, err := resolveRoute(context.Background(), cpMock, "registry.example.com/myapp:v1.2.3", "", "Dockerfile")
746529
747530 assert.NoError(t, err)
748531 }
@@ -11,193 +11,13 @@ import (
11 "strings"11 "strings"
12 "testing"12 "testing"
13 13
14- "github.com/bnema/gordon/internal/adapters/dto"14+ climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
15 "github.com/bnema/gordon/internal/adapters/in/cli/remote"15 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
16 "github.com/bnema/gordon/internal/domain"16 "github.com/bnema/gordon/internal/domain"
17 "github.com/stretchr/testify/assert"17 "github.com/stretchr/testify/assert"
18+ "github.com/stretchr/testify/mock"
18 )19 )
19 20
20-type resolveFromImageTestControlPlane struct {
21- getRoute func(context.Context, string) (*domain.Route, error)
22- findRoutesByImage func(context.Context, string) ([]domain.Route, error)
23-}
24-
25-var _ ControlPlane = (*resolveFromImageTestControlPlane)(nil)
26-
27-func (c *resolveFromImageTestControlPlane) ListRoutesWithDetails(context.Context) ([]remote.RouteInfo, error) {
28- panic("unexpected call")
29-}
30-
31-func (c *resolveFromImageTestControlPlane) GetHealth(context.Context) (map[string]*remote.RouteHealth, error) {
32- panic("unexpected call")
33-}
34-
35-func (c *resolveFromImageTestControlPlane) GetRoute(ctx context.Context, domainName string) (*domain.Route, error) {
36- if c.getRoute != nil {
37- return c.getRoute(ctx, domainName)
38- }
39- panic("unexpected call")
40-}
41-
42-func (c *resolveFromImageTestControlPlane) FindRoutesByImage(ctx context.Context, imageName string) ([]domain.Route, error) {
43- if c.findRoutesByImage != nil {
44- return c.findRoutesByImage(ctx, imageName)
45- }
46- return nil, nil
47-}
48-
49-func (c *resolveFromImageTestControlPlane) AddRoute(context.Context, domain.Route) error {
50- panic("unexpected call")
51-}
52-
53-func (c *resolveFromImageTestControlPlane) UpdateRoute(context.Context, domain.Route) error {
54- panic("unexpected call")
55-}
56-
57-func (c *resolveFromImageTestControlPlane) RemoveRoute(context.Context, string) error {
58- panic("unexpected call")
59-}
60-
61-func (c *resolveFromImageTestControlPlane) Bootstrap(context.Context, dto.BootstrapRequest) (*dto.BootstrapResponse, error) {
62- panic("unexpected call")
63-}
64-
65-func (c *resolveFromImageTestControlPlane) ListSecretsWithAttachments(context.Context, string) (*remote.SecretsListResult, error) {
66- panic("unexpected call")
67-}
68-
69-func (c *resolveFromImageTestControlPlane) SetSecrets(context.Context, string, map[string]string) error {
70- panic("unexpected call")
71-}
72-
73-func (c *resolveFromImageTestControlPlane) DeleteSecret(context.Context, string, string) error {
74- panic("unexpected call")
75-}
76-
77-func (c *resolveFromImageTestControlPlane) SetAttachmentSecrets(context.Context, string, string, map[string]string) error {
78- panic("unexpected call")
79-}
80-
81-func (c *resolveFromImageTestControlPlane) DeleteAttachmentSecret(context.Context, string, string, string) error {
82- panic("unexpected call")
83-}
84-
85-func (c *resolveFromImageTestControlPlane) GetAllAttachmentsConfig(context.Context) (map[string][]string, error) {
86- panic("unexpected call")
87-}
88-
89-func (c *resolveFromImageTestControlPlane) GetAttachmentsConfig(context.Context, string) ([]string, error) {
90- panic("unexpected call")
91-}
92-
93-func (c *resolveFromImageTestControlPlane) ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error) {
94- panic("unexpected call")
95-}
96-
97-func (c *resolveFromImageTestControlPlane) CleanupOrphanedAttachments(context.Context, string, bool) (*domain.CleanupReport, error) {
98- panic("unexpected call")
99-}
100-
101-func (c *resolveFromImageTestControlPlane) FindAttachmentTargetsByImage(context.Context, string) ([]string, error) {
102- panic("unexpected call")
103-}
104-
105-func (c *resolveFromImageTestControlPlane) AddAttachment(context.Context, string, string) error {
106- panic("unexpected call")
107-}
108-
109-func (c *resolveFromImageTestControlPlane) RemoveAttachment(context.Context, string, string) error {
110- panic("unexpected call")
111-}
112-
113-func (c *resolveFromImageTestControlPlane) GetAutoRouteAllowedDomains(context.Context) ([]string, error) {
114- panic("unexpected call")
115-}
116-
117-func (c *resolveFromImageTestControlPlane) AddAutoRouteAllowedDomain(context.Context, string) error {
118- panic("unexpected call")
119-}
120-
121-func (c *resolveFromImageTestControlPlane) RemoveAutoRouteAllowedDomain(context.Context, string) error {
122- panic("unexpected call")
123-}
124-
125-func (c *resolveFromImageTestControlPlane) GetTLSStatus(context.Context) (*dto.TLSStatusResponse, error) {
126- return &dto.TLSStatusResponse{}, nil
127-}
128-
129-func (c *resolveFromImageTestControlPlane) GetStatus(context.Context) (*remote.Status, error) {
130- panic("unexpected call")
131-}
132-
133-func (c *resolveFromImageTestControlPlane) Reload(context.Context) error {
134- panic("unexpected call")
135-}
136-
137-func (c *resolveFromImageTestControlPlane) ListNetworks(context.Context) ([]*domain.NetworkInfo, error) {
138- panic("unexpected call")
139-}
140-
141-func (c *resolveFromImageTestControlPlane) GetConfig(context.Context) (*remote.Config, error) {
142- panic("unexpected call")
143-}
144-
145-func (c *resolveFromImageTestControlPlane) DeployIntent(context.Context, string) error {
146- panic("unexpected call")
147-}
148-
149-func (c *resolveFromImageTestControlPlane) Deploy(context.Context, string) (*remote.DeployResult, error) {
150- panic("unexpected call")
151-}
152-
153-func (c *resolveFromImageTestControlPlane) Restart(context.Context, string, bool) (*remote.RestartResult, error) {
154- panic("unexpected call")
155-}
156-
157-func (c *resolveFromImageTestControlPlane) ListTags(context.Context, string) ([]string, error) {
158- panic("unexpected call")
159-}
160-
161-func (c *resolveFromImageTestControlPlane) ListBackups(context.Context, string) ([]dto.BackupJob, error) {
162- panic("unexpected call")
163-}
164-
165-func (c *resolveFromImageTestControlPlane) BackupStatus(context.Context) ([]dto.BackupJob, error) {
166- panic("unexpected call")
167-}
168-
169-func (c *resolveFromImageTestControlPlane) RunBackup(context.Context, string, string) (*dto.BackupRunResponse, error) {
170- panic("unexpected call")
171-}
172-
173-func (c *resolveFromImageTestControlPlane) DetectDatabases(context.Context, string) ([]dto.DatabaseInfo, error) {
174- panic("unexpected call")
175-}
176-
177-func (c *resolveFromImageTestControlPlane) GetProcessLogs(context.Context, int) ([]string, error) {
178- panic("unexpected call")
179-}
180-
181-func (c *resolveFromImageTestControlPlane) GetContainerLogs(context.Context, string, int) ([]string, error) {
182- panic("unexpected call")
183-}
184-
185-func (c *resolveFromImageTestControlPlane) StreamProcessLogs(context.Context, int) (<-chan string, error) {
186- panic("unexpected call")
187-}
188-
189-func (c *resolveFromImageTestControlPlane) StreamContainerLogs(context.Context, string, int) (<-chan string, error) {
190- panic("unexpected call")
191-}
192-
193-func (c *resolveFromImageTestControlPlane) ListVolumes(context.Context) ([]dto.Volume, error) {
194- panic("unexpected call")
195-}
196-
197-func (c *resolveFromImageTestControlPlane) PruneVolumes(context.Context, dto.VolumePruneRequest) (*dto.VolumePruneResponse, error) {
198- panic("unexpected call")
199-}
200-
201 func TestValidateBuildArg(t *testing.T) {21 func TestValidateBuildArg(t *testing.T) {
202 tests := []struct {22 tests := []struct {
203 name string23 name string
@@ -631,13 +451,10 @@ func TestParseLabelPair(t *testing.T) {
631 }451 }
632 452
633 func TestResolveFromImage_NoRouteSuggestsBootstrap(t *testing.T) {453 func TestResolveFromImage_NoRouteSuggestsBootstrap(t *testing.T) {
634- cp := &resolveFromImageTestControlPlane{454+ cpMock := climocks.NewMockControlPlane(t)
635- findRoutesByImage: func(context.Context, string) ([]domain.Route, error) {455+ cpMock.EXPECT().FindRoutesByImage(context.Background(), "myapp").Return(nil, nil).Once()
636- return nil, nil
637- },
638- }
639 456
640- _, _, _, err := resolveFromImage(context.Background(), cp, "myapp", "Dockerfile")457+ _, _, _, err := resolveFromImage(context.Background(), cpMock, "myapp", "Dockerfile")
641 458
642 assert.Error(t, err)459 assert.Error(t, err)
643 assert.Contains(t, err.Error(), `no route configured for image "myapp"`)460 assert.Contains(t, err.Error(), `no route configured for image "myapp"`)
@@ -645,18 +462,10 @@ func TestResolveFromImage_NoRouteSuggestsBootstrap(t *testing.T) {
645 }462 }
646 463
647 func TestResolveRoute_DottedBareImageUsesImageLookup(t *testing.T) {464 func TestResolveRoute_DottedBareImageUsesImageLookup(t *testing.T) {
648- cp := &resolveFromImageTestControlPlane{465+ cpMock := climocks.NewMockControlPlane(t)
649- findRoutesByImage: func(_ context.Context, imageName string) ([]domain.Route, error) {466+ cpMock.EXPECT().FindRoutesByImage(context.Background(), "my.app").Return([]domain.Route{{Domain: "app.example.com", Image: "registry.example.com/my.app:latest"}}, nil).Once()
650- assert.Equal(t, "my.app", imageName)
651- return []domain.Route{{Domain: "app.example.com", Image: "registry.example.com/my.app:latest"}}, nil
652- },
653- getRoute: func(context.Context, string) (*domain.Route, error) {
654- t.Fatalf("unexpected GetRoute call")
655- return nil, nil
656- },
657- }
658 467
659- registry, imageName, pushDomain, err := resolveRoute(context.Background(), cp, "my.app", "", "Dockerfile")468+ registry, imageName, pushDomain, err := resolveRoute(context.Background(), cpMock, "my.app", "", "Dockerfile")
660 469
661 assert.NoError(t, err)470 assert.NoError(t, err)
662 assert.Equal(t, "registry.example.com", registry)471 assert.Equal(t, "registry.example.com", registry)
@@ -665,21 +474,12 @@ func TestResolveRoute_DottedBareImageUsesImageLookup(t *testing.T) {
665 }474 }
666 475
667 func TestResolveRoute_DottedBareImageNoRoutesKeepsBootstrapError(t *testing.T) {476 func TestResolveRoute_DottedBareImageNoRoutesKeepsBootstrapError(t *testing.T) {
668- sawImageLookup := false477+ cpMock := climocks.NewMockControlPlane(t)
669- cp := &resolveFromImageTestControlPlane{478+ imageLookup := cpMock.EXPECT().FindRoutesByImage(context.Background(), "my.app").Return(nil, nil).Once()
670- findRoutesByImage: func(_ context.Context, imageName string) ([]domain.Route, error) {479+ routeLookup := cpMock.EXPECT().GetRoute(context.Background(), "my.app").Return(nil, domain.ErrRouteNotFound).Once()
671- sawImageLookup = true480+ mock.InOrder(imageLookup, routeLookup)
672- assert.Equal(t, "my.app", imageName)
673- return nil, nil
674- },
675- getRoute: func(_ context.Context, domainName string) (*domain.Route, error) {
676- assert.True(t, sawImageLookup, "expected image lookup before legacy domain lookup")
677- assert.Equal(t, "my.app", domainName)
678- return nil, domain.ErrRouteNotFound
679- },
680- }
681 481
682- _, _, _, err := resolveRoute(context.Background(), cp, "my.app", "", "Dockerfile")482+ _, _, _, err := resolveRoute(context.Background(), cpMock, "my.app", "", "Dockerfile")
683 483
684 assert.Error(t, err)484 assert.Error(t, err)
685 assert.True(t, errors.Is(err, domain.ErrNoRouteForImage))485 assert.True(t, errors.Is(err, domain.ErrNoRouteForImage))
@@ -696,21 +496,12 @@ func TestNoRouteForImageErrorWrapsSentinel(t *testing.T) {
696 }496 }
697 497
698 func TestResolveRoute_DottedBareDomainFallsBackToLegacyLookup(t *testing.T) {498 func TestResolveRoute_DottedBareDomainFallsBackToLegacyLookup(t *testing.T) {
699- sawImageLookup := false499+ cpMock := climocks.NewMockControlPlane(t)
700- cp := &resolveFromImageTestControlPlane{500+ imageLookup := cpMock.EXPECT().FindRoutesByImage(context.Background(), "app.example.com").Return(nil, nil).Once()
701- findRoutesByImage: func(_ context.Context, imageName string) ([]domain.Route, error) {501+ routeLookup := cpMock.EXPECT().GetRoute(context.Background(), "app.example.com").Return(&domain.Route{Domain: "app.example.com", Image: "registry.example.com/myapp:latest"}, nil).Once()
702- sawImageLookup = true502+ mock.InOrder(imageLookup, routeLookup)
703- assert.Equal(t, "app.example.com", imageName)
704- return nil, nil
705- },
706- getRoute: func(_ context.Context, domainName string) (*domain.Route, error) {
707- assert.True(t, sawImageLookup, "expected image lookup before legacy domain lookup")
708- assert.Equal(t, "app.example.com", domainName)
709- return &domain.Route{Domain: domainName, Image: "registry.example.com/myapp:latest"}, nil
710- },
711- }
712 503
713- registry, imageName, pushDomain, err := resolveRoute(context.Background(), cp, "app.example.com", "", "Dockerfile")504+ registry, imageName, pushDomain, err := resolveRoute(context.Background(), cpMock, "app.example.com", "", "Dockerfile")
714 505
715 assert.NoError(t, err)506 assert.NoError(t, err)
716 assert.Equal(t, "registry.example.com", registry)507 assert.Equal(t, "registry.example.com", registry)
@@ -719,14 +510,10 @@ func TestResolveRoute_DottedBareDomainFallsBackToLegacyLookup(t *testing.T) {
719 }510 }
720 511
721 func TestResolveRoute_TaggedImageStripsTagBeforeLookup(t *testing.T) {512 func TestResolveRoute_TaggedImageStripsTagBeforeLookup(t *testing.T) {
722- cp := &resolveFromImageTestControlPlane{513+ cpMock := climocks.NewMockControlPlane(t)
723- findRoutesByImage: func(_ context.Context, imageName string) ([]domain.Route, error) {514+ cpMock.EXPECT().FindRoutesByImage(context.Background(), "myapp").Return([]domain.Route{{Domain: "app.example.com", Image: "registry.example.com/myapp:latest"}}, nil).Once()
724- assert.Equal(t, "myapp", imageName)
725- return []domain.Route{{Domain: "app.example.com", Image: "registry.example.com/myapp:latest"}}, nil
726- },
727- }
728 515
729- registry, imageName, pushDomain, err := resolveRoute(context.Background(), cp, "myapp:v1.2.3", "", "Dockerfile")516+ registry, imageName, pushDomain, err := resolveRoute(context.Background(), cpMock, "myapp:v1.2.3", "", "Dockerfile")
730 517
731 assert.NoError(t, err)518 assert.NoError(t, err)
732 assert.Equal(t, "registry.example.com", registry)519 assert.Equal(t, "registry.example.com", registry)
@@ -735,14 +522,10 @@ func TestResolveRoute_TaggedImageStripsTagBeforeLookup(t *testing.T) {
735 }522 }
736 523
737 func TestResolveRoute_RegistryQualifiedTaggedImageUsesImageLookup(t *testing.T) {524 func TestResolveRoute_RegistryQualifiedTaggedImageUsesImageLookup(t *testing.T) {
738- cp := &resolveFromImageTestControlPlane{525+ cpMock := climocks.NewMockControlPlane(t)
739- findRoutesByImage: func(_ context.Context, imageName string) ([]domain.Route, error) {526+ cpMock.EXPECT().FindRoutesByImage(context.Background(), "registry.example.com/myapp").Return([]domain.Route{{Domain: "app.example.com", Image: "registry.example.com/myapp:latest"}}, nil).Once()
740- assert.Equal(t, "registry.example.com/myapp", imageName)
741- return []domain.Route{{Domain: "app.example.com", Image: "registry.example.com/myapp:latest"}}, nil
742- },
743- }
744 527
745- _, _, _, err := resolveRoute(context.Background(), cp, "registry.example.com/myapp:v1.2.3", "", "Dockerfile")528+ _, _, _, err := resolveRoute(context.Background(), cpMock, "registry.example.com/myapp:v1.2.3", "", "Dockerfile")
746 529
747 assert.NoError(t, err)530 assert.NoError(t, err)
748 }531 }
M internal/adapters/in/cli/routes.go +9 -13
@@ -33,9 +33,7 @@ func truncateImage(image string, maxLen int) string {
3333 return ""
3434 }
3535
36- if idx := strings.Index(image, "@sha256:"); idx != -1 {
37- name := image[:idx]
38- digest := image[idx+8:] // Skip "@sha256:"
36+ if name, digest, ok := strings.Cut(image, "@sha256:"); ok {
3937 if len(digest) > 12 {
4038 digest = digest[:12]
4139 }
@@ -349,17 +347,16 @@ func collectRoutesListAggregateSections(ctx context.Context, cfgPath string, dep
349347 remoteSections := make([]routeListSection, len(names))
350348 var wg sync.WaitGroup
351349 for i, name := range names {
352- i, name := i, name
353350 entry := remotes.entries[name]
354351 wg.Add(1)
355- go func() {
352+ go func(idx int, remoteName string, remoteEntry remote.RemoteEntry) {
356353 defer wg.Done()
357- section, err := deps.loadRemote(ctx, name, entry)
354+ section, err := deps.loadRemote(ctx, remoteName, remoteEntry)
358355 if err != nil && section.Error == "" {
359356 section.Error = err.Error()
360357 }
361- remoteSections[i] = normalizeRouteListRemoteSection(section, name, entry)
362- }()
358+ remoteSections[idx] = normalizeRouteListRemoteSection(section, remoteName, remoteEntry)
359+ }(i, name, entry)
363360 }
364361 wg.Wait()
365362
@@ -473,17 +470,16 @@ func collectRoutesStatusAggregateSections(ctx context.Context, cfgPath string, d
473470 remoteSections := make([]routeStatusSection, len(names))
474471 var wg sync.WaitGroup
475472 for i, name := range names {
476- i, name := i, name
477473 entry := remotes.entries[name]
478474 wg.Add(1)
479- go func() {
475+ go func(idx int, remoteName string, remoteEntry remote.RemoteEntry) {
480476 defer wg.Done()
481- section, err := deps.loadRemote(ctx, name, entry)
477+ section, err := deps.loadRemote(ctx, remoteName, remoteEntry)
482478 if err != nil && section.Error == "" {
483479 section.Error = err.Error()
484480 }
485- remoteSections[i] = normalizeRouteStatusRemoteSection(section, name, entry)
486- }()
481+ remoteSections[idx] = normalizeRouteStatusRemoteSection(section, remoteName, remoteEntry)
482+ }(i, name, entry)
487483 }
488484 wg.Wait()
489485
@@ -33,9 +33,7 @@ func truncateImage(image string, maxLen int) string {
33 return ""33 return ""
34 }34 }
35 35
36- if idx := strings.Index(image, "@sha256:"); idx != -1 {36+ if name, digest, ok := strings.Cut(image, "@sha256:"); ok {
37- name := image[:idx]
38- digest := image[idx+8:] // Skip "@sha256:"
39 if len(digest) > 12 {37 if len(digest) > 12 {
40 digest = digest[:12]38 digest = digest[:12]
41 }39 }
@@ -349,17 +347,16 @@ func collectRoutesListAggregateSections(ctx context.Context, cfgPath string, dep
349 remoteSections := make([]routeListSection, len(names))347 remoteSections := make([]routeListSection, len(names))
350 var wg sync.WaitGroup348 var wg sync.WaitGroup
351 for i, name := range names {349 for i, name := range names {
352- i, name := i, name
353 entry := remotes.entries[name]350 entry := remotes.entries[name]
354 wg.Add(1)351 wg.Add(1)
355- go func() {352+ go func(idx int, remoteName string, remoteEntry remote.RemoteEntry) {
356 defer wg.Done()353 defer wg.Done()
357- section, err := deps.loadRemote(ctx, name, entry)354+ section, err := deps.loadRemote(ctx, remoteName, remoteEntry)
358 if err != nil && section.Error == "" {355 if err != nil && section.Error == "" {
359 section.Error = err.Error()356 section.Error = err.Error()
360 }357 }
361- remoteSections[i] = normalizeRouteListRemoteSection(section, name, entry)358+ remoteSections[idx] = normalizeRouteListRemoteSection(section, remoteName, remoteEntry)
362- }()359+ }(i, name, entry)
363 }360 }
364 wg.Wait()361 wg.Wait()
365 362
@@ -473,17 +470,16 @@ func collectRoutesStatusAggregateSections(ctx context.Context, cfgPath string, d
473 remoteSections := make([]routeStatusSection, len(names))470 remoteSections := make([]routeStatusSection, len(names))
474 var wg sync.WaitGroup471 var wg sync.WaitGroup
475 for i, name := range names {472 for i, name := range names {
476- i, name := i, name
477 entry := remotes.entries[name]473 entry := remotes.entries[name]
478 wg.Add(1)474 wg.Add(1)
479- go func() {475+ go func(idx int, remoteName string, remoteEntry remote.RemoteEntry) {
480 defer wg.Done()476 defer wg.Done()
481- section, err := deps.loadRemote(ctx, name, entry)477+ section, err := deps.loadRemote(ctx, remoteName, remoteEntry)
482 if err != nil && section.Error == "" {478 if err != nil && section.Error == "" {
483 section.Error = err.Error()479 section.Error = err.Error()
484 }480 }
485- remoteSections[i] = normalizeRouteStatusRemoteSection(section, name, entry)481+ remoteSections[idx] = normalizeRouteStatusRemoteSection(section, remoteName, remoteEntry)
486- }()482+ }(i, name, entry)
487 }483 }
488 wg.Wait()484 wg.Wait()
489 485
M internal/adapters/in/cli/routes_test.go +34 -88
@@ -23,32 +23,6 @@ import (
2323 "github.com/stretchr/testify/require"
2424 )
2525
26-type routesShowTestControlPlane struct {
27- resolveFromImageTestControlPlane
28- getHealth func(context.Context) (map[string]*remote.RouteHealth, error)
29-}
30-
31-var _ ControlPlane = (*routesShowTestControlPlane)(nil)
32-
33-func (c *routesShowTestControlPlane) ListRoutesWithDetails(context.Context) ([]remote.RouteInfo, error) {
34- panic("unexpected call")
35-}
36-
37-func (c *routesShowTestControlPlane) GetHealth(ctx context.Context) (map[string]*remote.RouteHealth, error) {
38- if c.getHealth != nil {
39- return c.getHealth(ctx)
40- }
41- panic("unexpected call")
42-}
43-
44-func (c *routesShowTestControlPlane) ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error) {
45- panic("unexpected call")
46-}
47-
48-func (c *routesShowTestControlPlane) CleanupOrphanedAttachments(context.Context, string, bool) (*domain.CleanupReport, error) {
49- panic("unexpected call")
50-}
51-
5226 func TestTruncateImage(t *testing.T) {
5327 tests := []struct {
5428 name string
@@ -315,19 +289,12 @@ func TestRouteStatusTitle_PreservesProbeFailureError(t *testing.T) {
315289 }
316290
317291 func TestRunRoutesShow_JSONIncludesHealthError(t *testing.T) {
318- fake := &routesShowTestControlPlane{
319- resolveFromImageTestControlPlane: resolveFromImageTestControlPlane{
320- getRoute: func(context.Context, string) (*domain.Route, error) {
321- return &domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil
322- },
323- },
324- getHealth: func(context.Context) (map[string]*remote.RouteHealth, error) {
325- return nil, errors.New("probe failed")
326- },
327- }
292+ cpMock := climocks.NewMockControlPlane(t)
293+ cpMock.EXPECT().GetRoute(context.Background(), "app.example.com").Return(&domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil).Once()
294+ cpMock.EXPECT().GetHealth(context.Background()).Return(nil, errors.New("probe failed")).Once()
328295
329296 var out bytes.Buffer
330- err := runRoutesShow(context.Background(), fake, &out, "app.example.com", true)
297+ err := runRoutesShow(context.Background(), cpMock, &out, "app.example.com", true)
331298
332299 require.NoError(t, err)
333300
@@ -341,19 +308,12 @@ func TestRunRoutesShow_JSONIncludesHealthError(t *testing.T) {
341308 }
342309
343310 func TestRunRoutesShow_TextIncludesHealthWarning(t *testing.T) {
344- fake := &routesShowTestControlPlane{
345- resolveFromImageTestControlPlane: resolveFromImageTestControlPlane{
346- getRoute: func(context.Context, string) (*domain.Route, error) {
347- return &domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil
348- },
349- },
350- getHealth: func(context.Context) (map[string]*remote.RouteHealth, error) {
351- return nil, errors.New("probe failed")
352- },
353- }
311+ cpMock := climocks.NewMockControlPlane(t)
312+ cpMock.EXPECT().GetRoute(context.Background(), "app.example.com").Return(&domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil).Once()
313+ cpMock.EXPECT().GetHealth(context.Background()).Return(nil, errors.New("probe failed")).Once()
354314
355315 var out bytes.Buffer
356- err := runRoutesShow(context.Background(), fake, &out, "app.example.com", false)
316+ err := runRoutesShow(context.Background(), cpMock, &out, "app.example.com", false)
357317
358318 require.NoError(t, err)
359319
@@ -366,25 +326,18 @@ func TestRunRoutesShow_TextIncludesHealthWarning(t *testing.T) {
366326 }
367327
368328 func TestRunRoutesShow_JSONIncludesRouteProbeFailureAndUnknownContainerStatus(t *testing.T) {
369- fake := &routesShowTestControlPlane{
370- resolveFromImageTestControlPlane: resolveFromImageTestControlPlane{
371- getRoute: func(context.Context, string) (*domain.Route, error) {
372- return &domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil
373- },
374- },
375- getHealth: func(context.Context) (map[string]*remote.RouteHealth, error) {
376- return map[string]*remote.RouteHealth{
377- "app.example.com": {
378- HTTPStatus: 503,
379- ContainerStatus: "",
380- Error: "probe failed",
381- },
382- }, nil
329+ cpMock := climocks.NewMockControlPlane(t)
330+ cpMock.EXPECT().GetRoute(context.Background(), "app.example.com").Return(&domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil).Once()
331+ cpMock.EXPECT().GetHealth(context.Background()).Return(map[string]*remote.RouteHealth{
332+ "app.example.com": {
333+ HTTPStatus: 503,
334+ ContainerStatus: "",
335+ Error: "probe failed",
383336 },
384- }
337+ }, nil).Once()
385338
386339 var out bytes.Buffer
387- err := runRoutesShow(context.Background(), fake, &out, "app.example.com", true)
340+ err := runRoutesShow(context.Background(), cpMock, &out, "app.example.com", true)
388341
389342 require.NoError(t, err)
390343
@@ -398,25 +351,18 @@ func TestRunRoutesShow_JSONIncludesRouteProbeFailureAndUnknownContainerStatus(t
398351 }
399352
400353 func TestRunRoutesShow_TextIncludesRouteProbeFailureAndUnknownContainerStatus(t *testing.T) {
401- fake := &routesShowTestControlPlane{
402- resolveFromImageTestControlPlane: resolveFromImageTestControlPlane{
403- getRoute: func(context.Context, string) (*domain.Route, error) {
404- return &domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil
405- },
354+ cpMock := climocks.NewMockControlPlane(t)
355+ cpMock.EXPECT().GetRoute(context.Background(), "app.example.com").Return(&domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil).Once()
356+ cpMock.EXPECT().GetHealth(context.Background()).Return(map[string]*remote.RouteHealth{
357+ "app.example.com": {
358+ HTTPStatus: 503,
359+ ContainerStatus: "",
360+ Error: "probe failed",
406361 },
407- getHealth: func(context.Context) (map[string]*remote.RouteHealth, error) {
408- return map[string]*remote.RouteHealth{
409- "app.example.com": {
410- HTTPStatus: 503,
411- ContainerStatus: "",
412- Error: "probe failed",
413- },
414- }, nil
415- },
416- }
362+ }, nil).Once()
417363
418364 var out bytes.Buffer
419- err := runRoutesShow(context.Background(), fake, &out, "app.example.com", false)
365+ err := runRoutesShow(context.Background(), cpMock, &out, "app.example.com", false)
420366
421367 require.NoError(t, err)
422368
@@ -954,21 +900,21 @@ func TestAttachmentsForRouteDiagnosisUsesOwner(t *testing.T) {
954900 }
955901
956902 func TestRunRoutePurge_AttachmentsForceIsScopedToRoute(t *testing.T) {
957- cp := climocks.NewMockControlPlane(t)
903+ cpMock := climocks.NewMockControlPlane(t)
958904 orphanedAttachments := []domain.CleanupAttachment{
959905 {ContainerID: "app-attachment", Owner: "app.example.com", Name: "postgres"},
960906 {ContainerID: "other-attachment", Owner: "other.example.com", Name: "redis"},
961907 }
962- cp.EXPECT().GetRoute(context.Background(), "app.example.com").Return(&domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil).Once()
963- cp.EXPECT().ListRoutesWithDetails(context.Background()).Return(nil, nil).Once()
964- cp.EXPECT().GetHealth(context.Background()).Return(nil, nil).Once()
965- cp.EXPECT().ListVolumes(context.Background()).Return(nil, nil).Once()
966- cp.EXPECT().ListOrphanedAttachments(context.Background()).Return(orphanedAttachments, nil).Once()
967- cp.EXPECT().CleanupOrphanedAttachments(context.Background(), "app.example.com", true).Return(&domain.CleanupReport{
908+ cpMock.EXPECT().GetRoute(context.Background(), "app.example.com").Return(&domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil).Once()
909+ cpMock.EXPECT().ListRoutesWithDetails(context.Background()).Return(nil, nil).Once()
910+ cpMock.EXPECT().GetHealth(context.Background()).Return(nil, nil).Once()
911+ cpMock.EXPECT().ListVolumes(context.Background()).Return(nil, nil).Once()
912+ cpMock.EXPECT().ListOrphanedAttachments(context.Background()).Return(orphanedAttachments, nil).Once()
913+ cpMock.EXPECT().CleanupOrphanedAttachments(context.Background(), "app.example.com", true).Return(&domain.CleanupReport{
968914 RemovedContainers: []domain.CleanupContainer{{ID: "app-attachment", Name: "postgres"}},
969915 }, nil).Once()
970916
971- report, err := runRoutePurge(context.Background(), cp, "app.example.com", routePurgeOptions{Force: true, Attachments: true})
917+ report, err := runRoutePurge(context.Background(), cpMock, "app.example.com", routePurgeOptions{Force: true, Attachments: true})
972918
973919 require.NoError(t, err)
974920 require.NotNil(t, report)
@@ -23,32 +23,6 @@ import (
23 "github.com/stretchr/testify/require"23 "github.com/stretchr/testify/require"
24 )24 )
25 25
26-type routesShowTestControlPlane struct {
27- resolveFromImageTestControlPlane
28- getHealth func(context.Context) (map[string]*remote.RouteHealth, error)
29-}
30-
31-var _ ControlPlane = (*routesShowTestControlPlane)(nil)
32-
33-func (c *routesShowTestControlPlane) ListRoutesWithDetails(context.Context) ([]remote.RouteInfo, error) {
34- panic("unexpected call")
35-}
36-
37-func (c *routesShowTestControlPlane) GetHealth(ctx context.Context) (map[string]*remote.RouteHealth, error) {
38- if c.getHealth != nil {
39- return c.getHealth(ctx)
40- }
41- panic("unexpected call")
42-}
43-
44-func (c *routesShowTestControlPlane) ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error) {
45- panic("unexpected call")
46-}
47-
48-func (c *routesShowTestControlPlane) CleanupOrphanedAttachments(context.Context, string, bool) (*domain.CleanupReport, error) {
49- panic("unexpected call")
50-}
51-
52 func TestTruncateImage(t *testing.T) {26 func TestTruncateImage(t *testing.T) {
53 tests := []struct {27 tests := []struct {
54 name string28 name string
@@ -315,19 +289,12 @@ func TestRouteStatusTitle_PreservesProbeFailureError(t *testing.T) {
315 }289 }
316 290
317 func TestRunRoutesShow_JSONIncludesHealthError(t *testing.T) {291 func TestRunRoutesShow_JSONIncludesHealthError(t *testing.T) {
318- fake := &routesShowTestControlPlane{292+ cpMock := climocks.NewMockControlPlane(t)
319- resolveFromImageTestControlPlane: resolveFromImageTestControlPlane{293+ cpMock.EXPECT().GetRoute(context.Background(), "app.example.com").Return(&domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil).Once()
320- getRoute: func(context.Context, string) (*domain.Route, error) {294+ cpMock.EXPECT().GetHealth(context.Background()).Return(nil, errors.New("probe failed")).Once()
321- return &domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil
322- },
323- },
324- getHealth: func(context.Context) (map[string]*remote.RouteHealth, error) {
325- return nil, errors.New("probe failed")
326- },
327- }
328 295
329 var out bytes.Buffer296 var out bytes.Buffer
330- err := runRoutesShow(context.Background(), fake, &out, "app.example.com", true)297+ err := runRoutesShow(context.Background(), cpMock, &out, "app.example.com", true)
331 298
332 require.NoError(t, err)299 require.NoError(t, err)
333 300
@@ -341,19 +308,12 @@ func TestRunRoutesShow_JSONIncludesHealthError(t *testing.T) {
341 }308 }
342 309
343 func TestRunRoutesShow_TextIncludesHealthWarning(t *testing.T) {310 func TestRunRoutesShow_TextIncludesHealthWarning(t *testing.T) {
344- fake := &routesShowTestControlPlane{311+ cpMock := climocks.NewMockControlPlane(t)
345- resolveFromImageTestControlPlane: resolveFromImageTestControlPlane{312+ cpMock.EXPECT().GetRoute(context.Background(), "app.example.com").Return(&domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil).Once()
346- getRoute: func(context.Context, string) (*domain.Route, error) {313+ cpMock.EXPECT().GetHealth(context.Background()).Return(nil, errors.New("probe failed")).Once()
347- return &domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil
348- },
349- },
350- getHealth: func(context.Context) (map[string]*remote.RouteHealth, error) {
351- return nil, errors.New("probe failed")
352- },
353- }
354 314
355 var out bytes.Buffer315 var out bytes.Buffer
356- err := runRoutesShow(context.Background(), fake, &out, "app.example.com", false)316+ err := runRoutesShow(context.Background(), cpMock, &out, "app.example.com", false)
357 317
358 require.NoError(t, err)318 require.NoError(t, err)
359 319
@@ -366,25 +326,18 @@ func TestRunRoutesShow_TextIncludesHealthWarning(t *testing.T) {
366 }326 }
367 327
368 func TestRunRoutesShow_JSONIncludesRouteProbeFailureAndUnknownContainerStatus(t *testing.T) {328 func TestRunRoutesShow_JSONIncludesRouteProbeFailureAndUnknownContainerStatus(t *testing.T) {
369- fake := &routesShowTestControlPlane{329+ cpMock := climocks.NewMockControlPlane(t)
370- resolveFromImageTestControlPlane: resolveFromImageTestControlPlane{330+ cpMock.EXPECT().GetRoute(context.Background(), "app.example.com").Return(&domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil).Once()
371- getRoute: func(context.Context, string) (*domain.Route, error) {331+ cpMock.EXPECT().GetHealth(context.Background()).Return(map[string]*remote.RouteHealth{
372- return &domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil332+ "app.example.com": {
373- },333+ HTTPStatus: 503,
374- },334+ ContainerStatus: "",
375- getHealth: func(context.Context) (map[string]*remote.RouteHealth, error) {335+ Error: "probe failed",
376- return map[string]*remote.RouteHealth{
377- "app.example.com": {
378- HTTPStatus: 503,
379- ContainerStatus: "",
380- Error: "probe failed",
381- },
382- }, nil
383 },336 },
384- }337+ }, nil).Once()
385 338
386 var out bytes.Buffer339 var out bytes.Buffer
387- err := runRoutesShow(context.Background(), fake, &out, "app.example.com", true)340+ err := runRoutesShow(context.Background(), cpMock, &out, "app.example.com", true)
388 341
389 require.NoError(t, err)342 require.NoError(t, err)
390 343
@@ -398,25 +351,18 @@ func TestRunRoutesShow_JSONIncludesRouteProbeFailureAndUnknownContainerStatus(t
398 }351 }
399 352
400 func TestRunRoutesShow_TextIncludesRouteProbeFailureAndUnknownContainerStatus(t *testing.T) {353 func TestRunRoutesShow_TextIncludesRouteProbeFailureAndUnknownContainerStatus(t *testing.T) {
401- fake := &routesShowTestControlPlane{354+ cpMock := climocks.NewMockControlPlane(t)
402- resolveFromImageTestControlPlane: resolveFromImageTestControlPlane{355+ cpMock.EXPECT().GetRoute(context.Background(), "app.example.com").Return(&domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil).Once()
403- getRoute: func(context.Context, string) (*domain.Route, error) {356+ cpMock.EXPECT().GetHealth(context.Background()).Return(map[string]*remote.RouteHealth{
404- return &domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil357+ "app.example.com": {
405- },358+ HTTPStatus: 503,
359+ ContainerStatus: "",
360+ Error: "probe failed",
406 },361 },
407- getHealth: func(context.Context) (map[string]*remote.RouteHealth, error) {362+ }, nil).Once()
408- return map[string]*remote.RouteHealth{
409- "app.example.com": {
410- HTTPStatus: 503,
411- ContainerStatus: "",
412- Error: "probe failed",
413- },
414- }, nil
415- },
416- }
417 363
418 var out bytes.Buffer364 var out bytes.Buffer
419- err := runRoutesShow(context.Background(), fake, &out, "app.example.com", false)365+ err := runRoutesShow(context.Background(), cpMock, &out, "app.example.com", false)
420 366
421 require.NoError(t, err)367 require.NoError(t, err)
422 368
@@ -954,21 +900,21 @@ func TestAttachmentsForRouteDiagnosisUsesOwner(t *testing.T) {
954 }900 }
955 901
956 func TestRunRoutePurge_AttachmentsForceIsScopedToRoute(t *testing.T) {902 func TestRunRoutePurge_AttachmentsForceIsScopedToRoute(t *testing.T) {
957- cp := climocks.NewMockControlPlane(t)903+ cpMock := climocks.NewMockControlPlane(t)
958 orphanedAttachments := []domain.CleanupAttachment{904 orphanedAttachments := []domain.CleanupAttachment{
959 {ContainerID: "app-attachment", Owner: "app.example.com", Name: "postgres"},905 {ContainerID: "app-attachment", Owner: "app.example.com", Name: "postgres"},
960 {ContainerID: "other-attachment", Owner: "other.example.com", Name: "redis"},906 {ContainerID: "other-attachment", Owner: "other.example.com", Name: "redis"},
961 }907 }
962- cp.EXPECT().GetRoute(context.Background(), "app.example.com").Return(&domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil).Once()908+ cpMock.EXPECT().GetRoute(context.Background(), "app.example.com").Return(&domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil).Once()
963- cp.EXPECT().ListRoutesWithDetails(context.Background()).Return(nil, nil).Once()909+ cpMock.EXPECT().ListRoutesWithDetails(context.Background()).Return(nil, nil).Once()
964- cp.EXPECT().GetHealth(context.Background()).Return(nil, nil).Once()910+ cpMock.EXPECT().GetHealth(context.Background()).Return(nil, nil).Once()
965- cp.EXPECT().ListVolumes(context.Background()).Return(nil, nil).Once()911+ cpMock.EXPECT().ListVolumes(context.Background()).Return(nil, nil).Once()
966- cp.EXPECT().ListOrphanedAttachments(context.Background()).Return(orphanedAttachments, nil).Once()912+ cpMock.EXPECT().ListOrphanedAttachments(context.Background()).Return(orphanedAttachments, nil).Once()
967- cp.EXPECT().CleanupOrphanedAttachments(context.Background(), "app.example.com", true).Return(&domain.CleanupReport{913+ cpMock.EXPECT().CleanupOrphanedAttachments(context.Background(), "app.example.com", true).Return(&domain.CleanupReport{
968 RemovedContainers: []domain.CleanupContainer{{ID: "app-attachment", Name: "postgres"}},914 RemovedContainers: []domain.CleanupContainer{{ID: "app-attachment", Name: "postgres"}},
969 }, nil).Once()915 }, nil).Once()
970 916
971- report, err := runRoutePurge(context.Background(), cp, "app.example.com", routePurgeOptions{Force: true, Attachments: true})917+ report, err := runRoutePurge(context.Background(), cpMock, "app.example.com", routePurgeOptions{Force: true, Attachments: true})
972 918
973 require.NoError(t, err)919 require.NoError(t, err)
974 require.NotNil(t, report)920 require.NotNil(t, report)
M internal/adapters/in/cli/tls_test.go +49 -234
@@ -11,229 +11,47 @@ import (
1111 "github.com/stretchr/testify/require"
1212
1313 "github.com/bnema/gordon/internal/adapters/dto"
14- "github.com/bnema/gordon/internal/adapters/in/cli/remote"
15- "github.com/bnema/gordon/internal/domain"
14+ climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
1615 )
1716
18-// tlsTestControlPlane implements ControlPlane for TLS tests.
19-type tlsTestControlPlane struct {
20- getTLSStatus func(context.Context) (*dto.TLSStatusResponse, error)
21-}
22-
23-var _ ControlPlane = (*tlsTestControlPlane)(nil)
24-
25-func (c *tlsTestControlPlane) ListRoutesWithDetails(context.Context) ([]remote.RouteInfo, error) {
26- panic("unexpected call")
27-}
28-
29-func (c *tlsTestControlPlane) GetHealth(context.Context) (map[string]*remote.RouteHealth, error) {
30- panic("unexpected call")
31-}
32-
33-func (c *tlsTestControlPlane) GetRoute(context.Context, string) (*domain.Route, error) {
34- panic("unexpected call")
35-}
36-
37-func (c *tlsTestControlPlane) FindRoutesByImage(context.Context, string) ([]domain.Route, error) {
38- panic("unexpected call")
39-}
40-
41-func (c *tlsTestControlPlane) AddRoute(context.Context, domain.Route) error {
42- panic("unexpected call")
43-}
44-
45-func (c *tlsTestControlPlane) UpdateRoute(context.Context, domain.Route) error {
46- panic("unexpected call")
47-}
48-
49-func (c *tlsTestControlPlane) RemoveRoute(context.Context, string) error {
50- panic("unexpected call")
51-}
52-
53-func (c *tlsTestControlPlane) Bootstrap(context.Context, dto.BootstrapRequest) (*dto.BootstrapResponse, error) {
54- panic("unexpected call")
55-}
56-
57-func (c *tlsTestControlPlane) ListSecretsWithAttachments(context.Context, string) (*remote.SecretsListResult, error) {
58- panic("unexpected call")
59-}
60-
61-func (c *tlsTestControlPlane) SetSecrets(context.Context, string, map[string]string) error {
62- panic("unexpected call")
63-}
64-
65-func (c *tlsTestControlPlane) DeleteSecret(context.Context, string, string) error {
66- panic("unexpected call")
67-}
68-
69-func (c *tlsTestControlPlane) SetAttachmentSecrets(context.Context, string, string, map[string]string) error {
70- panic("unexpected call")
71-}
72-
73-func (c *tlsTestControlPlane) DeleteAttachmentSecret(context.Context, string, string, string) error {
74- panic("unexpected call")
75-}
76-
77-func (c *tlsTestControlPlane) GetAllAttachmentsConfig(context.Context) (map[string][]string, error) {
78- panic("unexpected call")
79-}
80-
81-func (c *tlsTestControlPlane) GetAttachmentsConfig(context.Context, string) ([]string, error) {
82- panic("unexpected call")
83-}
84-
85-func (c *tlsTestControlPlane) ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error) {
86- panic("unexpected call")
87-}
88-
89-func (c *tlsTestControlPlane) CleanupOrphanedAttachments(context.Context, string, bool) (*domain.CleanupReport, error) {
90- panic("unexpected call")
91-}
92-
93-func (c *tlsTestControlPlane) FindAttachmentTargetsByImage(context.Context, string) ([]string, error) {
94- panic("unexpected call")
95-}
96-
97-func (c *tlsTestControlPlane) AddAttachment(context.Context, string, string) error {
98- panic("unexpected call")
99-}
100-
101-func (c *tlsTestControlPlane) RemoveAttachment(context.Context, string, string) error {
102- panic("unexpected call")
103-}
104-
105-func (c *tlsTestControlPlane) GetAutoRouteAllowedDomains(context.Context) ([]string, error) {
106- panic("unexpected call")
107-}
108-
109-func (c *tlsTestControlPlane) AddAutoRouteAllowedDomain(context.Context, string) error {
110- panic("unexpected call")
111-}
112-
113-func (c *tlsTestControlPlane) RemoveAutoRouteAllowedDomain(context.Context, string) error {
114- panic("unexpected call")
115-}
116-
117-func (c *tlsTestControlPlane) GetTLSStatus(ctx context.Context) (*dto.TLSStatusResponse, error) {
118- if c.getTLSStatus != nil {
119- return c.getTLSStatus(ctx)
120- }
121- return nil, nil
122-}
123-
124-func (c *tlsTestControlPlane) GetStatus(context.Context) (*remote.Status, error) {
125- panic("unexpected call")
126-}
127-
128-func (c *tlsTestControlPlane) Reload(context.Context) error {
129- panic("unexpected call")
130-}
131-
132-func (c *tlsTestControlPlane) ListNetworks(context.Context) ([]*domain.NetworkInfo, error) {
133- panic("unexpected call")
134-}
135-
136-func (c *tlsTestControlPlane) GetConfig(context.Context) (*remote.Config, error) {
137- panic("unexpected call")
138-}
139-
140-func (c *tlsTestControlPlane) DeployIntent(context.Context, string) error {
141- panic("unexpected call")
142-}
143-
144-func (c *tlsTestControlPlane) Deploy(context.Context, string) (*remote.DeployResult, error) {
145- panic("unexpected call")
146-}
147-
148-func (c *tlsTestControlPlane) Restart(context.Context, string, bool) (*remote.RestartResult, error) {
149- panic("unexpected call")
150-}
151-
152-func (c *tlsTestControlPlane) ListTags(context.Context, string) ([]string, error) {
153- panic("unexpected call")
154-}
155-
156-func (c *tlsTestControlPlane) ListBackups(context.Context, string) ([]dto.BackupJob, error) {
157- panic("unexpected call")
158-}
159-
160-func (c *tlsTestControlPlane) BackupStatus(context.Context) ([]dto.BackupJob, error) {
161- panic("unexpected call")
162-}
163-
164-func (c *tlsTestControlPlane) RunBackup(context.Context, string, string) (*dto.BackupRunResponse, error) {
165- panic("unexpected call")
166-}
167-
168-func (c *tlsTestControlPlane) DetectDatabases(context.Context, string) ([]dto.DatabaseInfo, error) {
169- panic("unexpected call")
170-}
171-
172-func (c *tlsTestControlPlane) GetProcessLogs(context.Context, int) ([]string, error) {
173- panic("unexpected call")
174-}
175-
176-func (c *tlsTestControlPlane) GetContainerLogs(context.Context, string, int) ([]string, error) {
177- panic("unexpected call")
178-}
179-
180-func (c *tlsTestControlPlane) StreamProcessLogs(context.Context, int) (<-chan string, error) {
181- panic("unexpected call")
182-}
183-
184-func (c *tlsTestControlPlane) StreamContainerLogs(context.Context, string, int) (<-chan string, error) {
185- panic("unexpected call")
186-}
187-
188-func (c *tlsTestControlPlane) ListVolumes(context.Context) ([]dto.Volume, error) {
189- panic("unexpected call")
190-}
191-
192-func (c *tlsTestControlPlane) PruneVolumes(context.Context, dto.VolumePruneRequest) (*dto.VolumePruneResponse, error) {
193- panic("unexpected call")
194-}
195-
19617 func TestTLSStatus_HumanOutput(t *testing.T) {
19718 notAfter := time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC)
19819
199- cp := &tlsTestControlPlane{
200- getTLSStatus: func(_ context.Context) (*dto.TLSStatusResponse, error) {
201- return &dto.TLSStatusResponse{
202- ACMEEnabled: true,
203- ConfiguredMode: "acme",
204- EffectiveMode: "acme-staging",
205- SelectionReason: "configured",
206- TokenSource: "env",
207- Certificates: []dto.TLSCertificateEntry{
208- {
209- ID: "cert-abc123",
210- Names: []string{"example.com", "www.example.com"},
211- Status: "valid",
212- NotAfter: notAfter,
213- LastError: "",
214- },
215- },
216- Routes: []dto.TLSRouteCoverage{
217- {
218- Domain: "example.com",
219- Covered: true,
220- CoveredBy: "cert-abc123",
221- RequiredACME: true,
222- },
223- {
224- Domain: "internal.local",
225- Covered: false,
226- RequiredACME: false,
227- Error: "self-signed cert",
228- },
229- },
230- Errors: []string{"route internal.local has no ACME cert"},
231- }, nil
20+ cpMock := climocks.NewMockControlPlane(t)
21+ cpMock.EXPECT().GetTLSStatus(context.Background()).Return(&dto.TLSStatusResponse{
22+ ACMEEnabled: true,
23+ ConfiguredMode: "acme",
24+ EffectiveMode: "acme-staging",
25+ SelectionReason: "configured",
26+ TokenSource: "env",
27+ Certificates: []dto.TLSCertificateEntry{
28+ {
29+ ID: "cert-abc123",
30+ Names: []string{"example.com", "www.example.com"},
31+ Status: "valid",
32+ NotAfter: notAfter,
33+ LastError: "",
34+ },
35+ },
36+ Routes: []dto.TLSRouteCoverage{
37+ {
38+ Domain: "example.com",
39+ Covered: true,
40+ CoveredBy: "cert-abc123",
41+ RequiredACME: true,
42+ },
43+ {
44+ Domain: "internal.local",
45+ Covered: false,
46+ RequiredACME: false,
47+ Error: "self-signed cert",
48+ },
23249 },
233- }
50+ Errors: []string{"route internal.local has no ACME cert"},
51+ }, nil).Once()
23452
23553 var buf bytes.Buffer
236- err := runTLSStatusCmd(context.Background(), cp, &buf, false)
54+ err := runTLSStatusCmd(context.Background(), cpMock, &buf, false)
23755 require.NoError(t, err)
23856
23957 output := buf.String()
@@ -273,28 +91,25 @@ func TestTLSStatus_HumanOutput(t *testing.T) {
27391 func TestTLSStatus_JSONOutput(t *testing.T) {
27492 notAfter := time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC)
27593
276- cp := &tlsTestControlPlane{
277- getTLSStatus: func(_ context.Context) (*dto.TLSStatusResponse, error) {
278- return &dto.TLSStatusResponse{
279- ACMEEnabled: true,
280- ConfiguredMode: "acme",
281- EffectiveMode: "acme-staging",
282- TokenSource: "env",
283- Certificates: []dto.TLSCertificateEntry{
284- {
285- ID: "cert-abc123",
286- Names: []string{"example.com", "www.example.com"},
287- Status: "valid",
288- NotAfter: notAfter,
289- LastError: "",
290- },
291- },
292- }, nil
94+ cpMock := climocks.NewMockControlPlane(t)
95+ cpMock.EXPECT().GetTLSStatus(context.Background()).Return(&dto.TLSStatusResponse{
96+ ACMEEnabled: true,
97+ ConfiguredMode: "acme",
98+ EffectiveMode: "acme-staging",
99+ TokenSource: "env",
100+ Certificates: []dto.TLSCertificateEntry{
101+ {
102+ ID: "cert-abc123",
103+ Names: []string{"example.com", "www.example.com"},
104+ Status: "valid",
105+ NotAfter: notAfter,
106+ LastError: "",
107+ },
293108 },
294- }
109+ }, nil).Once()
295110
296111 var buf bytes.Buffer
297- err := runTLSStatusCmd(context.Background(), cp, &buf, true)
112+ err := runTLSStatusCmd(context.Background(), cpMock, &buf, true)
298113 require.NoError(t, err)
299114
300115 var result dto.TLSStatusResponse
@@ -11,229 +11,47 @@ import (
11 "github.com/stretchr/testify/require"11 "github.com/stretchr/testify/require"
12 12
13 "github.com/bnema/gordon/internal/adapters/dto"13 "github.com/bnema/gordon/internal/adapters/dto"
14- "github.com/bnema/gordon/internal/adapters/in/cli/remote"14+ climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
15- "github.com/bnema/gordon/internal/domain"
16 )15 )
17 16
18-// tlsTestControlPlane implements ControlPlane for TLS tests.
19-type tlsTestControlPlane struct {
20- getTLSStatus func(context.Context) (*dto.TLSStatusResponse, error)
21-}
22-
23-var _ ControlPlane = (*tlsTestControlPlane)(nil)
24-
25-func (c *tlsTestControlPlane) ListRoutesWithDetails(context.Context) ([]remote.RouteInfo, error) {
26- panic("unexpected call")
27-}
28-
29-func (c *tlsTestControlPlane) GetHealth(context.Context) (map[string]*remote.RouteHealth, error) {
30- panic("unexpected call")
31-}
32-
33-func (c *tlsTestControlPlane) GetRoute(context.Context, string) (*domain.Route, error) {
34- panic("unexpected call")
35-}
36-
37-func (c *tlsTestControlPlane) FindRoutesByImage(context.Context, string) ([]domain.Route, error) {
38- panic("unexpected call")
39-}
40-
41-func (c *tlsTestControlPlane) AddRoute(context.Context, domain.Route) error {
42- panic("unexpected call")
43-}
44-
45-func (c *tlsTestControlPlane) UpdateRoute(context.Context, domain.Route) error {
46- panic("unexpected call")
47-}
48-
49-func (c *tlsTestControlPlane) RemoveRoute(context.Context, string) error {
50- panic("unexpected call")
51-}
52-
53-func (c *tlsTestControlPlane) Bootstrap(context.Context, dto.BootstrapRequest) (*dto.BootstrapResponse, error) {
54- panic("unexpected call")
55-}
56-
57-func (c *tlsTestControlPlane) ListSecretsWithAttachments(context.Context, string) (*remote.SecretsListResult, error) {
58- panic("unexpected call")
59-}
60-
61-func (c *tlsTestControlPlane) SetSecrets(context.Context, string, map[string]string) error {
62- panic("unexpected call")
63-}
64-
65-func (c *tlsTestControlPlane) DeleteSecret(context.Context, string, string) error {
66- panic("unexpected call")
67-}
68-
69-func (c *tlsTestControlPlane) SetAttachmentSecrets(context.Context, string, string, map[string]string) error {
70- panic("unexpected call")
71-}
72-
73-func (c *tlsTestControlPlane) DeleteAttachmentSecret(context.Context, string, string, string) error {
74- panic("unexpected call")
75-}
76-
77-func (c *tlsTestControlPlane) GetAllAttachmentsConfig(context.Context) (map[string][]string, error) {
78- panic("unexpected call")
79-}
80-
81-func (c *tlsTestControlPlane) GetAttachmentsConfig(context.Context, string) ([]string, error) {
82- panic("unexpected call")
83-}
84-
85-func (c *tlsTestControlPlane) ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error) {
86- panic("unexpected call")
87-}
88-
89-func (c *tlsTestControlPlane) CleanupOrphanedAttachments(context.Context, string, bool) (*domain.CleanupReport, error) {
90- panic("unexpected call")
91-}
92-
93-func (c *tlsTestControlPlane) FindAttachmentTargetsByImage(context.Context, string) ([]string, error) {
94- panic("unexpected call")
95-}
96-
97-func (c *tlsTestControlPlane) AddAttachment(context.Context, string, string) error {
98- panic("unexpected call")
99-}
100-
101-func (c *tlsTestControlPlane) RemoveAttachment(context.Context, string, string) error {
102- panic("unexpected call")
103-}
104-
105-func (c *tlsTestControlPlane) GetAutoRouteAllowedDomains(context.Context) ([]string, error) {
106- panic("unexpected call")
107-}
108-
109-func (c *tlsTestControlPlane) AddAutoRouteAllowedDomain(context.Context, string) error {
110- panic("unexpected call")
111-}
112-
113-func (c *tlsTestControlPlane) RemoveAutoRouteAllowedDomain(context.Context, string) error {
114- panic("unexpected call")
115-}
116-
117-func (c *tlsTestControlPlane) GetTLSStatus(ctx context.Context) (*dto.TLSStatusResponse, error) {
118- if c.getTLSStatus != nil {
119- return c.getTLSStatus(ctx)
120- }
121- return nil, nil
122-}
123-
124-func (c *tlsTestControlPlane) GetStatus(context.Context) (*remote.Status, error) {
125- panic("unexpected call")
126-}
127-
128-func (c *tlsTestControlPlane) Reload(context.Context) error {
129- panic("unexpected call")
130-}
131-
132-func (c *tlsTestControlPlane) ListNetworks(context.Context) ([]*domain.NetworkInfo, error) {
133- panic("unexpected call")
134-}
135-
136-func (c *tlsTestControlPlane) GetConfig(context.Context) (*remote.Config, error) {
137- panic("unexpected call")
138-}
139-
140-func (c *tlsTestControlPlane) DeployIntent(context.Context, string) error {
141- panic("unexpected call")
142-}
143-
144-func (c *tlsTestControlPlane) Deploy(context.Context, string) (*remote.DeployResult, error) {
145- panic("unexpected call")
146-}
147-
148-func (c *tlsTestControlPlane) Restart(context.Context, string, bool) (*remote.RestartResult, error) {
149- panic("unexpected call")
150-}
151-
152-func (c *tlsTestControlPlane) ListTags(context.Context, string) ([]string, error) {
153- panic("unexpected call")
154-}
155-
156-func (c *tlsTestControlPlane) ListBackups(context.Context, string) ([]dto.BackupJob, error) {
157- panic("unexpected call")
158-}
159-
160-func (c *tlsTestControlPlane) BackupStatus(context.Context) ([]dto.BackupJob, error) {
161- panic("unexpected call")
162-}
163-
164-func (c *tlsTestControlPlane) RunBackup(context.Context, string, string) (*dto.BackupRunResponse, error) {
165- panic("unexpected call")
166-}
167-
168-func (c *tlsTestControlPlane) DetectDatabases(context.Context, string) ([]dto.DatabaseInfo, error) {
169- panic("unexpected call")
170-}
171-
172-func (c *tlsTestControlPlane) GetProcessLogs(context.Context, int) ([]string, error) {
173- panic("unexpected call")
174-}
175-
176-func (c *tlsTestControlPlane) GetContainerLogs(context.Context, string, int) ([]string, error) {
177- panic("unexpected call")
178-}
179-
180-func (c *tlsTestControlPlane) StreamProcessLogs(context.Context, int) (<-chan string, error) {
181- panic("unexpected call")
182-}
183-
184-func (c *tlsTestControlPlane) StreamContainerLogs(context.Context, string, int) (<-chan string, error) {
185- panic("unexpected call")
186-}
187-
188-func (c *tlsTestControlPlane) ListVolumes(context.Context) ([]dto.Volume, error) {
189- panic("unexpected call")
190-}
191-
192-func (c *tlsTestControlPlane) PruneVolumes(context.Context, dto.VolumePruneRequest) (*dto.VolumePruneResponse, error) {
193- panic("unexpected call")
194-}
195-
196 func TestTLSStatus_HumanOutput(t *testing.T) {17 func TestTLSStatus_HumanOutput(t *testing.T) {
197 notAfter := time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC)18 notAfter := time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC)
198 19
199- cp := &tlsTestControlPlane{20+ cpMock := climocks.NewMockControlPlane(t)
200- getTLSStatus: func(_ context.Context) (*dto.TLSStatusResponse, error) {21+ cpMock.EXPECT().GetTLSStatus(context.Background()).Return(&dto.TLSStatusResponse{
201- return &dto.TLSStatusResponse{22+ ACMEEnabled: true,
202- ACMEEnabled: true,23+ ConfiguredMode: "acme",
203- ConfiguredMode: "acme",24+ EffectiveMode: "acme-staging",
204- EffectiveMode: "acme-staging",25+ SelectionReason: "configured",
205- SelectionReason: "configured",26+ TokenSource: "env",
206- TokenSource: "env",27+ Certificates: []dto.TLSCertificateEntry{
207- Certificates: []dto.TLSCertificateEntry{28+ {
208- {29+ ID: "cert-abc123",
209- ID: "cert-abc123",30+ Names: []string{"example.com", "www.example.com"},
210- Names: []string{"example.com", "www.example.com"},31+ Status: "valid",
211- Status: "valid",32+ NotAfter: notAfter,
212- NotAfter: notAfter,33+ LastError: "",
213- LastError: "",34+ },
214- },35+ },
215- },36+ Routes: []dto.TLSRouteCoverage{
216- Routes: []dto.TLSRouteCoverage{37+ {
217- {38+ Domain: "example.com",
218- Domain: "example.com",39+ Covered: true,
219- Covered: true,40+ CoveredBy: "cert-abc123",
220- CoveredBy: "cert-abc123",41+ RequiredACME: true,
221- RequiredACME: true,42+ },
222- },43+ {
223- {44+ Domain: "internal.local",
224- Domain: "internal.local",45+ Covered: false,
225- Covered: false,46+ RequiredACME: false,
226- RequiredACME: false,47+ Error: "self-signed cert",
227- Error: "self-signed cert",48+ },
228- },
229- },
230- Errors: []string{"route internal.local has no ACME cert"},
231- }, nil
232 },49 },
233- }50+ Errors: []string{"route internal.local has no ACME cert"},
51+ }, nil).Once()
234 52
235 var buf bytes.Buffer53 var buf bytes.Buffer
236- err := runTLSStatusCmd(context.Background(), cp, &buf, false)54+ err := runTLSStatusCmd(context.Background(), cpMock, &buf, false)
237 require.NoError(t, err)55 require.NoError(t, err)
238 56
239 output := buf.String()57 output := buf.String()
@@ -273,28 +91,25 @@ func TestTLSStatus_HumanOutput(t *testing.T) {
273 func TestTLSStatus_JSONOutput(t *testing.T) {91 func TestTLSStatus_JSONOutput(t *testing.T) {
274 notAfter := time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC)92 notAfter := time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC)
275 93
276- cp := &tlsTestControlPlane{94+ cpMock := climocks.NewMockControlPlane(t)
277- getTLSStatus: func(_ context.Context) (*dto.TLSStatusResponse, error) {95+ cpMock.EXPECT().GetTLSStatus(context.Background()).Return(&dto.TLSStatusResponse{
278- return &dto.TLSStatusResponse{96+ ACMEEnabled: true,
279- ACMEEnabled: true,97+ ConfiguredMode: "acme",
280- ConfiguredMode: "acme",98+ EffectiveMode: "acme-staging",
281- EffectiveMode: "acme-staging",99+ TokenSource: "env",
282- TokenSource: "env",100+ Certificates: []dto.TLSCertificateEntry{
283- Certificates: []dto.TLSCertificateEntry{101+ {
284- {102+ ID: "cert-abc123",
285- ID: "cert-abc123",103+ Names: []string{"example.com", "www.example.com"},
286- Names: []string{"example.com", "www.example.com"},104+ Status: "valid",
287- Status: "valid",105+ NotAfter: notAfter,
288- NotAfter: notAfter,106+ LastError: "",
289- LastError: "",107+ },
290- },
291- },
292- }, nil
293 },108 },
294- }109+ }, nil).Once()
295 110
296 var buf bytes.Buffer111 var buf bytes.Buffer
297- err := runTLSStatusCmd(context.Background(), cp, &buf, true)112+ err := runTLSStatusCmd(context.Background(), cpMock, &buf, true)
298 require.NoError(t, err)113 require.NoError(t, err)
299 114
300 var result dto.TLSStatusResponse115 var result dto.TLSStatusResponse
M internal/adapters/in/cli/ui_adoption_test.go +11 -25
@@ -12,6 +12,7 @@ import (
1212 "testing"
1313
1414 "github.com/bnema/gordon/internal/adapters/dto"
15+ climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
1516 )
1617
1718 var uiAdoptionHelperCalls = map[string]struct{}{
@@ -54,8 +55,8 @@ func TestPresentationHelpers(t *testing.T) {
5455 }
5556
5657 func TestUIAdoption(t *testing.T) {
57- for _, expect := range uiAdoptionExpectations {
58- expect := expect
58+ for i := range uiAdoptionExpectations {
59+ expect := uiAdoptionExpectations[i]
5960 t.Run(expect.family, func(t *testing.T) {
6061 fset := token.NewFileSet()
6162 fileNode, err := parser.ParseFile(fset, expect.file, nil, parser.AllErrors)
@@ -64,7 +65,6 @@ func TestUIAdoption(t *testing.T) {
6465 }
6566
6667 for _, fnName := range expect.functions {
67- fnName := fnName
6868 t.Run(fnName, func(t *testing.T) {
6969 fn := findFuncDecl(fileNode, fnName)
7070 if fn == nil {
@@ -150,26 +150,6 @@ func isForbiddenRawPrintCall(call *ast.CallExpr, sel *ast.SelectorExpr) bool {
150150 return false
151151 }
152152
153-type imagesClientStub struct {
154- listResp []dto.Image
155- pruneResp *dto.ImagePruneResponse
156- err error
157-}
158-
159-func (s imagesClientStub) ListImages(context.Context) ([]dto.Image, error) {
160- if s.err != nil {
161- return nil, s.err
162- }
163- return s.listResp, nil
164-}
165-
166-func (s imagesClientStub) PruneImages(context.Context, dto.ImagePruneRequest) (*dto.ImagePruneResponse, error) {
167- if s.err != nil {
168- return nil, s.err
169- }
170- return s.pruneResp, nil
171-}
172-
173153 func TestUIAdoptionRuntimeSeams(t *testing.T) {
174154 uiAdoptionSeamMu.Lock()
175155 defer uiAdoptionSeamMu.Unlock()
@@ -201,13 +181,19 @@ func TestUIAdoptionRuntimeSeams(t *testing.T) {
201181 t.Fatalf("version command execution failed: %v", err)
202182 }
203183
184+ imagesMock := climocks.NewMockimagesClient(t)
185+ imagesMock.EXPECT().ListImages(context.Background()).Return([]dto.Image{{Repository: "repo/app", Tag: "latest", ID: "sha256:abc", Size: 1}}, nil).Once()
186+
204187 imgBuf := new(bytes.Buffer)
205- if err := runImagesList(context.Background(), imagesClientStub{listResp: []dto.Image{{Repository: "repo/app", Tag: "latest", ID: "sha256:abc", Size: 1}}}, imgBuf, false); err != nil {
188+ if err := runImagesList(context.Background(), imagesMock, imgBuf, false); err != nil {
206189 t.Fatalf("runImagesList failed: %v", err)
207190 }
208191
192+ pruneMock := climocks.NewMockimagesClient(t)
193+ pruneMock.EXPECT().ListImages(context.Background()).Return([]dto.Image{}, nil).Once()
194+
209195 pruneBuf := new(bytes.Buffer)
210- if err := runImagesPrune(context.Background(), imagesClientStub{listResp: []dto.Image{}, pruneResp: &dto.ImagePruneResponse{}}, imagesPruneOptions{DryRun: true}, pruneBuf); err != nil {
196+ if err := runImagesPrune(context.Background(), pruneMock, imagesPruneOptions{DryRun: true}, pruneBuf); err != nil {
211197 t.Fatalf("runImagesPrune failed: %v", err)
212198 }
213199
@@ -12,6 +12,7 @@ import (
12 "testing"12 "testing"
13 13
14 "github.com/bnema/gordon/internal/adapters/dto"14 "github.com/bnema/gordon/internal/adapters/dto"
15+ climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
15 )16 )
16 17
17 var uiAdoptionHelperCalls = map[string]struct{}{18 var uiAdoptionHelperCalls = map[string]struct{}{
@@ -54,8 +55,8 @@ func TestPresentationHelpers(t *testing.T) {
54 }55 }
55 56
56 func TestUIAdoption(t *testing.T) {57 func TestUIAdoption(t *testing.T) {
57- for _, expect := range uiAdoptionExpectations {58+ for i := range uiAdoptionExpectations {
58- expect := expect59+ expect := uiAdoptionExpectations[i]
59 t.Run(expect.family, func(t *testing.T) {60 t.Run(expect.family, func(t *testing.T) {
60 fset := token.NewFileSet()61 fset := token.NewFileSet()
61 fileNode, err := parser.ParseFile(fset, expect.file, nil, parser.AllErrors)62 fileNode, err := parser.ParseFile(fset, expect.file, nil, parser.AllErrors)
@@ -64,7 +65,6 @@ func TestUIAdoption(t *testing.T) {
64 }65 }
65 66
66 for _, fnName := range expect.functions {67 for _, fnName := range expect.functions {
67- fnName := fnName
68 t.Run(fnName, func(t *testing.T) {68 t.Run(fnName, func(t *testing.T) {
69 fn := findFuncDecl(fileNode, fnName)69 fn := findFuncDecl(fileNode, fnName)
70 if fn == nil {70 if fn == nil {
@@ -150,26 +150,6 @@ func isForbiddenRawPrintCall(call *ast.CallExpr, sel *ast.SelectorExpr) bool {
150 return false150 return false
151 }151 }
152 152
153-type imagesClientStub struct {
154- listResp []dto.Image
155- pruneResp *dto.ImagePruneResponse
156- err error
157-}
158-
159-func (s imagesClientStub) ListImages(context.Context) ([]dto.Image, error) {
160- if s.err != nil {
161- return nil, s.err
162- }
163- return s.listResp, nil
164-}
165-
166-func (s imagesClientStub) PruneImages(context.Context, dto.ImagePruneRequest) (*dto.ImagePruneResponse, error) {
167- if s.err != nil {
168- return nil, s.err
169- }
170- return s.pruneResp, nil
171-}
172-
173 func TestUIAdoptionRuntimeSeams(t *testing.T) {153 func TestUIAdoptionRuntimeSeams(t *testing.T) {
174 uiAdoptionSeamMu.Lock()154 uiAdoptionSeamMu.Lock()
175 defer uiAdoptionSeamMu.Unlock()155 defer uiAdoptionSeamMu.Unlock()
@@ -201,13 +181,19 @@ func TestUIAdoptionRuntimeSeams(t *testing.T) {
201 t.Fatalf("version command execution failed: %v", err)181 t.Fatalf("version command execution failed: %v", err)
202 }182 }
203 183
184+ imagesMock := climocks.NewMockimagesClient(t)
185+ imagesMock.EXPECT().ListImages(context.Background()).Return([]dto.Image{{Repository: "repo/app", Tag: "latest", ID: "sha256:abc", Size: 1}}, nil).Once()
186+
204 imgBuf := new(bytes.Buffer)187 imgBuf := new(bytes.Buffer)
205- if err := runImagesList(context.Background(), imagesClientStub{listResp: []dto.Image{{Repository: "repo/app", Tag: "latest", ID: "sha256:abc", Size: 1}}}, imgBuf, false); err != nil {188+ if err := runImagesList(context.Background(), imagesMock, imgBuf, false); err != nil {
206 t.Fatalf("runImagesList failed: %v", err)189 t.Fatalf("runImagesList failed: %v", err)
207 }190 }
208 191
192+ pruneMock := climocks.NewMockimagesClient(t)
193+ pruneMock.EXPECT().ListImages(context.Background()).Return([]dto.Image{}, nil).Once()
194+
209 pruneBuf := new(bytes.Buffer)195 pruneBuf := new(bytes.Buffer)
210- if err := runImagesPrune(context.Background(), imagesClientStub{listResp: []dto.Image{}, pruneResp: &dto.ImagePruneResponse{}}, imagesPruneOptions{DryRun: true}, pruneBuf); err != nil {196+ if err := runImagesPrune(context.Background(), pruneMock, imagesPruneOptions{DryRun: true}, pruneBuf); err != nil {
211 t.Fatalf("runImagesPrune failed: %v", err)197 t.Fatalf("runImagesPrune failed: %v", err)
212 }198 }
213 199
M internal/adapters/in/cli/volumes_test.go +31 -53
@@ -6,91 +6,69 @@ import (
66 "testing"
77
88 "github.com/stretchr/testify/assert"
9+ "github.com/stretchr/testify/mock"
910 "github.com/stretchr/testify/require"
1011
1112 "github.com/bnema/gordon/internal/adapters/dto"
13+ climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
1214 )
1315
14-type fakeVolumesClient struct {
15- listResp []dto.Volume
16- pruneResp *dto.VolumePruneResponse
17- listErr error
18- pruneErr error
19-
20- pruneCalls []dto.VolumePruneRequest
21-}
22-
23-func (f *fakeVolumesClient) ListVolumes(_ context.Context) ([]dto.Volume, error) {
24- return f.listResp, f.listErr
25-}
26-
27-func (f *fakeVolumesClient) PruneVolumes(_ context.Context, req dto.VolumePruneRequest) (*dto.VolumePruneResponse, error) {
28- f.pruneCalls = append(f.pruneCalls, req)
29- return f.pruneResp, f.pruneErr
30-}
31-
3216 func TestRunVolumesList_ShowsVolumes(t *testing.T) {
33- client := &fakeVolumesClient{
34- listResp: []dto.Volume{
35- {Name: "db-data", InUse: true, Containers: []string{"postgres"}, Size: 1024 * 1024},
36- {Name: "orphaned-vol", InUse: false, Size: 512},
37- },
38- }
17+ clientMock := climocks.NewMockvolumesClient(t)
18+ clientMock.EXPECT().ListVolumes(context.Background()).Return([]dto.Volume{
19+ {Name: "db-data", InUse: true, Containers: []string{"postgres"}, Size: 1024 * 1024},
20+ {Name: "orphaned-vol", InUse: false, Size: 512},
21+ }, nil).Once()
3922
4023 var out bytes.Buffer
41- err := runVolumesList(context.Background(), client, &out, false)
24+ err := runVolumesList(context.Background(), clientMock, &out, false)
4225 require.NoError(t, err)
4326 assert.Contains(t, out.String(), "db-data")
4427 assert.Contains(t, out.String(), "orphaned-vol")
4528 }
4629
4730 func TestRunVolumesList_JSON(t *testing.T) {
48- client := &fakeVolumesClient{
49- listResp: []dto.Volume{
50- {Name: "vol1", InUse: true},
51- },
52- }
31+ clientMock := climocks.NewMockvolumesClient(t)
32+ clientMock.EXPECT().ListVolumes(context.Background()).Return([]dto.Volume{
33+ {Name: "vol1", InUse: true},
34+ }, nil).Once()
5335
5436 var out bytes.Buffer
55- err := runVolumesList(context.Background(), client, &out, true)
37+ err := runVolumesList(context.Background(), clientMock, &out, true)
5638 require.NoError(t, err)
5739 assert.Contains(t, out.String(), `"name"`)
5840 }
5941
6042 func TestRunVolumesPrune_JSONExecutesActualPrune(t *testing.T) {
61- client := &fakeVolumesClient{
62- pruneResp: &dto.VolumePruneResponse{
63- VolumesRemoved: 1,
64- SpaceReclaimed: 1024,
65- Volumes: []dto.Volume{{Name: "orphan1", Size: 1024}},
66- },
43+ clientMock := climocks.NewMockvolumesClient(t)
44+ resp := &dto.VolumePruneResponse{
45+ VolumesRemoved: 1,
46+ SpaceReclaimed: 1024,
47+ Volumes: []dto.Volume{{Name: "orphan1", Size: 1024}},
6748 }
49+ previewCall := clientMock.EXPECT().PruneVolumes(context.Background(), dto.VolumePruneRequest{DryRun: true}).Return(resp, nil).Once()
50+ pruneCall := clientMock.EXPECT().PruneVolumes(context.Background(), dto.VolumePruneRequest{DryRun: false}).Return(resp, nil).Once()
51+ mock.InOrder(previewCall, pruneCall)
6852
6953 var out bytes.Buffer
70- err := runVolumesPrune(context.Background(), client, volumesPruneOptions{Json: true}, &out)
54+ err := runVolumesPrune(context.Background(), clientMock, volumesPruneOptions{Json: true}, &out)
7155 require.NoError(t, err)
72-
73- // Should have called PruneVolumes twice: once dry-run (preview), once actual
74- require.Len(t, client.pruneCalls, 2)
75- assert.True(t, client.pruneCalls[0].DryRun, "first call should be dry-run preview")
76- assert.False(t, client.pruneCalls[1].DryRun, "second call should be actual prune")
7756 assert.Contains(t, out.String(), `"volumes_removed"`)
7857 }
7958
8059 func TestRunVolumesPrune_DryRun(t *testing.T) {
81- client := &fakeVolumesClient{
82- pruneResp: &dto.VolumePruneResponse{
83- VolumesRemoved: 2,
84- SpaceReclaimed: 4096,
85- Volumes: []dto.Volume{
86- {Name: "orphan1", Size: 2048},
87- {Name: "orphan2", Size: 2048},
88- },
60+ clientMock := climocks.NewMockvolumesClient(t)
61+ clientMock.EXPECT().PruneVolumes(context.Background(), dto.VolumePruneRequest{DryRun: true}).Return(&dto.VolumePruneResponse{
62+ VolumesRemoved: 2,
63+ SpaceReclaimed: 4096,
64+ Volumes: []dto.Volume{
65+ {Name: "orphan1", Size: 2048},
66+ {Name: "orphan2", Size: 2048},
8967 },
90- }
68+ }, nil).Once()
9169
9270 var out bytes.Buffer
93- err := runVolumesPrune(context.Background(), client, volumesPruneOptions{DryRun: true}, &out)
71+ err := runVolumesPrune(context.Background(), clientMock, volumesPruneOptions{DryRun: true}, &out)
9472 require.NoError(t, err)
9573 assert.Contains(t, out.String(), "orphan1")
9674 }
@@ -6,91 +6,69 @@ import (
6 "testing"6 "testing"
7 7
8 "github.com/stretchr/testify/assert"8 "github.com/stretchr/testify/assert"
9+ "github.com/stretchr/testify/mock"
9 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
10 11
11 "github.com/bnema/gordon/internal/adapters/dto"12 "github.com/bnema/gordon/internal/adapters/dto"
13+ climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
12 )14 )
13 15
14-type fakeVolumesClient struct {
15- listResp []dto.Volume
16- pruneResp *dto.VolumePruneResponse
17- listErr error
18- pruneErr error
19-
20- pruneCalls []dto.VolumePruneRequest
21-}
22-
23-func (f *fakeVolumesClient) ListVolumes(_ context.Context) ([]dto.Volume, error) {
24- return f.listResp, f.listErr
25-}
26-
27-func (f *fakeVolumesClient) PruneVolumes(_ context.Context, req dto.VolumePruneRequest) (*dto.VolumePruneResponse, error) {
28- f.pruneCalls = append(f.pruneCalls, req)
29- return f.pruneResp, f.pruneErr
30-}
31-
32 func TestRunVolumesList_ShowsVolumes(t *testing.T) {16 func TestRunVolumesList_ShowsVolumes(t *testing.T) {
33- client := &fakeVolumesClient{17+ clientMock := climocks.NewMockvolumesClient(t)
34- listResp: []dto.Volume{18+ clientMock.EXPECT().ListVolumes(context.Background()).Return([]dto.Volume{
35- {Name: "db-data", InUse: true, Containers: []string{"postgres"}, Size: 1024 * 1024},19+ {Name: "db-data", InUse: true, Containers: []string{"postgres"}, Size: 1024 * 1024},
36- {Name: "orphaned-vol", InUse: false, Size: 512},20+ {Name: "orphaned-vol", InUse: false, Size: 512},
37- },21+ }, nil).Once()
38- }
39 22
40 var out bytes.Buffer23 var out bytes.Buffer
41- err := runVolumesList(context.Background(), client, &out, false)24+ err := runVolumesList(context.Background(), clientMock, &out, false)
42 require.NoError(t, err)25 require.NoError(t, err)
43 assert.Contains(t, out.String(), "db-data")26 assert.Contains(t, out.String(), "db-data")
44 assert.Contains(t, out.String(), "orphaned-vol")27 assert.Contains(t, out.String(), "orphaned-vol")
45 }28 }
46 29
47 func TestRunVolumesList_JSON(t *testing.T) {30 func TestRunVolumesList_JSON(t *testing.T) {
48- client := &fakeVolumesClient{31+ clientMock := climocks.NewMockvolumesClient(t)
49- listResp: []dto.Volume{32+ clientMock.EXPECT().ListVolumes(context.Background()).Return([]dto.Volume{
50- {Name: "vol1", InUse: true},33+ {Name: "vol1", InUse: true},
51- },34+ }, nil).Once()
52- }
53 35
54 var out bytes.Buffer36 var out bytes.Buffer
55- err := runVolumesList(context.Background(), client, &out, true)37+ err := runVolumesList(context.Background(), clientMock, &out, true)
56 require.NoError(t, err)38 require.NoError(t, err)
57 assert.Contains(t, out.String(), `"name"`)39 assert.Contains(t, out.String(), `"name"`)
58 }40 }
59 41
60 func TestRunVolumesPrune_JSONExecutesActualPrune(t *testing.T) {42 func TestRunVolumesPrune_JSONExecutesActualPrune(t *testing.T) {
61- client := &fakeVolumesClient{43+ clientMock := climocks.NewMockvolumesClient(t)
62- pruneResp: &dto.VolumePruneResponse{44+ resp := &dto.VolumePruneResponse{
63- VolumesRemoved: 1,45+ VolumesRemoved: 1,
64- SpaceReclaimed: 1024,46+ SpaceReclaimed: 1024,
65- Volumes: []dto.Volume{{Name: "orphan1", Size: 1024}},47+ Volumes: []dto.Volume{{Name: "orphan1", Size: 1024}},
66- },
67 }48 }
49+ previewCall := clientMock.EXPECT().PruneVolumes(context.Background(), dto.VolumePruneRequest{DryRun: true}).Return(resp, nil).Once()
50+ pruneCall := clientMock.EXPECT().PruneVolumes(context.Background(), dto.VolumePruneRequest{DryRun: false}).Return(resp, nil).Once()
51+ mock.InOrder(previewCall, pruneCall)
68 52
69 var out bytes.Buffer53 var out bytes.Buffer
70- err := runVolumesPrune(context.Background(), client, volumesPruneOptions{Json: true}, &out)54+ err := runVolumesPrune(context.Background(), clientMock, volumesPruneOptions{Json: true}, &out)
71 require.NoError(t, err)55 require.NoError(t, err)
72-
73- // Should have called PruneVolumes twice: once dry-run (preview), once actual
74- require.Len(t, client.pruneCalls, 2)
75- assert.True(t, client.pruneCalls[0].DryRun, "first call should be dry-run preview")
76- assert.False(t, client.pruneCalls[1].DryRun, "second call should be actual prune")
77 assert.Contains(t, out.String(), `"volumes_removed"`)56 assert.Contains(t, out.String(), `"volumes_removed"`)
78 }57 }
79 58
80 func TestRunVolumesPrune_DryRun(t *testing.T) {59 func TestRunVolumesPrune_DryRun(t *testing.T) {
81- client := &fakeVolumesClient{60+ clientMock := climocks.NewMockvolumesClient(t)
82- pruneResp: &dto.VolumePruneResponse{61+ clientMock.EXPECT().PruneVolumes(context.Background(), dto.VolumePruneRequest{DryRun: true}).Return(&dto.VolumePruneResponse{
83- VolumesRemoved: 2,62+ VolumesRemoved: 2,
84- SpaceReclaimed: 4096,63+ SpaceReclaimed: 4096,
85- Volumes: []dto.Volume{64+ Volumes: []dto.Volume{
86- {Name: "orphan1", Size: 2048},65+ {Name: "orphan1", Size: 2048},
87- {Name: "orphan2", Size: 2048},66+ {Name: "orphan2", Size: 2048},
88- },
89 },67 },
90- }68+ }, nil).Once()
91 69
92 var out bytes.Buffer70 var out bytes.Buffer
93- err := runVolumesPrune(context.Background(), client, volumesPruneOptions{DryRun: true}, &out)71+ err := runVolumesPrune(context.Background(), clientMock, volumesPruneOptions{DryRun: true}, &out)
94 require.NoError(t, err)72 require.NoError(t, err)
95 assert.Contains(t, out.String(), "orphan1")73 assert.Contains(t, out.String(), "orphan1")
96 }74 }

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