1 package host_test
2
3 import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "net/http"
8 "net/http/httptest"
9 "testing"
10 "time"
11
12 "github.com/gin-gonic/gin"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
15
16 "edge-infra.dev/pkg/sds/interlock/internal/errors"
17 "edge-infra.dev/pkg/sds/interlock/topic/host"
18 )
19
20 type TimeAssertionFunc func(t assert.TestingT, actual time.Time, msgAndArgs ...interface{}) bool
21
22 func TimeEqual(expected time.Time) TimeAssertionFunc {
23 return func(t assert.TestingT, actual time.Time, msgAndArgs ...interface{}) bool {
24 return assert.Equal(t, expected, actual, msgAndArgs...)
25 }
26 }
27
28 func WithinRange(start, end time.Time) TimeAssertionFunc {
29 return func(t assert.TestingT, actual time.Time, msgAndArgs ...interface{}) bool {
30 return assert.WithinRange(t, actual, start, end, msgAndArgs...)
31 }
32 }
33
34 func vncStateAssertion(expected interface{}, timeAssert TimeAssertionFunc) func(*testing.T, interface{}) {
35 return func(t *testing.T, actual interface{}) {
36 expectedVNC := expected.(*host.State)
37 actualVNC := actual.(*host.State)
38 for i := range expectedVNC.VNC {
39 actualTime, err := time.Parse(time.RFC3339, actualVNC.VNC[i].TimeStamp)
40 assert.NoError(t, err)
41 timeAssert(t, actualTime)
42
43
44 expectedVNC.VNC[i].TimeStamp = actualVNC.VNC[i].TimeStamp
45 assert.Equal(t, expectedVNC.VNC[i], actualVNC.VNC[i])
46 }
47 }
48 }
49
50 func genericAssertion(expected interface{}) func(*testing.T, interface{}) {
51 return func(t *testing.T, actual interface{}) {
52 assert.Equal(t, expected, actual)
53 }
54 }
55
56 type payload struct {
57 RequestID string `json:"requestId"`
58 Status string `json:"status"`
59 Connections int `json:"connections"`
60 TimeStamp time.Time `json:"timestamp,omitempty" time_format:"2006-01-02T15:04:05Z07:00"`
61 }
62
63 func TestVNCPost(t *testing.T) {
64 h, err := setupTestHostTopic(t, testHostname, "uid")
65 require.NoError(t, err)
66
67 r := gin.Default()
68 h.RegisterEndpoints(r)
69
70
71
72
73 testTime := time.Now().Add(48 * time.Hour).Round(time.Second).UTC()
74
75 tests := map[string]struct {
76 input payload
77 expectedStatus int
78 response interface{}
79 assertEqual func(*testing.T, interface{})
80 }{
81 "UpdateVNC VNC state": {
82 input: payload{
83 RequestID: "1",
84 Status: string(host.Accepted),
85 },
86 expectedStatus: http.StatusAccepted,
87 response: &host.State{},
88 assertEqual: vncStateAssertion(&host.State{
89 Hostname: testHostname,
90 VNC: host.VNCStates{
91 {
92 RequestID: "1",
93 Status: host.Accepted,
94 },
95 },
96 NodeUID: "uid",
97 }, WithinRange(time.Now().Add(-(10*time.Second)), time.Now())),
98 },
99 "UpdateVNC VNC state with timestamp field": {
100 input: payload{
101 RequestID: "1",
102 Status: string(host.Accepted),
103 TimeStamp: testTime,
104 },
105 expectedStatus: http.StatusAccepted,
106 response: &host.State{},
107 assertEqual: vncStateAssertion(&host.State{
108 Hostname: testHostname,
109 VNC: host.VNCStates{
110 {
111 RequestID: "1",
112 Status: host.Accepted,
113 TimeStamp: testTime.Format(time.RFC3339),
114 },
115 },
116 NodeUID: "uid",
117 }, TimeEqual(testTime)),
118 },
119 "UpdateVNC VNC state to CONNECTED": {
120 input: payload{
121 RequestID: "1",
122 Status: string(host.Connected),
123 Connections: 2,
124 TimeStamp: testTime,
125 },
126 expectedStatus: http.StatusAccepted,
127 response: &host.State{},
128 assertEqual: vncStateAssertion(&host.State{
129 Hostname: testHostname,
130 VNC: host.VNCStates{
131 {
132 RequestID: "1",
133 Status: host.Connected,
134 Connections: 2,
135 TimeStamp: testTime.Format(time.RFC3339),
136 },
137 },
138 NodeUID: "uid",
139 }, TimeEqual(testTime)),
140 },
141 "Failure nil VNC Status": {
142 input: payload{
143 RequestID: "1",
144 Status: "",
145 },
146 expectedStatus: http.StatusBadRequest,
147 response: &Errors{},
148 assertEqual: genericAssertion(&Errors{
149 Errors: []*errors.Error{
150 {
151 Detail: "Status is required",
152 },
153 },
154 }),
155 },
156 "Failure INVALID VNC Status": {
157 input: payload{
158 RequestID: "1",
159 Status: "INVALID",
160 },
161 expectedStatus: http.StatusBadRequest,
162 response: &Errors{},
163 assertEqual: genericAssertion(&Errors{
164 Errors: []*errors.Error{
165 {
166 Detail: "Key: 'postVNCPayload.Status' Error:Field validation for 'Status' failed on the 'oneof' tag",
167 },
168 },
169 }),
170 },
171 "Failure CONNECTED status with no connections": {
172 input: payload{
173 RequestID: "1",
174 Status: "CONNECTED",
175 },
176 expectedStatus: http.StatusBadRequest,
177 response: &Errors{},
178 assertEqual: genericAssertion(&Errors{
179 Errors: []*errors.Error{
180 {
181 Detail: `Key: 'postVNCPayload.Connections' Error:Field validation for 'Connections' failed on the 'is_zero_xor_is_connected' tag`,
182 },
183 },
184 }),
185 },
186 "Failure Non-CONNECTED status with connections": {
187 input: payload{
188 RequestID: "1",
189 Status: "REQUESTED",
190 Connections: 1,
191 },
192 expectedStatus: http.StatusBadRequest,
193 response: &Errors{},
194 assertEqual: genericAssertion(&Errors{
195 Errors: []*errors.Error{
196 {
197 Detail: `Key: 'postVNCPayload.Connections' Error:Field validation for 'Connections' failed on the 'is_zero_xor_is_connected' tag`,
198 },
199 },
200 }),
201 },
202 "Failure Negative Connections": {
203 input: payload{
204 RequestID: "1",
205 Status: "CONNECTED",
206 Connections: -1,
207 },
208 expectedStatus: http.StatusBadRequest,
209 response: &Errors{},
210 assertEqual: genericAssertion(&Errors{
211 Errors: []*errors.Error{
212 {
213 Detail: `Key: 'postVNCPayload.Connections' Error:Field validation for 'Connections' failed on the 'gte' tag`,
214 },
215 },
216 }),
217 },
218 }
219
220 for name, tc := range tests {
221 t.Run(name, func(t *testing.T) {
222 out, err := json.Marshal(tc.input)
223 require.NoError(t, err)
224
225 req, err := http.NewRequest(http.MethodPost, host.Path+"/vnc", bytes.NewReader(out))
226 require.NoError(t, err)
227
228 w := httptest.NewRecorder()
229 r.ServeHTTP(w, req)
230
231 require.Equal(t, tc.expectedStatus, w.Code)
232
233 require.NoError(t, json.Unmarshal(w.Body.Bytes(), tc.response))
234
235 tc.assertEqual(t, tc.response)
236 })
237 }
238 }
239
240 type putPayload []struct {
241 RequestID string `json:"requestId"`
242 Status string `json:"status"`
243 TimeStamp time.Time `json:"timestamp"`
244 }
245
246 func TestVNCPut(t *testing.T) {
247 h, err := setupTestHostTopic(t, testHostname, "a-uid")
248 require.NoError(t, err)
249
250 r := gin.Default()
251 h.RegisterEndpoints(r)
252
253 testTime := time.Now().Add(48 * time.Hour).Round(time.Second).UTC()
254
255 tests := map[string]struct {
256 setup putPayload
257 input []byte
258 expectedStatus int
259 response interface{}
260 assertEqual func(*testing.T, interface{})
261 }{
262 "No Payload": {
263 expectedStatus: http.StatusInternalServerError,
264 response: &Errors{},
265 assertEqual: genericAssertion(&Errors{
266 Errors: []*errors.Error{
267 {
268 Detail: "Internal Server Error",
269 },
270 },
271 }),
272 },
273 "No Setup: Empty": {
274 input: []byte(`[]`),
275 expectedStatus: http.StatusAccepted,
276 response: &host.State{},
277 assertEqual: vncStateAssertion(&host.State{
278 Hostname: testHostname,
279 VNC: host.VNCStates{},
280 NodeUID: "uid",
281 }, nil),
282 },
283 "No Setup: Add Payload": {
284 input: []byte(fmt.Sprintf(`
285 [
286 {
287 "requestID": "request1",
288 "status": "ACCEPTED",
289 "timestamp": "%[1]s"
290 },
291 {
292 "requestID": "request2",
293 "status": "REQUESTED",
294 "timestamp": "%[1]s"
295 },
296 {
297 "requestID": "request3",
298 "status": "CONNECTED",
299 "connections": 1,
300 "timestamp": "%[1]s"
301 }
302 ]`, testTime.Format(time.RFC3339))),
303 expectedStatus: http.StatusAccepted,
304 response: &host.State{},
305 assertEqual: vncStateAssertion(&host.State{
306 Hostname: testHostname,
307 VNC: host.VNCStates{
308 {
309 RequestID: "request1",
310 Status: host.Accepted,
311 TimeStamp: testTime.Format(time.RFC3339),
312 },
313 {
314 RequestID: "request2",
315 Status: host.Requested,
316 TimeStamp: testTime.Format(time.RFC3339),
317 },
318 {
319 RequestID: "request3",
320 Status: host.Connected,
321 Connections: 1,
322 TimeStamp: testTime.Format(time.RFC3339),
323 },
324 },
325 NodeUID: "uid",
326 }, TimeEqual(testTime)),
327 },
328 "Setup: Empty": {
329 setup: putPayload{
330 {
331 RequestID: "request1",
332 Status: string(host.Accepted),
333 TimeStamp: testTime,
334 },
335 {
336 RequestID: "request2",
337 Status: string(host.Requested),
338 TimeStamp: testTime,
339 },
340 },
341 input: []byte(`[]`),
342 expectedStatus: http.StatusAccepted,
343 response: &host.State{},
344 assertEqual: vncStateAssertion(&host.State{
345 Hostname: testHostname,
346 VNC: host.VNCStates{},
347 NodeUID: "uid",
348 }, TimeEqual(testTime)),
349 },
350 "Setup: Add Payload": {
351 setup: putPayload{
352 {
353 RequestID: "request_to_be_updated",
354 Status: string(host.Requested),
355 TimeStamp: time.Now(),
356 },
357 {
358 RequestID: "request_to_be_removed",
359 Status: string(host.Requested),
360 TimeStamp: time.Now(),
361 },
362 },
363 input: []byte(fmt.Sprintf(`
364 [
365 {
366 "requestID": "request_to_be_updated",
367 "status": "ACCEPTED",
368 "timestamp": "%[1]s"
369 },
370 {
371 "requestID": "request_to_be_added_1",
372 "status": "ACCEPTED",
373 "timestamp": "%[1]s"
374 },
375 {
376 "requestID": "request_to_be_added_2",
377 "status": "CONNECTED",
378 "connections": 1,
379 "timestamp": "%[1]s"
380 }
381 ]`, testTime.Format(time.RFC3339))),
382 expectedStatus: http.StatusAccepted,
383 response: &host.State{},
384 assertEqual: vncStateAssertion(&host.State{
385 Hostname: testHostname,
386 VNC: host.VNCStates{
387 {
388 RequestID: "request_to_be_updated",
389 Status: host.Accepted,
390 TimeStamp: testTime.Format(time.RFC3339),
391 },
392 {
393 RequestID: "request_to_be_added_1",
394 Status: host.Accepted,
395 TimeStamp: testTime.Format(time.RFC3339),
396 },
397 {
398 RequestID: "request_to_be_added_2",
399 Status: host.Connected,
400 Connections: 1,
401 TimeStamp: testTime.Format(time.RFC3339),
402 },
403 },
404 NodeUID: "uid",
405 }, TimeEqual(testTime)),
406 },
407 "Fail: No Timestamp": {
408 input: []byte(`[
409 {
410 "requestID": "1",
411 "status": "ACCEPTED"
412 }]`),
413 expectedStatus: http.StatusBadRequest,
414 response: &Errors{},
415 assertEqual: genericAssertion(&Errors{
416 Errors: []*errors.Error{
417 {
418 Detail: "Key: 'putVNCPayload.TimeStamp' Error:Field validation for 'TimeStamp' failed on the 'required' tag",
419 },
420 },
421 },
422 ),
423 },
424 "Fail: Unsupported Status": {
425 input: []byte(fmt.Sprintf(`[
426 {
427 "requestID": "1",
428 "status": "DROPPED",
429 "timestamp": "%[1]s"
430 }]`, testTime.Format(time.RFC3339))),
431 expectedStatus: http.StatusBadRequest,
432 response: &Errors{},
433 assertEqual: genericAssertion(&Errors{
434 Errors: []*errors.Error{
435 {
436 Detail: "Key: 'putVNCPayload.Status' Error:Field validation for 'Status' failed on the 'oneof' tag",
437 },
438 },
439 }),
440 },
441 "Fail: Multiple Binding Errors": {
442 input: []byte(fmt.Sprintf(`[
443 {
444 "requestID": "1",
445 "status": "ACCEPTED"
446 },
447 {
448 "requestID": "2",
449 "status": "INVALID",
450 "timestamp": "%[1]s"
451 },
452 {
453 "requestID": "3",
454 "status": "INVALID"
455 }]`, testTime.Format(time.RFC3339))),
456 expectedStatus: http.StatusBadRequest,
457 response: &Errors{},
458 assertEqual: genericAssertion(&Errors{
459 Errors: []*errors.Error{
460 {
461 Detail: "Key: 'putVNCPayload.TimeStamp' Error:Field validation for 'TimeStamp' failed on the 'required' tag",
462 },
463 {
464 Detail: "Key: 'putVNCPayload.Status' Error:Field validation for 'Status' failed on the 'oneof' tag",
465 },
466 {
467 Detail: "Key: 'putVNCPayload.Status' Error:Field validation for 'Status' failed on the 'oneof' tag\nKey: 'putVNCPayload.TimeStamp' Error:Field validation for 'TimeStamp' failed on the 'required' tag",
468 },
469 },
470 }),
471 },
472 "Fail: RequestID Validation Errors": {
473 input: []byte(fmt.Sprintf(`[
474 {
475 "requestID": "1",
476 "status": "ACCEPTED",
477 "timestamp": "%[1]s"
478 },
479 {
480 "requestID": "1",
481 "status": "ACCEPTED",
482 "timestamp": "%[1]s"
483 },
484 {
485 "requestID": "1",
486 "status": "ACCEPTED",
487 "timestamp": "%[1]s"
488 }]`, testTime.Format(time.RFC3339))),
489 expectedStatus: http.StatusBadRequest,
490 response: &Errors{},
491 assertEqual: genericAssertion(&Errors{
492 Errors: []*errors.Error{
493 {
494 Detail: "Key: 'putVNCPayload.RequestID' Error: Duplicate request id \"1\"",
495 },
496 {
497 Detail: "Key: 'putVNCPayload.RequestID' Error: Duplicate request id \"1\"",
498 },
499 },
500 }),
501 },
502 "Fail: Connections Validation Errors": {
503 input: []byte(fmt.Sprintf(`
504 [
505 {
506 "requestID": "1",
507 "status": "CONNECTED",
508 "timeStamp": "%[1]s"
509 },
510 {
511 "requestID": "2",
512 "status": "REQUESTED",
513 "connections": 1,
514 "timeStamp": "%[1]s"
515 },
516 {
517 "requestID": "3",
518 "status": "CONNECTED",
519 "connections": -1,
520 "timeStamp": "%[1]s"
521 }
522 ]`, testTime.Format(time.RFC3339))),
523 expectedStatus: http.StatusBadRequest,
524 response: &Errors{},
525 assertEqual: genericAssertion(&Errors{
526 Errors: []*errors.Error{
527 {
528 Detail: "Key: 'putVNCPayload.Connections' Error:Field validation for 'Connections' failed on the 'is_zero_xor_is_connected' tag",
529 },
530 {
531 Detail: "Key: 'putVNCPayload.Connections' Error:Field validation for 'Connections' failed on the 'is_zero_xor_is_connected' tag",
532 },
533 {
534 Detail: "Key: 'putVNCPayload.Connections' Error:Field validation for 'Connections' failed on the 'gte' tag",
535 },
536 },
537 }),
538 },
539 }
540
541 for name, tc := range tests {
542 t.Run(name, func(t *testing.T) {
543
544 for _, setupPayload := range tc.setup {
545 out, err := json.Marshal(setupPayload)
546 require.NoError(t, err)
547 req, err := http.NewRequest(http.MethodPost, host.Path+"/vnc", bytes.NewReader(out))
548 require.NoError(t, err)
549 w := httptest.NewRecorder()
550 r.ServeHTTP(w, req)
551 require.Equal(t, http.StatusAccepted, w.Code)
552 }
553
554 req, err := http.NewRequest(http.MethodPut, host.Path+"/vnc", bytes.NewReader(tc.input))
555 require.NoError(t, err)
556
557 w := httptest.NewRecorder()
558 r.ServeHTTP(w, req)
559
560 assert.Equal(t, tc.expectedStatus, w.Code)
561
562 assert.NoError(t, json.Unmarshal(w.Body.Bytes(), tc.response))
563
564 tc.assertEqual(t, tc.response)
565 })
566 }
567 }
568
View as plain text