1package cli
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "testing"
8 "time"
9
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12
13 "github.com/bnema/gordon/internal/adapters/dto"
14)
15
16type fakeImagesClient struct {
17 listImagesResp []dto.Image
18 listImagesErr error
19 pruneResp *dto.ImagePruneResponse
20 pruneErr error
21
22 listImagesCalls int
23 pruneCalls int
24 lastPruneKeep int
25}
26
27func (f *fakeImagesClient) ListImages(_ context.Context) ([]dto.Image, error) {
28 f.listImagesCalls++
29 if f.listImagesErr != nil {
30 return nil, f.listImagesErr
31 }
32 return f.listImagesResp, nil
33}
34
35func (f *fakeImagesClient) PruneImages(_ context.Context, keepLast int) (*dto.ImagePruneResponse, error) {
36 f.pruneCalls++
37 f.lastPruneKeep = keepLast
38 if f.pruneErr != nil {
39 return nil, f.pruneErr
40 }
41 return f.pruneResp, nil
42}
43
44func TestRunImagesList_PrintsRowsAndSummary(t *testing.T) {
45 createdAt := time.Date(2026, 2, 8, 12, 0, 0, 0, time.UTC)
46 client := &fakeImagesClient{
47 listImagesResp: []dto.Image{
48 {Repository: "registry.example.com/app", Tag: "latest", Size: 12_000_000, Created: createdAt, ID: "sha256:1111", Dangling: false},
49 {Repository: "<none>", Tag: "<none>", Size: 512_000, Created: createdAt, ID: "sha256:2222", Dangling: true},
50 },
51 }
52
53 var out bytes.Buffer
54 err := runImagesList(context.Background(), client, &out)
55 require.NoError(t, err)
56
57 text := out.String()
58 assert.Contains(t, text, "REPOSITORY")
59 assert.Contains(t, text, "registry.example.com/app")
60 assert.Contains(t, text, "latest")
61 assert.Contains(t, text, "Total images: 2 (dangling: 1)")
62 assert.Equal(t, 1, client.listImagesCalls)
63}
64
65func TestRunImagesList_EmptyOutput(t *testing.T) {
66 client := &fakeImagesClient{listImagesResp: []dto.Image{}}
67
68 var out bytes.Buffer
69 err := runImagesList(context.Background(), client, &out)
70 require.NoError(t, err)
71
72 text := out.String()
73 assert.Contains(t, text, "No images found")
74 assert.Contains(t, text, "Total images: 0")
75 assert.Equal(t, 1, client.listImagesCalls)
76}
77
78func TestRunImagesPrune_DryRunCallsListOnly(t *testing.T) {
79 client := &fakeImagesClient{
80 listImagesResp: []dto.Image{{Dangling: true}, {Dangling: false}, {Dangling: true}},
81 }
82
83 var out bytes.Buffer
84 err := runImagesPrune(context.Background(), client, imagesPruneOptions{DryRun: true, KeepLast: 3}, &out)
85 require.NoError(t, err)
86
87 text := out.String()
88 assert.Contains(t, text, "Dry run")
89 assert.Contains(t, text, "would prune 2 dangling runtime images")
90 assert.Contains(t, text, "would keep latest + 3 previous tags")
91 assert.Equal(t, 1, client.listImagesCalls)
92 assert.Equal(t, 0, client.pruneCalls)
93}
94
95func TestRunImagesPrune_DryRunKeepZeroSkipsRegistry(t *testing.T) {
96 client := &fakeImagesClient{listImagesResp: []dto.Image{{Dangling: true}}}
97
98 var out bytes.Buffer
99 err := runImagesPrune(context.Background(), client, imagesPruneOptions{DryRun: true, KeepLast: 0}, &out)
100 require.NoError(t, err)
101
102 text := out.String()
103 assert.Contains(t, text, "Registry cleanup skipped (--keep=0)")
104 assert.Equal(t, 1, client.listImagesCalls)
105 assert.Equal(t, 0, client.pruneCalls)
106}
107
108func TestRunImagesPrune_RuntimeOnlyForcesKeepZero(t *testing.T) {
109 client := &fakeImagesClient{pruneResp: &dto.ImagePruneResponse{}}
110
111 var out bytes.Buffer
112 err := runImagesPrune(context.Background(), client, imagesPruneOptions{KeepLast: 9, RuntimeOnly: true}, &out)
113 require.NoError(t, err)
114
115 assert.Equal(t, 1, client.pruneCalls)
116 assert.Equal(t, 0, client.lastPruneKeep)
117 assert.Contains(t, out.String(), "Registry cleanup skipped")
118}
119
120func TestRunImagesPrune_UsesKeepFlagWhenNotRuntimeOnly(t *testing.T) {
121 client := &fakeImagesClient{
122 pruneResp: &dto.ImagePruneResponse{
123 Runtime: dto.RuntimePruneResult{DeletedCount: 2, SpaceReclaimed: 4096},
124 Registry: dto.RegistryPruneResult{TagsRemoved: 3, BlobsRemoved: 1, SpaceReclaimed: 8192},
125 },
126 }
127
128 var out bytes.Buffer
129 err := runImagesPrune(context.Background(), client, imagesPruneOptions{KeepLast: 5}, &out)
130 require.NoError(t, err)
131
132 text := out.String()
133 assert.Equal(t, 1, client.pruneCalls)
134 assert.Equal(t, 5, client.lastPruneKeep)
135 assert.Contains(t, text, "Runtime: deleted=2")
136 assert.Contains(t, text, "Registry: tags_removed=3")
137}
138
139func TestRunImagesPrune_ReturnsRemoteErrors(t *testing.T) {
140 client := &fakeImagesClient{pruneErr: errors.New("request failed")}
141
142 err := runImagesPrune(context.Background(), client, imagesPruneOptions{KeepLast: 2}, &bytes.Buffer{})
143 require.Error(t, err)
144 assert.Contains(t, err.Error(), "failed to prune images")
145}
146
147func TestRunImagesPrune_RejectsNegativeKeep(t *testing.T) {
148 err := runImagesPrune(context.Background(), &fakeImagesClient{}, imagesPruneOptions{KeepLast: -1}, &bytes.Buffer{})
149 require.Error(t, err)
150 assert.Contains(t, err.Error(), "--keep must be >= 0")
151}
152
153func TestRunImagesPrune_RejectsEmptyResponse(t *testing.T) {
154 client := &fakeImagesClient{pruneResp: nil}
155 err := runImagesPrune(context.Background(), client, imagesPruneOptions{KeepLast: 1}, &bytes.Buffer{})
156 require.Error(t, err)
157 assert.Contains(t, err.Error(), "empty response")
158}