github.com/anchore/syft@v1.38.2/syft/source/snapsource/snapcraft_api_test.go (about) 1 package snapsource 2 3 import ( 4 "encoding/json" 5 "net/http" 6 "net/http/httptest" 7 "testing" 8 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/require" 11 ) 12 13 func TestSnapcraftClient_CheckSnapExists(t *testing.T) { 14 tests := []struct { 15 name string 16 snapName string 17 mockResponse snapFindResponse 18 statusCode int 19 expectedExists bool 20 expectedSnapID string 21 expectError require.ErrorAssertionFunc 22 errorContains string 23 }{ 24 { 25 name: "snap exists", 26 snapName: "jp-ledger", 27 statusCode: http.StatusOK, 28 mockResponse: snapFindResponse{ 29 Results: []struct { 30 Name string `json:"name"` 31 SnapID string `json:"snap-id"` 32 Snap struct{} `json:"snap"` 33 }{ 34 { 35 Name: "jp-ledger", 36 SnapID: "jyDlMmifyQhSWGPM9fnKc1HSD7E6c47e", 37 Snap: struct{}{}, 38 }, 39 }, 40 }, 41 expectedExists: true, 42 expectedSnapID: "jyDlMmifyQhSWGPM9fnKc1HSD7E6c47e", 43 expectError: require.NoError, 44 }, 45 { 46 name: "snap does not exist", 47 snapName: "nonexistent-snap", 48 statusCode: http.StatusOK, 49 mockResponse: snapFindResponse{ 50 Results: []struct { 51 Name string `json:"name"` 52 SnapID string `json:"snap-id"` 53 Snap struct{} `json:"snap"` 54 }{}, 55 }, 56 expectedExists: false, 57 expectedSnapID: "", 58 expectError: require.NoError, 59 }, 60 { 61 name: "multiple results - exact match found", 62 snapName: "test-snap", 63 statusCode: http.StatusOK, 64 mockResponse: snapFindResponse{ 65 Results: []struct { 66 Name string `json:"name"` 67 SnapID string `json:"snap-id"` 68 Snap struct{} `json:"snap"` 69 }{ 70 { 71 Name: "test-snap-extra", 72 SnapID: "wrong-id", 73 Snap: struct{}{}, 74 }, 75 { 76 Name: "test-snap", 77 SnapID: "correct-id", 78 Snap: struct{}{}, 79 }, 80 }, 81 }, 82 expectedExists: true, 83 expectedSnapID: "correct-id", 84 expectError: require.NoError, 85 }, 86 { 87 name: "find API returns 404", 88 snapName: "test", 89 statusCode: http.StatusNotFound, 90 expectError: require.Error, 91 errorContains: "find API request failed with status code 404", 92 }, 93 } 94 95 for _, tt := range tests { 96 t.Run(tt.name, func(t *testing.T) { 97 findServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 98 assert.Equal(t, defaultSeries, r.Header.Get("Snap-Device-Series")) 99 assert.Equal(t, tt.snapName, r.URL.Query().Get("name-startswith")) 100 101 w.WriteHeader(tt.statusCode) 102 if tt.statusCode == http.StatusOK { 103 responseBytes, err := json.Marshal(tt.mockResponse) 104 require.NoError(t, err) 105 w.Write(responseBytes) 106 } 107 })) 108 defer findServer.Close() 109 110 client := &snapcraftClient{ 111 FindAPIURL: findServer.URL, 112 HTTPClient: &http.Client{}, 113 } 114 115 exists, snapID, err := client.CheckSnapExists(tt.snapName) 116 tt.expectError(t, err) 117 if err != nil && tt.errorContains != "" { 118 assert.Contains(t, err.Error(), tt.errorContains) 119 return 120 } 121 122 assert.Equal(t, tt.expectedExists, exists) 123 assert.Equal(t, tt.expectedSnapID, snapID) 124 }) 125 } 126 } 127 128 func TestSnapcraftClient_GetSnapDownloadURL(t *testing.T) { 129 tests := []struct { 130 name string 131 snapID snapIdentity 132 infoResponse snapcraftInfo 133 infoStatusCode int 134 findResponse *snapFindResponse 135 findStatusCode int 136 expectedURL string 137 expectError require.ErrorAssertionFunc 138 errorContains string 139 }{ 140 { 141 name: "successful download URL retrieval", 142 snapID: snapIdentity{ 143 Name: "etcd", 144 Channel: "stable", 145 Architecture: "amd64", 146 }, 147 infoStatusCode: http.StatusOK, 148 infoResponse: snapcraftInfo{ 149 ChannelMap: []snapChannelMapEntry{ 150 { 151 Channel: snapChannel{ 152 Architecture: "amd64", 153 Name: "stable", 154 }, 155 Download: snapDownload{ 156 URL: "https://api.snapcraft.io/api/v1/snaps/download/etcd_123.snap", 157 }, 158 }, 159 }, 160 }, 161 expectedURL: "https://api.snapcraft.io/api/v1/snaps/download/etcd_123.snap", 162 expectError: require.NoError, 163 }, 164 { 165 name: "region-locked snap - exists but unavailable", 166 snapID: snapIdentity{ 167 Name: "jp-ledger", 168 Channel: "stable", 169 Architecture: "amd64", 170 }, 171 infoStatusCode: http.StatusNotFound, 172 findStatusCode: http.StatusOK, 173 findResponse: &snapFindResponse{ 174 Results: []struct { 175 Name string `json:"name"` 176 SnapID string `json:"snap-id"` 177 Snap struct{} `json:"snap"` 178 }{ 179 { 180 Name: "jp-ledger", 181 SnapID: "jyDlMmifyQhSWGPM9fnKc1HSD7E6c47e", 182 Snap: struct{}{}, 183 }, 184 }, 185 }, 186 expectError: require.Error, 187 errorContains: "found snap 'jp-ledger' (id=jyDlMmifyQhSWGPM9fnKc1HSD7E6c47e) but it is unavailable for download", 188 }, 189 { 190 name: "snap truly does not exist", 191 snapID: snapIdentity{ 192 Name: "nonexistent", 193 Channel: "stable", 194 Architecture: "amd64", 195 }, 196 infoStatusCode: http.StatusNotFound, 197 findStatusCode: http.StatusOK, 198 findResponse: &snapFindResponse{ 199 Results: []struct { 200 Name string `json:"name"` 201 SnapID string `json:"snap-id"` 202 Snap struct{} `json:"snap"` 203 }{}, 204 }, 205 expectError: require.Error, 206 errorContains: "no snap found with name 'nonexistent'", 207 }, 208 { 209 name: "multiple architectures - find correct one", 210 snapID: snapIdentity{ 211 Name: "mysql", 212 Channel: "stable", 213 Architecture: "arm64", 214 }, 215 infoStatusCode: http.StatusOK, 216 infoResponse: snapcraftInfo{ 217 ChannelMap: []snapChannelMapEntry{ 218 { 219 Channel: snapChannel{ 220 Architecture: "amd64", 221 Name: "stable", 222 }, 223 Download: snapDownload{ 224 URL: "https://api.snapcraft.io/api/v1/snaps/download/mysql_amd64.snap", 225 }, 226 }, 227 { 228 Channel: snapChannel{ 229 Architecture: "arm64", 230 Name: "stable", 231 }, 232 Download: snapDownload{ 233 URL: "https://api.snapcraft.io/api/v1/snaps/download/mysql_arm64.snap", 234 }, 235 }, 236 }, 237 }, 238 expectedURL: "https://api.snapcraft.io/api/v1/snaps/download/mysql_arm64.snap", 239 expectError: require.NoError, 240 }, 241 { 242 name: "snap not found - no matching architecture", 243 snapID: snapIdentity{ 244 Name: "etcd", 245 Channel: "stable", 246 Architecture: "s390x", 247 }, 248 infoStatusCode: http.StatusOK, 249 infoResponse: snapcraftInfo{ 250 ChannelMap: []snapChannelMapEntry{ 251 { 252 Channel: snapChannel{ 253 Architecture: "amd64", 254 Name: "stable", 255 }, 256 Download: snapDownload{ 257 URL: "https://api.snapcraft.io/api/v1/snaps/download/etcd_123.snap", 258 }, 259 }, 260 }, 261 }, 262 expectError: require.Error, 263 errorContains: "no matching snap found", 264 }, 265 { 266 name: "API returns 500", 267 snapID: snapIdentity{ 268 Name: "etcd", 269 Channel: "stable", 270 Architecture: "amd64", 271 }, 272 infoStatusCode: http.StatusInternalServerError, 273 expectError: require.Error, 274 errorContains: "API request failed with status code 500", 275 }, 276 { 277 name: "find API fails when checking 404", 278 snapID: snapIdentity{ 279 Name: "test-snap", 280 Channel: "stable", 281 Architecture: "amd64", 282 }, 283 infoStatusCode: http.StatusNotFound, 284 findStatusCode: http.StatusInternalServerError, 285 expectError: require.Error, 286 errorContains: "failed to check if snap exists", 287 }, 288 } 289 290 for _, tt := range tests { 291 t.Run(tt.name, func(t *testing.T) { 292 if tt.expectError == nil { 293 tt.expectError = require.NoError 294 } 295 296 infoServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 297 assert.Equal(t, defaultSeries, r.Header.Get("Snap-Device-Series")) 298 299 expectedPath := "/" + tt.snapID.Name 300 assert.Equal(t, expectedPath, r.URL.Path) 301 302 w.WriteHeader(tt.infoStatusCode) 303 304 if tt.infoStatusCode == http.StatusOK { 305 responseBytes, err := json.Marshal(tt.infoResponse) 306 require.NoError(t, err) 307 w.Write(responseBytes) 308 } 309 })) 310 defer infoServer.Close() 311 312 var findServer *httptest.Server 313 if tt.findResponse != nil || tt.findStatusCode != 0 { 314 findServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 315 assert.Equal(t, defaultSeries, r.Header.Get("Snap-Device-Series")) 316 assert.Equal(t, tt.snapID.Name, r.URL.Query().Get("name-startswith")) 317 318 statusCode := tt.findStatusCode 319 if statusCode == 0 { 320 statusCode = http.StatusOK 321 } 322 w.WriteHeader(statusCode) 323 324 if tt.findResponse != nil && statusCode == http.StatusOK { 325 responseBytes, err := json.Marshal(tt.findResponse) 326 require.NoError(t, err) 327 w.Write(responseBytes) 328 } 329 })) 330 defer findServer.Close() 331 } 332 333 client := &snapcraftClient{ 334 InfoAPIURL: infoServer.URL + "/", 335 HTTPClient: &http.Client{}, 336 } 337 if findServer != nil { 338 client.FindAPIURL = findServer.URL 339 } 340 341 url, err := client.GetSnapDownloadURL(tt.snapID) 342 tt.expectError(t, err) 343 if err != nil { 344 if tt.errorContains != "" { 345 assert.Contains(t, err.Error(), tt.errorContains) 346 } 347 return 348 } 349 assert.Equal(t, tt.expectedURL, url) 350 }) 351 } 352 } 353 354 func TestSnapcraftClient_GetSnapDownloadURL_InvalidJSON(t *testing.T) { 355 infoServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 356 w.WriteHeader(http.StatusOK) 357 w.Write([]byte("invalid json")) 358 })) 359 defer infoServer.Close() 360 361 client := &snapcraftClient{ 362 InfoAPIURL: infoServer.URL + "/", 363 HTTPClient: &http.Client{}, 364 } 365 366 snapID := snapIdentity{ 367 Name: "etcd", 368 Channel: "stable", 369 Architecture: "amd64", 370 } 371 372 _, err := client.GetSnapDownloadURL(snapID) 373 assert.Error(t, err) 374 assert.Contains(t, err.Error(), "failed to parse JSON response") 375 } 376 377 func TestNewSnapcraftClient(t *testing.T) { 378 client := newSnapcraftClient() 379 380 assert.Equal(t, "https://api.snapcraft.io/v2/snaps/info/", client.InfoAPIURL) 381 assert.Equal(t, "https://api.snapcraft.io/v2/snaps/find", client.FindAPIURL) 382 assert.NotNil(t, client.HTTPClient) 383 }