Commit f4b8121f authored by Heidi Berry's avatar Heidi Berry Committed by Timo Furrer
Browse files

Add remaining feature flag endpoints

Changelog: Improvements
parent e0098134
Loading
Loading
Loading
Loading
+77 −11
Original line number Diff line number Diff line
@@ -26,7 +26,9 @@ type (
	// FeaturesServiceInterface defines all the API methods for the FeaturesService
	FeaturesServiceInterface interface {
		ListFeatures(options ...RequestOptionFunc) ([]*Feature, *Response, error)
		SetFeatureFlag(name string, value any, options ...RequestOptionFunc) (*Feature, *Response, error)
		ListFeatureDefinitions(options ...RequestOptionFunc) ([]*FeatureDefinition, *Response, error)
		SetFeatureFlag(name string, opt *SetFeatureFlagOptions, options ...RequestOptionFunc) (*Feature, *Response, error)
		DeleteFeatureFlag(name string, options ...RequestOptionFunc) (*Response, error)
	}

	// FeaturesService handles the communication with the application FeaturesService
@@ -47,6 +49,7 @@ type Feature struct {
	Name       string `json:"name"`
	State      string `json:"state"`
	Gates      []Gate
	Definition *FeatureDefinition `json:"definition"`
}

// Gate represents a gate of a GitLab feature flag.
@@ -79,19 +82,67 @@ func (s *FeaturesService) ListFeatures(options ...RequestOptionFunc) ([]*Feature
	return f, resp, nil
}

// SetFeatureFlag sets or creates a feature flag gate
// FeatureDefinition represents a Feature Definition.
//
// GitLab API docs:
// https://docs.gitlab.com/api/features/#set-or-create-a-feature
func (s *FeaturesService) SetFeatureFlag(name string, value any, options ...RequestOptionFunc) (*Feature, *Response, error) {
	u := fmt.Sprintf("features/%s", url.PathEscape(name))
// https://docs.gitlab.com/api/features/#list-all-feature-definitions
type FeatureDefinition struct {
	Name            string `json:"name"`
	IntroducedByURL string `json:"introduced_by_url"`
	RolloutIssueURL string `json:"rollout_issue_url"`
	Milestone       string `json:"milestone"`
	LogStateChanges bool   `json:"log_state_changes"`
	Type            string `json:"type"`
	Group           string `json:"group"`
	DefaultEnabled  bool   `json:"default_enabled"`
}

func (fd FeatureDefinition) String() string {
	return Stringify(fd)
}

// ListFeatureDefinitions gets a lists of all feature definitions.
//
// GitLab API docs:
// https://docs.gitlab.com/api/features/#list-all-feature-definitions
func (s *FeaturesService) ListFeatureDefinitions(options ...RequestOptionFunc) ([]*FeatureDefinition, *Response, error) {
	req, err := s.client.NewRequest(http.MethodGet, "features/definitions", nil, options)
	if err != nil {
		return nil, nil, err
	}

	var fd []*FeatureDefinition
	resp, err := s.client.Do(req, &fd)
	if err != nil {
		return nil, resp, err
	}
	return fd, resp, nil
}

	opt := struct {
// SetFeatureFlagOptions represents the available options for
// SetFeatureFlag().
//
// GitLab API docs:
// https://docs.gitlab.com/api/features/#set-or-create-a-feature
type SetFeatureFlagOptions struct {
	Value        any    `url:"value" json:"value"`
	}{
		value,
	Key          string `url:"key" json:"key"`
	FeatureGroup string `url:"feature_group" json:"feature_group"`
	User         string `url:"user" json:"user"`
	Group        string `url:"group" json:"group"`
	Namespace    string `url:"namespace" json:"namespace"`
	Project      string `url:"project" json:"project"`
	Repository   string `url:"repository" json:"repository"`
	Force        bool   `url:"force" json:"force"`
}

// SetFeatureFlag sets or creates a feature flag gate
//
// GitLab API docs:
// https://docs.gitlab.com/api/features/#set-or-create-a-feature
func (s *FeaturesService) SetFeatureFlag(name string, opt *SetFeatureFlagOptions, options ...RequestOptionFunc) (*Feature, *Response, error) {
	u := fmt.Sprintf("features/%s", url.PathEscape(name))

	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
	if err != nil {
		return nil, nil, err
@@ -104,3 +155,18 @@ func (s *FeaturesService) SetFeatureFlag(name string, value any, options ...Requ
	}
	return f, resp, nil
}

// DeleteFeatureFlag deletes a feature flag.
//
// GitLab API docs:
// https://docs.gitlab.com/api/features/#delete-a-feature
func (s *FeaturesService) DeleteFeatureFlag(name string, options ...RequestOptionFunc) (*Response, error) {
	u := fmt.Sprintf("features/%s", url.PathEscape(name))

	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
	if err != nil {
		return nil, err
	}

	return s.client.Do(req, nil)
}
+67 −14
Original line number Diff line number Diff line
@@ -19,8 +19,9 @@ package gitlab
import (
	"fmt"
	"net/http"
	"reflect"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestListFeatureFlags(t *testing.T) {
@@ -49,10 +50,9 @@ func TestListFeatureFlags(t *testing.T) {
	`)
	})

	features, _, err := client.Features.ListFeatures()
	if err != nil {
		t.Errorf("Features.ListFeatures returned error: %v", err)
	}
	features, resp, err := client.Features.ListFeatures()
	assert.NoError(t, err)
	assert.NotNil(t, resp)

	want := []*Feature{
		{Name: "experimental_feature", State: "off", Gates: []Gate{
@@ -60,9 +60,48 @@ func TestListFeatureFlags(t *testing.T) {
		}},
		{Name: "new_library", State: "on"},
	}
	if !reflect.DeepEqual(want, features) {
		t.Errorf("Features.ListFeatures returned %+v, want %+v", features, want)
	assert.Equal(t, want, features)
}

func TestListFeatureDefinitions(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/features/definitions", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `
		[
			{
				"name": "geo_pages_deployment_replication",
				"introduced_by_url": "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68662",
				"rollout_issue_url": "https://gitlab.com/gitlab-org/gitlab/-/issues/337676",
				"milestone": "14.3",
				"log_state_changes": null,
				"type": "development",
				"group": "group::geo",
				"default_enabled": true
			}
		]
		`)
	})

	definitions, resp, err := client.Features.ListFeatureDefinitions()
	assert.NoError(t, err)
	assert.NotNil(t, resp)

	want := []*FeatureDefinition{
		{
			Name:            "geo_pages_deployment_replication",
			IntroducedByURL: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68662",
			RolloutIssueURL: "https://gitlab.com/gitlab-org/gitlab/-/issues/337676",
			Milestone:       "14.3",
			Type:            "development",
			Group:           "group::geo",
			DefaultEnabled:  true,
		},
	}

	assert.Equal(t, want, definitions)
}

func TestSetFeatureFlag(t *testing.T) {
@@ -89,10 +128,13 @@ func TestSetFeatureFlag(t *testing.T) {
		`)
	})

	feature, _, err := client.Features.SetFeatureFlag("new_library", "30")
	if err != nil {
		t.Errorf("Features.SetFeatureFlag returned error: %v", err)
	}
	feature, resp, err := client.Features.SetFeatureFlag("new_library", &SetFeatureFlagOptions{
		Value:        false,
		Key:          "boolean",
		FeatureGroup: "experiment",
	})
	assert.NoError(t, err)
	assert.NotNil(t, resp)

	want := &Feature{
		Name:  "new_library",
@@ -102,7 +144,18 @@ func TestSetFeatureFlag(t *testing.T) {
			{Key: "percentage_of_time", Value: 30.0},
		},
	}
	if !reflect.DeepEqual(want, feature) {
		t.Errorf("Features.SetFeatureFlag returned %+v, want %+v", feature, want)
	assert.Equal(t, want, feature)
}

func TestDeleteFeatureFlag(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/features/new_library", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodDelete)
	})

	resp, err := client.Features.DeleteFeatureFlag("new_library")
	assert.NoError(t, err)
	assert.NotNil(t, resp)
}
+94 −6
Original line number Diff line number Diff line
@@ -40,6 +40,94 @@ func (m *MockFeaturesServiceInterface) EXPECT() *MockFeaturesServiceInterfaceMoc
	return m.recorder
}

// DeleteFeatureFlag mocks base method.
func (m *MockFeaturesServiceInterface) DeleteFeatureFlag(name string, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
	m.ctrl.T.Helper()
	varargs := []any{name}
	for _, a := range options {
		varargs = append(varargs, a)
	}
	ret := m.ctrl.Call(m, "DeleteFeatureFlag", varargs...)
	ret0, _ := ret[0].(*gitlab.Response)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// DeleteFeatureFlag indicates an expected call of DeleteFeatureFlag.
func (mr *MockFeaturesServiceInterfaceMockRecorder) DeleteFeatureFlag(name any, options ...any) *MockFeaturesServiceInterfaceDeleteFeatureFlagCall {
	mr.mock.ctrl.T.Helper()
	varargs := append([]any{name}, options...)
	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFeatureFlag", reflect.TypeOf((*MockFeaturesServiceInterface)(nil).DeleteFeatureFlag), varargs...)
	return &MockFeaturesServiceInterfaceDeleteFeatureFlagCall{Call: call}
}

// MockFeaturesServiceInterfaceDeleteFeatureFlagCall wrap *gomock.Call
type MockFeaturesServiceInterfaceDeleteFeatureFlagCall struct {
	*gomock.Call
}

// Return rewrite *gomock.Call.Return
func (c *MockFeaturesServiceInterfaceDeleteFeatureFlagCall) Return(arg0 *gitlab.Response, arg1 error) *MockFeaturesServiceInterfaceDeleteFeatureFlagCall {
	c.Call = c.Call.Return(arg0, arg1)
	return c
}

// Do rewrite *gomock.Call.Do
func (c *MockFeaturesServiceInterfaceDeleteFeatureFlagCall) Do(f func(string, ...gitlab.RequestOptionFunc) (*gitlab.Response, error)) *MockFeaturesServiceInterfaceDeleteFeatureFlagCall {
	c.Call = c.Call.Do(f)
	return c
}

// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockFeaturesServiceInterfaceDeleteFeatureFlagCall) DoAndReturn(f func(string, ...gitlab.RequestOptionFunc) (*gitlab.Response, error)) *MockFeaturesServiceInterfaceDeleteFeatureFlagCall {
	c.Call = c.Call.DoAndReturn(f)
	return c
}

// ListFeatureDefinitions mocks base method.
func (m *MockFeaturesServiceInterface) ListFeatureDefinitions(options ...gitlab.RequestOptionFunc) ([]*gitlab.FeatureDefinition, *gitlab.Response, error) {
	m.ctrl.T.Helper()
	varargs := []any{}
	for _, a := range options {
		varargs = append(varargs, a)
	}
	ret := m.ctrl.Call(m, "ListFeatureDefinitions", varargs...)
	ret0, _ := ret[0].([]*gitlab.FeatureDefinition)
	ret1, _ := ret[1].(*gitlab.Response)
	ret2, _ := ret[2].(error)
	return ret0, ret1, ret2
}

// ListFeatureDefinitions indicates an expected call of ListFeatureDefinitions.
func (mr *MockFeaturesServiceInterfaceMockRecorder) ListFeatureDefinitions(options ...any) *MockFeaturesServiceInterfaceListFeatureDefinitionsCall {
	mr.mock.ctrl.T.Helper()
	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFeatureDefinitions", reflect.TypeOf((*MockFeaturesServiceInterface)(nil).ListFeatureDefinitions), options...)
	return &MockFeaturesServiceInterfaceListFeatureDefinitionsCall{Call: call}
}

// MockFeaturesServiceInterfaceListFeatureDefinitionsCall wrap *gomock.Call
type MockFeaturesServiceInterfaceListFeatureDefinitionsCall struct {
	*gomock.Call
}

// Return rewrite *gomock.Call.Return
func (c *MockFeaturesServiceInterfaceListFeatureDefinitionsCall) Return(arg0 []*gitlab.FeatureDefinition, arg1 *gitlab.Response, arg2 error) *MockFeaturesServiceInterfaceListFeatureDefinitionsCall {
	c.Call = c.Call.Return(arg0, arg1, arg2)
	return c
}

// Do rewrite *gomock.Call.Do
func (c *MockFeaturesServiceInterfaceListFeatureDefinitionsCall) Do(f func(...gitlab.RequestOptionFunc) ([]*gitlab.FeatureDefinition, *gitlab.Response, error)) *MockFeaturesServiceInterfaceListFeatureDefinitionsCall {
	c.Call = c.Call.Do(f)
	return c
}

// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockFeaturesServiceInterfaceListFeatureDefinitionsCall) DoAndReturn(f func(...gitlab.RequestOptionFunc) ([]*gitlab.FeatureDefinition, *gitlab.Response, error)) *MockFeaturesServiceInterfaceListFeatureDefinitionsCall {
	c.Call = c.Call.DoAndReturn(f)
	return c
}

