1package cli
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "net/url"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13
14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/require"
16)
17
18func TestInferPushRemote_UsesUniqueMatchingSavedRemote(t *testing.T) {
19 prod := newTagsTestServer(t, func(image string) ([]string, int) {
20 assert.Equal(t, "myapp", image)
21 return []string{"latest"}, http.StatusOK
22 })
23 staging := newTagsTestServer(t, func(image string) ([]string, int) {
24 assert.Equal(t, "myapp", image)
25 return nil, http.StatusOK
26 })
27
28 configurePushRemoteInferenceTestEnv(t, `
29[remotes.prod]
30url = "`+prod.URL+`"
31
32[remotes.staging]
33url = "`+staging.URL+`"
34`)
35
36 resolved, err := inferPushRemote(context.Background(), "myapp", "", "Dockerfile")
37 require.NoError(t, err)
38 require.NotNil(t, resolved)
39 assert.Equal(t, "prod", resolved.Name)
40 assert.Equal(t, prod.URL, resolved.URL)
41}
42
43func TestInferPushRemote_ReturnsAmbiguousErrorWhenMultipleRemotesMatch(t *testing.T) {
44 prod := newTagsTestServer(t, func(image string) ([]string, int) {
45 return []string{"latest"}, http.StatusOK
46 })
47 staging := newTagsTestServer(t, func(image string) ([]string, int) {
48 return []string{"latest"}, http.StatusOK
49 })
50
51 configurePushRemoteInferenceTestEnv(t, `
52[remotes.prod]
53url = "`+prod.URL+`"
54
55[remotes.staging]
56url = "`+staging.URL+`"
57`)
58
59 resolved, err := inferPushRemote(context.Background(), "myapp", "", "Dockerfile")
60 require.Error(t, err)
61 assert.Nil(t, resolved)
62 assert.Contains(t, err.Error(), `multiple saved remotes match image "myapp"`)
63 assert.Contains(t, err.Error(), "prod")
64 assert.Contains(t, err.Error(), "staging")
65}
66
67func TestInferPushRemote_SkipsGuessWhenExplicitRemoteSelected(t *testing.T) {
68 originalRemoteFlag := remoteFlag
69 t.Cleanup(func() {
70 remoteFlag = originalRemoteFlag
71 })
72
73 called := false
74 prod := newTagsTestServer(t, func(image string) ([]string, int) {
75 called = true
76 return []string{"latest"}, http.StatusOK
77 })
78
79 configurePushRemoteInferenceTestEnv(t, `
80[remotes.prod]
81url = "`+prod.URL+`"
82`)
83 t.Setenv("GORDON_REMOTE", "")
84 remoteFlag = "prod"
85
86 resolved, err := inferPushRemote(context.Background(), "myapp", "", "Dockerfile")
87 require.NoError(t, err)
88 assert.Nil(t, resolved)
89 assert.False(t, called)
90}
91
92func TestInferPushRemote_SkipsGuessWhenActiveRemoteConfigured(t *testing.T) {
93 called := false
94 prod := newTagsTestServer(t, func(image string) ([]string, int) {
95 called = true
96 return []string{"latest"}, http.StatusOK
97 })
98
99 configurePushRemoteInferenceTestEnv(t, `
100active = "prod"
101
102[remotes.prod]
103url = "`+prod.URL+`"
104`)
105
106 resolved, err := inferPushRemote(context.Background(), "myapp", "", "Dockerfile")
107 require.NoError(t, err)
108 assert.Nil(t, resolved)
109 assert.False(t, called)
110}
111
112func TestInferPushRemote_FailsSafeWhenSomeRemotesCannotBeProbed(t *testing.T) {
113 prod := newTagsTestServer(t, func(image string) ([]string, int) {
114 return []string{"latest"}, http.StatusOK
115 })
116 broken := newTagsTestServer(t, func(image string) ([]string, int) {
117 return nil, http.StatusInternalServerError
118 })
119
120 configurePushRemoteInferenceTestEnv(t, `
121[remotes.prod]
122url = "`+prod.URL+`"
123
124[remotes.broken]
125url = "`+broken.URL+`"
126`)
127
128 resolved, err := inferPushRemote(context.Background(), "myapp", "", "Dockerfile")
129 require.Error(t, err)
130 assert.Nil(t, resolved)
131 assert.Contains(t, err.Error(), `could not safely infer remote for image "myapp"`)
132 assert.Contains(t, err.Error(), "broken")
133}
134
135func TestInferRemoteForRepository_ReturnsAmbiguousErrorWhenMultipleRemotesMatch(t *testing.T) {
136 prod := newTagsTestServer(t, func(repository string) ([]string, int) {
137 return []string{"latest", "v1.0.0"}, http.StatusOK
138 })
139 staging := newTagsTestServer(t, func(repository string) ([]string, int) {
140 return []string{"latest"}, http.StatusOK
141 })
142
143 configurePushRemoteInferenceTestEnv(t, `
144[remotes.prod]
145url = "`+prod.URL+`"
146
147[remotes.staging]
148url = "`+staging.URL+`"
149`)
150
151 resolved, err := inferRemoteForRepository(context.Background(), "myapp")
152 require.Error(t, err)
153 assert.Nil(t, resolved)
154 assert.Contains(t, err.Error(), `multiple saved remotes match repository "myapp"`)
155}
156
157func configurePushRemoteInferenceTestEnv(t *testing.T, remotesTOML string) {
158 t.Helper()
159
160 originalRemoteFlag := remoteFlag
161 originalTokenFlag := tokenFlag
162 originalInsecureTLSFlag := insecureTLSFlag
163 t.Cleanup(func() {
164 remoteFlag = originalRemoteFlag
165 tokenFlag = originalTokenFlag
166 insecureTLSFlag = originalInsecureTLSFlag
167 })
168
169 remoteFlag = ""
170 tokenFlag = ""
171 insecureTLSFlag = false
172 t.Setenv("GORDON_REMOTE", "")
173 t.Setenv("GORDON_TOKEN", "")
174 t.Setenv("GORDON_INSECURE", "")
175 t.Setenv("HOME", t.TempDir())
176
177 configHome := t.TempDir()
178 t.Setenv("XDG_CONFIG_HOME", configHome)
179 cliConfigPath := filepath.Join(configHome, "gordon", "remotes.toml")
180 require.NoError(t, os.MkdirAll(filepath.Dir(cliConfigPath), 0o755))
181 require.NoError(t, os.WriteFile(cliConfigPath, []byte(strings.TrimSpace(remotesTOML)), 0o600))
182}
183
184func newTagsTestServer(t *testing.T, handler func(repository string) ([]string, int)) *httptest.Server {
185 t.Helper()
186 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
187 handleListTagsProbe(t, w, r, handler)
188 }))
189 t.Cleanup(server.Close)
190 return server
191}
192
193func handleListTagsProbe(t *testing.T, w http.ResponseWriter, r *http.Request, handler func(repository string) ([]string, int)) {
194 t.Helper()
195 if handler == nil {
196 http.NotFound(w, r)
197 return
198 }
199 repository, err := url.PathUnescape(strings.TrimPrefix(r.URL.Path, "/admin/tags/"))
200 if !assert.NoError(t, err) {
201 return
202 }
203 tags, status := handler(repository)
204 if status >= http.StatusBadRequest {
205 w.Header().Set("Content-Type", "application/json")
206 w.WriteHeader(status)
207 if status == http.StatusNotFound {
208 if !assert.NoError(t, json.NewEncoder(w).Encode(map[string]string{"error": "repository not found"})) {
209 return
210 }
211 return
212 }
213 if !assert.NoError(t, json.NewEncoder(w).Encode(map[string]string{"error": "boom"})) {
214 return
215 }
216 return
217 }
218 w.Header().Set("Content-Type", "application/json")
219 if !assert.NoError(t, json.NewEncoder(w).Encode(map[string]any{"repository": repository, "tags": tags})) {
220 return
221 }
222}