github.com/saucelabs/saucectl@v0.175.1/internal/http/testcomposer.go (about) 1 package http 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "mime/multipart" 10 "net/http" 11 "net/textproto" 12 "strings" 13 "time" 14 15 "github.com/saucelabs/saucectl/internal/framework" 16 "github.com/saucelabs/saucectl/internal/iam" 17 ) 18 19 // TestComposer service 20 type TestComposer struct { 21 HTTPClient *http.Client 22 URL string // e.g.) https://api.<region>.saucelabs.net 23 Credentials iam.Credentials 24 } 25 26 // FrameworkResponse represents the response body for framework information. 27 type FrameworkResponse struct { 28 Name string `json:"name"` 29 Version string `json:"version"` 30 EOLDate time.Time `json:"eolDate"` 31 RemovalDate time.Time `json:"removalDate"` 32 Runner runner `json:"runner"` 33 Platforms []struct { 34 Name string 35 Browsers []string 36 } `json:"platforms"` 37 BrowserDefaults map[string]string `json:"browserDefaults"` 38 } 39 40 // TokenResponse represents the response body for slack token. 41 type TokenResponse struct { 42 Token string `json:"token"` 43 } 44 45 type runner struct { 46 CloudRunnerVersion string `json:"cloudRunnerVersion"` 47 DockerImage string `json:"dockerImage"` 48 GitRelease string `json:"gitRelease"` 49 } 50 51 func NewTestComposer(url string, creds iam.Credentials, timeout time.Duration) TestComposer { 52 return TestComposer{ 53 HTTPClient: &http.Client{ 54 Timeout: timeout, 55 Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, 56 }, 57 URL: url, 58 Credentials: creds, 59 } 60 } 61 62 // GetSlackToken gets slack token. 63 func (c *TestComposer) GetSlackToken(ctx context.Context) (string, error) { 64 url := fmt.Sprintf("%s/v1/testcomposer/users/%s/settings/slack", c.URL, c.Credentials.Username) 65 66 req, err := NewRequestWithContext(ctx, http.MethodGet, url, nil) 67 if err != nil { 68 return "", err 69 } 70 req.SetBasicAuth(c.Credentials.Username, c.Credentials.AccessKey) 71 72 var resp TokenResponse 73 if err := c.doJSONResponse(req, 200, &resp); err != nil { 74 return "", err 75 } 76 77 return resp.Token, nil 78 } 79 80 func (c *TestComposer) doJSONResponse(req *http.Request, expectStatus int, v interface{}) error { 81 res, err := c.HTTPClient.Do(req) 82 if err != nil { 83 return err 84 } 85 defer res.Body.Close() 86 87 if res.StatusCode != expectStatus { 88 body, _ := io.ReadAll(res.Body) 89 return fmt.Errorf("unexpected status '%d' from test-composer: %s", res.StatusCode, body) 90 } 91 92 return json.NewDecoder(res.Body).Decode(v) 93 } 94 95 // UploadAsset uploads an asset to the specified jobID. 96 func (c *TestComposer) UploadAsset(jobID string, realDevice bool, fileName string, contentType string, content []byte) error { 97 var b bytes.Buffer 98 w := multipart.NewWriter(&b) 99 h := make(textproto.MIMEHeader) 100 h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "file", fileName)) 101 h.Set("Content-Type", contentType) 102 wr, err := w.CreatePart(h) 103 if err != nil { 104 return err 105 } 106 if _, err = wr.Write(content); err != nil { 107 return err 108 } 109 if err = w.Close(); err != nil { 110 return err 111 } 112 113 req, err := NewRequestWithContext(context.Background(), http.MethodPut, 114 fmt.Sprintf("%s/v1/testcomposer/jobs/%s/assets", c.URL, jobID), &b) 115 if err != nil { 116 return err 117 } 118 req.SetBasicAuth(c.Credentials.Username, c.Credentials.AccessKey) 119 req.Header.Set("Content-Type", w.FormDataContentType()) 120 121 resp, err := c.HTTPClient.Do(req) 122 if err != nil { 123 return err 124 } 125 defer resp.Body.Close() 126 127 if resp.StatusCode >= http.StatusInternalServerError { 128 return ErrServerError 129 } 130 131 if resp.StatusCode == http.StatusNotFound { 132 return ErrJobNotFound 133 } 134 135 if resp.StatusCode != http.StatusOK { 136 body, _ := io.ReadAll(resp.Body) 137 return fmt.Errorf("assets upload request failed; unexpected response code:'%d', msg:'%v'", resp.StatusCode, string(body)) 138 } 139 140 var assetsResponse struct { 141 Uploaded []string `json:"uploaded"` 142 Errors []string `json:"errors,omitempty"` 143 } 144 if err = json.NewDecoder(resp.Body).Decode(&assetsResponse); err != nil { 145 return err 146 } 147 if len(assetsResponse.Errors) > 0 { 148 return fmt.Errorf("upload failed: %v", strings.Join(assetsResponse.Errors, ",")) 149 } 150 return nil 151 } 152 153 // Frameworks returns the list of available frameworks. 154 func (c *TestComposer) Frameworks(ctx context.Context) ([]string, error) { 155 url := fmt.Sprintf("%s/v2/testcomposer/frameworks", c.URL) 156 157 req, err := NewRequestWithContext(ctx, http.MethodGet, url, nil) 158 if err != nil { 159 return []string{}, err 160 } 161 req.SetBasicAuth(c.Credentials.Username, c.Credentials.AccessKey) 162 163 var resp []framework.Framework 164 if err = c.doJSONResponse(req, 200, &resp); err != nil { 165 return []string{}, err 166 } 167 return uniqFrameworkNameSet(resp), nil 168 } 169 170 // Versions return the list of available versions for a specific framework and region. 171 func (c *TestComposer) Versions(ctx context.Context, frameworkName string) ([]framework.Metadata, error) { 172 url := fmt.Sprintf("%s/v2/testcomposer/frameworks?frameworkName=%s", c.URL, frameworkName) 173 174 req, err := NewRequestWithContext(ctx, http.MethodGet, url, nil) 175 if err != nil { 176 return []framework.Metadata{}, err 177 } 178 req.SetBasicAuth(c.Credentials.Username, c.Credentials.AccessKey) 179 180 var resp []FrameworkResponse 181 if err = c.doJSONResponse(req, 200, &resp); err != nil { 182 return []framework.Metadata{}, err 183 } 184 185 var frameworks []framework.Metadata 186 for _, f := range resp { 187 var platforms []framework.Platform 188 for _, p := range f.Platforms { 189 platforms = append(platforms, framework.Platform{ 190 PlatformName: p.Name, 191 BrowserNames: p.Browsers, 192 }) 193 } 194 frameworks = append(frameworks, framework.Metadata{ 195 FrameworkName: f.Name, 196 FrameworkVersion: f.Version, 197 EOLDate: f.EOLDate, 198 RemovalDate: f.RemovalDate, 199 DockerImage: f.Runner.DockerImage, 200 GitRelease: f.Runner.GitRelease, 201 Platforms: platforms, 202 CloudRunnerVersion: f.Runner.CloudRunnerVersion, 203 BrowserDefaults: f.BrowserDefaults, 204 }) 205 } 206 return frameworks, nil 207 } 208 209 func uniqFrameworkNameSet(frameworks []framework.Framework) []string { 210 var fws []string 211 mp := map[string]bool{} 212 213 for _, fw := range frameworks { 214 _, present := mp[fw.Name] 215 216 if !present { 217 mp[fw.Name] = true 218 fws = append(fws, fw.Name) 219 } 220 } 221 return fws 222 }