// ListFeatures mocks base method.
func (m *MockFeaturesServiceInterface) ListFeatures(options ...gitlab.RequestOptionFunc) ([]*gitlab.Feature, *gitlab.Response, error) {
	m.ctrl.T.Helper()
@@ -85,9 +173,9 @@ func (c *MockFeaturesServiceInterfaceListFeaturesCall) DoAndReturn(f func(...git
}

// SetFeatureFlag mocks base method.
func (m *MockFeaturesServiceInterface) SetFeatureFlag(name string, value any, options ...gitlab.RequestOptionFunc) (*gitlab.Feature, *gitlab.Response, error) {
func (m *MockFeaturesServiceInterface) SetFeatureFlag(name string, opt *gitlab.SetFeatureFlagOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Feature, *gitlab.Response, error) {
	m.ctrl.T.Helper()
	varargs := []any{name, value}
	varargs := []any{name, opt}
	for _, a := range options {
		varargs = append(varargs, a)
	}
@@ -99,9 +187,9 @@ func (m *MockFeaturesServiceInterface) SetFeatureFlag(name string, value any, op
}

// SetFeatureFlag indicates an expected call of SetFeatureFlag.
func (mr *MockFeaturesServiceInterfaceMockRecorder) SetFeatureFlag(name, value any, options ...any) *MockFeaturesServiceInterfaceSetFeatureFlagCall {
func (mr *MockFeaturesServiceInterfaceMockRecorder) SetFeatureFlag(name, opt any, options ...any) *MockFeaturesServiceInterfaceSetFeatureFlagCall {
	mr.mock.ctrl.T.Helper()
	varargs := append([]any{name, value}, options...)
	varargs := append([]any{name, opt}, options...)
	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFeatureFlag", reflect.TypeOf((*MockFeaturesServiceInterface)(nil).SetFeatureFlag), varargs...)
	return &MockFeaturesServiceInterfaceSetFeatureFlagCall{Call: call}
}
@@ -118,13 +206,13 @@ func (c *MockFeaturesServiceInterfaceSetFeatureFlagCall) Return(arg0 *gitlab.Fea
}

// Do rewrite *gomock.Call.Do
func (c *MockFeaturesServiceInterfaceSetFeatureFlagCall) Do(f func(string, any, ...gitlab.RequestOptionFunc) (*gitlab.Feature, *gitlab.Response, error)) *MockFeaturesServiceInterfaceSetFeatureFlagCall {
func (c *MockFeaturesServiceInterfaceSetFeatureFlagCall) Do(f func(string, *gitlab.SetFeatureFlagOptions, ...gitlab.RequestOptionFunc) (*gitlab.Feature, *gitlab.Response, error)) *MockFeaturesServiceInterfaceSetFeatureFlagCall {
	c.Call = c.Call.Do(f)
	return c
}

// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockFeaturesServiceInterfaceSetFeatureFlagCall) DoAndReturn(f func(string, any, ...gitlab.RequestOptionFunc) (*gitlab.Feature, *gitlab.Response, error)) *MockFeaturesServiceInterfaceSetFeatureFlagCall {
func (c *MockFeaturesServiceInterfaceSetFeatureFlagCall) DoAndReturn(f func(string, *gitlab.SetFeatureFlagOptions, ...gitlab.RequestOptionFunc) (*gitlab.Feature, *gitlab.Response, error)) *MockFeaturesServiceInterfaceSetFeatureFlagCall {
	c.Call = c.Call.DoAndReturn(f)
	return c
}