package server import ( "bytes" "context" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "edge-infra.dev/pkg/lib/fog" "edge-infra.dev/pkg/sds/emergencyaccess/apierror" errorhandler "edge-infra.dev/pkg/sds/emergencyaccess/apierror/handler" "edge-infra.dev/pkg/sds/emergencyaccess/eagateway" "edge-infra.dev/pkg/sds/emergencyaccess/middleware" "edge-infra.dev/pkg/sds/emergencyaccess/msgdata" "edge-infra.dev/pkg/sds/emergencyaccess/remotecli" "edge-infra.dev/pkg/sds/emergencyaccess/requestservice" "edge-infra.dev/pkg/sds/emergencyaccess/types" "github.com/DATA-DOG/go-sqlmock" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) var ( defaultCommand = "echo hello" ) type sendTestRCLI struct { eagateway.RemoteCLI commandID string userID string sessionID string request msgdata.Request } func (rcli *sendTestRCLI) Send(_ context.Context, userID, sessionID, commandID string, request msgdata.Request, _ ...remotecli.RCLIOption) error { rcli.userID = userID rcli.sessionID = sessionID rcli.commandID = commandID rcli.request = request return nil } func createSendRequest(c *gin.Context, target types.Target, sessionID, command string, darkmode bool) (*http.Request, error) { payload := types.SendPayload{ Command: command, SessionID: sessionID, Target: target, AuthDetails: types.AuthDetails{ DarkMode: darkmode, }, } message, err := json.Marshal(payload) if err != nil { return nil, err } req, err := http.NewRequestWithContext(c, http.MethodPost, "/ea/sendCommand", bytes.NewBuffer(message)) if err != nil { return nil, err } setAuthHeaders(req) return req, nil } func newRequestService(t *testing.T) *requestservice.RequestService { t.Helper() db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) assert.NoError(t, err) mock.ExpectQuery(`SELECT value FROM watched_field_objects INNER JOIN watched_field_values ON watched_field_values.object_id = watched_field_objects.object_id INNER JOIN terminals ON watched_field_objects.cluster_edge_id = terminals.cluster_edge_id WHERE watched_field_objects.cluster_edge_id = $1 AND kind = 'Node' AND terminals.terminal_id = $2 AND jsonpath = '$.metadata.labels["feature.node.kubernetes.io/ien-version"]' AND deleted IS NOT true ORDER BY watched_at DESC LIMIT 1 ;`). WithArgs(`storeID`, `terminalID`). WillReturnRows(sqlmock.NewRows([]string{"value"}). AddRow("v1.14.0"), ) requestService, err := requestservice.New(db) assert.NoError(t, err) t.Cleanup(func() { assert.NoError(t, mock.ExpectationsWereMet()) }) t.Cleanup(func() { db.Close() }) return requestService } func TestSendSuccess(t *testing.T) { // Setup r := httptest.NewRecorder() gin.SetMode(gin.TestMode) c, ginEngine := gin.CreateTestContext(r) authServer, url := authserviceServer(http.StatusOK, WithMiddleware(verifyUserAuthHeaders(t))) defer authServer.Close() requestService := newRequestService(t) rcli := &sendTestRCLI{} _, err := New(eagateway.Config{AuthServiceHost: url}, ginEngine, newLogger(), rcli, requestService) assert.NoError(t, err) // Test sessionID := "TestSendSuccess" target := defaultTarget command := defaultCommand userID := "user" req, err := createSendRequest(c, target, sessionID, command, false) assert.NoError(t, err) ginEngine.ServeHTTP(r, req) // Confirm correlation ID is used as commandID correlationID := r.Result().Header.Get(middleware.CorrelationIDKey) expReq, err := msgdata.NewV1_0Request(defaultCommand) assert.NoError(t, err) expected := &sendTestRCLI{ request: expReq, sessionID: sessionID, commandID: correlationID, userID: userID, } assert.Equal(t, http.StatusOK, r.Result().StatusCode) assert.Equal(t, expected, rcli) } func TestSendBadPayload(t *testing.T) { // Setup r := httptest.NewRecorder() gin.SetMode(gin.TestMode) c, ginEngine := gin.CreateTestContext(r) authServer, url := authserviceServer(http.StatusForbidden) defer authServer.Close() rcli := &sendTestRCLI{} _, err := New(eagateway.Config{AuthServiceHost: url}, ginEngine, newLogger(), rcli, nil) assert.NoError(t, err) // Test req, err := http.NewRequestWithContext(c, http.MethodPost, "/ea/sendCommand", nil) assert.NoError(t, err) ginEngine.ServeHTTP(r, req) assert.Equal(t, http.StatusBadRequest, r.Result().StatusCode) assert.Empty(t, rcli) // Assert rcli.Send isn't called } func TestSendUnauthorizedCommand(t *testing.T) { // Setup r := httptest.NewRecorder() gin.SetMode(gin.TestMode) c, ginEngine := gin.CreateTestContext(r) authServer, url := authserviceServer(http.StatusOK) defer authServer.Close() requestService := newRequestService(t) rcli := &sendTestRCLI{} _, err := New(eagateway.Config{AuthServiceHost: url}, ginEngine, newLogger(), rcli, requestService) assert.NoError(t, err) // Test sessionID := "TestSendUnauthorizedCommand" target := defaultTarget command := badCommand req, err := createSendRequest(c, target, sessionID, command, false) assert.NoError(t, err) ginEngine.ServeHTTP(r, req) assert.Equal(t, http.StatusForbidden, r.Result().StatusCode) assert.Empty(t, rcli) // Assert rcli.Send isn't called // Assert eagateway Response includes appropriate APIError body, err := io.ReadAll(r.Result().Body) assert.NoError(t, err) var resp errorhandler.ErrorResponse err = json.Unmarshal(body, &resp) assert.NoError(t, err) assert.Equal(t, apierror.ErrUnauthorizedCommand, resp.ErrorCode) } // checks sendcommand updates user error IF darkmode is enabled AND authcommand fails func TestDarkmodeUserErrorUnauthorizedCommand(t *testing.T) { // Setup r := httptest.NewRecorder() gin.SetMode(gin.TestMode) c, ginEngine := gin.CreateTestContext(r) authServer, url := authserviceServer(http.StatusOK) defer authServer.Close() requestService := newRequestService(t) rcli := &sendTestRCLI{} _, err := New(eagateway.Config{AuthServiceHost: url}, ginEngine, newLogger(), rcli, requestService) assert.NoError(t, err) // create a bad request sessionID := "TestDarkmodeUserErrorUnauthorizedCommand" target := defaultTarget command := badCommand req, err := createSendRequest(c, target, sessionID, command, true) assert.NoError(t, err) ginEngine.ServeHTTP(r, req) assert.Equal(t, http.StatusForbidden, r.Result().StatusCode) // TEST // parse the result outErr := errorhandler.ParseJSONAPIError(r.Result().Body) // cast it to apiErr apiErr, ok := outErr.(apierror.APIError) assert.True(t, ok) // DarkMode no longer provides a nice user error assert.Equal(t, "", strings.Join(apiErr.UserError(), "")) } func TestExtractPayloadSuccess(t *testing.T) { r := httptest.NewRecorder() gin.SetMode(gin.TestMode) c, _ := gin.CreateTestContext(r) sessionID := "TestSendSuccess" command := defaultCommand target := defaultTarget req, err := createSendRequest(c, target, sessionID, command, false) assert.NoError(t, err) c.Request = req payload, err := extractSendPayload(c) expected := types.SendPayload{ Target: target, SessionID: sessionID, Command: command, } assert.Equal(t, expected, payload) assert.NoError(t, err) } func TestExtractPayloadFail(t *testing.T) { r := httptest.NewRecorder() gin.SetMode(gin.TestMode) c, _ := gin.CreateTestContext(r) req, err := createSendRequest(c, types.Target{}, "", "", false) assert.NoError(t, err) c.Request = req payload, err := extractSendPayload(c) assert.Equal(t, types.SendPayload{}, payload) assert.Error(t, err) } func TestSendAuditLog(t *testing.T) { // Setup r := httptest.NewRecorder() gin.SetMode(gin.TestMode) c, ginEngine := gin.CreateTestContext(r) authServer, url := authserviceServer(http.StatusOK) defer authServer.Close() rcli := &sendTestRCLI{} // test logger writes to a byte buffer so it can be read from test b := bytes.Buffer{} log := fog.New(fog.To(&b)) _, err := New(eagateway.Config{AuthServiceHost: url}, ginEngine, log, rcli, nil) assert.NoError(t, err) // Execute send api call sessionID := "AuditLogCheck" target := defaultTarget command := defaultCommand req, err := createSendRequest(c, target, sessionID, command, false) assert.NoError(t, err) ginEngine.ServeHTTP(r, req) //test assert.True(t, validateAuditLog(&b, "Send API Called")) }