...

Source file src/edge-infra.dev/pkg/sds/emergencyaccess/rules/server/server_test.go

Documentation: edge-infra.dev/pkg/sds/emergencyaccess/rules/server

     1  package server
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/gin-gonic/gin"
    13  	"github.com/go-logr/logr"
    14  	"github.com/stretchr/testify/assert"
    15  
    16  	rulesengine "edge-infra.dev/pkg/sds/emergencyaccess/rules"
    17  )
    18  
    19  type postNamesMock struct {
    20  	RulesEngine
    21  
    22  	dataRet rulesengine.AddNameResult
    23  	errRet  error
    24  
    25  	callCount int
    26  	names     []string
    27  }
    28  
    29  func (pnm *postNamesMock) AddCommands(_ context.Context, commands []rulesengine.PostCommandPayload) (rulesengine.AddNameResult, error) {
    30  	pnm.callCount = pnm.callCount + 1
    31  	for _, command := range commands {
    32  		pnm.names = append(pnm.names, command.Name)
    33  	}
    34  	return pnm.dataRet, pnm.errRet
    35  }
    36  
    37  func (pnm *postNamesMock) AddPrivileges(_ context.Context, privs []rulesengine.PostPrivilegePayload) (rulesengine.AddNameResult, error) {
    38  	pnm.callCount = pnm.callCount + 1
    39  	for _, priv := range privs {
    40  		pnm.names = append(pnm.names, priv.Name)
    41  	}
    42  	return pnm.dataRet, pnm.errRet
    43  }
    44  
    45  func TestPostNames(t *testing.T) {
    46  	t.Parallel()
    47  
    48  	tests := map[string]struct {
    49  		url     string
    50  		reqBody string
    51  
    52  		mockDataRet rulesengine.AddNameResult
    53  		mockErrRet  error
    54  
    55  		expMockCalledCount int
    56  		expNames           []string
    57  		expCode            int
    58  
    59  		jsonAssert StringAssertionFunc
    60  	}{
    61  		"Post Commands Ok": {
    62  			url: "/admin/commands",
    63  			reqBody: `[
    64  				{"name": "ls"},
    65  				{"name": "cat"}
    66  			]`,
    67  
    68  			expMockCalledCount: 1,
    69  			expNames:           []string{"ls", "cat"},
    70  			expCode:            http.StatusOK,
    71  
    72  			jsonAssert: JSONEmpty(),
    73  		},
    74  		"Post Commands Invalid JSON": {
    75  			url:     "/admin/commands",
    76  			reqBody: `[{"nam`,
    77  
    78  			expMockCalledCount: 0,
    79  			expCode:            http.StatusBadRequest,
    80  
    81  			jsonAssert: JSONEmpty(),
    82  		},
    83  		"Post Commands Invalid Payload": {
    84  			url: "/admin/commands",
    85  			reqBody: `[
    86  				{"name": ""},
    87  			]`,
    88  
    89  			expMockCalledCount: 0,
    90  			expCode:            http.StatusBadRequest,
    91  
    92  			jsonAssert: JSONEmpty(),
    93  		},
    94  		"Post Commands Rules Engine Error": {
    95  			url: "/admin/commands",
    96  			reqBody: `[
    97  				{"name": "ls"},
    98  				{"name": "cat"}
    99  			]`,
   100  
   101  			mockDataRet: rulesengine.AddNameResult{},
   102  			mockErrRet:  fmt.Errorf("an error occurred"),
   103  
   104  			expMockCalledCount: 1,
   105  			expNames:           []string{"ls", "cat"},
   106  			expCode:            http.StatusInternalServerError,
   107  
   108  			jsonAssert: JSONEmpty(),
   109  		},
   110  		"Post Commands Rules Engine Conflict": {
   111  			url: "/admin/commands",
   112  			reqBody: `[
   113  				{"name": "ls"},
   114  				{"name": "cat"}
   115  			]`,
   116  
   117  			mockDataRet: rulesengine.AddNameResult{Conflicts: []string{"ls", "cat"}},
   118  
   119  			expMockCalledCount: 1,
   120  			expNames:           []string{"ls", "cat"},
   121  			expCode:            http.StatusConflict,
   122  
   123  			jsonAssert: JSONEq(`{
   124  				"conflicts": [
   125  					"ls",
   126  					"cat"
   127  				]
   128  			}`),
   129  		},
   130  		"Post Privilege Ok": {
   131  			url: "/admin/privileges",
   132  			reqBody: `[
   133  				{"name": "basic"},
   134  				{"name": "admin"}
   135  			]`,
   136  
   137  			expMockCalledCount: 1,
   138  			expNames:           []string{"basic", "admin"},
   139  			expCode:            http.StatusOK,
   140  
   141  			jsonAssert: JSONEmpty(),
   142  		},
   143  		"Post Privilege Invalid JSON": {
   144  			url:     "/admin/privileges",
   145  			reqBody: `[{"nam`,
   146  
   147  			expMockCalledCount: 0,
   148  			expCode:            http.StatusBadRequest,
   149  
   150  			jsonAssert: JSONEmpty(),
   151  		},
   152  		"Post Privilege Invalid Payload": {
   153  			url: "/admin/privileges",
   154  			reqBody: `[
   155  				{"name": ""},
   156  			]`,
   157  
   158  			expMockCalledCount: 0,
   159  			expCode:            http.StatusBadRequest,
   160  
   161  			jsonAssert: JSONEmpty(),
   162  		},
   163  		"Post Privilege Rules Engine Error": {
   164  			url: "/admin/privileges",
   165  			reqBody: `[
   166  				{"name": "basic"},
   167  				{"name": "admin"}
   168  			]`,
   169  
   170  			mockDataRet: rulesengine.AddNameResult{},
   171  			mockErrRet:  fmt.Errorf("an error occurred"),
   172  
   173  			expMockCalledCount: 1,
   174  			expNames:           []string{"basic", "admin"},
   175  			expCode:            http.StatusInternalServerError,
   176  
   177  			jsonAssert: JSONEmpty(),
   178  		},
   179  		"Post Privilege Rules Engine Conflict": {
   180  			url: "/admin/privileges",
   181  			reqBody: `[
   182  				{"name": "basic"},
   183  				{"name": "admin"}
   184  			]`,
   185  
   186  			mockDataRet: rulesengine.AddNameResult{Conflicts: []string{"basic", "admin"}},
   187  
   188  			expMockCalledCount: 1,
   189  			expNames:           []string{"basic", "admin"},
   190  			expCode:            http.StatusConflict,
   191  
   192  			jsonAssert: JSONEq(`{
   193  				"conflicts": [
   194  					"basic",
   195  					"admin"
   196  				]
   197  			}`),
   198  		},
   199  	}
   200  
   201  	for name, tc := range tests {
   202  		tc := tc
   203  		t.Run(name, func(t *testing.T) {
   204  			t.Parallel()
   205  
   206  			ruleseng := postNamesMock{
   207  				dataRet: tc.mockDataRet,
   208  				errRet:  tc.mockErrRet,
   209  			}
   210  
   211  			r := httptest.NewRecorder()
   212  			_, ginEngine := getTestGinContext(r)
   213  			_, err := New(ginEngine, &ruleseng, newLogger())
   214  			assert.NoError(t, err)
   215  
   216  			req, err := http.NewRequest(http.MethodPost, tc.url, strings.NewReader(tc.reqBody))
   217  			assert.NoError(t, err)
   218  
   219  			ginEngine.ServeHTTP(r, req)
   220  
   221  			assert.Equal(t, tc.expCode, r.Result().StatusCode)
   222  
   223  			assert.Equal(t, tc.expMockCalledCount, ruleseng.callCount)
   224  			assert.Equal(t, tc.expNames, ruleseng.names)
   225  
   226  			tc.jsonAssert(t, r.Body.String())
   227  		})
   228  	}
   229  }
   230  
   231  type deleteMock struct {
   232  	RulesEngine
   233  
   234  	command, privilege string
   235  
   236  	retDelete rulesengine.DeleteResult
   237  	retErr    error
   238  }
   239  
   240  func (dm *deleteMock) DeleteCommand(_ context.Context, name string) (rulesengine.DeleteResult, error) {
   241  	dm.command = name
   242  	return dm.retDelete, dm.retErr
   243  }
   244  
   245  func (dm *deleteMock) DeletePrivilege(_ context.Context, name string) (rulesengine.DeleteResult, error) {
   246  	dm.privilege = name
   247  	return dm.retDelete, dm.retErr
   248  }
   249  
   250  func (dm *deleteMock) DeleteDefaultRule(_ context.Context, commandName string, privilegeName string) (rulesengine.DeleteResult, error) {
   251  	dm.command = commandName
   252  	dm.privilege = privilegeName
   253  	return dm.retDelete, dm.retErr
   254  }
   255  
   256  func TestDelete(t *testing.T) {
   257  	t.Parallel()
   258  
   259  	tests := map[string]struct {
   260  		url       string
   261  		command   string
   262  		privilege string
   263  
   264  		retRes rulesengine.DeleteResult
   265  		retErr error
   266  
   267  		expStatus int
   268  		expOut    StringAssertionFunc
   269  	}{
   270  		"Delete Command: Success": {
   271  			command: "ls",
   272  			url:     "/admin/commands/ls",
   273  
   274  			retRes: rulesengine.DeleteResult{RowsAffected: 1},
   275  			retErr: nil,
   276  
   277  			expStatus: http.StatusOK,
   278  			expOut:    JSONEmpty(),
   279  		},
   280  		"Delete Command: No Change With Errors": {
   281  			command: "ls",
   282  			url:     "/admin/commands/ls",
   283  
   284  			retRes: rulesengine.DeleteResult{RowsAffected: 0, Errors: []rulesengine.Error{{Type: rulesengine.UnknownCommand, Command: "ls"}}},
   285  			retErr: nil,
   286  
   287  			expStatus: http.StatusNotFound,
   288  			expOut:    JSONEq(`{"errors":[{"type":"Unknown Command","command":"ls"}]}`),
   289  		},
   290  		"Delete Command: Rule With Conflicts": {
   291  			command: "ls",
   292  			url:     "/admin/commands/ls",
   293  
   294  			retRes: rulesengine.DeleteResult{RowsAffected: 0, Errors: []rulesengine.Error{{Type: rulesengine.Conflict, Command: "ls"}}},
   295  			retErr: nil,
   296  
   297  			expStatus: http.StatusConflict,
   298  			expOut:    JSONEq(`{"errors":[{"type":"Conflict","command":"ls"}]}`),
   299  		},
   300  		"Delete Command: Application Error": {
   301  			command: "ls",
   302  			url:     "/admin/commands/ls",
   303  
   304  			retRes: rulesengine.DeleteResult{RowsAffected: 1},
   305  			retErr: fmt.Errorf("an error"),
   306  
   307  			expStatus: http.StatusInternalServerError,
   308  			expOut:    JSONEmpty(),
   309  		},
   310  		"Delete Privilege: Success": {
   311  			privilege: "basic",
   312  			url:       "/admin/privileges/basic",
   313  
   314  			retRes: rulesengine.DeleteResult{RowsAffected: 1},
   315  			retErr: nil,
   316  
   317  			expStatus: http.StatusOK,
   318  			expOut:    JSONEmpty(),
   319  		},
   320  		"Delete Privilege: No Change With Errors": {
   321  			privilege: "basic",
   322  			url:       "/admin/privileges/basic",
   323  
   324  			retRes: rulesengine.DeleteResult{RowsAffected: 0, Errors: []rulesengine.Error{{Type: rulesengine.UnknownCommand, Command: "ls"}}},
   325  			retErr: nil,
   326  
   327  			expStatus: http.StatusNotFound,
   328  			expOut:    JSONEq(`{"errors":[{"type":"Unknown Command","command":"ls"}]}`),
   329  		},
   330  		"Delete Privilege: With Conflicts": {
   331  			privilege: "basic",
   332  			url:       "/admin/privileges/basic",
   333  
   334  			retRes: rulesengine.DeleteResult{RowsAffected: 0, Errors: []rulesengine.Error{{Type: rulesengine.Conflict, Command: "ls"}}},
   335  			retErr: nil,
   336  
   337  			expStatus: http.StatusConflict,
   338  			expOut:    JSONEq(`{"errors":[{"type":"Conflict","command":"ls"}]}`),
   339  		},
   340  		"Delete Privilege: Application Error": {
   341  			privilege: "basic",
   342  			url:       "/admin/privileges/basic",
   343  
   344  			retRes: rulesengine.DeleteResult{RowsAffected: 1},
   345  			retErr: fmt.Errorf("an error"),
   346  
   347  			expStatus: http.StatusInternalServerError,
   348  			expOut:    JSONEmpty(),
   349  		},
   350  		"Delete Rule: Success": {
   351  			command:   "ls",
   352  			privilege: "basic",
   353  			url:       "/admin/rules/default/commands/ls/privileges/basic",
   354  
   355  			retRes: rulesengine.DeleteResult{RowsAffected: 1},
   356  			retErr: nil,
   357  
   358  			expStatus: http.StatusOK,
   359  			expOut:    JSONEmpty(),
   360  		},
   361  		"Delete Rule: No Change With Errors": {
   362  			command:   "ls",
   363  			privilege: "basic",
   364  			url:       "/admin/rules/default/commands/ls/privileges/basic",
   365  
   366  			retRes: rulesengine.DeleteResult{RowsAffected: 0, Errors: []rulesengine.Error{{Type: rulesengine.UnknownCommand, Command: "ls"}}},
   367  			retErr: nil,
   368  
   369  			expStatus: http.StatusNotFound,
   370  			expOut:    JSONEq(`{"errors":[{"type":"Unknown Command","command":"ls"}]}`),
   371  		},
   372  		"Delete Rule: With Conflicts": {
   373  			command:   "ls",
   374  			privilege: "basic",
   375  			url:       "/admin/rules/default/commands/ls/privileges/basic",
   376  
   377  			retRes: rulesengine.DeleteResult{RowsAffected: 0, Errors: []rulesengine.Error{{Type: rulesengine.Conflict, Command: "ls"}}},
   378  			retErr: nil,
   379  
   380  			expStatus: http.StatusConflict,
   381  			expOut:    JSONEq(`{"errors":[{"type":"Conflict","command":"ls"}]}`),
   382  		},
   383  		"Delete Rule: Application Error": {
   384  			command:   "ls",
   385  			privilege: "basic",
   386  			url:       "/admin/rules/default/commands/ls/privileges/basic",
   387  
   388  			retRes: rulesengine.DeleteResult{RowsAffected: 1},
   389  			retErr: fmt.Errorf("an error"),
   390  
   391  			expStatus: http.StatusInternalServerError,
   392  			expOut:    JSONEmpty(),
   393  		},
   394  		"Delete rule: No command": {
   395  			command:   "",
   396  			privilege: "",
   397  			url:       "/admin/rules/default/commands//privileges/basic",
   398  
   399  			retErr: fmt.Errorf("an error"),
   400  
   401  			expStatus: http.StatusBadRequest,
   402  			expOut:    JSONEmpty(),
   403  		},
   404  		"Delete rule: No Privilege Path": {
   405  			command:   "",
   406  			privilege: "",
   407  			url:       "/admin/rules/default/commands/ls/privileges",
   408  
   409  			retErr: fmt.Errorf("an error"),
   410  
   411  			// This status is returned by gin before reaching any of our handlers
   412  			// This is why the expected Output response formatting is different to
   413  			// all other apis
   414  			expStatus: http.StatusNotFound,
   415  			expOut:    StringEqual("404 page not found"),
   416  		},
   417  		"Delete rule: No Privilege": {
   418  			command:   "",
   419  			privilege: "",
   420  			url:       "/admin/rules/default/commands/ls/privileges/",
   421  
   422  			retErr: fmt.Errorf("an error"),
   423  
   424  			// This status is returned by gin before reaching any of our handlers
   425  			// This is why the expected Output response formatting is different to
   426  			// all other apis
   427  			expStatus: http.StatusNotFound,
   428  			expOut:    StringEqual("404 page not found"),
   429  		},
   430  	}
   431  
   432  	for name, tc := range tests {
   433  		tc := tc
   434  		t.Run(name, func(t *testing.T) {
   435  			t.Parallel()
   436  
   437  			log := newLogger()
   438  			mreng := deleteMock{
   439  				retDelete: tc.retRes,
   440  				retErr:    tc.retErr,
   441  			}
   442  
   443  			r := httptest.NewRecorder()
   444  			_, ginEngine := getTestGinContext(r)
   445  			_, err := New(ginEngine, &mreng, log)
   446  			assert.NoError(t, err)
   447  
   448  			req, err := http.NewRequest(
   449  				http.MethodDelete,
   450  				tc.url,
   451  				nil,
   452  			)
   453  			assert.NoError(t, err)
   454  
   455  			ginEngine.ServeHTTP(r, req)
   456  
   457  			assert.Equal(t, tc.expStatus, r.Result().StatusCode)
   458  			assert.Equal(t, tc.command, mreng.command)
   459  			assert.Equal(t, tc.privilege, mreng.privilege)
   460  
   461  			tc.expOut(t, r.Body.String())
   462  		})
   463  	}
   464  }
   465  
   466  func TestReadNil(t *testing.T) {
   467  	tests := map[string]struct {
   468  		URL     string
   469  		payload any
   470  	}{
   471  		"Read Rule": {
   472  			URL: "/admin/rules/default/commands/ls",
   473  		},
   474  		"Read Command": {
   475  			URL: "/admin/commands/ls",
   476  		},
   477  		"Read Priv": {
   478  			URL: "/admin/privileges/basic",
   479  		},
   480  	}
   481  	for name, tc := range tests {
   482  		t.Run(name, func(t *testing.T) {
   483  			log := newLogger()
   484  			t.Setenv("RCLI_RES_DATA_DIR", "./testdata")
   485  
   486  			ruleseng := MockRulesEngine{}
   487  
   488  			r := httptest.NewRecorder()
   489  			_, ginEngine := getTestGinContext(r)
   490  			_, err := New(ginEngine, &ruleseng, log)
   491  			assert.Nil(t, err)
   492  
   493  			req, err := http.NewRequest(http.MethodGet, tc.URL, nil)
   494  			assert.NoError(t, err)
   495  			ginEngine.ServeHTTP(r, req)
   496  			assert.Equal(t, http.StatusOK, r.Result().StatusCode)
   497  			assert.Equal(t, r.Body.String(), "null")
   498  		})
   499  	}
   500  }
   501  
   502  func TestHealth(t *testing.T) {
   503  	t.Parallel()
   504  
   505  	tests := map[string]struct {
   506  		checks []func() error
   507  
   508  		expCode int
   509  		expData string
   510  	}{
   511  		"No checks": {
   512  			checks:  nil,
   513  			expCode: http.StatusOK,
   514  			expData: "ok",
   515  		},
   516  		"Passing check": {
   517  			checks:  []func() error{func() error { return nil }},
   518  			expCode: http.StatusOK,
   519  			expData: "ok",
   520  		},
   521  		"Failing check": {
   522  			checks:  []func() error{func() error { return fmt.Errorf("this is bad") }},
   523  			expCode: http.StatusServiceUnavailable,
   524  			expData: "failed health check: this is bad",
   525  		},
   526  		"Two checks": {
   527  			checks:  []func() error{func() error { return nil }, func() error { return fmt.Errorf("this is bad") }},
   528  			expCode: http.StatusServiceUnavailable,
   529  			expData: "failed health check: this is bad",
   530  		},
   531  	}
   532  
   533  	for name, tc := range tests {
   534  		tc := tc
   535  		t.Run(name, func(t *testing.T) {
   536  			t.Parallel()
   537  
   538  			r := httptest.NewRecorder()
   539  			gin.SetMode(gin.TestMode)
   540  			_, ginEngine := gin.CreateTestContext(r)
   541  
   542  			_, err := New(ginEngine, nil, logr.Discard(), tc.checks...)
   543  			assert.NoError(t, err)
   544  
   545  			req, err := http.NewRequest(http.MethodGet, "/health", nil)
   546  			assert.NoError(t, err)
   547  
   548  			ginEngine.ServeHTTP(r, req)
   549  
   550  			assert.Equal(t, tc.expCode, r.Result().StatusCode)
   551  
   552  			data, err := io.ReadAll(r.Body)
   553  			assert.NoError(t, err)
   554  			assert.Equal(t, tc.expData, string(data))
   555  		})
   556  	}
   557  }
   558  

View as plain text