github.com/saucelabs/saucectl@v0.175.1/internal/http/appstore_test.go (about) 1 package http 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/http" 8 "net/http/httptest" 9 "os" 10 "path" 11 "reflect" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/hashicorp/go-retryablehttp" 17 "github.com/saucelabs/saucectl/internal/storage" 18 "github.com/stretchr/testify/assert" 19 "github.com/xtgo/uuid" 20 21 "gotest.tools/v3/fs" 22 ) 23 24 func TestAppStore_UploadStream(t *testing.T) { 25 // mock test values 26 itemID := uuid.NewRandom().String() 27 itemName := "hello.txt" 28 uploadTimestamp := time.Now().Round(1 * time.Second) 29 testUser := "test" 30 testPass := "test" 31 32 dir := fs.NewDir(t, "checksums", fs.WithFile(itemName, "world!")) 33 defer dir.Remove() 34 35 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 36 if r.Method != http.MethodPost { 37 w.WriteHeader(http.StatusMethodNotAllowed) 38 return 39 } 40 41 if r.URL.Path != "/v1/storage/upload" { 42 w.WriteHeader(404) 43 _, _ = w.Write([]byte("incorrect path")) 44 return 45 } 46 47 user, pass, _ := r.BasicAuth() 48 if user != testUser || pass != testPass { 49 w.WriteHeader(http.StatusForbidden) 50 _, _ = w.Write([]byte(http.StatusText(http.StatusForbidden))) 51 return 52 } 53 54 if err := r.ParseForm(); err != nil { 55 w.WriteHeader(400) 56 _, _ = fmt.Fprintf(w, "failed to parse form post: %v", err) 57 return 58 } 59 60 reader, err := r.MultipartReader() 61 if err != nil { 62 w.WriteHeader(400) 63 _, _ = fmt.Fprintf(w, "failed to read multipart form: %v", err) 64 return 65 } 66 67 p, err := reader.NextPart() 68 if err == io.EOF { 69 w.WriteHeader(400) 70 _, _ = fmt.Fprintf(w, "unexpected early end of multipart data: %v", err) 71 return 72 } 73 if err != nil { 74 w.WriteHeader(400) 75 _, _ = fmt.Fprintf(w, "failed to retrieve next part in multipart: %v", err) 76 return 77 } 78 79 size, _ := io.Copy(io.Discard, p) 80 81 w.WriteHeader(201) 82 _ = json.NewEncoder(w).Encode(UploadResponse{Item{ 83 ID: itemID, 84 Name: p.FileName(), 85 UploadTimestamp: uploadTimestamp.Unix(), 86 Size: int(size), 87 }}) 88 })) 89 defer server.Close() 90 91 type fields struct { 92 HTTPClient *retryablehttp.Client 93 URL string 94 Username string 95 AccessKey string 96 } 97 type args struct { 98 filename string 99 } 100 tests := []struct { 101 name string 102 fields fields 103 args args 104 want storage.Item 105 wantErr bool 106 }{ 107 { 108 name: "successfully upload file", 109 fields: fields{ 110 HTTPClient: NewRetryableClient(10 * time.Second), 111 URL: server.URL, 112 Username: testUser, 113 AccessKey: testPass, 114 }, 115 args: args{dir.Join("hello.txt")}, 116 want: storage.Item{ 117 ID: itemID, 118 Name: "hello.txt", 119 Uploaded: uploadTimestamp, 120 Size: 6, 121 }, 122 wantErr: false, 123 }, 124 { 125 name: "wrong credentials", 126 fields: fields{ 127 HTTPClient: NewRetryableClient(10 * time.Second), 128 URL: server.URL, 129 Username: testUser + "1", 130 AccessKey: testPass + "1", 131 }, 132 args: args{dir.Join("hello.txt")}, 133 want: storage.Item{}, 134 wantErr: true, 135 }, 136 } 137 for _, tt := range tests { 138 t.Run(tt.name, func(t *testing.T) { 139 s := &AppStore{ 140 HTTPClient: tt.fields.HTTPClient, 141 URL: tt.fields.URL, 142 Username: tt.fields.Username, 143 AccessKey: tt.fields.AccessKey, 144 } 145 146 f, err := os.Open(tt.args.filename) 147 if err != nil { 148 t.Error(err) 149 } 150 defer func(f *os.File) { 151 _ = f.Close() 152 }(f) 153 154 got, err := s.UploadStream(tt.args.filename, "", f) 155 if (err != nil) != tt.wantErr { 156 t.Errorf("UploadStream() error = %v, wantErr %v", err, tt.wantErr) 157 return 158 } 159 if !reflect.DeepEqual(got, tt.want) { 160 t.Errorf("UploadStream() got = %v, want %v", got, tt.want) 161 } 162 }) 163 } 164 } 165 166 func TestAppStore_List(t *testing.T) { 167 testUser := "test" 168 testPass := "test" 169 170 // Items that are known to the mock server. 171 items := []Item{ 172 { 173 ID: uuid.NewRandom().String(), 174 Name: "hello.app", 175 UploadTimestamp: time.Now().Add(-1 * time.Hour).Unix(), 176 }, 177 { 178 ID: uuid.NewRandom().String(), 179 Name: "world.app", 180 UploadTimestamp: time.Now().Add(-1 * time.Hour).Unix(), 181 }, 182 } 183 184 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 185 if r.Method != http.MethodGet { 186 w.WriteHeader(http.StatusMethodNotAllowed) 187 return 188 } 189 190 if r.URL.Path != "/v1/storage/files" { 191 w.WriteHeader(404) 192 _, _ = w.Write([]byte("incorrect path")) 193 return 194 } 195 196 user, pass, _ := r.BasicAuth() 197 if user != testUser || pass != testPass { 198 w.WriteHeader(http.StatusForbidden) 199 _, _ = w.Write([]byte(http.StatusText(http.StatusForbidden))) 200 return 201 } 202 203 var filteredItems []Item 204 filtered := false 205 206 nameQuery := r.URL.Query().Get("name") 207 if r.URL.Query().Get("name") != "" { 208 filtered = true 209 for _, v := range items { 210 if v.Name == nameQuery { 211 filteredItems = append(filteredItems, v) 212 } 213 } 214 } 215 216 // Return all items if no filter was applied. 217 if !filtered { 218 filteredItems = items 219 } 220 221 w.WriteHeader(200) 222 _ = json.NewEncoder(w).Encode(ListResponse{Items: filteredItems}) 223 })) 224 defer server.Close() 225 226 type fields struct { 227 HTTPClient *retryablehttp.Client 228 URL string 229 Username string 230 AccessKey string 231 } 232 type args struct { 233 opts storage.ListOptions 234 } 235 tests := []struct { 236 name string 237 fields fields 238 args args 239 want storage.List 240 wantErr bool 241 }{ 242 { 243 name: "query all", 244 fields: fields{ 245 HTTPClient: NewRetryableClient(10 * time.Second), 246 URL: server.URL, 247 Username: testUser, 248 AccessKey: testPass, 249 }, 250 args: args{}, 251 want: storage.List{ 252 Items: []storage.Item{ 253 { 254 ID: items[0].ID, 255 Name: items[0].Name, 256 Size: items[0].Size, 257 Uploaded: time.Unix(items[0].UploadTimestamp, 0), 258 }, 259 { 260 ID: items[1].ID, 261 Name: items[1].Name, 262 Size: items[1].Size, 263 Uploaded: time.Unix(items[1].UploadTimestamp, 0), 264 }, 265 }, 266 }, 267 wantErr: false, 268 }, 269 { 270 name: "query subset", 271 fields: fields{ 272 HTTPClient: NewRetryableClient(10 * time.Second), 273 URL: server.URL, 274 Username: testUser, 275 AccessKey: testPass, 276 }, 277 args: args{ 278 opts: storage.ListOptions{Name: items[0].Name}, 279 }, 280 want: storage.List{ 281 Items: []storage.Item{ 282 { 283 ID: items[0].ID, 284 Name: items[0].Name, 285 Size: items[0].Size, 286 Uploaded: time.Unix(items[0].UploadTimestamp, 0), 287 }, 288 }, 289 }, 290 wantErr: false, 291 }, 292 { 293 name: "wrong credentials", 294 fields: fields{ 295 HTTPClient: NewRetryableClient(10 * time.Second), 296 URL: server.URL, 297 Username: testUser + "1", 298 AccessKey: testPass + "1", 299 }, 300 args: args{}, 301 want: storage.List{}, 302 wantErr: true, 303 }, 304 } 305 for _, tt := range tests { 306 t.Run(tt.name, func(t *testing.T) { 307 s := &AppStore{ 308 HTTPClient: tt.fields.HTTPClient, 309 URL: tt.fields.URL, 310 Username: tt.fields.Username, 311 AccessKey: tt.fields.AccessKey, 312 } 313 got, err := s.List(tt.args.opts) 314 if (err != nil) != tt.wantErr { 315 t.Errorf("List() error = %v, wantErr %v", err, tt.wantErr) 316 return 317 } 318 if !reflect.DeepEqual(got, tt.want) { 319 t.Errorf("List() got = %v, want %v", got, tt.want) 320 } 321 }) 322 } 323 } 324 325 func TestAppStore_Delete(t *testing.T) { 326 testUser := "test" 327 testPass := "test" 328 329 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 330 if r.Method != http.MethodDelete { 331 w.WriteHeader(http.StatusMethodNotAllowed) 332 return 333 } 334 335 if !strings.HasPrefix(r.URL.Path, "/v1/storage/files/") { 336 w.WriteHeader(http.StatusNotImplemented) 337 _, _ = w.Write([]byte("incorrect path")) 338 return 339 } 340 println(path.Base(r.URL.Path)) 341 if path.Base(r.URL.Path) == "" { 342 w.WriteHeader(http.StatusBadRequest) 343 _, _ = w.Write([]byte("missing file id")) 344 return 345 } 346 347 user, pass, _ := r.BasicAuth() 348 if user != testUser || pass != testPass { 349 w.WriteHeader(http.StatusForbidden) 350 _, _ = w.Write([]byte(http.StatusText(http.StatusForbidden))) 351 return 352 } 353 354 w.WriteHeader(200) 355 // The real server's response body contains a JSON that describes the 356 // deleted item. We don't need that for this test. 357 })) 358 defer server.Close() 359 360 type fields struct { 361 HTTPClient *retryablehttp.Client 362 URL string 363 Username string 364 AccessKey string 365 } 366 type args struct { 367 id string 368 } 369 tests := []struct { 370 name string 371 fields fields 372 args args 373 wantErr assert.ErrorAssertionFunc 374 }{ 375 { 376 name: "delete item successfully", 377 fields: fields{ 378 HTTPClient: NewRetryableClient(10 * time.Second), 379 URL: server.URL, 380 Username: testUser, 381 AccessKey: testPass, 382 }, 383 args: args{id: uuid.NewRandom().String()}, 384 wantErr: assert.NoError, 385 }, 386 { 387 name: "fail on wrong credentials", 388 fields: fields{ 389 HTTPClient: NewRetryableClient(10 * time.Second), 390 URL: server.URL, 391 Username: testUser + "1", 392 AccessKey: testPass + "1", 393 }, 394 args: args{id: uuid.NewRandom().String()}, 395 wantErr: assert.Error, 396 }, 397 { 398 name: "fail when no ID was specified", 399 fields: fields{ 400 HTTPClient: NewRetryableClient(10 * time.Second), 401 URL: server.URL, 402 Username: testUser, 403 AccessKey: testPass, 404 }, 405 args: args{id: ""}, 406 wantErr: assert.Error, 407 }, 408 } 409 for _, tt := range tests { 410 t.Run(tt.name, func(t *testing.T) { 411 s := &AppStore{ 412 HTTPClient: tt.fields.HTTPClient, 413 URL: tt.fields.URL, 414 Username: tt.fields.Username, 415 AccessKey: tt.fields.AccessKey, 416 } 417 tt.wantErr(t, s.Delete(tt.args.id), fmt.Sprintf("Delete(%v)", tt.args.id)) 418 }) 419 } 420 }