Feature Toggles: Create API for updating feature toggle state from the feature toggle admin page (#73022)

* create roles for writing feature toggles

* create update endpoint / handler

* api changes

* add feature toggle validations

* hide toggles based on their state

* make FlagFeatureToggle read only

* add username log

* add username string

* refactor for better readability

* refactor unit tests so we can do more validations

* some skeletoning for the set tests

* write unit tests for updater

* break helper functions out

* update sample ini to match defaults

* add more logic to ReadOnly label

* add user documentation

* fix lint issue

* Update docs/sources/setup-grafana/configure-grafana/_index.md

Co-authored-by: J Stickler <julie.stickler@grafana.com>

* Update docs/sources/setup-grafana/configure-grafana/_index.md

Co-authored-by: J Stickler <julie.stickler@grafana.com>

* Update docs/sources/setup-grafana/configure-grafana/_index.md

Co-authored-by: J Stickler <julie.stickler@grafana.com>

* Update docs/sources/setup-grafana/configure-grafana/_index.md

Co-authored-by: J Stickler <julie.stickler@grafana.com>

* Update docs/sources/setup-grafana/configure-grafana/_index.md

Co-authored-by: J Stickler <julie.stickler@grafana.com>

* Update docs/sources/setup-grafana/configure-grafana/_index.md

Co-authored-by: J Stickler <julie.stickler@grafana.com>

---------

Co-authored-by: IbrahimCSAE <ibrahim.mdev@gmail.com>
Co-authored-by: J Stickler <julie.stickler@grafana.com>
This commit is contained in:
Michael Mandrus
2023-08-09 11:32:28 -04:00
committed by GitHub
parent d9695eb507
commit 779e0fe311
11 changed files with 629 additions and 92 deletions
+14 -1
View File
@@ -434,6 +434,19 @@ func (hs *HTTPServer) declareFixedRoles() error {
Grants: []string{"Admin"},
}
featuremgmtWriterRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Name: "fixed:featuremgmt:writer",
DisplayName: "Feature Management writer",
Description: "Write feature toggles",
Group: "Feature Management",
Permissions: []ac.Permission{
{Action: ac.ActionFeatureManagementWrite},
},
},
Grants: []string{"Admin"},
}
return hs.accesscontrolService.DeclareFixedRoles(
provisioningWriterRole, datasourcesReaderRole, builtInDatasourceReader, datasourcesWriterRole,
datasourcesIdReaderRole, orgReaderRole, orgWriterRole,
@@ -441,7 +454,7 @@ func (hs *HTTPServer) declareFixedRoles() error {
annotationsReaderRole, dashboardAnnotationsWriterRole, annotationsWriterRole,
dashboardsCreatorRole, dashboardsReaderRole, dashboardsWriterRole,
foldersCreatorRole, foldersReaderRole, foldersWriterRole, apikeyReaderRole, apikeyWriterRole,
publicDashboardsWriterRole, featuremgmtReaderRole,
publicDashboardsWriterRole, featuremgmtReaderRole, featuremgmtWriterRole,
)
}
+1
View File
@@ -430,6 +430,7 @@ func (hs *HTTPServer) registerRoutes() {
if hs.Features.IsEnabled(featuremgmt.FlagFeatureToggleAdminPage) {
apiRoute.Group("/featuremgmt", func(featuremgmtRoute routing.RouteRegister) {
featuremgmtRoute.Get("/", authorize(ac.EvalPermission(ac.ActionFeatureManagementRead)), hs.GetFeatureToggles)
featuremgmtRoute.Post("/", authorize(ac.EvalPermission(ac.ActionFeatureManagementWrite)), hs.UpdateFeatureToggle)
})
}
+77 -12
View File
@@ -1,30 +1,95 @@
package api
import (
"fmt"
"net/http"
"github.com/grafana/grafana/pkg/api/response"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
func (hs *HTTPServer) GetFeatureToggles(ctx *contextmodel.ReqContext) response.Response {
featureMgmtCfg := hs.Cfg.FeatureManagement
features := hs.Features.GetFlags()
cfg := hs.Cfg.FeatureManagement
enabledFeatures := hs.Features.GetEnabled(ctx.Req.Context())
for i := 0; i < len(features); {
ft := features[i]
if _, ok := featureMgmtCfg.HiddenToggles[ft.Name]; ok {
features = append(features[:i], features[i+1:]...) // remove feature
// object being returned
dtos := make([]featuremgmt.FeatureToggleDTO, 0)
// loop through features an add features that should be visible to dtos
for _, ft := range hs.Features.GetFlags() {
if isFeatureHidden(ft, cfg.HiddenToggles) {
continue
}
if _, ok := featureMgmtCfg.ReadOnlyToggles[ft.Name]; ok {
features[i].ReadOnly = true
dto := featuremgmt.FeatureToggleDTO{
Name: ft.Name,
Description: ft.Description,
Enabled: enabledFeatures[ft.Name],
ReadOnly: !isFeatureWriteable(ft, cfg.ReadOnlyToggles) || !isFeatureEditingAllowed(*hs.Cfg),
}
features[i].Enabled = enabledFeatures[ft.Name]
i++
dtos = append(dtos, dto)
}
return response.JSON(http.StatusOK, features)
return response.JSON(http.StatusOK, dtos)
}
func (hs *HTTPServer) UpdateFeatureToggle(ctx *contextmodel.ReqContext) response.Response {
featureMgmtCfg := hs.Cfg.FeatureManagement
if !featureMgmtCfg.AllowEditing {
return response.Error(http.StatusForbidden, "feature toggles are read-only", fmt.Errorf("feature toggles are configured to be read-only"))
}
if featureMgmtCfg.UpdateControllerUrl == "" {
return response.Error(http.StatusInternalServerError, "feature toggles service is misconfigured", fmt.Errorf("[feature_management]update_controller_url is not set"))
}
cmd := featuremgmt.UpdateFeatureTogglesCommand{}
if err := web.Bind(ctx.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
for _, t := range cmd.FeatureToggles {
// make sure flag exists, and only continue if flag is writeable
if f, ok := hs.Features.LookupFlag(t.Name); ok && isFeatureWriteable(f, hs.Cfg.FeatureManagement.ReadOnlyToggles) {
hs.log.Info("UpdateFeatureToggle: updating toggle", "toggle_name", t.Name, "enabled", t.Enabled, "username", ctx.SignedInUser.Login)
// TODO build payload
} else {
hs.log.Warn("UpdateFeatureToggle: invalid toggle passed in", "toggle_name", t.Name)
return response.Error(http.StatusBadRequest, "invalid toggle passed in", fmt.Errorf("invalid toggle passed in: %s", t.Name))
}
}
// TODO: post to featureMgmtCfg.UpdateControllerUrl and return response status
hs.log.Warn("UpdateFeatureToggle: function is unimplemented")
return response.Error(http.StatusNotImplemented, "UpdateFeatureToggle is unimplemented", fmt.Errorf("UpdateFeatureToggle is unimplemented"))
}
// isFeatureHidden returns whether a toggle should be hidden from the admin page.
// filters out statuses Unknown, Experimental, and Private Preview
func isFeatureHidden(flag featuremgmt.FeatureFlag, hideCfg map[string]struct{}) bool {
if _, ok := hideCfg[flag.Name]; ok {
return true
}
return flag.Stage == featuremgmt.FeatureStageUnknown || flag.Stage == featuremgmt.FeatureStageExperimental || flag.Stage == featuremgmt.FeatureStagePrivatePreview
}
// isFeatureWriteable returns whether a toggle on the admin page can be updated by the user.
// only allows writing of GA and Deprecated toggles, and excludes the feature toggle admin page toggle
func isFeatureWriteable(flag featuremgmt.FeatureFlag, readOnlyCfg map[string]struct{}) bool {
if _, ok := readOnlyCfg[flag.Name]; ok {
return false
}
if flag.Name == featuremgmt.FlagFeatureToggleAdminPage {
return false
}
return flag.Stage == featuremgmt.FeatureStageGeneralAvailability || flag.Stage == featuremgmt.FeatureStageDeprecated
}
// isFeatureEditingAllowed checks if the backend is properly configured to allow feature toggle changes from the UI
func isFeatureEditingAllowed(cfg setting.Cfg) bool {
return cfg.FeatureManagement.AllowEditing && cfg.FeatureManagement.UpdateControllerUrl != ""
}
+441 -72
View File
@@ -1,11 +1,14 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"testing"
"net/http"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org/orgtest"
@@ -18,80 +21,446 @@ import (
)
func TestGetFeatureToggles(t *testing.T) {
type testCase struct {
desc string
permissions []accesscontrol.Permission
features []interface{}
expectedCode int
hiddenTogles map[string]struct{}
readOnlyToggles map[string]struct{}
}
readPermissions := []accesscontrol.Permission{{Action: accesscontrol.ActionFeatureManagementRead}}
tests := []testCase{
{
desc: "should not be able to get feature toggles without permissions",
permissions: []accesscontrol.Permission{},
features: []interface{}{},
expectedCode: http.StatusForbidden,
},
{
desc: "should be able to get feature toggles with correct permissions",
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionFeatureManagementRead}},
features: []interface{}{"toggle1", true, "toggle2", false},
expectedCode: http.StatusOK,
},
{
desc: "hidden toggles are not present in the response",
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionFeatureManagementRead}},
features: []interface{}{"toggle1", true, "toggle2", false},
expectedCode: http.StatusOK,
hiddenTogles: map[string]struct{}{"toggle1": {}},
},
{
desc: "read only toggles have the readOnly field set",
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionFeatureManagementRead}},
features: []interface{}{"toggle1", true, "toggle2", false},
expectedCode: http.StatusOK,
hiddenTogles: map[string]struct{}{"toggle1": {}},
readOnlyToggles: map[string]struct{}{"toggle2": {}},
},
}
t.Run("should not be able to get feature toggles without permissions", func(t *testing.T) {
result := runGetScenario(t, []*featuremgmt.FeatureFlag{}, setting.FeatureMgmtSettings{}, []accesscontrol.Permission{}, http.StatusForbidden)
assert.Len(t, result, 0)
})
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.FeatureManagement.HiddenToggles = tt.hiddenTogles
cfg.FeatureManagement.ReadOnlyToggles = tt.readOnlyToggles
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = cfg
hs.Features = featuremgmt.WithFeatures(append([]interface{}{"featureToggleAdminPage", true}, tt.features...)...)
hs.orgService = orgtest.NewOrgServiceFake()
hs.userService = &usertest.FakeUserService{
ExpectedUser: &user.User{ID: 1},
}
})
t.Run("should be able to get feature toggles with correct permissions", func(t *testing.T) {
features := []*featuremgmt.FeatureFlag{
{
Name: "toggle1",
Enabled: true,
Stage: featuremgmt.FeatureStageGeneralAvailability,
}, {
Name: "toggle2",
Enabled: false,
Stage: featuremgmt.FeatureStageGeneralAvailability,
},
}
req := webtest.RequestWithSignedInUser(server.NewGetRequest("/api/featuremgmt"), userWithPermissions(1, tt.permissions))
res, err := server.SendJSON(req)
require.NoError(t, err)
defer func() { require.NoError(t, res.Body.Close()) }()
assert.Equal(t, tt.expectedCode, res.StatusCode)
result := runGetScenario(t, features, setting.FeatureMgmtSettings{}, readPermissions, http.StatusOK)
assert.Len(t, result, 2)
t1, _ := findResult(t, result, "toggle1")
assert.True(t, t1.Enabled)
t2, _ := findResult(t, result, "toggle2")
assert.False(t, t2.Enabled)
})
if tt.expectedCode == http.StatusOK {
var result []featuremgmt.FeatureFlag
err := json.NewDecoder(res.Body).Decode(&result)
require.NoError(t, err)
t.Run("toggles hidden by config are not present in the response", func(t *testing.T) {
features := []*featuremgmt.FeatureFlag{
{
Name: "toggle1",
Enabled: true,
Stage: featuremgmt.FeatureStageGeneralAvailability,
}, {
Name: "toggle2",
Enabled: false,
Stage: featuremgmt.FeatureStageGeneralAvailability,
},
}
settings := setting.FeatureMgmtSettings{
HiddenToggles: map[string]struct{}{"toggle1": {}},
}
for _, ft := range result {
if _, ok := tt.hiddenTogles[ft.Name]; ok {
t.Fail()
}
if _, ok := tt.readOnlyToggles[ft.Name]; ok {
assert.True(t, ft.ReadOnly, tt.desc)
}
}
assert.Equal(t, 3-len(tt.hiddenTogles), len(result), tt.desc)
}
result := runGetScenario(t, features, settings, readPermissions, http.StatusOK)
assert.Len(t, result, 1)
assert.Equal(t, "toggle2", result[0].Name)
})
t.Run("toggles that are read-only by config have the readOnly field set", func(t *testing.T) {
features := []*featuremgmt.FeatureFlag{
{
Name: "toggle1",
Enabled: true,
Stage: featuremgmt.FeatureStageGeneralAvailability,
}, {
Name: "toggle2",
Enabled: false,
Stage: featuremgmt.FeatureStageGeneralAvailability,
},
}
settings := setting.FeatureMgmtSettings{
HiddenToggles: map[string]struct{}{"toggle1": {}},
ReadOnlyToggles: map[string]struct{}{"toggle2": {}},
AllowEditing: true,
UpdateControllerUrl: "bogus",
}
result := runGetScenario(t, features, settings, readPermissions, http.StatusOK)
assert.Len(t, result, 1)
assert.Equal(t, "toggle2", result[0].Name)
assert.True(t, result[0].ReadOnly)
})
t.Run("feature toggle defailts", func(t *testing.T) {
features := []*featuremgmt.FeatureFlag{
{
Name: "toggle1",
Stage: featuremgmt.FeatureStageUnknown,
}, {
Name: "toggle2",
Stage: featuremgmt.FeatureStageExperimental,
}, {
Name: "toggle3",
Stage: featuremgmt.FeatureStagePrivatePreview,
}, {
Name: "toggle4",
Stage: featuremgmt.FeatureStagePublicPreview,
}, {
Name: "toggle5",
Stage: featuremgmt.FeatureStageGeneralAvailability,
}, {
Name: "toggle6",
Stage: featuremgmt.FeatureStageDeprecated,
},
}
t.Run("unknown, experimental, and private preview toggles are hidden by default", func(t *testing.T) {
result := runGetScenario(t, features, setting.FeatureMgmtSettings{}, readPermissions, http.StatusOK)
assert.Len(t, result, 3)
_, ok := findResult(t, result, "toggle1")
assert.False(t, ok)
_, ok = findResult(t, result, "toggle2")
assert.False(t, ok)
_, ok = findResult(t, result, "toggle3")
assert.False(t, ok)
})
}
t.Run("only public preview and GA are writeable by default", func(t *testing.T) {
settings := setting.FeatureMgmtSettings{
AllowEditing: true,
UpdateControllerUrl: "bogus",
}
result := runGetScenario(t, features, settings, readPermissions, http.StatusOK)
assert.Len(t, result, 3)
t4, ok := findResult(t, result, "toggle4")
assert.True(t, ok)
assert.True(t, t4.ReadOnly)
t5, ok := findResult(t, result, "toggle5")
assert.True(t, ok)
assert.False(t, t5.ReadOnly)
t6, ok := findResult(t, result, "toggle6")
assert.True(t, ok)
assert.False(t, t6.ReadOnly)
})
t.Run("all toggles are read-only when server is misconfigured", func(t *testing.T) {
settings := setting.FeatureMgmtSettings{
AllowEditing: false,
UpdateControllerUrl: "",
}
result := runGetScenario(t, features, settings, readPermissions, http.StatusOK)
assert.Len(t, result, 3)
t4, ok := findResult(t, result, "toggle4")
assert.True(t, ok)
assert.True(t, t4.ReadOnly)
t5, ok := findResult(t, result, "toggle5")
assert.True(t, ok)
assert.True(t, t5.ReadOnly)
t6, ok := findResult(t, result, "toggle6")
assert.True(t, ok)
assert.True(t, t6.ReadOnly)
})
})
}
func TestSetFeatureToggles(t *testing.T) {
writePermissions := []accesscontrol.Permission{{Action: accesscontrol.ActionFeatureManagementWrite}}
t.Run("fails without adequate permissions", func(t *testing.T) {
res := runSetScenario(t, nil, nil, setting.FeatureMgmtSettings{}, []accesscontrol.Permission{}, http.StatusForbidden)
defer func() { require.NoError(t, res.Body.Close()) }()
})
t.Run("fails when toggle editing is not enabled", func(t *testing.T) {
res := runSetScenario(t, nil, nil, setting.FeatureMgmtSettings{}, writePermissions, http.StatusForbidden)
defer func() { require.NoError(t, res.Body.Close()) }()
p := readBody(t, res.Body)
assert.Equal(t, "feature toggles are read-only", p["message"])
})
t.Run("fails when update toggle url is not set", func(t *testing.T) {
s := setting.FeatureMgmtSettings{
AllowEditing: true,
}
res := runSetScenario(t, nil, nil, s, writePermissions, http.StatusInternalServerError)
defer func() { require.NoError(t, res.Body.Close()) }()
p := readBody(t, res.Body)
assert.Equal(t, "feature toggles service is misconfigured", p["message"])
})
t.Run("fails with non-existent toggle", func(t *testing.T) {
features := []*featuremgmt.FeatureFlag{
{
Name: "toggle1",
Enabled: true,
Stage: featuremgmt.FeatureStageGeneralAvailability,
}, {
Name: "toggle2",
Enabled: false,
Stage: featuremgmt.FeatureStageGeneralAvailability,
},
}
updates := []featuremgmt.FeatureToggleDTO{
{
Name: "toggle3",
Enabled: true,
},
}
s := setting.FeatureMgmtSettings{
AllowEditing: true,
UpdateControllerUrl: "random",
}
res := runSetScenario(t, features, updates, s, writePermissions, http.StatusBadRequest)
defer func() { require.NoError(t, res.Body.Close()) }()
p := readBody(t, res.Body)
assert.Equal(t, "invalid toggle passed in", p["message"])
})
t.Run("fails with read-only toggles", func(t *testing.T) {
features := []*featuremgmt.FeatureFlag{
{
Name: featuremgmt.FlagFeatureToggleAdminPage,
Enabled: true,
Stage: featuremgmt.FeatureStageGeneralAvailability,
}, {
Name: "toggle2",
Enabled: false,
Stage: featuremgmt.FeatureStagePublicPreview,
}, {
Name: "toggle3",
Enabled: false,
Stage: featuremgmt.FeatureStageGeneralAvailability,
},
}
s := setting.FeatureMgmtSettings{
AllowEditing: true,
UpdateControllerUrl: "random",
ReadOnlyToggles: map[string]struct{}{
"toggle3": {},
},
}
t.Run("because it is the feature toggle admin page toggle", func(t *testing.T) {
updates := []featuremgmt.FeatureToggleDTO{
{
Name: featuremgmt.FlagFeatureToggleAdminPage,
Enabled: true,
},
}
res := runSetScenario(t, features, updates, s, writePermissions, http.StatusBadRequest)
defer func() { require.NoError(t, res.Body.Close()) }()
p := readBody(t, res.Body)
assert.Equal(t, fmt.Sprintf("invalid toggle passed in: %s", featuremgmt.FlagFeatureToggleAdminPage), p["error"])
})
t.Run("because it is not GA or Deprecated", func(t *testing.T) {
updates := []featuremgmt.FeatureToggleDTO{
{
Name: "toggle2",
Enabled: true,
},
}
res := runSetScenario(t, features, updates, s, writePermissions, http.StatusBadRequest)
defer func() { require.NoError(t, res.Body.Close()) }()
p := readBody(t, res.Body)
assert.Equal(t, "invalid toggle passed in: toggle2", p["error"])
})
t.Run("because it is configured to be read-only", func(t *testing.T) {
updates := []featuremgmt.FeatureToggleDTO{
{
Name: "toggle3",
Enabled: true,
},
}
res := runSetScenario(t, features, updates, s, writePermissions, http.StatusBadRequest)
defer func() { require.NoError(t, res.Body.Close()) }()
p := readBody(t, res.Body)
assert.Equal(t, "invalid toggle passed in: toggle3", p["error"])
})
})
t.Run("succeeds with all conditions met", func(t *testing.T) {
features := []*featuremgmt.FeatureFlag{
{
Name: featuremgmt.FlagFeatureToggleAdminPage,
Enabled: true,
Stage: featuremgmt.FeatureStageGeneralAvailability,
}, {
Name: "toggle2",
Enabled: false,
Stage: featuremgmt.FeatureStagePublicPreview,
}, {
Name: "toggle3",
Enabled: false,
Stage: featuremgmt.FeatureStageGeneralAvailability,
}, {
Name: "toggle4",
Enabled: false,
Stage: featuremgmt.FeatureStageGeneralAvailability,
}, {
Name: "toggle5",
Enabled: false,
Stage: featuremgmt.FeatureStageDeprecated,
},
}
s := setting.FeatureMgmtSettings{
AllowEditing: true,
UpdateControllerUrl: "random",
ReadOnlyToggles: map[string]struct{}{
"toggle3": {},
},
}
updates := []featuremgmt.FeatureToggleDTO{
{
Name: "toggle4",
Enabled: true,
}, {
Name: "toggle5",
Enabled: false,
},
}
// TODO: check for success status after the handler is fully implemented
res := runSetScenario(t, features, updates, s, writePermissions, http.StatusNotImplemented)
defer func() { require.NoError(t, res.Body.Close()) }()
p := readBody(t, res.Body)
assert.Equal(t, "UpdateFeatureToggle is unimplemented", p["message"])
})
}
func findResult(t *testing.T, result []featuremgmt.FeatureToggleDTO, name string) (featuremgmt.FeatureToggleDTO, bool) {
t.Helper()
for _, t := range result {
if t.Name == name {
return t, true
}
}
return featuremgmt.FeatureToggleDTO{}, false
}
func readBody(t *testing.T, rc io.ReadCloser) map[string]interface{} {
t.Helper()
b, err := io.ReadAll(rc)
require.NoError(t, err)
payload := map[string]interface{}{}
require.NoError(t, json.Unmarshal(b, &payload))
return payload
}
func runGetScenario(
t *testing.T,
features []*featuremgmt.FeatureFlag,
settings setting.FeatureMgmtSettings,
permissions []accesscontrol.Permission,
expectedCode int,
) []featuremgmt.FeatureToggleDTO {
// Set up server and send request
cfg := setting.NewCfg()
cfg.FeatureManagement = settings
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = cfg
hs.Features = featuremgmt.WithFeatureFlags(append([]*featuremgmt.FeatureFlag{{
Name: featuremgmt.FlagFeatureToggleAdminPage,
Enabled: true,
Stage: featuremgmt.FeatureStageGeneralAvailability,
}}, features...))
hs.orgService = orgtest.NewOrgServiceFake()
hs.userService = &usertest.FakeUserService{
ExpectedUser: &user.User{ID: 1},
}
hs.log = log.New("test")
})
req := webtest.RequestWithSignedInUser(server.NewGetRequest("/api/featuremgmt"), userWithPermissions(1, permissions))
res, err := server.SendJSON(req)
defer func() { require.NoError(t, res.Body.Close()) }()
// Do some general checks for every request
require.NoError(t, err)
require.Equal(t, expectedCode, res.StatusCode)
if res.StatusCode >= 400 {
return nil
}
var result []featuremgmt.FeatureToggleDTO
err = json.NewDecoder(res.Body).Decode(&result)
require.NoError(t, err)
for i := 0; i < len(result); {
ft := result[i]
// Always make sure admin page toggle is read-only, then remove it to make assertions easier
if ft.Name == featuremgmt.FlagFeatureToggleAdminPage {
assert.True(t, ft.ReadOnly)
result = append(result[:i], result[i+1:]...)
continue
}
// Make sure toggles explicitly marked "hidden" by config are hidden
if _, ok := cfg.FeatureManagement.HiddenToggles[ft.Name]; ok {
t.Fail()
}
// Make sure toggles explicitly marked "read only" by config are read only
if _, ok := cfg.FeatureManagement.ReadOnlyToggles[ft.Name]; ok {
assert.True(t, ft.ReadOnly)
}
i++
}
return result
}
func runSetScenario(
t *testing.T,
serverFeatures []*featuremgmt.FeatureFlag,
updateFeatures []featuremgmt.FeatureToggleDTO,
settings setting.FeatureMgmtSettings,
permissions []accesscontrol.Permission,
expectedCode int,
) *http.Response {
// Set up server and send request
cfg := setting.NewCfg()
cfg.FeatureManagement = settings
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = cfg
hs.Features = featuremgmt.WithFeatureFlags(append([]*featuremgmt.FeatureFlag{{
Name: featuremgmt.FlagFeatureToggleAdminPage,
Enabled: true,
Stage: featuremgmt.FeatureStageGeneralAvailability,
}}, serverFeatures...))
hs.orgService = orgtest.NewOrgServiceFake()
hs.userService = &usertest.FakeUserService{
ExpectedUser: &user.User{ID: 1},
}
hs.log = log.New("test")
})
cmd := featuremgmt.UpdateFeatureTogglesCommand{
FeatureToggles: updateFeatures,
}
b, err := json.Marshal(cmd)
require.NoError(t, err)
req := webtest.RequestWithSignedInUser(server.NewPostRequest("/api/featuremgmt", bytes.NewReader(b)), userWithPermissions(1, permissions))
res, err := server.SendJSON(req)
require.NoError(t, err)
require.NotNil(t, res)
require.Equal(t, expectedCode, res.StatusCode)
return res
}