diff --git a/pkg/services/ssosettings/api/api_test.go b/pkg/services/ssosettings/api/api_test.go index cca1fa9ee63..153d125c6b4 100644 --- a/pkg/services/ssosettings/api/api_test.go +++ b/pkg/services/ssosettings/api/api_test.go @@ -1,3 +1,149 @@ package api -// TODO: add tests when you implement the final version of the API endpoint +import ( + "errors" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/ssosettings" + "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/web/webtest" +) + +func TestSSOSettingsAPI_Delete(t *testing.T) { + type TestCase struct { + desc string + key string + action string + scope string + expectedError error + expectedServiceCall bool + expectedStatusCode int + } + + tests := []TestCase{ + { + desc: "successfully deletes SSO settings", + key: "azuread", + action: "settings:write", + scope: "settings:auth.azuread:*", + expectedError: nil, + expectedServiceCall: true, + expectedStatusCode: http.StatusNoContent, + }, + { + desc: "fails when action doesn't match", + key: "azuread", + action: "settings:read", + scope: "settings:auth.azuread:*", + expectedError: nil, + expectedServiceCall: false, + expectedStatusCode: http.StatusForbidden, + }, + { + desc: "fails when scope doesn't match", + key: "azuread", + action: "settings:write", + scope: "settings:auth.azuread:read", + expectedError: nil, + expectedServiceCall: false, + expectedStatusCode: http.StatusForbidden, + }, + { + desc: "fails when scope contains another provider", + key: "azuread", + action: "settings:write", + scope: "settings:auth.github:*", + expectedError: nil, + expectedServiceCall: false, + expectedStatusCode: http.StatusForbidden, + }, + { + desc: "fails with not found when key is empty", + key: "", + action: "settings:write", + scope: "settings:auth.azuread:*", + expectedError: nil, + expectedServiceCall: false, + expectedStatusCode: http.StatusNotFound, + }, + { + desc: "fails with not found when key was not found", + key: "azuread", + action: "settings:write", + scope: "settings:auth.azuread:*", + expectedError: ssosettings.ErrNotFound, + expectedServiceCall: true, + expectedStatusCode: http.StatusNotFound, + }, + { + desc: "fails with internal server error when service returns an error", + key: "azuread", + action: "settings:write", + scope: "settings:auth.azuread:*", + expectedError: errors.New("something went wrong"), + expectedServiceCall: true, + expectedStatusCode: http.StatusInternalServerError, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + service := ssosettingstests.NewMockService(t) + if tt.expectedServiceCall { + service.On("Delete", mock.Anything, tt.key).Return(tt.expectedError).Once() + } + server := setupTests(t, service) + + path := fmt.Sprintf("/api/v1/sso-settings/%s", tt.key) + req := server.NewRequest(http.MethodDelete, path, nil) + webtest.RequestWithSignedInUser(req, &user.SignedInUser{ + OrgRole: org.RoleEditor, + OrgID: 1, + Permissions: getPermissionsForActionAndScope(tt.action, tt.scope), + }) + res, err := server.SendJSON(req) + require.NoError(t, err) + + require.Equal(t, tt.expectedStatusCode, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + } +} + +func getPermissionsForActionAndScope(action, scope string) map[int64]map[string][]string { + return map[int64]map[string][]string{ + 1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{{ + Action: action, Scope: scope, + }}), + } +} + +func setupTests(t *testing.T, service ssosettings.Service) *webtest.Server { + t.Helper() + + cfg := setting.NewCfg() + logger := log.NewNopLogger() + + api := &Api{ + Log: logger, + RouteRegister: routing.NewRouteRegister(), + AccessControl: acimpl.ProvideAccessControl(cfg), + SSOSettingsService: service, + } + + api.RegisterAPIEndpoints() + + return webtest.NewServer(t, api.RouteRegister) +} diff --git a/pkg/services/ssosettings/ssosettings.go b/pkg/services/ssosettings/ssosettings.go index f7ecc7cbae8..1b6994fc613 100644 --- a/pkg/services/ssosettings/ssosettings.go +++ b/pkg/services/ssosettings/ssosettings.go @@ -16,6 +16,8 @@ var ( ) // Service is a SSO settings service +// +//go:generate mockery --name Service --structname MockService --outpkg ssosettingstests --filename service_mock.go --output ./ssosettingstests/ type Service interface { // List returns all SSO settings from DB and config files List(ctx context.Context, requester identity.Requester) ([]*models.SSOSetting, error) diff --git a/pkg/services/ssosettings/ssosettingstests/service_mock.go b/pkg/services/ssosettings/ssosettingstests/service_mock.go new file mode 100644 index 00000000000..ba4f0328ce4 --- /dev/null +++ b/pkg/services/ssosettings/ssosettingstests/service_mock.go @@ -0,0 +1,137 @@ +// Code generated by mockery v2.37.1. DO NOT EDIT. + +package ssosettingstests + +import ( + context "context" + + identity "github.com/grafana/grafana/pkg/services/auth/identity" + mock "github.com/stretchr/testify/mock" + + models "github.com/grafana/grafana/pkg/services/ssosettings/models" + + ssosettings "github.com/grafana/grafana/pkg/services/ssosettings" +) + +// MockService is an autogenerated mock type for the Service type +type MockService struct { + mock.Mock +} + +// Delete provides a mock function with given fields: ctx, provider +func (_m *MockService) Delete(ctx context.Context, provider string) error { + ret := _m.Called(ctx, provider) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, provider) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetForProvider provides a mock function with given fields: ctx, provider +func (_m *MockService) GetForProvider(ctx context.Context, provider string) (*models.SSOSetting, error) { + ret := _m.Called(ctx, provider) + + var r0 *models.SSOSetting + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*models.SSOSetting, error)); ok { + return rf(ctx, provider) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *models.SSOSetting); ok { + r0 = rf(ctx, provider) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.SSOSetting) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, provider) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ctx, requester +func (_m *MockService) List(ctx context.Context, requester identity.Requester) ([]*models.SSOSetting, error) { + ret := _m.Called(ctx, requester) + + var r0 []*models.SSOSetting + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) ([]*models.SSOSetting, error)); ok { + return rf(ctx, requester) + } + if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) []*models.SSOSetting); ok { + r0 = rf(ctx, requester) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.SSOSetting) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, identity.Requester) error); ok { + r1 = rf(ctx, requester) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Patch provides a mock function with given fields: ctx, provider, data +func (_m *MockService) Patch(ctx context.Context, provider string, data map[string]interface{}) error { + ret := _m.Called(ctx, provider, data) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]interface{}) error); ok { + r0 = rf(ctx, provider, data) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RegisterReloadable provides a mock function with given fields: ctx, provider, reloadable +func (_m *MockService) RegisterReloadable(ctx context.Context, provider string, reloadable ssosettings.Reloadable) { + _m.Called(ctx, provider, reloadable) +} + +// Reload provides a mock function with given fields: ctx, provider +func (_m *MockService) Reload(ctx context.Context, provider string) { + _m.Called(ctx, provider) +} + +// Upsert provides a mock function with given fields: ctx, provider, data +func (_m *MockService) Upsert(ctx context.Context, provider string, data map[string]interface{}) error { + ret := _m.Called(ctx, provider, data) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, map[string]interface{}) error); ok { + r0 = rf(ctx, provider, data) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockService(t interface { + mock.TestingT + Cleanup(func()) +}) *MockService { + mock := &MockService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}