]>
Commit | Line | Data |
---|---|---|
1 | package statuscake | |
2 | ||
3 | import ( | |
4 | "encoding/json" | |
5 | "fmt" | |
6 | "net/url" | |
7 | "reflect" | |
8 | "strings" | |
9 | ) | |
10 | ||
11 | const queryStringTag = "querystring" | |
12 | ||
13 | // Test represents a statuscake Test | |
14 | type Test struct { | |
15 | // TestID 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 | // CustomHeader. A special header that will be sent along with the HTTP tests. | |
25 | CustomHeader string `json:"CustomHeader" querystring:"CustomHeader"` | |
26 | ||
27 | // Use to populate the test with a custom user agent | |
28 | UserAgent string `json:"UserAgent" queryString:"UserAgent"` | |
29 | ||
30 | // Test location, either an IP (for TCP and Ping) or a fully qualified URL for other TestTypes | |
31 | WebsiteURL string `json:"WebsiteURL" querystring:"WebsiteURL"` | |
32 | ||
33 | // A Port to use on TCP Tests | |
34 | Port int `json:"Port" querystring:"Port"` | |
35 | ||
36 | // Contact group ID - deprecated in favor of ContactGroup but still provided in the API detail response | |
37 | ContactID int `json:"ContactID"` | |
38 | ||
39 | // Contact group IDs - will return list of ints or empty if not provided | |
40 | ContactGroup []string `json:"ContactGroup" querystring:"ContactGroup"` | |
41 | ||
42 | // Current status at last test | |
43 | Status string `json:"Status"` | |
44 | ||
45 | // 1 Day Uptime | |
46 | Uptime float64 `json:"Uptime"` | |
47 | ||
48 | // Any test locations seperated by a comma (using the Node Location IDs) | |
49 | NodeLocations []string `json:"NodeLocations" querystring:"NodeLocations"` | |
50 | ||
51 | // Timeout in an int form representing seconds. | |
52 | Timeout int `json:"Timeout" querystring:"Timeout"` | |
53 | ||
54 | // A URL to ping if a site goes down. | |
55 | PingURL string `json:"PingURL" querystring:"PingURL"` | |
56 | ||
57 | Confirmation int `json:"Confirmation,string" querystring:"Confirmation"` | |
58 | ||
59 | // The number of seconds between checks. | |
60 | CheckRate int `json:"CheckRate" querystring:"CheckRate"` | |
61 | ||
62 | // A Basic Auth User account to use to login | |
63 | BasicUser string `json:"BasicUser" querystring:"BasicUser"` | |
64 | ||
65 | // If BasicUser is set then this should be the password for the BasicUser | |
66 | BasicPass string `json:"BasicPass" querystring:"BasicPass"` | |
67 | ||
68 | // Set 1 to enable public reporting, 0 to disable | |
69 | Public int `json:"Public" querystring:"Public"` | |
70 | ||
71 | // A URL to a image to use for public reporting | |
72 | LogoImage string `json:"LogoImage" querystring:"LogoImage"` | |
73 | ||
74 | // Set to 0 to use branding (default) or 1 to disable public reporting branding | |
75 | Branding int `json:"Branding" querystring:"Branding"` | |
76 | ||
77 | // Used internally by the statuscake API | |
78 | WebsiteHost string `json:"WebsiteHost" querystring:"WebsiteHost"` | |
79 | ||
80 | // Enable virus checking or not. 1 to enable | |
81 | Virus int `json:"Virus" querystring:"Virus"` | |
82 | ||
83 | // A string that should either be found or not found. | |
84 | FindString string `json:"FindString" querystring:"FindString"` | |
85 | ||
86 | // If the above string should be found to trigger a alert. true will trigger if FindString found | |
87 | DoNotFind bool `json:"DoNotFind" querystring:"DoNotFind"` | |
88 | ||
89 | // What type of test type to use. Accepted values are HTTP, TCP, PING | |
90 | TestType string `json:"TestType" querystring:"TestType"` | |
91 | ||
92 | // Use 1 to TURN OFF real browser testing | |
93 | RealBrowser int `json:"RealBrowser" querystring:"RealBrowser"` | |
94 | ||
95 | // How many minutes to wait before sending an alert | |
96 | TriggerRate int `json:"TriggerRate" querystring:"TriggerRate"` | |
97 | ||
98 | // Tags should be seperated by a comma - no spacing between tags (this,is,a set,of,tags) | |
99 | TestTags []string `json:"TestTags" querystring:"TestTags"` | |
100 | ||
101 | // Comma Seperated List of StatusCodes to Trigger Error on (on Update will replace, so send full list each time) | |
102 | StatusCodes string `json:"StatusCodes" querystring:"StatusCodes"` | |
103 | ||
104 | // Set to 1 to enable the Cookie Jar. Required for some redirects. | |
105 | UseJar int `json:"UseJar" querystring:"UseJar"` | |
106 | ||
107 | // Raw POST data seperated by an ampersand | |
108 | PostRaw string `json:"PostRaw" querystring:"PostRaw"` | |
109 | ||
110 | // Use to specify the expected Final URL in the testing process | |
111 | FinalEndpoint string `json:"FinalEndpoint" querystring:"FinalEndpoint"` | |
112 | ||
113 | // Use to enable SSL validation | |
114 | EnableSSLAlert bool `json:"EnableSSLAlert" querystring:"EnableSSLAlert"` | |
115 | ||
116 | // Use to specify whether redirects should be followed | |
117 | FollowRedirect bool `json:"FollowRedirect" querystring:"FollowRedirect"` | |
118 | } | |
119 | ||
120 | // Validate checks if the Test is valid. If it's invalid, it returns a ValidationError with all invalid fields. It returns nil otherwise. | |
121 | func (t *Test) Validate() error { | |
122 | e := make(ValidationError) | |
123 | ||
124 | if t.WebsiteName == "" { | |
125 | e["WebsiteName"] = "is required" | |
126 | } | |
127 | ||
128 | if t.WebsiteURL == "" { | |
129 | e["WebsiteURL"] = "is required" | |
130 | } | |
131 | ||
132 | if t.Timeout != 0 && (t.Timeout < 6 || t.Timeout > 99) { | |
133 | e["Timeout"] = "must be 0 or between 6 and 99" | |
134 | } | |
135 | ||
136 | if t.Confirmation < 0 || t.Confirmation > 9 { | |
137 | e["Confirmation"] = "must be between 0 and 9" | |
138 | } | |
139 | ||
140 | if t.CheckRate < 0 || t.CheckRate > 23999 { | |
141 | e["CheckRate"] = "must be between 0 and 23999" | |
142 | } | |
143 | ||
144 | if t.Public < 0 || t.Public > 1 { | |
145 | e["Public"] = "must be 0 or 1" | |
146 | } | |
147 | ||
148 | if t.Virus < 0 || t.Virus > 1 { | |
149 | e["Virus"] = "must be 0 or 1" | |
150 | } | |
151 | ||
152 | if t.TestType != "HTTP" && t.TestType != "TCP" && t.TestType != "PING" { | |
153 | e["TestType"] = "must be HTTP, TCP, or PING" | |
154 | } | |
155 | ||
156 | if t.RealBrowser < 0 || t.RealBrowser > 1 { | |
157 | e["RealBrowser"] = "must be 0 or 1" | |
158 | } | |
159 | ||
160 | if t.TriggerRate < 0 || t.TriggerRate > 59 { | |
161 | e["TriggerRate"] = "must be between 0 and 59" | |
162 | } | |
163 | ||
164 | if t.PostRaw != "" && t.TestType != "HTTP" { | |
165 | e["PostRaw"] = "must be HTTP to submit a POST request" | |
166 | } | |
167 | ||
168 | if t.FinalEndpoint != "" && t.TestType != "HTTP" { | |
169 | e["FinalEndpoint"] = "must be a Valid URL" | |
170 | } | |
171 | ||
172 | if t.CustomHeader != "" { | |
173 | var jsonVerifiable map[string]interface{} | |
174 | if json.Unmarshal([]byte(t.CustomHeader), &jsonVerifiable) != nil { | |
175 | e["CustomHeader"] = "must be provided as json string" | |
176 | } | |
177 | } | |
178 | ||
179 | if len(e) > 0 { | |
180 | return e | |
181 | } | |
182 | ||
183 | return nil | |
184 | } | |
185 | ||
186 | // ToURLValues returns url.Values of all fields required to create/update a Test. | |
187 | func (t Test) ToURLValues() url.Values { | |
188 | values := make(url.Values) | |
189 | st := reflect.TypeOf(t) | |
190 | sv := reflect.ValueOf(t) | |
191 | for i := 0; i < st.NumField(); i++ { | |
192 | sf := st.Field(i) | |
193 | tag := sf.Tag.Get(queryStringTag) | |
194 | ft := sf.Type | |
195 | if ft.Name() == "" && ft.Kind() == reflect.Ptr { | |
196 | // Follow pointer. | |
197 | ft = ft.Elem() | |
198 | } | |
199 | ||
200 | v := sv.Field(i) | |
201 | options := sf.Tag.Get("querystringoptions") | |
202 | omit := options == "omitempty" && isEmptyValue(v) | |
203 | ||
204 | if tag != "" && !omit { | |
205 | values.Set(tag, valueToQueryStringValue(v)) | |
206 | } | |
207 | } | |
208 | ||
209 | return values | |
210 | } | |
211 | ||
212 | func isEmptyValue(v reflect.Value) bool { | |
213 | switch v.Kind() { | |
214 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String: | |
215 | return v.Len() == 0 | |
216 | case reflect.Bool: | |
217 | return !v.Bool() | |
218 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | |
219 | return v.Int() == 0 | |
220 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: | |
221 | return v.Uint() == 0 | |
222 | case reflect.Float32, reflect.Float64: | |
223 | return v.Float() == 0 | |
224 | case reflect.Interface, reflect.Ptr: | |
225 | return v.IsNil() | |
226 | } | |
227 | ||
228 | return false | |
229 | } | |
230 | ||
231 | func valueToQueryStringValue(v reflect.Value) string { | |
232 | if v.Type().Name() == "bool" { | |
233 | if v.Bool() { | |
234 | return "1" | |
235 | } | |
236 | ||
237 | return "0" | |
238 | } | |
239 | ||
240 | if v.Type().Kind() == reflect.Slice { | |
241 | if ss, ok := v.Interface().([]string); ok { | |
242 | return strings.Join(ss, ",") | |
243 | } | |
244 | } | |
245 | ||
246 | return fmt.Sprint(v) | |
247 | } | |
248 | ||
249 | // Tests is a client that implements the `Tests` API. | |
250 | type Tests interface { | |
251 | All() ([]*Test, error) | |
252 | AllWithFilter(url.Values) ([]*Test, error) | |
253 | Detail(int) (*Test, error) | |
254 | Update(*Test) (*Test, error) | |
255 | Delete(TestID int) error | |
256 | } | |
257 | ||
258 | type tests struct { | |
259 | client apiClient | |
260 | } | |
261 | ||
262 | func newTests(c apiClient) Tests { | |
263 | return &tests{ | |
264 | client: c, | |
265 | } | |
266 | } | |
267 | ||
268 | func (tt *tests) All() ([]*Test, error) { | |
269 | resp, err := tt.client.get("/Tests", nil) | |
270 | if err != nil { | |
271 | return nil, err | |
272 | } | |
273 | defer resp.Body.Close() | |
274 | ||
275 | var tests []*Test | |
276 | err = json.NewDecoder(resp.Body).Decode(&tests) | |
277 | ||
278 | return tests, err | |
279 | } | |
280 | ||
281 | func (tt *tests) AllWithFilter(filterOptions url.Values) ([]*Test, error) { | |
282 | resp, err := tt.client.get("/Tests", filterOptions) | |
283 | if err != nil { | |
284 | return nil, err | |
285 | } | |
286 | defer resp.Body.Close() | |
287 | ||
288 | var tests []*Test | |
289 | err = json.NewDecoder(resp.Body).Decode(&tests) | |
290 | ||
291 | return tests, err | |
292 | } | |
293 | ||
294 | func (tt *tests) Update(t *Test) (*Test, error) { | |
295 | resp, err := tt.client.put("/Tests/Update", t.ToURLValues()) | |
296 | if err != nil { | |
297 | return nil, err | |
298 | } | |
299 | defer resp.Body.Close() | |
300 | ||
301 | var ur updateResponse | |
302 | err = json.NewDecoder(resp.Body).Decode(&ur) | |
303 | if err != nil { | |
304 | return nil, err | |
305 | } | |
306 | ||
307 | if !ur.Success { | |
308 | return nil, &updateError{Issues: ur.Issues, Message: ur.Message} | |
309 | } | |
310 | ||
311 | t2 := *t | |
312 | t2.TestID = ur.InsertID | |
313 | ||
314 | return &t2, err | |
315 | } | |
316 | ||
317 | func (tt *tests) Delete(testID int) error { | |
318 | resp, err := tt.client.delete("/Tests/Details", url.Values{"TestID": {fmt.Sprint(testID)}}) | |
319 | if err != nil { | |
320 | return err | |
321 | } | |
322 | defer resp.Body.Close() | |
323 | ||
324 | var dr deleteResponse | |
325 | err = json.NewDecoder(resp.Body).Decode(&dr) | |
326 | if err != nil { | |
327 | return err | |
328 | } | |
329 | ||
330 | if !dr.Success { | |
331 | return &deleteError{Message: dr.Error} | |
332 | } | |
333 | ||
334 | return nil | |
335 | } | |
336 | ||
337 | func (tt *tests) Detail(testID int) (*Test, error) { | |
338 | resp, err := tt.client.get("/Tests/Details", url.Values{"TestID": {fmt.Sprint(testID)}}) | |
339 | if err != nil { | |
340 | return nil, err | |
341 | } | |
342 | defer resp.Body.Close() | |
343 | ||
344 | var dr *detailResponse | |
345 | err = json.NewDecoder(resp.Body).Decode(&dr) | |
346 | if err != nil { | |
347 | return nil, err | |
348 | } | |
349 | ||
350 | return dr.test(), nil | |
351 | } |