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  }