...

Source file src/edge-infra.dev/pkg/sds/interlock/topic/host/vnc_state_int_test.go

Documentation: edge-infra.dev/pkg/sds/interlock/topic/host

     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  			// Now that we have tested actualTime, add it to expectedVNC so we can
    43  			// do a full equal assertion.
    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  	// We need to round testTime to the nearest second because otherwise we will be testing
    71  	// against some extra values that get lost in time.RFC3339 format, like milliseconds.
    72  	// Timezone is set to UTC to avoid pipeline errors.
    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  			// Set up a pre-defined state before test
   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