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