package server import ( "context" "fmt" "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/assert" rulesengine "edge-infra.dev/pkg/sds/emergencyaccess/rules" ) // testing helper type type helper interface { Helper() } type StringAssertionFunc func(t assert.TestingT, actual string, msgAndArgs ...interface{}) bool func JSONEq(expected string) StringAssertionFunc { return func(t assert.TestingT, actual string, msgAndArgs ...interface{}) bool { if help, ok := t.(helper); ok { help.Helper() } return assert.JSONEq(t, expected, actual, msgAndArgs...) } } func StringEqual(expected string) StringAssertionFunc { return func(t assert.TestingT, actual string, msgAndArgs ...interface{}) bool { if help, ok := t.(helper); ok { help.Helper() } return assert.Equal(t, expected, actual, msgAndArgs...) } } func JSONEmpty() StringAssertionFunc { return func(t assert.TestingT, actual string, msgAndArgs ...interface{}) bool { if help, ok := t.(helper); ok { help.Helper() } return assert.Empty(t, actual, msgAndArgs...) } } type GetAllBannerRulesMock struct { RulesEngine AllOut []rulesengine.ReadBannerRule AllErr error } func (mc GetAllBannerRulesMock) ReadRulesForAllBanners(_ context.Context) ([]rulesengine.ReadBannerRule, error) { return mc.AllOut, mc.AllErr } func TestGetAllRulesForAllBanners(t *testing.T) { t.Parallel() tests := map[string]struct { rengOut []rulesengine.ReadBannerRule rengErr error expStatus int expOut string }{ "error return": { rengOut: nil, rengErr: fmt.Errorf("an error"), expStatus: http.StatusInternalServerError, expOut: "", }, "Rules Returned": { expStatus: http.StatusOK, expOut: `[{ "command": { "id": "abcd", "name": "ls" }, "banners": [{ "banner": { "id": "efgh", "name": "banner" }, "privileges": [{ "id": "ijkl", "name": "read" }] }] }]`, rengErr: nil, rengOut: []rulesengine.ReadBannerRule{ { Command: rulesengine.Command{Name: "ls", ID: "abcd"}, Banners: []rulesengine.BannerPrivOverrides{ { Banner: rulesengine.Banner{BannerName: "banner", BannerID: "efgh"}, Privileges: []rulesengine.Privilege{ {Name: "read", ID: "ijkl"}, }, }, }, }, }, }, } for name, tc := range tests { tc := tc t.Run(name, func(t *testing.T) { t.Parallel() log := newLogger() ruleseng := GetAllBannerRulesMock{ AllOut: tc.rengOut, AllErr: tc.rengErr, } r := httptest.NewRecorder() _, ginEngine := getTestGinContext(r) _, err := New(ginEngine, &ruleseng, log) assert.NoError(t, err) req, err := http.NewRequest(http.MethodGet, "/admin/rules/banner/commands", nil) assert.NoError(t, err) ginEngine.ServeHTTP(r, req) assert.Equal(t, tc.expStatus, r.Result().StatusCode) if tc.expOut != "" { assert.JSONEq(t, tc.expOut, r.Body.String()) } else { assert.Empty(t, r.Body.String()) } }) } } type getBannerMock struct { RulesEngine bannerName string retRules []rulesengine.Rule retErr error } func (mc *getBannerMock) ReadRulesForBanner(_ context.Context, bannerName string) ([]rulesengine.Rule, error) { mc.bannerName = bannerName return mc.retRules, mc.retErr } func TestReadAllRulesInBanner(t *testing.T) { t.Parallel() tests := map[string]struct { url string // Values rulesengine should return retRules []rulesengine.Rule retErr error // value rulesengine should be called with expBanner string // Expected http response expStatusCode int expOutput StringAssertionFunc }{ "Empty list return": { url: "/admin/rules/banner/commands?bannerName=myBanner", expBanner: "myBanner", retRules: []rulesengine.Rule{}, retErr: nil, expStatusCode: http.StatusOK, expOutput: JSONEq("null"), }, "Nil return": { url: "/admin/rules/banner/commands?bannerName=myBanner", expBanner: "myBanner", retRules: nil, retErr: nil, expStatusCode: http.StatusOK, expOutput: JSONEq("null"), }, "Error return": { url: "/admin/rules/banner/commands?bannerName=aBanner", expBanner: "aBanner", retRules: []rulesengine.Rule{}, retErr: fmt.Errorf("an error"), expStatusCode: http.StatusInternalServerError, expOutput: JSONEmpty(), }, "Ok return": { url: "/admin/rules/banner/commands?bannerName=myBanner", expBanner: "myBanner", retRules: []rulesengine.Rule{{Command: rulesengine.Command{Name: "ls", ID: "efgh"}, Privileges: []rulesengine.Privilege{{Name: "read", ID: "abcd"}}}}, retErr: nil, expStatusCode: http.StatusOK, expOutput: JSONEq(`[{ "command": { "name": "ls", "id": "efgh" }, "privileges": [{ "name": "read", "id": "abcd" }] }]`), }, "Missing Banner Name": { url: "/admin/rules/banner/commands?bannerName=", expBanner: "", retRules: []rulesengine.Rule{{Command: rulesengine.Command{Name: "ls", ID: "efgh"}, Privileges: []rulesengine.Privilege{{Name: "read", ID: "abcd"}}}}, retErr: nil, expStatusCode: http.StatusBadRequest, expOutput: JSONEmpty(), }, } for name, tc := range tests { tc := tc t.Run(name, func(t *testing.T) { t.Parallel() log := newLogger() ruleseng := getBannerMock{ retRules: tc.retRules, retErr: tc.retErr, } r := httptest.NewRecorder() _, ginEngine := getTestGinContext(r) _, err := New(ginEngine, &ruleseng, log) assert.NoError(t, err) req, err := http.NewRequest(http.MethodGet, tc.url, nil) assert.NoError(t, err) ginEngine.ServeHTTP(r, req) assert.Equal(t, tc.expStatusCode, r.Result().StatusCode) tc.expOutput(t, r.Body.String()) }) } } func TestDeleteRuleErrors(t *testing.T) { t.Parallel() tests := map[string]struct { url string expStatus int expOut StringAssertionFunc }{ "Delete Rule called without a bannerName": { url: "/admin/rules/banner/commands/rm/privileges/basic?bannerName=", expStatus: http.StatusBadRequest, expOut: JSONEmpty(), }, "Delete Rule called without a query parameter": { url: "/admin/rules/banner/commands/rm/privileges/basic", expStatus: http.StatusBadRequest, expOut: JSONEmpty(), }, "Delete Rule called without a command": { url: "/admin/rules/banner/commands//privileges/basic?bannerName=myBanner", expStatus: http.StatusBadRequest, expOut: JSONEmpty(), }, } // Create mock with no implementation to ensure server panics (returns 500) // if rulesengine is called deleteMock := struct { RulesEngine }{} for name, tc := range tests { tc := tc t.Run(name, func(t *testing.T) { t.Parallel() log := newLogger() r := httptest.NewRecorder() _, ginEngine := getTestGinContext(r) _, err := New(ginEngine, deleteMock, log) assert.NoError(t, err) req, err := http.NewRequest( http.MethodDelete, tc.url, nil, ) assert.NoError(t, err) ginEngine.ServeHTTP(r, req) assert.Equal(t, tc.expStatus, r.Result().StatusCode) tc.expOut(t, r.Body.String()) }) } } type postBannerRulesMock struct { RulesEngine dataRet rulesengine.AddRuleResult errRet error callCount int bannerName string rules rulesengine.WriteRules } func (pr *postBannerRulesMock) AddBannerRules(_ context.Context, bannerName string, rules rulesengine.WriteRules) (rulesengine.AddRuleResult, error) { pr.callCount = pr.callCount + 1 pr.bannerName = bannerName pr.rules = rules return pr.dataRet, pr.errRet } func TestPostRulesInBanner(t *testing.T) { t.Parallel() tests := map[string]struct { url string reqBody string mockDataRet rulesengine.AddRuleResult mockErrRet error expMockCalledCount int expCalledBanner string expCalledRules rulesengine.WriteRules expCode int expOut string }{ "Ok": { url: "/admin/rules/banner/commands?bannerName=myBanner", reqBody: `[ {"command": "ls", "privileges": ["read","write"]}, {"command": "cat", "privileges": ["read","write"]} ]`, expMockCalledCount: 1, expCalledBanner: "myBanner", expCalledRules: rulesengine.WriteRules{ {Command: "ls", Privileges: []string{"read", "write"}}, {Command: "cat", Privileges: []string{"read", "write"}}, }, expCode: http.StatusOK, expOut: ``, }, "Invalid JSON": { url: "/admin/rules/banner/commands?bannerName=myBanner", reqBody: `[{"comm`, expMockCalledCount: 0, expCode: http.StatusBadRequest, expOut: ``, }, "Invalid payload": { url: "/admin/rules/banner/commands?bannerName=myBanner", reqBody: `[{"command": "", "privileges": ["read"]}]`, expMockCalledCount: 0, expCode: http.StatusBadRequest, expOut: ``, }, "Rulesengine Error": { url: "/admin/rules/banner/commands?bannerName=aBanner", reqBody: `[ {"command": "ls", "privileges": ["read","write"]}, {"command": "cat", "privileges": ["read","write"]} ]`, mockDataRet: rulesengine.AddRuleResult{}, mockErrRet: fmt.Errorf("an error occurred"), expMockCalledCount: 1, expCalledBanner: "aBanner", expCalledRules: rulesengine.WriteRules{ {Command: "ls", Privileges: []string{"read", "write"}}, {Command: "cat", Privileges: []string{"read", "write"}}, }, expCode: http.StatusInternalServerError, expOut: ``, }, "Rulesengine Conflict": { url: "/admin/rules/banner/commands?bannerName=myBanner", reqBody: `[ {"command": "ls", "privileges": ["read","write"]}, {"command": "cat", "privileges": ["read","write"]} ]`, mockDataRet: rulesengine.AddRuleResult{Errors: []rulesengine.Error{ {Banner: "myBanner", Type: rulesengine.UnknownBanner}, {Command: "ls", Type: rulesengine.UnknownCommand}, }}, mockErrRet: nil, expMockCalledCount: 1, expCalledBanner: "myBanner", expCalledRules: rulesengine.WriteRules{ {Command: "ls", Privileges: []string{"read", "write"}}, {Command: "cat", Privileges: []string{"read", "write"}}, }, expCode: http.StatusNotFound, expOut: `{ "errors": [ {"banner": "myBanner", "type":"Unknown Banner"}, {"command":"ls","type":"Unknown Command"} ] }`, }, "Post with Missing Banner": { url: "/admin/rules/banner/commands?bannerName=", reqBody: `[ {"command": "ls", "privileges": ["read","write"]}, {"command": "cat", "privileges": ["read","write"]} ]`, expMockCalledCount: 0, expCode: http.StatusBadRequest, expOut: ``, }, "Post with Missing Query string": { url: "/admin/rules/banner/commands", reqBody: `[ {"command": "ls", "privileges": ["read","write"]}, {"command": "cat", "privileges": ["read","write"]} ]`, expMockCalledCount: 0, expCode: http.StatusBadRequest, expOut: ``, }, } for name, tc := range tests { tc := tc t.Run(name, func(t *testing.T) { t.Parallel() ruleseng := postBannerRulesMock{ dataRet: tc.mockDataRet, errRet: tc.mockErrRet, } log := newLogger() r := httptest.NewRecorder() _, ginEngine := getTestGinContext(r) _, err := New(ginEngine, &ruleseng, log) assert.NoError(t, err) req, err := http.NewRequest(http.MethodPost, tc.url, strings.NewReader(tc.reqBody)) assert.NoError(t, err) ginEngine.ServeHTTP(r, req) assert.Equal(t, tc.expCode, r.Result().StatusCode) assert.Equal(t, tc.expMockCalledCount, ruleseng.callCount) assert.Equal(t, tc.expCalledBanner, ruleseng.bannerName) assert.Equal(t, tc.expCalledRules, ruleseng.rules) if tc.expOut == "" { assert.Empty(t, r.Body.String()) } else { assert.JSONEq(t, tc.expOut, r.Body.String()) } }) } } type getRulesForSpecificCommandForAllBannersMock struct { RulesEngine Out rulesengine.ReadBannerRule Err error commandName string } func (m *getRulesForSpecificCommandForAllBannersMock) ReadBannerRulesForCommand(_ context.Context, commandName string) (rulesengine.ReadBannerRule, error) { m.commandName = commandName return m.Out, m.Err } func TestGetRulesForSpecificCommandForAllBanners(t *testing.T) { t.Parallel() tests := map[string]struct { commandName string rengOut rulesengine.ReadBannerRule rengErr error expStatus int expOut StringAssertionFunc }{ "Standard": { commandName: "ls", rengOut: rulesengine.ReadBannerRule{ Command: rulesengine.Command{Name: "ls", ID: "a"}, Banners: []rulesengine.BannerPrivOverrides{ { Banner: rulesengine.Banner{BannerName: "myBanner", BannerID: "b"}, Privileges: []rulesengine.Privilege{ {Name: "ea-read", ID: "c"}, }, }, }, }, rengErr: nil, expStatus: http.StatusOK, expOut: JSONEq(`{ "command": {"name": "ls", "id": "a"}, "banners": [ { "banner": {"name": "myBanner", "id": "b"}, "privileges": [ {"name":"ea-read","id":"c"} ] } ] }`), }, "Error Return": { commandName: "cat", rengOut: rulesengine.ReadBannerRule{}, rengErr: fmt.Errorf("an error"), expStatus: http.StatusInternalServerError, expOut: JSONEmpty(), }, "Empty Return": { commandName: "cat", rengOut: rulesengine.ReadBannerRule{}, rengErr: nil, expStatus: http.StatusOK, expOut: JSONEq(`null`), }, } for name, tc := range tests { tc := tc t.Run(name, func(t *testing.T) { t.Parallel() log := newLogger() ruleseng := getRulesForSpecificCommandForAllBannersMock{ Out: tc.rengOut, Err: tc.rengErr, } r := httptest.NewRecorder() _, ginEngine := getTestGinContext(r) _, err := New(ginEngine, &ruleseng, log) assert.NoError(t, err) req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/admin/rules/banner/commands/%s", tc.commandName), nil) assert.NoError(t, err) ginEngine.ServeHTTP(r, req) assert.Equal(t, tc.expStatus, r.Result().StatusCode) assert.Equal(t, tc.commandName, ruleseng.commandName) tc.expOut(t, r.Body.String()) }) } } type getRulesForSpecificCommandForBannerMock struct { RulesEngine Out rulesengine.Rule Err error bannerName string commandName string } func (m *getRulesForSpecificCommandForBannerMock) ReadBannerRulesForCommandAndBanner(_ context.Context, bannerName string, commandName string) (rulesengine.Rule, error) { m.bannerName = bannerName m.commandName = commandName return m.Out, m.Err } func TestReadAllRulesInBannerForCommand(t *testing.T) { t.Parallel() tests := map[string]struct { bannerName string commandName string expBannerName StringAssertionFunc expCommandName StringAssertionFunc rengOut rulesengine.Rule rengErr error expStatus int expOut StringAssertionFunc }{ "No Banner": { bannerName: "", commandName: "ls", expBannerName: JSONEmpty(), expCommandName: JSONEmpty(), rengOut: rulesengine.Rule{}, rengErr: nil, expStatus: http.StatusBadRequest, expOut: JSONEmpty(), }, "Standard": { bannerName: "myBanner2", commandName: "ls", rengOut: rulesengine.Rule{ Command: rulesengine.Command{Name: "ls", ID: "a"}, Privileges: []rulesengine.Privilege{ {Name: "ea-read", ID: "c"}, }, }, rengErr: nil, expStatus: http.StatusOK, expOut: JSONEq(`{ "command": {"name": "ls", "id": "a"}, "privileges": [ {"name":"ea-read","id":"c"} ] }`), }, "Error Return": { bannerName: "myBanner", commandName: "cat", rengOut: rulesengine.Rule{}, rengErr: fmt.Errorf("an error"), expStatus: http.StatusInternalServerError, expOut: JSONEmpty(), }, "Empty Return": { bannerName: "myBanner", commandName: "cat", rengOut: rulesengine.Rule{}, rengErr: nil, expStatus: http.StatusOK, expOut: JSONEq(`null`), }, } for name, tc := range tests { tc := tc t.Run(name, func(t *testing.T) { t.Parallel() log := newLogger() ruleseng := getRulesForSpecificCommandForBannerMock{ Out: tc.rengOut, Err: tc.rengErr, } r := httptest.NewRecorder() _, ginEngine := getTestGinContext(r) _, err := New(ginEngine, &ruleseng, log) assert.NoError(t, err) req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/admin/rules/banner/commands/%s?bannerName=%s", tc.commandName, tc.bannerName), nil) assert.NoError(t, err) ginEngine.ServeHTTP(r, req) assert.Equal(t, tc.expStatus, r.Result().StatusCode) if tc.expBannerName != nil { tc.expBannerName(t, ruleseng.bannerName) } else { assert.Equal(t, tc.bannerName, ruleseng.bannerName) } if tc.expCommandName != nil { tc.expCommandName(t, ruleseng.commandName) } else { assert.Equal(t, tc.commandName, ruleseng.commandName) } tc.expOut(t, r.Body.String()) }) } } type deleteBannerMock struct { RulesEngine banner, command, privilege string retDelete rulesengine.DeleteResult retErr error } func (mc *deleteBannerMock) DeletePrivilegeFromBannerRule(_ context.Context, bannerName string, commandName string, privilegeName string) (rulesengine.DeleteResult, error) { mc.banner = bannerName mc.command = commandName mc.privilege = privilegeName return mc.retDelete, mc.retErr } func TestDeletePrivilegeFromBannerRule(t *testing.T) { t.Parallel() tests := map[string]struct { banner string command string privilege string retRes rulesengine.DeleteResult retErr error expStatus int expOut StringAssertionFunc }{ "Delete rule Success": { banner: "myBanner", command: "ls", privilege: "read", retRes: rulesengine.DeleteResult{RowsAffected: 1}, retErr: nil, expStatus: http.StatusOK, expOut: JSONEmpty(), }, "Delete rule No Change With Errors": { banner: "myBanner", command: "ls", privilege: "read", retRes: rulesengine.DeleteResult{RowsAffected: 0, Errors: []rulesengine.Error{{Type: rulesengine.UnknownBanner, Banner: "myBanner"}}}, retErr: nil, expStatus: http.StatusNotFound, expOut: JSONEq(`{"errors":[{"type":"Unknown Banner","banner":"myBanner"}]}`), }, // TODO Is this test required/expected? "Delete rule With Conflicts": { banner: "myBanner", command: "ls", privilege: "read", retRes: rulesengine.DeleteResult{RowsAffected: 0, Errors: []rulesengine.Error{{Type: rulesengine.Conflict, Banner: "myBanner"}}}, retErr: nil, expStatus: http.StatusNotFound, expOut: JSONEq(`{"errors":[{"type":"Conflict","banner":"myBanner"}]}`), }, "Delete rule application error": { banner: "myBanner", command: "ls", privilege: "read", retRes: rulesengine.DeleteResult{RowsAffected: 1}, retErr: fmt.Errorf("an error"), expStatus: http.StatusInternalServerError, expOut: JSONEmpty(), }, } for name, tc := range tests { tc := tc t.Run(name, func(t *testing.T) { t.Parallel() log := newLogger() ruleseng := deleteBannerMock{ retDelete: tc.retRes, retErr: tc.retErr, } r := httptest.NewRecorder() _, ginEngine := getTestGinContext(r) _, err := New(ginEngine, &ruleseng, log) assert.NoError(t, err) req, err := http.NewRequest( http.MethodDelete, "/admin/rules/banner/commands/"+tc.command+"/privileges/"+tc.privilege+"?bannerName="+tc.banner, nil, ) assert.NoError(t, err) ginEngine.ServeHTTP(r, req) assert.Equal(t, tc.expStatus, r.Result().StatusCode) tc.expOut(t, r.Body.String()) assert.Equal(t, tc.banner, ruleseng.banner) assert.Equal(t, tc.command, ruleseng.command) assert.Equal(t, tc.privilege, ruleseng.privilege) }) } }