aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/github.com/DreamItGetIT/statuscake
diff options
context:
space:
mode:
authorJake Champlin <jake@gnu.space>2017-06-09 17:54:32 +0000
committerJake Champlin <jake@gnu.space>2017-06-09 17:54:32 +0000
commit9b12e4fe6f3c95986f1f3ec791636c58ca7e7583 (patch)
tree38f5f12bec0e488a12f0459a7356e6b7de7d8f84 /vendor/github.com/DreamItGetIT/statuscake
parentcec3de8a3bcaffd21dedd1bf42da4b490cae7e16 (diff)
downloadterraform-provider-statuscake-9b12e4fe6f3c95986f1f3ec791636c58ca7e7583.tar.gz
terraform-provider-statuscake-9b12e4fe6f3c95986f1f3ec791636c58ca7e7583.tar.zst
terraform-provider-statuscake-9b12e4fe6f3c95986f1f3ec791636c58ca7e7583.zip
Transfer of provider code
Diffstat (limited to 'vendor/github.com/DreamItGetIT/statuscake')
-rw-r--r--vendor/github.com/DreamItGetIT/statuscake/LICENSE21
-rw-r--r--vendor/github.com/DreamItGetIT/statuscake/README.md5
-rw-r--r--vendor/github.com/DreamItGetIT/statuscake/client.go170
-rw-r--r--vendor/github.com/DreamItGetIT/statuscake/doc.go34
-rw-r--r--vendor/github.com/DreamItGetIT/statuscake/errors.go80
-rw-r--r--vendor/github.com/DreamItGetIT/statuscake/makefile11
-rw-r--r--vendor/github.com/DreamItGetIT/statuscake/responses.go70
-rw-r--r--vendor/github.com/DreamItGetIT/statuscake/tests.go298
8 files changed, 689 insertions, 0 deletions
diff --git a/vendor/github.com/DreamItGetIT/statuscake/LICENSE b/vendor/github.com/DreamItGetIT/statuscake/LICENSE
new file mode 100644
index 0000000..5442aad
--- /dev/null
+++ b/vendor/github.com/DreamItGetIT/statuscake/LICENSE
@@ -0,0 +1,21 @@
1The MIT License (MIT)
2
3Copyright (c) 2015 DreamItGetIT
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in all
13copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21SOFTWARE.
diff --git a/vendor/github.com/DreamItGetIT/statuscake/README.md b/vendor/github.com/DreamItGetIT/statuscake/README.md
new file mode 100644
index 0000000..f1e4eaf
--- /dev/null
+++ b/vendor/github.com/DreamItGetIT/statuscake/README.md
@@ -0,0 +1,5 @@
1# statuscake
2
3`statuscake` is a Go pkg that implements a client for the [statuscake]("https://statuscake.com") API.
4
5More documentation and examples at [http://godoc.org/github.com/DreamItGetIT/statuscake](http://godoc.org/github.com/DreamItGetIT/statuscake).
diff --git a/vendor/github.com/DreamItGetIT/statuscake/client.go b/vendor/github.com/DreamItGetIT/statuscake/client.go
new file mode 100644
index 0000000..6094be5
--- /dev/null
+++ b/vendor/github.com/DreamItGetIT/statuscake/client.go
@@ -0,0 +1,170 @@
1package statuscake
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "io"
8 "io/ioutil"
9 "net/http"
10 "net/url"
11 "strings"
12)
13
14const apiBaseURL = "https://app.statuscake.com/API"
15
16type responseBody struct {
17 io.Reader
18}
19
20func (r *responseBody) Close() error {
21 return nil
22}
23
24// Auth wraps the authorisation headers required for each request
25type Auth struct {
26 Username string
27 Apikey string
28}
29
30func (a *Auth) validate() error {
31 e := make(ValidationError)
32
33 if a.Username == "" {
34 e["Username"] = "is required"
35 }
36
37 if a.Apikey == "" {
38 e["Apikey"] = "is required"
39 }
40
41 if len(e) > 0 {
42 return e
43 }
44
45 return nil
46}
47
48type httpClient interface {
49 Do(*http.Request) (*http.Response, error)
50}
51
52type apiClient interface {
53 get(string, url.Values) (*http.Response, error)
54 delete(string, url.Values) (*http.Response, error)
55 put(string, url.Values) (*http.Response, error)
56}
57
58// Client is the http client that wraps the remote API.
59type Client struct {
60 c httpClient
61 username string
62 apiKey string
63 testsClient Tests
64}
65
66// New returns a new Client
67func New(auth Auth) (*Client, error) {
68 if err := auth.validate(); err != nil {
69 return nil, err
70 }
71
72 return &Client{
73 c: &http.Client{},
74 username: auth.Username,
75 apiKey: auth.Apikey,
76 }, nil
77}
78
79func (c *Client) newRequest(method string, path string, v url.Values, body io.Reader) (*http.Request, error) {
80 url := fmt.Sprintf("%s%s", apiBaseURL, path)
81 if v != nil {
82 url = fmt.Sprintf("%s?%s", url, v.Encode())
83 }
84
85 r, err := http.NewRequest(method, url, body)
86 if err != nil {
87 return nil, err
88 }
89
90 r.Header.Set("Username", c.username)
91 r.Header.Set("API", c.apiKey)
92
93 return r, nil
94}
95
96func (c *Client) doRequest(r *http.Request) (*http.Response, error) {
97 resp, err := c.c.Do(r)
98 if err != nil {
99 return nil, err
100 }
101 defer resp.Body.Close()
102
103 if resp.StatusCode < 200 || resp.StatusCode > 299 {
104 return nil, &httpError{
105 status: resp.Status,
106 statusCode: resp.StatusCode,
107 }
108 }
109
110 var aer autheticationErrorResponse
111
112 // We read and save the response body so that if we don't have error messages
113 // we can set it again for future usage
114 b, err := ioutil.ReadAll(resp.Body)
115 if err != nil {
116 return nil, err
117 }
118
119 err = json.Unmarshal(b, &aer)
120 if err == nil && aer.ErrNo == 0 && aer.Error != "" {
121 return nil, &AuthenticationError{
122 errNo: aer.ErrNo,
123 message: aer.Error,
124 }
125 }
126
127 resp.Body = &responseBody{
128 Reader: bytes.NewReader(b),
129 }
130
131 return resp, nil
132}
133
134func (c *Client) get(path string, v url.Values) (*http.Response, error) {
135 r, err := c.newRequest("GET", path, v, nil)
136 if err != nil {
137 return nil, err
138 }
139
140 return c.doRequest(r)
141}
142
143func (c *Client) put(path string, v url.Values) (*http.Response, error) {
144 r, err := c.newRequest("PUT", path, nil, strings.NewReader(v.Encode()))
145 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
146
147 if err != nil {
148 return nil, err
149 }
150
151 return c.doRequest(r)
152}
153
154func (c *Client) delete(path string, v url.Values) (*http.Response, error) {
155 r, err := c.newRequest("DELETE", path, v, nil)
156 if err != nil {
157 return nil, err
158 }
159
160 return c.doRequest(r)
161}
162
163// Tests returns a client that implements the `Tests` API.
164func (c *Client) Tests() Tests {
165 if c.testsClient == nil {
166 c.testsClient = newTests(c)
167 }
168
169 return c.testsClient
170}
diff --git a/vendor/github.com/DreamItGetIT/statuscake/doc.go b/vendor/github.com/DreamItGetIT/statuscake/doc.go
new file mode 100644
index 0000000..fa68d94
--- /dev/null
+++ b/vendor/github.com/DreamItGetIT/statuscake/doc.go
@@ -0,0 +1,34 @@
1// Package statuscake implements a client for statuscake.com API.
2//
3// // list all `Tests`
4// c, err := statuscake.New(statuscake.Auth{Username: username, Apikey: apikey})
5// if err != nil {
6// log.Fatal(err)
7// }
8//
9// tests, err := c.Tests().All()
10// if err != nil {
11// log.Fatal(err)
12// }
13//
14// // delete a `Test`
15// err = c.Tests().Delete(TestID)
16//
17// // create a test
18// t := &statuscake.Test{
19// WebsiteName: "Foo",
20// WebsiteURL: "htto://example.com",
21// ... other required args...
22// }
23//
24// if err = t.Validate(); err != nil {
25// log.Fatal(err)
26// }
27//
28// t2 := c.Tests().Update(t)
29// fmt.Printf("New Test created with id: %d\n", t2.TestID)
30//
31// // get Tests details
32// t, err := tt.Detail(id)
33// ...
34package statuscake
diff --git a/vendor/github.com/DreamItGetIT/statuscake/errors.go b/vendor/github.com/DreamItGetIT/statuscake/errors.go
new file mode 100644
index 0000000..4c51991
--- /dev/null
+++ b/vendor/github.com/DreamItGetIT/statuscake/errors.go
@@ -0,0 +1,80 @@
1package statuscake
2
3import (
4 "fmt"
5 "strings"
6)
7
8// APIError implements the error interface an it's used when the API response has errors.
9type APIError interface {
10 APIError() string
11}
12
13type httpError struct {
14 status string
15 statusCode int
16}
17
18func (e *httpError) Error() string {
19 return fmt.Sprintf("HTTP error: %d - %s", e.statusCode, e.status)
20}
21
22// ValidationError is a map where the key is the invalid field and the value is a message describing why the field is invalid.
23type ValidationError map[string]string
24
25func (e ValidationError) Error() string {
26 var messages []string
27
28 for k, v := range e {
29 m := fmt.Sprintf("%s %s", k, v)
30 messages = append(messages, m)
31 }
32
33 return strings.Join(messages, ", ")
34}
35
36type updateError struct {
37 Issues interface{}
38}
39
40func (e *updateError) Error() string {
41 var messages []string
42
43 if issues, ok := e.Issues.(map[string]interface{}); ok {
44 for k, v := range issues {
45 m := fmt.Sprintf("%s %s", k, v)
46 messages = append(messages, m)
47 }
48 } else if issues, ok := e.Issues.([]interface{}); ok {
49 for _, v := range issues {
50 m := fmt.Sprint(v)
51 messages = append(messages, m)
52 }
53 }
54
55 return strings.Join(messages, ", ")
56}
57
58// APIError returns the error specified in the API response
59func (e *updateError) APIError() string {
60 return e.Error()
61}
62
63type deleteError struct {
64 Message string
65}
66
67func (e *deleteError) Error() string {
68 return e.Message
69}
70
71// AuthenticationError implements the error interface and it's returned
72// when API responses have authentication errors
73type AuthenticationError struct {
74 errNo int
75 message string
76}
77
78func (e *AuthenticationError) Error() string {
79 return fmt.Sprintf("%d, %s", e.errNo, e.message)
80}
diff --git a/vendor/github.com/DreamItGetIT/statuscake/makefile b/vendor/github.com/DreamItGetIT/statuscake/makefile
new file mode 100644
index 0000000..946f6d9
--- /dev/null
+++ b/vendor/github.com/DreamItGetIT/statuscake/makefile
@@ -0,0 +1,11 @@
1.PHONY: default lint test
2
3default: lint test
4
5lint:
6 @golint ./...
7 @go vet ./...
8
9test:
10 go test ${GOTEST_ARGS} ./...
11
diff --git a/vendor/github.com/DreamItGetIT/statuscake/responses.go b/vendor/github.com/DreamItGetIT/statuscake/responses.go
new file mode 100644
index 0000000..b9216b7
--- /dev/null
+++ b/vendor/github.com/DreamItGetIT/statuscake/responses.go
@@ -0,0 +1,70 @@
1package statuscake
2
3type autheticationErrorResponse struct {
4 ErrNo int
5 Error string
6}
7
8type updateResponse struct {
9 Issues interface{} `json:"Issues"`
10 Success bool `json:"Success"`
11 Message string `json:"Message"`
12 InsertID int `json:"InsertID"`
13}
14
15type deleteResponse struct {
16 Success bool `json:"Success"`
17 Error string `json:"Error"`
18}
19
20type detailResponse struct {
21 Method string `json:"Method"`
22 TestID int `json:"TestID"`
23 TestType string `json:"TestType"`
24 Paused bool `json:"Paused"`
25 WebsiteName string `json:"WebsiteName"`
26 URI string `json:"URI"`
27 ContactID int `json:"ContactID"`
28 Status string `json:"Status"`
29 Uptime float64 `json:"Uptime"`
30 CheckRate int `json:"CheckRate"`
31 Timeout int `json:"Timeout"`
32 LogoImage string `json:"LogoImage"`
33 Confirmation int `json:"Confirmation,string"`
34 WebsiteHost string `json:"WebsiteHost"`
35 NodeLocations []string `json:"NodeLocations"`
36 FindString string `json:"FindString"`
37 DoNotFind bool `json:"DoNotFind"`
38 LastTested string `json:"LastTested"`
39 NextLocation string `json:"NextLocation"`
40 Port int `json:"Port"`
41 Processing bool `json:"Processing"`
42 ProcessingState string `json:"ProcessingState"`
43 ProcessingOn string `json:"ProcessingOn"`
44 DownTimes int `json:"DownTimes,string"`
45 Sensitive bool `json:"Sensitive"`
46 TriggerRate int `json:"TriggerRate,string"`
47}
48
49func (d *detailResponse) test() *Test {
50 return &Test{
51 TestID: d.TestID,
52 TestType: d.TestType,
53 Paused: d.Paused,
54 WebsiteName: d.WebsiteName,
55 WebsiteURL: d.URI,
56 ContactID: d.ContactID,
57 Status: d.Status,
58 Uptime: d.Uptime,
59 CheckRate: d.CheckRate,
60 Timeout: d.Timeout,
61 LogoImage: d.LogoImage,
62 Confirmation: d.Confirmation,
63 WebsiteHost: d.WebsiteHost,
64 NodeLocations: d.NodeLocations,
65 FindString: d.FindString,
66 DoNotFind: d.DoNotFind,
67 Port: d.Port,
68 TriggerRate: d.TriggerRate,
69 }
70}
diff --git a/vendor/github.com/DreamItGetIT/statuscake/tests.go b/vendor/github.com/DreamItGetIT/statuscake/tests.go
new file mode 100644
index 0000000..4053e53
--- /dev/null
+++ b/vendor/github.com/DreamItGetIT/statuscake/tests.go
@@ -0,0 +1,298 @@
1package statuscake
2
3import (
4 "encoding/json"
5 "fmt"
6 "net/url"
7 "reflect"
8 "strings"
9)
10
11const queryStringTag = "querystring"
12
13// Test represents a statuscake Test
14type Test struct {
15 // ThiTestID is an int, use this to get more details about this test. If not provided will insert a new check, else will update
16 TestID int `json:"TestID" querystring:"TestID" querystringoptions:"omitempty"`
17
18 // Sent tfalse To Unpause and true To Pause.
19 Paused bool `json:"Paused" querystring:"Paused"`
20
21 // Website name. Tags are stripped out
22 WebsiteName string `json:"WebsiteName" querystring:"WebsiteName"`
23
24 // Test location, either an IP (for TCP and Ping) or a fully qualified URL for other TestTypes
25 WebsiteURL string `json:"WebsiteURL" querystring:"WebsiteURL"`
26
27 // A Port to use on TCP Tests
28 Port int `json:"Port" querystring:"Port"`
29
30 // Contact group ID - will return int of contact group used else 0
31 ContactID int `json:"ContactID" querystring:"ContactGroup"`
32
33 // Current status at last test
34 Status string `json:"Status"`
35
36 // 7 Day Uptime
37 Uptime float64 `json:"Uptime"`
38
39 // Any test locations seperated by a comma (using the Node Location IDs)
40 NodeLocations []string `json:"NodeLocations" querystring:"NodeLocations"`
41
42 // Timeout in an int form representing seconds.
43 Timeout int `json:"Timeout" querystring:"Timeout"`
44
45 // A URL to ping if a site goes down.
46 PingURL string `json:"PingURL" querystring:"PingURL"`
47
48 Confirmation int `json:"Confirmationi,string" querystring:"Confirmation"`
49
50 // The number of seconds between checks.
51 CheckRate int `json:"CheckRate" querystring:"CheckRate"`
52
53 // A Basic Auth User account to use to login
54 BasicUser string `json:"BasicUser" querystring:"BasicUser"`
55
56 // If BasicUser is set then this should be the password for the BasicUser
57 BasicPass string `json:"BasicPass" querystring:"BasicPass"`
58
59 // Set 1 to enable public reporting, 0 to disable
60 Public int `json:"Public" querystring:"Public"`
61
62 // A URL to a image to use for public reporting
63 LogoImage string `json:"LogoImage" querystring:"LogoImage"`
64
65 // Set to 0 to use branding (default) or 1 to disable public reporting branding
66 Branding int `json:"Branding" querystring:"Branding"`
67
68 // Used internally by the statuscake API
69 WebsiteHost string `json:"WebsiteHost"`
70
71 // Enable virus checking or not. 1 to enable
72 Virus int `json:"Virus" querystring:"Virus"`
73
74 // A string that should either be found or not found.
75 FindString string `json:"FindString" querystring:"FindString"`
76
77 // If the above string should be found to trigger a alert. true will trigger if FindString found
78 DoNotFind bool `json:"DoNotFind" querystring:"DoNotFind"`
79
80 // What type of test type to use. Accepted values are HTTP, TCP, PING
81 TestType string `json:"TestType" querystring:"TestType"`
82
83 // Use 1 to TURN OFF real browser testing
84 RealBrowser int `json:"RealBrowser" querystring:"RealBrowser"`
85
86 // How many minutes to wait before sending an alert
87 TriggerRate int `json:"TriggerRate" querystring:"TriggerRate"`
88
89 // Tags should be seperated by a comma - no spacing between tags (this,is,a set,of,tags)
90 TestTags string `json:"TestTags" querystring:"TestTags"`
91
92 // Comma Seperated List of StatusCodes to Trigger Error on (on Update will replace, so send full list each time)
93 StatusCodes string `json:"StatusCodes" querystring:"StatusCodes"`
94}
95
96// Validate checks if the Test is valid. If it's invalid, it returns a ValidationError with all invalid fields. It returns nil otherwise.
97func (t *Test) Validate() error {
98 e := make(ValidationError)
99
100 if t.WebsiteName == "" {
101 e["WebsiteName"] = "is required"
102 }
103
104 if t.WebsiteURL == "" {
105 e["WebsiteURL"] = "is required"
106 }
107
108 if t.Timeout != 0 && (t.Timeout < 6 || t.Timeout > 99) {
109 e["Timeout"] = "must be 0 or between 6 and 99"
110 }
111
112 if t.Confirmation < 0 || t.Confirmation > 9 {
113 e["Confirmation"] = "must be between 0 and 9"
114 }
115
116 if t.CheckRate < 0 || t.CheckRate > 23999 {
117 e["CheckRate"] = "must be between 0 and 23999"
118 }
119
120 if t.Public < 0 || t.Public > 1 {
121 e["Public"] = "must be 0 or 1"
122 }
123
124 if t.Virus < 0 || t.Virus > 1 {
125 e["Virus"] = "must be 0 or 1"
126 }
127
128 if t.TestType != "HTTP" && t.TestType != "TCP" && t.TestType != "PING" {
129 e["TestType"] = "must be HTTP, TCP, or PING"
130 }
131
132 if t.RealBrowser < 0 || t.RealBrowser > 1 {
133 e["RealBrowser"] = "must be 0 or 1"
134 }
135
136 if t.TriggerRate < 0 || t.TriggerRate > 59 {
137 e["TriggerRate"] = "must be between 0 and 59"
138 }
139
140 if len(e) > 0 {
141 return e
142 }
143
144 return nil
145}
146
147// ToURLValues returns url.Values of all fields required to create/update a Test.
148func (t Test) ToURLValues() url.Values {
149 values := make(url.Values)
150 st := reflect.TypeOf(t)
151 sv := reflect.ValueOf(t)
152 for i := 0; i < st.NumField(); i++ {
153 sf := st.Field(i)
154 tag := sf.Tag.Get(queryStringTag)
155 ft := sf.Type
156 if ft.Name() == "" && ft.Kind() == reflect.Ptr {
157 // Follow pointer.
158 ft = ft.Elem()
159 }
160
161 v := sv.Field(i)
162 options := sf.Tag.Get("querystringoptions")
163 omit := options == "omitempty" && isEmptyValue(v)
164
165 if tag != "" && !omit {
166 values.Set(tag, valueToQueryStringValue(v))
167 }
168 }
169
170 return values
171}
172
173func isEmptyValue(v reflect.Value) bool {
174 switch v.Kind() {
175 case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
176 return v.Len() == 0
177 case reflect.Bool:
178 return !v.Bool()
179 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
180 return v.Int() == 0
181 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
182 return v.Uint() == 0
183 case reflect.Float32, reflect.Float64:
184 return v.Float() == 0
185 case reflect.Interface, reflect.Ptr:
186 return v.IsNil()
187 }
188
189 return false
190}
191
192func valueToQueryStringValue(v reflect.Value) string {
193 if v.Type().Name() == "bool" {
194 if v.Bool() {
195 return "1"
196 }
197
198 return "0"
199 }
200
201 if v.Type().Kind() == reflect.Slice {
202 if ss, ok := v.Interface().([]string); ok {
203 return strings.Join(ss, ",")
204 }
205 }
206
207 return fmt.Sprint(v)
208}
209
210// Tests is a client that implements the `Tests` API.
211type Tests interface {
212 All() ([]*Test, error)
213 Detail(int) (*Test, error)
214 Update(*Test) (*Test, error)
215 Delete(TestID int) error
216}
217
218type tests struct {
219 client apiClient
220}
221
222func newTests(c apiClient) Tests {
223 return &tests{
224 client: c,
225 }
226}
227
228func (tt *tests) All() ([]*Test, error) {
229 resp, err := tt.client.get("/Tests", nil)
230 if err != nil {
231 return nil, err
232 }
233 defer resp.Body.Close()
234
235 var tests []*Test
236 err = json.NewDecoder(resp.Body).Decode(&tests)
237
238 return tests, err
239}
240
241func (tt *tests) Update(t *Test) (*Test, error) {
242 resp, err := tt.client.put("/Tests/Update", t.ToURLValues())
243 if err != nil {
244 return nil, err
245 }
246 defer resp.Body.Close()
247
248 var ur updateResponse
249 err = json.NewDecoder(resp.Body).Decode(&ur)
250 if err != nil {
251 return nil, err
252 }
253
254 if !ur.Success {
255 return nil, &updateError{Issues: ur.Issues}
256 }
257
258 t2 := *t
259 t2.TestID = ur.InsertID
260
261 return &t2, err
262}
263
264func (tt *tests) Delete(testID int) error {
265 resp, err := tt.client.delete("/Tests/Details", url.Values{"TestID": {fmt.Sprint(testID)}})
266 if err != nil {
267 return err
268 }
269 defer resp.Body.Close()
270
271 var dr deleteResponse
272 err = json.NewDecoder(resp.Body).Decode(&dr)
273 if err != nil {
274 return err
275 }
276
277 if !dr.Success {
278 return &deleteError{Message: dr.Error}
279 }
280
281 return nil
282}
283
284func (tt *tests) Detail(testID int) (*Test, error) {
285 resp, err := tt.client.get("/Tests/Details", url.Values{"TestID": {fmt.Sprint(testID)}})
286 if err != nil {
287 return nil, err
288 }
289 defer resp.Body.Close()
290
291 var dr *detailResponse
292 err = json.NewDecoder(resp.Body).Decode(&dr)
293 if err != nil {
294 return nil, err
295 }
296
297 return dr.test(), nil
298}