1package cli
2
3import (
4 "os"
5 "path/filepath"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9)
10
11func TestValidateBuildArg(t *testing.T) {
12 tests := []struct {
13 name string
14 arg string
15 wantErr bool
16 }{
17 {"valid simple", "FOO=bar", false},
18 {"valid with underscore key", "_FOO=bar", false},
19 {"valid with numbers in key", "FOO123=bar", false},
20 {"valid empty value", "FOO=", false},
21 {"valid complex value", "FOO=bar baz=qux", false},
22 {"invalid no equals", "FOO", true},
23 {"invalid starts with number", "1FOO=bar", true},
24 {"invalid starts with dash", "-FOO=bar", true},
25 {"invalid special chars in key", "FOO-BAR=baz", true},
26 {"invalid empty string", "", true},
27 {"invalid equals only", "=value", true},
28 }
29
30 for _, tt := range tests {
31 t.Run(tt.name, func(t *testing.T) {
32 err := validateBuildArg(tt.arg)
33 if tt.wantErr {
34 assert.Error(t, err)
35 } else {
36 assert.NoError(t, err)
37 }
38 })
39 }
40}
41
42func TestParseImageRef(t *testing.T) {
43 tests := []struct {
44 name string
45 image string
46 wantRegistry string
47 wantName string
48 wantTag string
49 }{
50 {"full ref", "reg.example.com/myapp:v1.0.0", "reg.example.com", "myapp", "v1.0.0"},
51 {"no tag", "reg.example.com/myapp", "reg.example.com", "myapp", "latest"},
52 {"latest tag", "reg.example.com/myapp:latest", "reg.example.com", "myapp", "latest"},
53 {"no slash", "myapp:v1.0.0", "", "", ""},
54 }
55
56 for _, tt := range tests {
57 t.Run(tt.name, func(t *testing.T) {
58 registry, name, tag := parseImageRef(tt.image)
59 assert.Equal(t, tt.wantRegistry, registry)
60 assert.Equal(t, tt.wantName, name)
61 assert.Equal(t, tt.wantTag, tag)
62 })
63 }
64}
65
66func TestBuildAndPush_BuildArgs(t *testing.T) {
67 // Verify buildImageArgs produces --load instead of --push
68 args := buildImageArgs("v1.0.0", "linux/amd64", "Dockerfile", []string{"CGO_ENABLED=0"}, "reg.example.com/app:v1.0.0", "reg.example.com/app:latest")
69
70 assert.Contains(t, args, "--load")
71 assert.NotContains(t, args, "--push")
72 assert.Contains(t, args, "--platform")
73 assert.Contains(t, args, "-f")
74 assert.Contains(t, args, "Dockerfile")
75 assert.Contains(t, args, "VERSION")
76 assert.NotContains(t, args, "VERSION=v1.0.0")
77}
78
79func TestParseTagRef(t *testing.T) {
80 tests := []struct {
81 name string
82 ref string
83 want string
84 }{
85 {name: "github tag ref", ref: "refs/tags/v1.2.3", want: "v1.2.3"},
86 {name: "peeled tag ref", ref: "refs/tags/v1.2.3^{}", want: "v1.2.3"},
87 {name: "branch ref", ref: "refs/heads/main", want: ""},
88 {name: "empty", ref: "", want: ""},
89 }
90
91 for _, tt := range tests {
92 t.Run(tt.name, func(t *testing.T) {
93 assert.Equal(t, tt.want, parseTagRef(tt.ref))
94 })
95 }
96}
97
98func TestVersionFromTagRefs(t *testing.T) {
99 tests := []struct {
100 name string
101 env map[string]string
102 want string
103 }{
104 {
105 name: "github ref tag",
106 env: map[string]string{"GITHUB_REF": "refs/tags/v2.0.0"},
107 want: "v2.0.0",
108 },
109 {
110 name: "github ref type and name",
111 env: map[string]string{
112 "GITHUB_REF_TYPE": "tag",
113 "GITHUB_REF_NAME": "v2.1.0",
114 },
115 want: "v2.1.0",
116 },
117 {
118 name: "gitlab commit tag",
119 env: map[string]string{"CI_COMMIT_TAG": "v3.0.0"},
120 want: "v3.0.0",
121 },
122 {
123 name: "azure source branch tag ref",
124 env: map[string]string{"BUILD_SOURCEBRANCH": "refs/tags/v4.0.0"},
125 want: "v4.0.0",
126 },
127 {
128 name: "no tag refs",
129 env: map[string]string{"GITHUB_REF": "refs/heads/main"},
130 want: "",
131 },
132 }
133
134 for _, tt := range tests {
135 t.Run(tt.name, func(t *testing.T) {
136 got := versionFromTagRefs(func(key string) string {
137 return tt.env[key]
138 })
139 assert.Equal(t, tt.want, got)
140 })
141 }
142}
143
144func TestBuildImageArgs_CustomDockerfile(t *testing.T) {
145 args := buildImageArgs("v1.0.0", "linux/amd64", "docker/app/Dockerfile", nil, "reg.example.com/app:v1.0.0", "reg.example.com/app:latest")
146
147 assert.Contains(t, args, "-f")
148 assert.Contains(t, args, "docker/app/Dockerfile")
149}
150
151func TestParseDockerfileLabels(t *testing.T) {
152 tests := []struct {
153 name string
154 content string
155 expected map[string]string
156 }{
157 {
158 name: "simple label",
159 content: `LABEL gordon.domain="myapp.example.com"`,
160 expected: map[string]string{
161 "gordon.domain": "myapp.example.com",
162 },
163 },
164 {
165 name: "multiple labels",
166 content: `LABEL gordon.domain="myapp.example.com"
167LABEL gordon.port="8080"
168LABEL gordon.health="/health"`,
169 expected: map[string]string{
170 "gordon.domain": "myapp.example.com",
171 "gordon.port": "8080",
172 "gordon.health": "/health",
173 },
174 },
175 {
176 name: "multi-label on one line",
177 content: `LABEL gordon.domain="myapp.example.com" gordon.port="8080"`,
178 expected: map[string]string{
179 "gordon.domain": "myapp.example.com",
180 "gordon.port": "8080",
181 },
182 },
183 {
184 name: "unquoted value",
185 content: `LABEL gordon.domain=myapp.example.com`,
186 expected: map[string]string{
187 "gordon.domain": "myapp.example.com",
188 },
189 },
190 {
191 name: "single-quoted value",
192 content: `LABEL gordon.domain='myapp.example.com'`,
193 expected: map[string]string{
194 "gordon.domain": "myapp.example.com",
195 },
196 },
197 {
198 name: "non-gordon labels ignored",
199 content: `LABEL maintainer="John" gordon.domain="myapp.example.com"`,
200 expected: map[string]string{
201 "gordon.domain": "myapp.example.com",
202 },
203 },
204 {
205 name: "comma-separated domains",
206 content: `LABEL gordon.domains="app1.example.com,app2.example.com"`,
207 expected: map[string]string{
208 "gordon.domains": "app1.example.com,app2.example.com",
209 },
210 },
211 {
212 name: "no labels",
213 content: `FROM alpine:latest`,
214 expected: map[string]string{},
215 },
216 {
217 name: "comments and empty lines",
218 content: "# This is a comment\n\nLABEL gordon.domain=\"test.com\"",
219 expected: map[string]string{"gordon.domain": "test.com"},
220 },
221 }
222
223 for _, tt := range tests {
224 t.Run(tt.name, func(t *testing.T) {
225 // Write a temp Dockerfile
226 dir := t.TempDir()
227 dockerfile := filepath.Join(dir, "Dockerfile")
228 err := os.WriteFile(dockerfile, []byte(tt.content), 0644)
229 assert.NoError(t, err)
230
231 labels := parseDockerfileLabels(dockerfile)
232 assert.Equal(t, tt.expected, labels)
233 })
234 }
235}
236
237func TestParseDockerfileLabels_NonExistent(t *testing.T) {
238 labels := parseDockerfileLabels("/nonexistent/Dockerfile")
239 assert.Empty(t, labels)
240}
241
242func TestDetectImageName_FromDockerfileLabels(t *testing.T) {
243 dir := t.TempDir()
244 dockerfile := filepath.Join(dir, "Dockerfile")
245 err := os.WriteFile(dockerfile, []byte(`FROM alpine
246LABEL gordon.domain="myapp.example.com"
247`), 0644)
248 assert.NoError(t, err)
249
250 name, err := detectImageName(dockerfile)
251 assert.NoError(t, err)
252 assert.Equal(t, "myapp.example.com", name)
253}
254
255func TestDetectImageName_FallbackToDir(t *testing.T) {
256 dir := t.TempDir()
257 dockerfile := filepath.Join(dir, "Dockerfile")
258 err := os.WriteFile(dockerfile, []byte(`FROM alpine
259`), 0644)
260 assert.NoError(t, err)
261
262 // When no gordon.domain label, falls back to cwd basename
263 name, err := detectImageName(dockerfile)
264 assert.NoError(t, err)
265 assert.NotEmpty(t, name)
266}
267
268func TestSplitLabelPairs(t *testing.T) {
269 tests := []struct {
270 name string
271 content string
272 expected []string
273 }{
274 {
275 name: "single pair",
276 content: `gordon.domain="test.com"`,
277 expected: []string{`gordon.domain="test.com"`},
278 },
279 {
280 name: "two pairs",
281 content: `gordon.domain="test.com" gordon.port="8080"`,
282 expected: []string{`gordon.domain="test.com"`, `gordon.port="8080"`},
283 },
284 {
285 name: "unquoted",
286 content: `gordon.domain=test.com gordon.port=8080`,
287 expected: []string{`gordon.domain=test.com`, `gordon.port=8080`},
288 },
289 {
290 name: "quoted with spaces in value",
291 content: `gordon.domain="my app.com"`,
292 expected: []string{`gordon.domain="my app.com"`},
293 },
294 }
295
296 for _, tt := range tests {
297 t.Run(tt.name, func(t *testing.T) {
298 result := splitLabelPairs(tt.content)
299 assert.Equal(t, tt.expected, result)
300 })
301 }
302}
303
304func TestParseLabelPair(t *testing.T) {
305 tests := []struct {
306 name string
307 pair string
308 wantKey string
309 wantValue string
310 wantOk bool
311 }{
312 {"quoted", `gordon.domain="test.com"`, "gordon.domain", "test.com", true},
313 {"unquoted", `gordon.domain=test.com`, "gordon.domain", "test.com", true},
314 {"single quoted", `gordon.domain='test.com'`, "gordon.domain", "test.com", true},
315 {"empty value", `gordon.domain=`, "gordon.domain", "", true},
316 {"no equals", `gordon.domain`, "", "", false},
317 }
318
319 for _, tt := range tests {
320 t.Run(tt.name, func(t *testing.T) {
321 key, value, ok := parseLabelPair(tt.pair)
322 assert.Equal(t, tt.wantOk, ok)
323 if ok {
324 assert.Equal(t, tt.wantKey, key)
325 assert.Equal(t, tt.wantValue, value)
326 }
327 })
328 }
329}