github.com/saucelabs/saucectl@v0.175.1/internal/http/resto_test.go (about) 1 package http 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "math/rand" 8 "net/http" 9 "net/http/httptest" 10 "reflect" 11 "sort" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/hashicorp/go-retryablehttp" 17 "github.com/stretchr/testify/assert" 18 19 "github.com/saucelabs/saucectl/internal/build" 20 "github.com/saucelabs/saucectl/internal/job" 21 tunnels "github.com/saucelabs/saucectl/internal/tunnel" 22 "github.com/saucelabs/saucectl/internal/vmd" 23 ) 24 25 func TestResto_GetJobDetails(t *testing.T) { 26 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 var err error 28 switch r.URL.Path { 29 case "/rest/v1.1/test/jobs/1": 30 completeStatusResp := []byte(`{"browser_short_version": "85", "video_url": "https://localhost/jobs/1/video.mp4", "creation_time": 1605637528, "custom-data": null, "browser_version": "85.0.4183.83", "owner": "test", "automation_backend": "webdriver", "id": "1", "collects_automator_log": false, "record_screenshots": true, "record_video": true, "build": null, "passed": null, "public": "team", "assigned_tunnel_id": null, "status": "complete", "log_url": "https://localhost/jobs/1/selenium-server.log", "start_time": 1605637528, "proxied": false, "modification_time": 1605637554, "tags": [], "name": null, "commands_not_successful": 4, "consolidated_status": "complete", "selenium_version": null, "manual": false, "end_time": 1605637554, "error": null, "os": "Windows 10", "breakpointed": null, "browser": "googlechrome"}`) 31 _, err = w.Write(completeStatusResp) 32 case "/rest/v1.1/test/jobs/2": 33 errorStatusResp := []byte(`{"browser_short_version": "85", "video_url": "https://localhost/jobs/2/video.mp4", "creation_time": 1605637528, "custom-data": null, "browser_version": "85.0.4183.83", "owner": "test", "automation_backend": "webdriver", "id": "2", "collects_automator_log": false, "record_screenshots": true, "record_video": true, "build": null, "passed": null, "public": "team", "assigned_tunnel_id": null, "status": "error", "log_url": "https://localhost/jobs/2/selenium-server.log", "start_time": 1605637528, "proxied": false, "modification_time": 1605637554, "tags": [], "name": null, "commands_not_successful": 4, "consolidated_status": "error", "selenium_version": null, "manual": false, "end_time": 1605637554, "error": "User Abandoned Test -- User terminated", "os": "Windows 10", "breakpointed": null, "browser": "googlechrome"}`) 34 _, err = w.Write(errorStatusResp) 35 case "/rest/v1.1/test/jobs/3": 36 w.WriteHeader(http.StatusNotFound) 37 case "/rest/v1.1/test/jobs/4": 38 w.WriteHeader(http.StatusUnauthorized) 39 default: 40 w.WriteHeader(http.StatusInternalServerError) 41 } 42 43 if err != nil { 44 t.Errorf("failed to respond: %v", err) 45 } 46 })) 47 defer ts.Close() 48 timeout := 3 * time.Second 49 50 testCases := []struct { 51 name string 52 client Resto 53 jobID string 54 expectedResp job.Job 55 expectedErr error 56 }{ 57 { 58 name: "get job details with ID 1 and status 'complete'", 59 client: NewResto(ts.URL, "test", "123", timeout), 60 jobID: "1", 61 expectedResp: job.Job{ 62 ID: "1", 63 Passed: false, 64 Status: "complete", 65 Error: "", 66 BrowserName: "googlechrome", 67 BrowserVersion: "85", 68 Framework: "webdriver", 69 OS: "Windows", 70 OSVersion: "10", 71 }, 72 expectedErr: nil, 73 }, 74 { 75 name: "get job details with ID 2 and status 'error'", 76 client: NewResto(ts.URL, "test", "123", timeout), 77 jobID: "2", 78 expectedResp: job.Job{ 79 ID: "2", 80 Passed: false, 81 Status: "error", 82 Error: "User Abandoned Test -- User terminated", 83 BrowserName: "googlechrome", 84 BrowserVersion: "85", 85 Framework: "webdriver", 86 OS: "Windows", 87 OSVersion: "10", 88 }, 89 expectedErr: nil, 90 }, 91 { 92 name: "job not found error from external API", 93 client: NewResto(ts.URL, "test", "123", timeout), 94 jobID: "3", 95 expectedResp: job.Job{}, 96 expectedErr: ErrJobNotFound, 97 }, 98 { 99 name: "http status is not 200, but 401 from external API", 100 client: NewResto(ts.URL, "test", "123", timeout), 101 jobID: "4", 102 expectedResp: job.Job{}, 103 expectedErr: errors.New("job status request failed; unexpected response code:'401', msg:''"), 104 }, 105 { 106 name: "internal server error from external API", 107 client: NewResto(ts.URL, "test", "123", timeout), 108 jobID: "333", 109 expectedResp: job.Job{}, 110 expectedErr: errors.New("internal server error"), 111 }, 112 } 113 114 for _, tc := range testCases { 115 t.Run(tc.name, func(t *testing.T) { 116 tc.client.Client.RetryWaitMax = 1 * time.Millisecond 117 got, err := tc.client.ReadJob(context.Background(), tc.jobID, false) 118 assert.Equal(t, tc.expectedResp, got) 119 if err != nil { 120 assert.Equal(t, tc.expectedErr, err) 121 } 122 }) 123 } 124 } 125 126 func TestResto_GetJobStatus(t *testing.T) { 127 rand.Seed(time.Now().UnixNano()) 128 129 var retryCount int 130 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 131 var err error 132 switch r.URL.Path { 133 case "/rest/v1.1/test/jobs/1": 134 details := &restoJob{ 135 ID: "1", 136 Passed: false, 137 Status: "new", 138 Error: "", 139 } 140 randJobStatus(details, true) 141 142 resp, _ := json.Marshal(details) 143 _, err = w.Write(resp) 144 case "/rest/v1.1/test/jobs/2": 145 details := &restoJob{ 146 ID: "2", 147 Passed: false, 148 Status: "in progress", 149 Error: "User Abandoned Test -- User terminated", 150 } 151 randJobStatus(details, false) 152 153 resp, _ := json.Marshal(details) 154 _, err = w.Write(resp) 155 case "/rest/v1.1/test/jobs/3": 156 w.WriteHeader(http.StatusNotFound) 157 case "/rest/v1.1/test/jobs/4": 158 w.WriteHeader(http.StatusUnauthorized) 159 case "/rest/v1.1/test/jobs/5": 160 if retryCount < 2 { 161 w.WriteHeader(http.StatusInternalServerError) 162 retryCount++ 163 return 164 } 165 details := &restoJob{ 166 ID: "5", 167 Passed: false, 168 Status: "new", 169 Error: "", 170 } 171 randJobStatus(details, true) 172 173 resp, _ := json.Marshal(details) 174 _, err = w.Write(resp) 175 default: 176 w.WriteHeader(http.StatusInternalServerError) 177 } 178 179 if err != nil { 180 t.Errorf("failed to respond: %v", err) 181 } 182 })) 183 defer ts.Close() 184 timeout := 3 * time.Second 185 186 testCases := []struct { 187 name string 188 client Resto 189 jobID string 190 expectedResp job.Job 191 expectedErr error 192 }{ 193 { 194 name: "get job details with ID 1 and status 'complete'", 195 client: NewResto(ts.URL, "test", "123", timeout), 196 jobID: "1", 197 expectedResp: job.Job{ 198 ID: "1", 199 Passed: false, 200 Status: "complete", 201 Error: "", 202 }, 203 expectedErr: nil, 204 }, 205 { 206 name: "get job details with ID 2 and status 'error'", 207 client: NewResto(ts.URL, "test", "123", timeout), 208 jobID: "2", 209 expectedResp: job.Job{ 210 ID: "2", 211 Passed: false, 212 Status: "error", 213 Error: "User Abandoned Test -- User terminated", 214 }, 215 expectedErr: nil, 216 }, 217 { 218 name: "user not found error from external API", 219 client: NewResto(ts.URL, "test", "123", timeout), 220 jobID: "3", 221 expectedResp: job.Job{}, 222 expectedErr: ErrJobNotFound, 223 }, 224 { 225 name: "http status is not 200, but 401 from external API", 226 client: NewResto(ts.URL, "test", "123", timeout), 227 jobID: "4", 228 expectedResp: job.Job{}, 229 expectedErr: errors.New("job status request failed; unexpected response code:'401', msg:''"), 230 }, 231 { 232 name: "unexpected status code from external API", 233 client: NewResto(ts.URL, "test", "123", timeout), 234 jobID: "333", 235 expectedResp: job.Job{}, 236 expectedErr: errors.New("internal server error"), 237 }, 238 { 239 name: "get job details with ID 5. retry 2 times and succeed", 240 client: NewResto(ts.URL, "test", "123", timeout), 241 jobID: "5", 242 expectedResp: job.Job{ 243 ID: "5", 244 Passed: false, 245 Status: "complete", 246 Error: "", 247 }, 248 expectedErr: nil, 249 }, 250 } 251 252 for _, tc := range testCases { 253 t.Run(tc.name, func(t *testing.T) { 254 tc.client.Client.RetryWaitMax = 1 * time.Millisecond 255 got, err := tc.client.PollJob(context.Background(), tc.jobID, 10*time.Millisecond, 0, false) 256 assert.Equal(t, got, tc.expectedResp) 257 if err != nil { 258 assert.Equal(t, tc.expectedErr, err) 259 } 260 }) 261 } 262 } 263 264 func TestResto_GetJobAssetFileNames(t *testing.T) { 265 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 266 var err error 267 switch r.URL.Path { 268 case "/rest/v1/test/jobs/1/assets": 269 completeStatusResp := []byte(`{"console.log": "console.log", "examples__actions.spec.js.mp4": "examples__actions.spec.js.mp4", "examples__actions.spec.js.json": "examples__actions.spec.js.json", "video.mp4": "video.mp4", "selenium-log": null, "sauce-log": null, "examples__actions.spec.js.xml": "examples__actions.spec.js.xml", "video": "video.mp4", "screenshots": []}`) 270 _, err = w.Write(completeStatusResp) 271 case "/rest/v1/test/jobs/2/assets": 272 w.WriteHeader(http.StatusNotFound) 273 case "/rest/v1/test/jobs/3/assets": 274 w.WriteHeader(http.StatusUnauthorized) 275 default: 276 w.WriteHeader(http.StatusInternalServerError) 277 completeStatusResp := []byte(`{"console.log": "console.log", "examples__actions.spec.js.mp4": "examples__actions.spec.js.mp4", "examples__actions.spec.js.json": "examples__actions.spec.js.json", "video.mp4": "video.mp4", "selenium-log": null, "sauce-log": null, "examples__actions.spec.js.xml": "examples__actions.spec.js.xml", "video": "video.mp4", "screenshots": []}`) 278 _, err = w.Write(completeStatusResp) 279 } 280 281 if err != nil { 282 t.Errorf("failed to respond: %v", err) 283 } 284 })) 285 defer ts.Close() 286 timeout := 3 * time.Second 287 288 testCases := []struct { 289 name string 290 client Resto 291 jobID string 292 expectedResp []string 293 expectedErr error 294 }{ 295 { 296 name: "get job asset with ID 1", 297 client: NewResto(ts.URL, "test", "123", timeout), 298 jobID: "1", 299 expectedResp: []string{"console.log", "examples__actions.spec.js.mp4", "examples__actions.spec.js.json", "video.mp4", "examples__actions.spec.js.xml"}, 300 expectedErr: nil, 301 }, 302 { 303 name: "get job asset with ID 2", 304 client: NewResto(ts.URL, "test", "123", timeout), 305 jobID: "2", 306 expectedResp: nil, 307 expectedErr: ErrJobNotFound, 308 }, 309 { 310 name: "get job asset with ID 3", 311 client: NewResto(ts.URL, "test", "123", timeout), 312 jobID: "3", 313 expectedResp: nil, 314 expectedErr: errors.New("job assets list request failed; unexpected response code:'401', msg:''"), 315 }, 316 { 317 name: "get job asset with ID 4", 318 client: NewResto(ts.URL, "test", "123", timeout), 319 jobID: "4", 320 expectedResp: nil, 321 expectedErr: errors.New("internal server error"), 322 }, 323 } 324 for _, tc := range testCases { 325 t.Run(tc.name, func(t *testing.T) { 326 tc.client.Client.RetryWaitMax = 1 * time.Millisecond 327 got, err := tc.client.GetJobAssetFileNames(context.Background(), tc.jobID, false) 328 sort.Strings(tc.expectedResp) 329 sort.Strings(got) 330 assert.Equal(t, tc.expectedResp, got) 331 if err != nil { 332 assert.Equal(t, tc.expectedErr, err) 333 } 334 }) 335 } 336 } 337 338 func TestResto_GetJobAssetFileContent(t *testing.T) { 339 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 340 var err error 341 switch r.URL.Path { 342 case "/rest/v1/test/jobs/1/assets/console.log": 343 fileContent := []byte(`Sauce Cypress Runner 0.2.3`) 344 _, err = w.Write(fileContent) 345 case "/rest/v1/test/jobs/2/assets/console.log": 346 w.WriteHeader(http.StatusNotFound) 347 case "/rest/v1/test/jobs/3/assets/console.log": 348 w.WriteHeader(http.StatusUnauthorized) 349 fileContent := []byte(`unauthorized`) 350 _, err = w.Write(fileContent) 351 default: 352 w.WriteHeader(http.StatusInternalServerError) 353 } 354 355 if err != nil { 356 t.Errorf("failed to respond: %v", err) 357 } 358 })) 359 defer ts.Close() 360 timeout := 3 * time.Second 361 362 testCases := []struct { 363 name string 364 client Resto 365 jobID string 366 expectedResp []byte 367 expectedErr error 368 }{ 369 { 370 name: "get job asset with ID 1", 371 client: NewResto(ts.URL, "test", "123", timeout), 372 jobID: "1", 373 expectedResp: []byte(`Sauce Cypress Runner 0.2.3`), 374 expectedErr: nil, 375 }, 376 { 377 name: "get job asset with ID 333 and Internal Server Error ", 378 client: NewResto(ts.URL, "test", "123", timeout), 379 jobID: "333", 380 expectedResp: nil, 381 expectedErr: errors.New("internal server error"), 382 }, 383 { 384 name: "get job asset with ID 2", 385 client: NewResto(ts.URL, "test", "123", timeout), 386 jobID: "2", 387 expectedResp: nil, 388 expectedErr: ErrAssetNotFound, 389 }, 390 { 391 name: "get job asset with ID 3", 392 client: NewResto(ts.URL, "test", "123", timeout), 393 jobID: "3", 394 expectedResp: nil, 395 expectedErr: errors.New("job status request failed; unexpected response code:'401', msg:'unauthorized'"), 396 }, 397 } 398 399 for _, tc := range testCases { 400 t.Run(tc.name, func(t *testing.T) { 401 tc.client.Client.RetryWaitMax = 1 * time.Millisecond 402 got, err := tc.client.GetJobAssetFileContent(context.Background(), tc.jobID, "console.log", false) 403 assert.Equal(t, got, tc.expectedResp) 404 if err != nil { 405 assert.Equal(t, tc.expectedErr, err) 406 } 407 }) 408 } 409 } 410 411 func randJobStatus(j *restoJob, isComplete bool) { 412 min := 1 413 max := 10 414 randNum := rand.Intn(max-min+1) + min 415 416 status := "error" 417 if isComplete { 418 status = "complete" 419 } 420 421 if randNum >= 5 { 422 j.Status = status 423 } 424 } 425 426 func TestResto_TestStop(t *testing.T) { 427 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 428 var err error 429 switch r.URL.Path { 430 case "/rest/v1/test/jobs/1/stop": 431 completeStatusResp := []byte(`{"browser_short_version": "85", "video_url": "https://localhost/jobs/1/video.mp4", "creation_time": 1605637528, "custom-data": null, "browser_version": "85.0.4183.83", "owner": "test", "automation_backend": "webdriver", "id": "1", "collects_automator_log": false, "record_screenshots": true, "record_video": true, "build": null, "passed": null, "public": "team", "assigned_tunnel_id": null, "status": "complete", "log_url": "https://localhost/jobs/1/selenium-server.log", "start_time": 1605637528, "proxied": false, "modification_time": 1605637554, "tags": [], "name": null, "commands_not_successful": 4, "consolidated_status": "complete", "selenium_version": null, "manual": false, "end_time": 1605637554, "error": null, "os": "Windows 10", "breakpointed": null, "browser": "googlechrome"}`) 432 _, err = w.Write(completeStatusResp) 433 case "/rest/v1/test/jobs/2/stop": 434 errorStatusResp := []byte(`{"browser_short_version": "85", "video_url": "https://localhost/jobs/2/video.mp4", "creation_time": 1605637528, "custom-data": null, "browser_version": "85.0.4183.83", "owner": "test", "automation_backend": "webdriver", "id": "2", "collects_automator_log": false, "record_screenshots": true, "record_video": true, "build": null, "passed": null, "public": "team", "assigned_tunnel_id": null, "status": "error", "log_url": "https://localhost/jobs/2/selenium-server.log", "start_time": 1605637528, "proxied": false, "modification_time": 1605637554, "tags": [], "name": null, "commands_not_successful": 4, "consolidated_status": "error", "selenium_version": null, "manual": false, "end_time": 1605637554, "error": "User Abandoned Test -- User terminated", "os": "Windows 10", "breakpointed": null, "browser": "googlechrome"}`) 435 _, err = w.Write(errorStatusResp) 436 case "/rest/v1/test/jobs/3/stop": 437 w.WriteHeader(http.StatusNotFound) 438 case "/rest/v1/test/jobs/4/stop": 439 w.WriteHeader(http.StatusUnauthorized) 440 default: 441 w.WriteHeader(http.StatusInternalServerError) 442 } 443 444 if err != nil { 445 t.Errorf("failed to respond: %v", err) 446 } 447 })) 448 defer ts.Close() 449 timeout := 3 * time.Second 450 451 testCases := []struct { 452 name string 453 client Resto 454 jobID string 455 expectedResp job.Job 456 expectedErr error 457 }{ 458 { 459 name: "get job details with ID 2 and status 'error'", 460 client: NewResto(ts.URL, "test", "123", timeout), 461 jobID: "2", 462 expectedResp: job.Job{ 463 ID: "2", 464 Passed: false, 465 Status: "error", 466 Error: "User Abandoned Test -- User terminated", 467 BrowserName: "googlechrome", 468 BrowserVersion: "85", 469 Framework: "webdriver", 470 OS: "Windows", 471 OSVersion: "10", 472 }, 473 expectedErr: nil, 474 }, 475 { 476 name: "job not found error from external API", 477 client: NewResto(ts.URL, "test", "123", timeout), 478 jobID: "3", 479 expectedResp: job.Job{}, 480 expectedErr: ErrJobNotFound, 481 }, 482 { 483 name: "http status is not 200, but 401 from external API", 484 client: NewResto(ts.URL, "test", "123", timeout), 485 jobID: "4", 486 expectedResp: job.Job{}, 487 expectedErr: errors.New("job status request failed; unexpected response code:'401', msg:''"), 488 }, 489 { 490 name: "internal server error from external API", 491 client: NewResto(ts.URL, "test", "123", timeout), 492 jobID: "333", 493 expectedResp: job.Job{}, 494 expectedErr: errors.New("internal server error"), 495 }, 496 } 497 498 for _, tc := range testCases { 499 t.Run(tc.name, func(t *testing.T) { 500 tc.client.Client.RetryWaitMax = 1 * time.Millisecond 501 got, err := tc.client.StopJob(context.Background(), tc.jobID, false) 502 assert.Equal(t, tc.expectedResp, got) 503 if err != nil { 504 assert.Equal(t, tc.expectedErr, err) 505 } 506 }) 507 } 508 } 509 510 func TestResto_GetVirtualDevices(t *testing.T) { 511 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 512 var err error 513 switch r.URL.Path { 514 case "/rest/v1.1/info/platforms/all": 515 w.WriteHeader(http.StatusOK) 516 _, err = w.Write([]byte(`[{"long_name": "Samsung Galaxy S7 FHD GoogleAPI Emulator", "short_version": "7.0"},{"long_name": "Samsung Galaxy S9 HD GoogleAPI Emulator", "short_version": "8.0"},{"long_name": "iPhone 6s Simulator", "short_version": "11.0"},{"long_name": "iPhone 8 Plus Simulator", "short_version": "14.3"}]`)) 517 default: 518 w.WriteHeader(http.StatusInternalServerError) 519 } 520 521 if err != nil { 522 t.Errorf("failed to respond: %v", err) 523 } 524 })) 525 526 client := retryablehttp.NewClient() 527 client.HTTPClient = ts.Client() 528 client.RetryWaitMax = 1 * time.Millisecond 529 c := &Resto{ 530 Client: client, 531 URL: ts.URL, 532 Username: "dummy-user", 533 AccessKey: "dummy-key", 534 } 535 536 type args struct { 537 ctx context.Context 538 kind string 539 } 540 tests := []struct { 541 name string 542 args args 543 want []vmd.VirtualDevice 544 wantErr bool 545 }{ 546 { 547 name: "iOS Virtual Devices", 548 args: args{ 549 ctx: context.Background(), 550 kind: vmd.IOSSimulator, 551 }, 552 want: []vmd.VirtualDevice{ 553 {Name: "iPhone 6s Simulator", OSVersion: []string{"11.0"}}, 554 {Name: "iPhone 8 Plus Simulator", OSVersion: []string{"14.3"}}, 555 }, 556 }, 557 { 558 name: "Android Virtual Devices", 559 args: args{ 560 ctx: context.Background(), 561 kind: vmd.AndroidEmulator, 562 }, 563 want: []vmd.VirtualDevice{ 564 {Name: "Samsung Galaxy S7 FHD GoogleAPI Emulator", OSVersion: []string{"7.0"}}, 565 {Name: "Samsung Galaxy S9 HD GoogleAPI Emulator", OSVersion: []string{"8.0"}}, 566 }, 567 }, 568 } 569 for _, tt := range tests { 570 t.Run(tt.name, func(t *testing.T) { 571 got, err := c.GetVirtualDevices(tt.args.ctx, tt.args.kind) 572 if (err != nil) != tt.wantErr { 573 t.Errorf("GetVirtualDevices() error = %v, wantErr %v", err, tt.wantErr) 574 return 575 } 576 if !reflect.DeepEqual(got, tt.want) { 577 t.Errorf("GetVirtualDevices() got = %v, want %v", got, tt.want) 578 } 579 }) 580 } 581 } 582 583 func TestResto_isTunnelRunning(t *testing.T) { 584 var responseBody string 585 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 586 var err error 587 switch r.URL.Path { 588 case "/rest/v1/DummyUser/tunnels": 589 w.WriteHeader(http.StatusOK) 590 _, err = w.Write([]byte(responseBody)) 591 default: 592 w.WriteHeader(http.StatusInternalServerError) 593 } 594 595 if err != nil { 596 t.Errorf("failed to respond: %v", err) 597 } 598 })) 599 defer ts.Close() 600 client := retryablehttp.NewClient() 601 client.HTTPClient = ts.Client() 602 client.RetryWaitMax = 1 * time.Millisecond 603 c := Resto{ 604 Client: client, 605 URL: ts.URL, 606 Username: "DummyUser", 607 AccessKey: "DummyKey", 608 } 609 610 type args struct { 611 ctx context.Context 612 id string 613 parent string 614 filter tunnels.Filter 615 } 616 tests := []struct { 617 name string 618 response string 619 args args 620 wantErr bool 621 }{ 622 { 623 name: "Not found", 624 response: `{}`, 625 args: args{ 626 ctx: context.Background(), 627 id: "not-found", 628 }, 629 wantErr: true, 630 }, 631 { 632 name: "Tunnel found", 633 response: `{"DummyUser":[{"id":"found-tunnel","owner":"DummyUser","status":"running"}]}`, 634 args: args{ 635 ctx: context.Background(), 636 id: "found-tunnel", 637 }, 638 wantErr: false, 639 }, 640 { 641 name: "Tunnel not found (other user)", 642 response: `{"OtherDummyUser":[{"id":"found-tunnel","owner":"OtherDummyUser","status":"running"}]}`, 643 args: args{ 644 ctx: context.Background(), 645 id: "found-tunnel", 646 }, 647 wantErr: true, 648 }, 649 { 650 name: "Tunnel found (other user)", 651 response: `{"OtherDummyUser":[{"id":"found-tunnel","owner":"OtherDummyUser","status":"running"}]}`, 652 args: args{ 653 ctx: context.Background(), 654 id: "found-tunnel", 655 parent: "OtherDummyUser", 656 }, 657 wantErr: false, 658 }, 659 } 660 for _, tt := range tests { 661 t.Run(tt.name, func(t *testing.T) { 662 responseBody = tt.response 663 if err := c.isTunnelRunning(tt.args.ctx, tt.args.id, tt.args.parent, tt.args.filter); (err != nil) != tt.wantErr { 664 t.Errorf("isTunnelRunning() error = %v, wantErr %v", err, tt.wantErr) 665 } 666 }) 667 } 668 } 669 670 func TestResto_GetBuildID(t *testing.T) { 671 testCases := []struct { 672 name string 673 statusCode int 674 responseBody []byte 675 want string 676 wantErr error 677 }{ 678 { 679 name: "happy case", 680 statusCode: http.StatusOK, 681 responseBody: []byte(`{"id": "happy-build-id"}`), 682 want: "happy-build-id", 683 wantErr: nil, 684 }, 685 { 686 name: "job not found", 687 statusCode: http.StatusNotFound, 688 responseBody: nil, 689 want: "", 690 wantErr: errors.New("unexpected statusCode: 404"), 691 }, 692 { 693 name: "validation error", 694 statusCode: http.StatusUnprocessableEntity, 695 responseBody: nil, 696 want: "", 697 wantErr: errors.New("unexpected statusCode: 422"), 698 }, 699 { 700 name: "unparseable response", 701 statusCode: http.StatusOK, 702 responseBody: []byte(`{"id": "bad-json-response"`), 703 want: "", 704 wantErr: errors.New("unexpected EOF"), 705 }, 706 } 707 for _, tt := range testCases { 708 // arrange 709 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 710 w.WriteHeader(tt.statusCode) 711 _, _ = w.Write(tt.responseBody) 712 })) 713 defer ts.Close() 714 715 client := NewResto(ts.URL, "user", "key", 3*time.Second) 716 client.Client.RetryWaitMax = 1 * time.Millisecond 717 718 // act 719 bid, err := client.GetBuildID(context.Background(), "some-job-id", build.VDC) 720 721 // assert 722 assert.Equal(t, bid, tt.want) 723 if err != nil { 724 assert.True(t, strings.Contains(err.Error(), tt.wantErr.Error())) 725 } 726 } 727 }