]>
Commit | Line | Data |
---|---|---|
9b12e4fe JC |
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 { | |
ccc9fb69 | 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 |
9b12e4fe JC |
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 | ||
a88e9b9b AG |
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 | ||
9b12e4fe JC |
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 | ||
ccc9fb69 MS |
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"` | |
9b12e4fe JC |
41 | |
42 | // Current status at last test | |
43 | Status string `json:"Status"` | |
44 | ||
45 | // 7 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 | ||
76778ad2 | 57 | Confirmation int `json:"Confirmation,string" querystring:"Confirmation"` |
9b12e4fe JC |
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"` | |
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) | |
ccc9fb69 | 99 | TestTags []string `json:"TestTags" querystring:"TestTags"` |
9b12e4fe JC |
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"` | |
a88e9b9b AG |
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 specify whether redirects should be followed | |
114 | FollowRedirect bool `json:"FollowRedirect" querystring:"FollowRedirect"` | |
9b12e4fe JC |
115 | } |
116 | ||
117 | // Validate checks if the Test is valid. If it's invalid, it returns a ValidationError with all invalid fields. It returns nil otherwise. | |
118 | func (t *Test) Validate() error { | |
119 | e := make(ValidationError) | |
120 | ||
121 | if t.WebsiteName == "" { | |
122 | e["WebsiteName"] = "is required" | |
123 | } | |
124 | ||
125 | if t.WebsiteURL == "" { | |
126 | e["WebsiteURL"] = "is required" | |
127 | } | |
128 | ||
129 | if t.Timeout != 0 && (t.Timeout < 6 || t.Timeout > 99) { | |
130 | e["Timeout"] = "must be 0 or between 6 and 99" | |
131 | } | |
132 | ||
133 | if t.Confirmation < 0 || t.Confirmation > 9 { | |
134 | e["Confirmation"] = "must be between 0 and 9" | |
135 | } | |
136 | ||
137 | if t.CheckRate < 0 || t.CheckRate > 23999 { | |
138 | e["CheckRate"] = "must be between 0 and 23999" | |
139 | } | |
140 | ||
141 | if t.Public < 0 || t.Public > 1 { | |
142 | e["Public"] = "must be 0 or 1" | |
143 | } | |
144 | ||
145 | if t.Virus < 0 || t.Virus > 1 { | |
146 | e["Virus"] = "must be 0 or 1" | |
147 | } | |
148 | ||
149 | if t.TestType != "HTTP" && t.TestType != "TCP" && t.TestType != "PING" { | |
150 | e["TestType"] = "must be HTTP, TCP, or PING" | |
151 | } | |
152 | ||
153 | if t.RealBrowser < 0 || t.RealBrowser > 1 { | |
154 | e["RealBrowser"] = "must be 0 or 1" | |
155 | } | |
156 | ||
157 | if t.TriggerRate < 0 || t.TriggerRate > 59 { | |
158 | e["TriggerRate"] = "must be between 0 and 59" | |
159 | } | |
160 | ||
a88e9b9b AG |
161 | if t.PostRaw != "" && t.TestType != "HTTP" { |
162 | e["PostRaw"] = "must be HTTP to submit a POST request" | |
163 | } | |
164 | ||
165 | if t.FinalEndpoint != "" && t.TestType != "HTTP" { | |
166 | e["FinalEndpoint"] = "must be a Valid URL" | |
167 | } | |
168 | ||
169 | var jsonVerifiable map[string]interface{} | |
170 | if json.Unmarshal([]byte(t.CustomHeader), &jsonVerifiable) != nil { | |
171 | e["CustomHeader"] = "must be provided as json string" | |
172 | } | |
173 | ||
9b12e4fe JC |
174 | if len(e) > 0 { |
175 | return e | |
176 | } | |
177 | ||
178 | return nil | |
179 | } | |
180 | ||
181 | // ToURLValues returns url.Values of all fields required to create/update a Test. | |
182 | func (t Test) ToURLValues() url.Values { | |
183 | values := make(url.Values) | |
184 | st := reflect.TypeOf(t) | |
185 | sv := reflect.ValueOf(t) | |
186 | for i := 0; i < st.NumField(); i++ { | |
187 | sf := st.Field(i) | |
188 | tag := sf.Tag.Get(queryStringTag) | |
189 | ft := sf.Type | |
190 | if ft.Name() == "" && ft.Kind() == reflect.Ptr { | |
191 | // Follow pointer. | |
192 | ft = ft.Elem() | |
193 | } | |
194 | ||
195 | v := sv.Field(i) | |
196 | options := sf.Tag.Get("querystringoptions") | |
197 | omit := options == "omitempty" && isEmptyValue(v) | |
198 | ||
199 | if tag != "" && !omit { | |
200 | values.Set(tag, valueToQueryStringValue(v)) | |
201 | } | |
202 | } | |
203 | ||
204 | return values | |
205 | } | |
206 | ||
207 | func isEmptyValue(v reflect.Value) bool { | |
208 | switch v.Kind() { | |
209 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String: | |
210 | return v.Len() == 0 | |
211 | case reflect.Bool: | |
212 | return !v.Bool() | |
213 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | |
214 | return v.Int() == 0 | |
215 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: | |
216 | return v.Uint() == 0 | |
217 | case reflect.Float32, reflect.Float64: | |
218 | return v.Float() == 0 | |
219 | case reflect.Interface, reflect.Ptr: | |
220 | return v.IsNil() | |
221 | } | |
222 | ||
223 | return false | |
224 | } | |
225 | ||
226 | func valueToQueryStringValue(v reflect.Value) string { | |
227 | if v.Type().Name() == "bool" { | |
228 | if v.Bool() { | |
229 | return "1" | |
230 | } | |
231 | ||
232 | return "0" | |
233 | } | |
234 | ||
235 | if v.Type().Kind() == reflect.Slice { | |
236 | if ss, ok := v.Interface().([]string); ok { | |
237 | return strings.Join(ss, ",") | |
238 | } | |
239 | } | |
240 | ||
241 | return fmt.Sprint(v) | |
242 | } | |
243 | ||
244 | // Tests is a client that implements the `Tests` API. | |
245 | type Tests interface { | |
246 | All() ([]*Test, error) | |
247 | Detail(int) (*Test, error) | |
248 | Update(*Test) (*Test, error) | |
249 | Delete(TestID int) error | |
250 | } | |
251 | ||
252 | type tests struct { | |
253 | client apiClient | |
254 | } | |
255 | ||
256 | func newTests(c apiClient) Tests { | |
257 | return &tests{ | |
258 | client: c, | |
259 | } | |
260 | } | |
261 | ||
262 | func (tt *tests) All() ([]*Test, error) { | |
263 | resp, err := tt.client.get("/Tests", nil) | |
264 | if err != nil { | |
265 | return nil, err | |
266 | } | |
267 | defer resp.Body.Close() | |
268 | ||
269 | var tests []*Test | |
270 | err = json.NewDecoder(resp.Body).Decode(&tests) | |
271 | ||
272 | return tests, err | |
273 | } | |
274 | ||
275 | func (tt *tests) Update(t *Test) (*Test, error) { | |
276 | resp, err := tt.client.put("/Tests/Update", t.ToURLValues()) | |
277 | if err != nil { | |
278 | return nil, err | |
279 | } | |
280 | defer resp.Body.Close() | |
281 | ||
282 | var ur updateResponse | |
283 | err = json.NewDecoder(resp.Body).Decode(&ur) | |
284 | if err != nil { | |
285 | return nil, err | |
286 | } | |
287 | ||
288 | if !ur.Success { | |
ccc9fb69 | 289 | return nil, &updateError{Issues: ur.Issues, Message: ur.Message} |
9b12e4fe JC |
290 | } |
291 | ||
292 | t2 := *t | |
293 | t2.TestID = ur.InsertID | |
294 | ||
295 | return &t2, err | |
296 | } | |
297 | ||
298 | func (tt *tests) Delete(testID int) error { | |
299 | resp, err := tt.client.delete("/Tests/Details", url.Values{"TestID": {fmt.Sprint(testID)}}) | |
300 | if err != nil { | |
301 | return err | |
302 | } | |
303 | defer resp.Body.Close() | |
304 | ||
305 | var dr deleteResponse | |
306 | err = json.NewDecoder(resp.Body).Decode(&dr) | |
307 | if err != nil { | |
308 | return err | |
309 | } | |
310 | ||
311 | if !dr.Success { | |
312 | return &deleteError{Message: dr.Error} | |
313 | } | |
314 | ||
315 | return nil | |
316 | } | |
317 | ||
318 | func (tt *tests) Detail(testID int) (*Test, error) { | |
319 | resp, err := tt.client.get("/Tests/Details", url.Values{"TestID": {fmt.Sprint(testID)}}) | |
320 | if err != nil { | |
321 | return nil, err | |
322 | } | |
323 | defer resp.Body.Close() | |
324 | ||
325 | var dr *detailResponse | |
326 | err = json.NewDecoder(resp.Body).Decode(&dr) | |
327 | if err != nil { | |
328 | return nil, err | |
329 | } | |
330 | ||
331 | return dr.test(), nil | |
332 | } |