|
|
|
@ -2,297 +2,74 @@ package api |
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
"bytes" |
|
|
|
|
"context" |
|
|
|
|
"encoding/json" |
|
|
|
|
"errors" |
|
|
|
|
"net/http" |
|
|
|
|
"net/http/httptest" |
|
|
|
|
"testing" |
|
|
|
|
"time" |
|
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert" |
|
|
|
|
"github.com/stretchr/testify/mock" |
|
|
|
|
"github.com/stretchr/testify/require" |
|
|
|
|
"gopkg.in/ini.v1" |
|
|
|
|
"gopkg.in/macaron.v1" |
|
|
|
|
|
|
|
|
|
"github.com/grafana/grafana/pkg/api/routing" |
|
|
|
|
"github.com/grafana/grafana/pkg/infra/db" |
|
|
|
|
"github.com/grafana/grafana/pkg/infra/db/dbtest" |
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log" |
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log/logtest" |
|
|
|
|
"github.com/grafana/grafana/pkg/middleware" |
|
|
|
|
"github.com/grafana/grafana/pkg/models" // Alias required due to scimmodels
|
|
|
|
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" |
|
|
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt" |
|
|
|
|
"github.com/grafana/grafana/pkg/services/org" |
|
|
|
|
"github.com/grafana/grafana/pkg/services/scimsettings" |
|
|
|
|
scimmodels "github.com/grafana/grafana/pkg/services/scimsettings/models" |
|
|
|
|
"github.com/grafana/grafana/pkg/services/scimsettings/scimsettingstest" |
|
|
|
|
"github.com/grafana/grafana/pkg/services/user" |
|
|
|
|
"github.com/grafana/grafana/pkg/setting" |
|
|
|
|
"github.com/grafana/grafana/pkg/web" |
|
|
|
|
"github.com/grafana/grafana/pkg/web/webtest" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
const ( |
|
|
|
|
testOrgID = 1 |
|
|
|
|
testUserID = 1 |
|
|
|
|
scimAPIPath = "/api/scim/settings" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
// setupTestServer initializes the API with mocks and sets up a test server.
|
|
|
|
|
func setupTestServer(t *testing.T, cfg *setting.Cfg, features *featuremgmt.FeatureManager) *SCIMSettingsAPI { |
|
|
|
|
func setupSCIMSettingsAPITest(t *testing.T, scimEnabled bool, mockService *scimsettingstest.MockService) *webtest.Server { |
|
|
|
|
t.Helper() |
|
|
|
|
|
|
|
|
|
routeRegister := routing.NewRouteRegister() |
|
|
|
|
// Mock middleware dependencies if needed, here we just use a basic logger
|
|
|
|
|
cfg.Logger = log.NewNopLogger() |
|
|
|
|
middleware.Provide(cfg) // Initialize necessary middleware
|
|
|
|
|
|
|
|
|
|
api := ProvideSCIMSettingsAPI(cfg, routeRegister, features) |
|
|
|
|
api.RegisterAPIEndpoints() |
|
|
|
|
|
|
|
|
|
// We return the API instance directly for handler testing
|
|
|
|
|
// Route registration testing needs the routeRegister
|
|
|
|
|
return api |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// newTestContext creates a new ReqContext for testing handlers.
|
|
|
|
|
func newTestContext(method, path string, body []byte) *contextmodel.ReqContext { |
|
|
|
|
req := httptest.NewRequest(method, path, bytes.NewReader(body)) |
|
|
|
|
req.Header.Set("Content-Type", "application/json") |
|
|
|
|
resp := httptest.NewRecorder() |
|
|
|
|
mac := web.NewMacaron() // Use web.NewMacaron() or appropriate context setup
|
|
|
|
|
// Setup routing context if needed by handlers or middleware
|
|
|
|
|
mac.Use(middleware.ReqSignedIn) // Basic middleware for context setup
|
|
|
|
|
mac.Use(middleware.ReqGrafanaAdmin) // Ensure admin check is part of context setup for testing
|
|
|
|
|
|
|
|
|
|
// Create a base context with necessary values
|
|
|
|
|
baseCtx := &contextmodel.ReqContext{ |
|
|
|
|
Context: context.Background(), |
|
|
|
|
Req: req, |
|
|
|
|
Resp: web.NewResponseWriter(method, resp), |
|
|
|
|
SignedInUser: &contextmodel.SignedInUser{UserID: testUserID, OrgID: testOrgID, IsGrafanaAdmin: true}, // Assume admin for most tests initially
|
|
|
|
|
AllowAnonymous: false, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// If routing parameters are needed, they would be set here via web.Params or context
|
|
|
|
|
// Example: req = req.WithContext(context.WithValue(req.Context(), web.ParamsKey, map[string]string{"id": "1"}))
|
|
|
|
|
|
|
|
|
|
return baseCtx |
|
|
|
|
features := featuremgmt.WithFeatures() |
|
|
|
|
if scimEnabled { |
|
|
|
|
features = featuremgmt.WithFeatures(featuremgmt.FlagEnableSCIM) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestRegisterAPIEndpoints(t *testing.T) { |
|
|
|
|
t.Run("should not register routes if enableSCIM feature toggle is disabled", func(t *testing.T) { |
|
|
|
|
cfg := setting.NewCfg() |
|
|
|
|
features := fakes.NewFakeFeatureManager(map[string]bool{featuremgmt.FlagEnableSCIM: false}) |
|
|
|
|
routeRegister := routing.NewRouteRegister() |
|
|
|
|
middleware.Provide(cfg) |
|
|
|
|
|
|
|
|
|
api := ProvideSCIMSettingsAPI(cfg, routeRegister, features) |
|
|
|
|
api.RegisterAPIEndpoints() |
|
|
|
|
|
|
|
|
|
routes := routeRegister.GetRoutes() |
|
|
|
|
assert.Len(t, routes, 0, "Expected no routes to be registered") |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("should register routes if enableSCIM feature toggle is enabled", func(t *testing.T) { |
|
|
|
|
cfg := setting.NewCfg() |
|
|
|
|
features := fakes.NewFakeFeatureManager(map[string]bool{featuremgmt.FlagEnableSCIM: true}) |
|
|
|
|
routeRegister := routing.NewRouteRegister() |
|
|
|
|
middleware.Provide(cfg) |
|
|
|
|
|
|
|
|
|
api := ProvideSCIMSettingsAPI(cfg, routeRegister, features) |
|
|
|
|
api := ProvideSCIMSettingsAPI(cfg, routeRegister, features.(*featuremgmt.FeatureManager), mockService) |
|
|
|
|
api.RegisterAPIEndpoints() |
|
|
|
|
|
|
|
|
|
routes := routeRegister.GetRoutes() |
|
|
|
|
require.Len(t, routes, 1, "Expected one route group to be registered") |
|
|
|
|
|
|
|
|
|
group := routes[0] |
|
|
|
|
require.Len(t, group.Routes, 2, "Expected two routes within the group") |
|
|
|
|
|
|
|
|
|
getRoute := group.Routes[0] |
|
|
|
|
putRoute := group.Routes[1] |
|
|
|
|
|
|
|
|
|
assert.Equal(t, http.MethodGet, getRoute.Method) |
|
|
|
|
assert.Equal(t, scimAPIPath, getRoute.Pattern) |
|
|
|
|
// Note: Checking exact middleware function pointers can be brittle.
|
|
|
|
|
// We assume ReqGrafanaAdmin is correctly applied if routes are registered.
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, http.MethodPut, putRoute.Method) |
|
|
|
|
assert.Equal(t, scimAPIPath, putRoute.Pattern) |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestGetSCIMSettings(t *testing.T) { |
|
|
|
|
t.Run("when user is Grafana Admin", func(t *testing.T) { |
|
|
|
|
cfg := setting.NewCfg() |
|
|
|
|
features := fakes.NewFakeFeatureManager(map[string]bool{featuremgmt.FlagEnableSCIM: true}) |
|
|
|
|
api := setupTestServer(t, cfg, features) |
|
|
|
|
|
|
|
|
|
t.Run("should return settings from config when section and keys exist", func(t *testing.T) { |
|
|
|
|
// Prepare INI data
|
|
|
|
|
iniData := ` |
|
|
|
|
[auth.scim] |
|
|
|
|
user_sync_enabled = true |
|
|
|
|
group_sync_enabled = true |
|
|
|
|
` |
|
|
|
|
rawIni, err := ini.Load([]byte(iniData)) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
api.cfg.Raw = rawIni |
|
|
|
|
|
|
|
|
|
c := newTestContext(http.MethodGet, scimAPIPath, nil) |
|
|
|
|
c.SignedInUser.IsGrafanaAdmin = true |
|
|
|
|
|
|
|
|
|
resp := api.getSCIMSettings(c) |
|
|
|
|
require.Equal(t, http.StatusOK, resp.Status()) |
|
|
|
|
|
|
|
|
|
var result scimSettingsDTO |
|
|
|
|
require.NoError(t, json.Unmarshal(resp.Body(), &result)) |
|
|
|
|
assert.True(t, result.UserSyncEnabled) |
|
|
|
|
assert.True(t, result.GroupSyncEnabled) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("should return default false when section or keys are missing", func(t *testing.T) { |
|
|
|
|
// Prepare empty INI data
|
|
|
|
|
rawIni, err := ini.Load([]byte("")) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
api.cfg.Raw = rawIni |
|
|
|
|
|
|
|
|
|
c := newTestContext(http.MethodGet, scimAPIPath, nil) |
|
|
|
|
c.SignedInUser.IsGrafanaAdmin = true |
|
|
|
|
|
|
|
|
|
resp := api.getSCIMSettings(c) |
|
|
|
|
require.Equal(t, http.StatusOK, resp.Status()) |
|
|
|
|
|
|
|
|
|
var result scimSettingsDTO |
|
|
|
|
require.NoError(t, json.Unmarshal(resp.Body(), &result)) |
|
|
|
|
assert.False(t, result.UserSyncEnabled) |
|
|
|
|
assert.False(t, result.GroupSyncEnabled) |
|
|
|
|
}) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("when user is not Grafana Admin", func(t *testing.T) { |
|
|
|
|
cfg := setting.NewCfg() |
|
|
|
|
features := fakes.NewFakeFeatureManager(map[string]bool{featuremgmt.FlagEnableSCIM: true}) |
|
|
|
|
api := setupTestServer(t, cfg, features) |
|
|
|
|
|
|
|
|
|
rawIni, err := ini.Load([]byte("")) // Config content doesn't matter here
|
|
|
|
|
require.NoError(t, err) |
|
|
|
|
api.cfg.Raw = rawIni |
|
|
|
|
|
|
|
|
|
c := newTestContext(http.MethodGet, scimAPIPath, nil) |
|
|
|
|
c.SignedInUser.IsGrafanaAdmin = false // Set user as non-admin
|
|
|
|
|
|
|
|
|
|
// We expect the ReqGrafanaAdmin middleware (simulated in newTestContext setup)
|
|
|
|
|
// to handle the authorization before the handler is called.
|
|
|
|
|
// In a real HTTP test, we'd check the status code directly from the response recorder.
|
|
|
|
|
// Since we test the handler directly, we rely on the test setup assuming middleware ran.
|
|
|
|
|
// A more robust test would involve a full HTTP server test.
|
|
|
|
|
// For now, let's just assert that if the handler *was* called (it shouldn't be),
|
|
|
|
|
// it would still work, but the critical part is the middleware check.
|
|
|
|
|
// This test mainly documents the non-admin scenario.
|
|
|
|
|
|
|
|
|
|
// If we were doing a full HTTP test:
|
|
|
|
|
// recorder := httptest.NewRecorder()
|
|
|
|
|
// req := httptest.NewRequest(http.MethodGet, scimAPIPath, nil)
|
|
|
|
|
// // Setup non-admin user in request context/session
|
|
|
|
|
// server.ServeHTTP(recorder, req)
|
|
|
|
|
// assert.Equal(t, http.StatusForbidden, recorder.Code)
|
|
|
|
|
|
|
|
|
|
// Direct handler call test (less ideal for middleware checks):
|
|
|
|
|
resp := api.getSCIMSettings(c) // This wouldn't actually be called if middleware blocks
|
|
|
|
|
assert.Equal(t, http.StatusOK, resp.Status()) // Shows handler logic is ok, but middleware prevents access
|
|
|
|
|
}) |
|
|
|
|
return webtest.NewServer(t, routeRegister) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestUpdateSCIMSettings(t *testing.T) { |
|
|
|
|
t.Run("when user is Grafana Admin", func(t *testing.T) { |
|
|
|
|
cfg := setting.NewCfg() |
|
|
|
|
features := fakes.NewFakeFeatureManager(map[string]bool{featuremgmt.FlagEnableSCIM: true}) |
|
|
|
|
api := setupTestServer(t, cfg, features) |
|
|
|
|
func TestSCIMSettingsAPI_RegisterAPIEndpoints(t *testing.T) { |
|
|
|
|
t.Run("should not register routes if enableSCIM feature toggle is disabled", func(t *testing.T) { |
|
|
|
|
mockService := scimsettingstest.NewMockService(t) |
|
|
|
|
server := setupSCIMSettingsAPITest(t, false, mockService) |
|
|
|
|
|
|
|
|
|
t.Run("should return success message for valid payload", func(t *testing.T) { |
|
|
|
|
payload := scimSettingsDTO{ |
|
|
|
|
UserSyncEnabled: true, |
|
|
|
|
GroupSyncEnabled: false, |
|
|
|
|
} |
|
|
|
|
bodyBytes, err := json.Marshal(payload) |
|
|
|
|
req := server.NewGetRequest(scimAPIPath) |
|
|
|
|
resp, err := server.Send(req) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
|
|
|
|
|
c := newTestContext(http.MethodPut, scimAPIPath, bodyBytes) |
|
|
|
|
c.SignedInUser.IsGrafanaAdmin = true |
|
|
|
|
|
|
|
|
|
resp := api.updateSCIMSettings(c) |
|
|
|
|
require.Equal(t, http.StatusOK, resp.Status()) |
|
|
|
|
|
|
|
|
|
var result map[string]string |
|
|
|
|
require.NoError(t, json.Unmarshal(resp.Body(), &result)) |
|
|
|
|
assert.Contains(t, result["message"], "Grafana restart is required") |
|
|
|
|
|
|
|
|
|
// Optional: Check logs if a mock logger with capture capabilities was used
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("should return bad request for invalid payload", func(t *testing.T) { |
|
|
|
|
invalidBody := []byte(`{invalid json`) // Malformed JSON
|
|
|
|
|
|
|
|
|
|
c := newTestContext(http.MethodPut, scimAPIPath, invalidBody) |
|
|
|
|
c.SignedInUser.IsGrafanaAdmin = true |
|
|
|
|
|
|
|
|
|
resp := api.updateSCIMSettings(c) |
|
|
|
|
require.Equal(t, http.StatusBadRequest, resp.Status()) |
|
|
|
|
|
|
|
|
|
// Check error message if needed
|
|
|
|
|
var result map[string]string |
|
|
|
|
require.NoError(t, json.Unmarshal(resp.Body(), &result)) |
|
|
|
|
assert.Contains(t, result["message"], "Invalid request payload") |
|
|
|
|
}) |
|
|
|
|
require.Equal(t, http.StatusNotFound, resp.StatusCode) |
|
|
|
|
require.NoError(t, resp.Body.Close()) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("when user is not Grafana Admin", func(t *testing.T) { |
|
|
|
|
cfg := setting.NewCfg() |
|
|
|
|
features := fakes.NewFakeFeatureManager(map[string]bool{featuremgmt.FlagEnableSCIM: true}) |
|
|
|
|
api := setupTestServer(t, cfg, features) |
|
|
|
|
t.Run("should register routes if enableSCIM feature toggle is enabled", func(t *testing.T) { |
|
|
|
|
mockService := scimsettingstest.NewMockService(t) |
|
|
|
|
server := setupSCIMSettingsAPITest(t, true, mockService) |
|
|
|
|
|
|
|
|
|
payload := scimSettingsDTO{UserSyncEnabled: true, GroupSyncEnabled: true} |
|
|
|
|
bodyBytes, err := json.Marshal(payload) |
|
|
|
|
// When a route is registered but unauthorized, we get 401 instead of 404
|
|
|
|
|
req := server.NewGetRequest(scimAPIPath) |
|
|
|
|
resp, err := server.Send(req) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
|
|
|
|
|
c := newTestContext(http.MethodPut, scimAPIPath, bodyBytes) |
|
|
|
|
c.SignedInUser.IsGrafanaAdmin = false // Set user as non-admin
|
|
|
|
|
|
|
|
|
|
// Again, expecting middleware to block this.
|
|
|
|
|
// Direct handler call test:
|
|
|
|
|
resp := api.updateSCIMSettings(c) // This wouldn't actually be called
|
|
|
|
|
assert.Equal(t, http.StatusOK, resp.Status()) // Handler logic works, but access denied by middleware
|
|
|
|
|
|
|
|
|
|
// If we were doing a full HTTP test:
|
|
|
|
|
// recorder := httptest.NewRecorder()
|
|
|
|
|
// req := httptest.NewRequest(http.MethodPut, scimAPIPath, bytes.NewReader(bodyBytes))
|
|
|
|
|
// req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
// // Setup non-admin user in request context/session
|
|
|
|
|
// server.ServeHTTP(recorder, req)
|
|
|
|
|
// assert.Equal(t, http.StatusForbidden, recorder.Code)
|
|
|
|
|
require.Equal(t, http.StatusUnauthorized, resp.StatusCode) |
|
|
|
|
require.NoError(t, resp.Body.Close()) |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func setupAPITest(t *testing.T) (*SCIMSettingsAPI, *scimsettings.MockService, *featuremgmt.FeatureManager) { |
|
|
|
|
t.Helper() |
|
|
|
|
|
|
|
|
|
mockService := scimsettings.NewMockService(t) |
|
|
|
|
features := featuremgmt.WithFeatures(featuremgmt.FlagEnableSCIM) // Ensure feature is enabled
|
|
|
|
|
cfg := setting.NewCfg() |
|
|
|
|
cfg.StaticRootPath = "../../public" // Adjust path if needed for renderer
|
|
|
|
|
cfg.Logger = logtest.NewFake(t) // Use a fake logger for API tests if needed
|
|
|
|
|
|
|
|
|
|
api := ProvideSCIMSettingsAPI(cfg, routing.NewRouteRegister(), features, mockService) |
|
|
|
|
|
|
|
|
|
return api, mockService, features |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestSCIMSettingsAPI_GetSettings(t *testing.T) { |
|
|
|
|
api, mockService, features := setupAPITest(t) |
|
|
|
|
|
|
|
|
|
server := setupTestServer(t, api, features, "/api/scim/settings", http.MethodGet, api.getSCIMSettings, middleware.ReqGrafanaAdmin) |
|
|
|
|
|
|
|
|
|
now := time.Now() |
|
|
|
|
expectedSettings := &scimmodels.ScimSettings{ |
|
|
|
|
ID: 1, |
|
|
|
@ -308,92 +85,106 @@ func TestSCIMSettingsAPI_GetSettings(t *testing.T) { |
|
|
|
|
|
|
|
|
|
t.Run("As Grafana Admin", func(t *testing.T) { |
|
|
|
|
t.Run("should return settings when service returns them", func(t *testing.T) { |
|
|
|
|
mockService := scimsettingstest.NewMockService(t) |
|
|
|
|
mockService.On("Get", mock.Anything).Return(expectedSettings, nil).Once() |
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodGet, "/api/scim/settings", nil) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
sc := setupScenarioContext(t, req.URL.String()) |
|
|
|
|
sc.Req = req |
|
|
|
|
sc.IsGrafanaAdmin = true |
|
|
|
|
sc.contextHandler.GetContextProvider().SetContext(sc.Req, sc.Context) |
|
|
|
|
server := setupSCIMSettingsAPITest(t, true, mockService) |
|
|
|
|
req := server.NewGetRequest(scimAPIPath) |
|
|
|
|
|
|
|
|
|
api.getSCIMSettings(sc.Context) |
|
|
|
|
// Create admin user
|
|
|
|
|
req = webtest.RequestWithSignedInUser(req, &user.SignedInUser{ |
|
|
|
|
UserID: 1, |
|
|
|
|
OrgID: 1, |
|
|
|
|
OrgRole: org.RoleAdmin, |
|
|
|
|
IsGrafanaAdmin: true, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
require.Equal(t, http.StatusOK, sc.Resp.Code) |
|
|
|
|
var respDTO scimSettingsDTO |
|
|
|
|
err = json.Unmarshal(sc.Resp.Body.Bytes(), &respDTO) |
|
|
|
|
resp, err := server.Send(req) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
assert.Equal(t, expectedDTO, respDTO) |
|
|
|
|
require.Equal(t, http.StatusOK, resp.StatusCode) |
|
|
|
|
|
|
|
|
|
var result scimSettingsDTO |
|
|
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result)) |
|
|
|
|
require.NoError(t, resp.Body.Close()) |
|
|
|
|
|
|
|
|
|
assert.Equal(t, expectedDTO, result) |
|
|
|
|
mockService.AssertExpectations(t) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("should return 500 if service returns an error", func(t *testing.T) { |
|
|
|
|
mockService := scimsettingstest.NewMockService(t) |
|
|
|
|
testErr := errors.New("database broke") |
|
|
|
|
mockService.On("Get", mock.Anything).Return(nil, testErr).Once() |
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodGet, "/api/scim/settings", nil) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
sc := setupScenarioContext(t, req.URL.String()) |
|
|
|
|
sc.Req = req |
|
|
|
|
sc.IsGrafanaAdmin = true |
|
|
|
|
sc.contextHandler.GetContextProvider().SetContext(sc.Req, sc.Context) |
|
|
|
|
server := setupSCIMSettingsAPITest(t, true, mockService) |
|
|
|
|
req := server.NewGetRequest(scimAPIPath) |
|
|
|
|
|
|
|
|
|
api.getSCIMSettings(sc.Context) |
|
|
|
|
// Create admin user
|
|
|
|
|
req = webtest.RequestWithSignedInUser(req, &user.SignedInUser{ |
|
|
|
|
UserID: 1, |
|
|
|
|
OrgID: 1, |
|
|
|
|
OrgRole: org.RoleAdmin, |
|
|
|
|
IsGrafanaAdmin: true, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
resp, err := server.Send(req) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
require.Equal(t, http.StatusInternalServerError, resp.StatusCode) |
|
|
|
|
require.NoError(t, resp.Body.Close()) |
|
|
|
|
|
|
|
|
|
require.Equal(t, http.StatusInternalServerError, sc.Resp.Code) |
|
|
|
|
// Optionally check response body for error message
|
|
|
|
|
mockService.AssertExpectations(t) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("should return 500 if service returns ErrSettingsNotFound (should be handled by service/default)", func(t *testing.T) { |
|
|
|
|
// Note: Depending on Get implementation, ErrSettingsNotFound might return default values instead of an error.
|
|
|
|
|
// Adjust this test based on the actual service behavior for ErrSettingsNotFound.
|
|
|
|
|
// Assuming Get propagates the error for this test case.
|
|
|
|
|
t.Run("should return 500 if service returns ErrSettingsNotFound", func(t *testing.T) { |
|
|
|
|
mockService := scimsettingstest.NewMockService(t) |
|
|
|
|
mockService.On("Get", mock.Anything).Return(nil, scimsettings.ErrSettingsNotFound).Once() |
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodGet, "/api/scim/settings", nil) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
sc := setupScenarioContext(t, req.URL.String()) |
|
|
|
|
sc.Req = req |
|
|
|
|
sc.IsGrafanaAdmin = true |
|
|
|
|
sc.contextHandler.GetContextProvider().SetContext(sc.Req, sc.Context) |
|
|
|
|
server := setupSCIMSettingsAPITest(t, true, mockService) |
|
|
|
|
req := server.NewGetRequest(scimAPIPath) |
|
|
|
|
|
|
|
|
|
api.getSCIMSettings(sc.Context) |
|
|
|
|
// Create admin user
|
|
|
|
|
req = webtest.RequestWithSignedInUser(req, &user.SignedInUser{ |
|
|
|
|
UserID: 1, |
|
|
|
|
OrgID: 1, |
|
|
|
|
OrgRole: org.RoleAdmin, |
|
|
|
|
IsGrafanaAdmin: true, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
resp, err := server.Send(req) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
require.Equal(t, http.StatusInternalServerError, resp.StatusCode) |
|
|
|
|
require.NoError(t, resp.Body.Close()) |
|
|
|
|
|
|
|
|
|
require.Equal(t, http.StatusInternalServerError, sc.Resp.Code) // Or check for default DTO with 200 OK if defaults are returned
|
|
|
|
|
mockService.AssertExpectations(t) |
|
|
|
|
}) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("As non-Admin", func(t *testing.T) { |
|
|
|
|
// Mock service call to avoid nil pointer if handler is somehow reached
|
|
|
|
|
mockService.On("Get", mock.Anything).Return(expectedSettings, nil).Maybe() |
|
|
|
|
mockService := scimsettingstest.NewMockService(t) |
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodGet, "/api/scim/settings", nil) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
sc := setupScenarioContext(t, req.URL.String()) |
|
|
|
|
sc.Req = req |
|
|
|
|
sc.IsGrafanaAdmin = false // Ensure not admin
|
|
|
|
|
sc.contextHandler.GetContextProvider().SetContext(sc.Req, sc.Context) |
|
|
|
|
server := setupSCIMSettingsAPITest(t, true, mockService) |
|
|
|
|
req := server.NewGetRequest(scimAPIPath) |
|
|
|
|
|
|
|
|
|
server.Mux.ServeHTTP(sc.Resp, sc.Req) |
|
|
|
|
// Create non-admin user
|
|
|
|
|
req = webtest.RequestWithSignedInUser(req, &user.SignedInUser{ |
|
|
|
|
UserID: 1, |
|
|
|
|
OrgID: 1, |
|
|
|
|
OrgRole: org.RoleViewer, |
|
|
|
|
IsGrafanaAdmin: false, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
require.Equal(t, http.StatusForbidden, sc.Resp.Code) |
|
|
|
|
resp, err := server.Send(req) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
require.Equal(t, http.StatusForbidden, resp.StatusCode) |
|
|
|
|
require.NoError(t, resp.Body.Close()) |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestSCIMSettingsAPI_UpdateSettings(t *testing.T) { |
|
|
|
|
api, mockService, features := setupAPITest(t) |
|
|
|
|
|
|
|
|
|
server := setupTestServer(t, api, features, "/api/scim/settings", http.MethodPut, api.updateSCIMSettings, middleware.ReqGrafanaAdmin) |
|
|
|
|
|
|
|
|
|
updateCmd := scimSettingsDTO{ |
|
|
|
|
UserSyncEnabled: true, |
|
|
|
|
GroupSyncEnabled: true, |
|
|
|
|
} |
|
|
|
|
cmdBytes, err := json.Marshal(updateCmd) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
|
|
|
|
|
expectedModel := &scimmodels.ScimSettings{ |
|
|
|
|
UserSyncEnabled: updateCmd.UserSyncEnabled, |
|
|
|
|
GroupSyncEnabled: updateCmd.GroupSyncEnabled, |
|
|
|
@ -401,170 +192,119 @@ func TestSCIMSettingsAPI_UpdateSettings(t *testing.T) { |
|
|
|
|
|
|
|
|
|
t.Run("As Grafana Admin", func(t *testing.T) { |
|
|
|
|
t.Run("should update settings when service succeeds", func(t *testing.T) { |
|
|
|
|
mockService := scimsettingstest.NewMockService(t) |
|
|
|
|
mockService.On("Update", mock.Anything, mock.MatchedBy(func(s *scimmodels.ScimSettings) bool { |
|
|
|
|
return s.UserSyncEnabled == expectedModel.UserSyncEnabled && s.GroupSyncEnabled == expectedModel.GroupSyncEnabled |
|
|
|
|
return s.UserSyncEnabled == expectedModel.UserSyncEnabled && |
|
|
|
|
s.GroupSyncEnabled == expectedModel.GroupSyncEnabled |
|
|
|
|
})).Return(nil).Once() |
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodPut, "/api/scim/settings", bytes.NewReader(cmdBytes)) |
|
|
|
|
server := setupSCIMSettingsAPITest(t, true, mockService) |
|
|
|
|
|
|
|
|
|
bodyBytes, err := json.Marshal(updateCmd) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
|
|
|
|
|
req := server.NewRequest(http.MethodPut, scimAPIPath, bytes.NewReader(bodyBytes)) |
|
|
|
|
req.Header.Set("Content-Type", "application/json") |
|
|
|
|
sc := setupScenarioContext(t, req.URL.String()) |
|
|
|
|
sc.Req = req |
|
|
|
|
sc.IsGrafanaAdmin = true |
|
|
|
|
sc.contextHandler.GetContextProvider().SetContext(sc.Req, sc.Context) |
|
|
|
|
|
|
|
|
|
api.updateSCIMSettings(sc.Context) |
|
|
|
|
// Create admin user
|
|
|
|
|
req = webtest.RequestWithSignedInUser(req, &user.SignedInUser{ |
|
|
|
|
UserID: 1, |
|
|
|
|
OrgID: 1, |
|
|
|
|
OrgRole: org.RoleAdmin, |
|
|
|
|
IsGrafanaAdmin: true, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
require.Equal(t, http.StatusOK, sc.Resp.Code) |
|
|
|
|
var resp map[string]string |
|
|
|
|
err = json.Unmarshal(sc.Resp.Body.Bytes(), &resp) |
|
|
|
|
resp, err := server.Send(req) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
assert.Equal(t, "SCIM settings updated successfully.", resp["message"]) |
|
|
|
|
require.Equal(t, http.StatusOK, resp.StatusCode) |
|
|
|
|
|
|
|
|
|
var result map[string]string |
|
|
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result)) |
|
|
|
|
require.NoError(t, resp.Body.Close()) |
|
|
|
|
|
|
|
|
|
assert.Equal(t, "SCIM settings updated successfully.", result["message"]) |
|
|
|
|
mockService.AssertExpectations(t) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("should return 400 if request body is invalid", func(t *testing.T) { |
|
|
|
|
req, err := http.NewRequest(http.MethodPut, "/api/scim/settings", bytes.NewReader([]byte("{invalid json"))) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
mockService := scimsettingstest.NewMockService(t) |
|
|
|
|
// No expectations since we shouldn't call the service
|
|
|
|
|
|
|
|
|
|
server := setupSCIMSettingsAPITest(t, true, mockService) |
|
|
|
|
|
|
|
|
|
req := server.NewRequest(http.MethodPut, scimAPIPath, bytes.NewReader([]byte("{invalid json"))) |
|
|
|
|
req.Header.Set("Content-Type", "application/json") |
|
|
|
|
sc := setupScenarioContext(t, req.URL.String()) |
|
|
|
|
sc.Req = req |
|
|
|
|
sc.IsGrafanaAdmin = true |
|
|
|
|
sc.contextHandler.GetContextProvider().SetContext(sc.Req, sc.Context) |
|
|
|
|
|
|
|
|
|
api.updateSCIMSettings(sc.Context) |
|
|
|
|
// Create admin user
|
|
|
|
|
req = webtest.RequestWithSignedInUser(req, &user.SignedInUser{ |
|
|
|
|
UserID: 1, |
|
|
|
|
OrgID: 1, |
|
|
|
|
OrgRole: org.RoleAdmin, |
|
|
|
|
IsGrafanaAdmin: true, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
resp, err := server.Send(req) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
require.Equal(t, http.StatusBadRequest, resp.StatusCode) |
|
|
|
|
require.NoError(t, resp.Body.Close()) |
|
|
|
|
|
|
|
|
|
require.Equal(t, http.StatusBadRequest, sc.Resp.Code) |
|
|
|
|
// No calls expected to the service
|
|
|
|
|
mockService.AssertNotCalled(t, "Update", mock.Anything, mock.Anything) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("should return 500 if service update fails", func(t *testing.T) { |
|
|
|
|
mockService := scimsettingstest.NewMockService(t) |
|
|
|
|
testErr := errors.New("database broke on update") |
|
|
|
|
mockService.On("Update", mock.Anything, mock.Anything).Return(testErr).Once() |
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodPut, "/api/scim/settings", bytes.NewReader(cmdBytes)) |
|
|
|
|
server := setupSCIMSettingsAPITest(t, true, mockService) |
|
|
|
|
|
|
|
|
|
bodyBytes, err := json.Marshal(updateCmd) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
req.Header.Set("Content-Type", "application/json") |
|
|
|
|
sc := setupScenarioContext(t, req.URL.String()) |
|
|
|
|
sc.Req = req |
|
|
|
|
sc.IsGrafanaAdmin = true |
|
|
|
|
sc.contextHandler.GetContextProvider().SetContext(sc.Req, sc.Context) |
|
|
|
|
|
|
|
|
|
api.updateSCIMSettings(sc.Context) |
|
|
|
|
req := server.NewRequest(http.MethodPut, scimAPIPath, bytes.NewReader(bodyBytes)) |
|
|
|
|
req.Header.Set("Content-Type", "application/json") |
|
|
|
|
|
|
|
|
|
require.Equal(t, http.StatusInternalServerError, sc.Resp.Code) |
|
|
|
|
mockService.AssertExpectations(t) |
|
|
|
|
}) |
|
|
|
|
// Create admin user
|
|
|
|
|
req = webtest.RequestWithSignedInUser(req, &user.SignedInUser{ |
|
|
|
|
UserID: 1, |
|
|
|
|
OrgID: 1, |
|
|
|
|
OrgRole: org.RoleAdmin, |
|
|
|
|
IsGrafanaAdmin: true, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
t.Run("As non-Admin", func(t *testing.T) { |
|
|
|
|
// Mock service call to avoid nil pointer if handler is somehow reached
|
|
|
|
|
mockService.On("Update", mock.Anything, mock.Anything).Return(nil).Maybe() |
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodPut, "/api/scim/settings", bytes.NewReader(cmdBytes)) |
|
|
|
|
resp, err := server.Send(req) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
req.Header.Set("Content-Type", "application/json") |
|
|
|
|
sc := setupScenarioContext(t, req.URL.String()) |
|
|
|
|
sc.Req = req |
|
|
|
|
sc.IsGrafanaAdmin = false // Ensure not admin
|
|
|
|
|
sc.contextHandler.GetContextProvider().SetContext(sc.Req, sc.Context) |
|
|
|
|
require.Equal(t, http.StatusInternalServerError, resp.StatusCode) |
|
|
|
|
require.NoError(t, resp.Body.Close()) |
|
|
|
|
|
|
|
|
|
server.Mux.ServeHTTP(sc.Resp, sc.Req) |
|
|
|
|
|
|
|
|
|
require.Equal(t, http.StatusForbidden, sc.Resp.Code) |
|
|
|
|
// No calls expected to the service
|
|
|
|
|
mockService.AssertNotCalled(t, "Update", mock.Anything, mock.Anything) |
|
|
|
|
mockService.AssertExpectations(t) |
|
|
|
|
}) |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// --- Helper Functions (Potentially move to a shared test util package) ---
|
|
|
|
|
t.Run("As non-Admin", func(t *testing.T) { |
|
|
|
|
mockService := scimsettingstest.NewMockService(t) |
|
|
|
|
|
|
|
|
|
// Minimal server setup for handler tests
|
|
|
|
|
type testServer struct { |
|
|
|
|
*httptest.Server |
|
|
|
|
RouteRegister routing.RouteRegister |
|
|
|
|
Mux *macaron.Macaron |
|
|
|
|
} |
|
|
|
|
server := setupSCIMSettingsAPITest(t, true, mockService) |
|
|
|
|
|
|
|
|
|
func setupTestServer(t *testing.T, api *SCIMSettingsAPI, features *featuremgmt.FeatureManager, route string, method string, handler interface{}, middlewares ...web.Handler) *testServer { |
|
|
|
|
t.Helper() |
|
|
|
|
m := macaron.New() |
|
|
|
|
// Use the context handler from the actual API package
|
|
|
|
|
contextHandler := ProvideContextHandler() |
|
|
|
|
m.Use(contextHandler.Middleware) |
|
|
|
|
|
|
|
|
|
rr := routing.NewRouteRegister() |
|
|
|
|
// Need to re-assign routeRegister and features to the api instance used in tests
|
|
|
|
|
api.routeRegister = rr |
|
|
|
|
api.features = features |
|
|
|
|
|
|
|
|
|
// Mimic RegisterAPIEndpoints structure
|
|
|
|
|
api.RegisterAPIEndpoints() // Call the actual registration logic
|
|
|
|
|
|
|
|
|
|
// Setup renderer
|
|
|
|
|
m.Use(web.Renderer(macaron.RenderOptions{Directory: api.cfg.StaticRootPath})) |
|
|
|
|
rr.Register(m) // Register routes collected by api.RegisterAPIEndpoints()
|
|
|
|
|
|
|
|
|
|
ts := httptest.NewServer(m) |
|
|
|
|
t.Cleanup(ts.Close) |
|
|
|
|
|
|
|
|
|
return &testServer{ |
|
|
|
|
Server: ts, |
|
|
|
|
RouteRegister: rr, |
|
|
|
|
Mux: m, |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
bodyBytes, err := json.Marshal(updateCmd) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
|
|
|
|
|
// setupScenarioContext creates a context for handler tests
|
|
|
|
|
func setupScenarioContext(t *testing.T, url string) *scenarioContext { |
|
|
|
|
t.Helper() |
|
|
|
|
contextHandler := ProvideContextHandler() |
|
|
|
|
sc := &scenarioContext{ |
|
|
|
|
t: t, |
|
|
|
|
url: url, |
|
|
|
|
Resp: httptest.NewRecorder(), |
|
|
|
|
Context: contextmodel.NewReqContext(nil), // Will be populated by middleware
|
|
|
|
|
unitOfWork: &dbtest.FakeUnitOfWork{}, // Fake UoW if needed
|
|
|
|
|
contextHandler: contextHandler, // Store the handler
|
|
|
|
|
} |
|
|
|
|
req := server.NewRequest(http.MethodPut, scimAPIPath, bytes.NewReader(bodyBytes)) |
|
|
|
|
req.Header.Set("Content-Type", "application/json") |
|
|
|
|
|
|
|
|
|
// Set default user/org info
|
|
|
|
|
sc.SignedInUser = &user.SignedInUser{ |
|
|
|
|
// Create non-admin user
|
|
|
|
|
req = webtest.RequestWithSignedInUser(req, &user.SignedInUser{ |
|
|
|
|
UserID: 1, |
|
|
|
|
OrgID: 1, |
|
|
|
|
OrgRole: models.ROLE_VIEWER, // Default role
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Apply IsGrafanaAdmin to the SignedInUser struct
|
|
|
|
|
sc.SignedInUser.IsGrafanaAdmin = sc.IsGrafanaAdmin |
|
|
|
|
if sc.IsGrafanaAdmin { |
|
|
|
|
sc.SignedInUser.OrgRole = models.ROLE_ADMIN |
|
|
|
|
} |
|
|
|
|
OrgRole: org.RoleViewer, |
|
|
|
|
IsGrafanaAdmin: false, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// We need to associate the base request with the context handler
|
|
|
|
|
// The actual macaron context gets created within the middleware chain
|
|
|
|
|
baseReq, _ := http.NewRequest("GET", url, nil) // Method doesn't matter much here
|
|
|
|
|
sc.Req = baseReq.WithContext(context.WithValue(baseReq.Context(), contextmodel.ReqContextKey{}, sc.Context)) |
|
|
|
|
sc.Context.Req = sc.Req |
|
|
|
|
sc.Context.Logger = log.NewNopLogger() |
|
|
|
|
sc.Context.SignedInUser = sc.SignedInUser |
|
|
|
|
sc.Context.OrgID = sc.SignedInUser.OrgID |
|
|
|
|
sc.Context.IsSignedIn = true |
|
|
|
|
sc.Context.IsGrafanaAdmin = sc.IsGrafanaAdmin |
|
|
|
|
|
|
|
|
|
return sc |
|
|
|
|
} |
|
|
|
|
resp, err := server.Send(req) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
require.Equal(t, http.StatusForbidden, resp.StatusCode) |
|
|
|
|
require.NoError(t, resp.Body.Close()) |
|
|
|
|
|
|
|
|
|
// scenarioContext holds state for a single API scenario test
|
|
|
|
|
type scenarioContext struct { |
|
|
|
|
t *testing.T |
|
|
|
|
Req *http.Request |
|
|
|
|
Resp *httptest.ResponseRecorder |
|
|
|
|
url string |
|
|
|
|
Context *contextmodel.ReqContext |
|
|
|
|
SignedInUser *user.SignedInUser |
|
|
|
|
IsGrafanaAdmin bool // Control admin status easily
|
|
|
|
|
unitOfWork db.UnitOfWork |
|
|
|
|
contextHandler contextmodel.ContextHandler // Store the context handler
|
|
|
|
|
mockService.AssertNotCalled(t, "Update", mock.Anything, mock.Anything) |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|