Skip to content

Commit

Permalink
feat: add feature flag status request [IDE-171] (#454)
Browse files Browse the repository at this point in the history
  • Loading branch information
Cata authored Mar 11, 2024
1 parent 0823cf0 commit 888780b
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 11 deletions.
49 changes: 49 additions & 0 deletions domain/ide/command/get_feature_flag_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* © 2023 Snyk Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package command

import (
"context"

"github.com/snyk/snyk-ls/application/config"
"github.com/snyk/snyk-ls/domain/snyk"
"github.com/snyk/snyk-ls/infrastructure/snyk_api"
)

type featureFlagStatus struct {
command snyk.CommandData
apiClient snyk_api.SnykApiClient
}

func (cmd *featureFlagStatus) Command() snyk.CommandData {
return cmd.command
}

func (cmd *featureFlagStatus) Execute(ctx context.Context) (any, error) {
if config.CurrentConfig().Token() == "" {
return nil, nil
}

args := cmd.command.Arguments
if len(args) < 1 {
return nil, nil
}

ff := args[0].(snyk_api.FeatureFlagType)
featureFlagResponse, err := cmd.apiClient.FeatureFlagSettings(ff)
return featureFlagResponse, err
}
54 changes: 54 additions & 0 deletions domain/ide/command/get_feature_flag_status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* © 2023 Snyk Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package command

import (
"context"
"testing"

"github.com/stretchr/testify/assert"

"github.com/snyk/snyk-ls/domain/snyk"
"github.com/snyk/snyk-ls/infrastructure/snyk_api"
"github.com/snyk/snyk-ls/internal/testutil"
)

func Test_ApiClient_FeatureFlagIsEnabled(t *testing.T) {
testutil.UnitTest(t)

// Arrange
var featureFlagType snyk_api.FeatureFlagType = "snykCodeConsistentIgnores"
expectedResponse := snyk_api.FFResponse{Ok: true}

fakeApiClient := &snyk_api.FakeApiClient{}
fakeApiClient.SetResponse("FeatureFlagSettings", expectedResponse)

// Pass the featureFlagType to the command
featureFlagStatusCmd := featureFlagStatus{
apiClient: fakeApiClient,
command: snyk.CommandData{Arguments: []interface{}{featureFlagType}},
}

// Execute the command
result, err := featureFlagStatusCmd.Execute(context.Background())

// Assert
assert.NoError(t, err)
ffResponse, ok := result.(snyk_api.FFResponse)
assert.True(t, ok)
assert.True(t, ffResponse.Ok)
}
33 changes: 33 additions & 0 deletions infrastructure/snyk_api/fake_api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,30 @@ type FakeApiClient struct {
LocalCodeEngine LocalCodeEngine
AutofixEnabled bool
ApiError *SnykApiError
Responses map[string]any
}

var (
mutex = &sync.Mutex{}
)

func (f *FakeApiClient) SetResponse(method string, response any) {
if f.Responses == nil {
f.Responses = make(map[string]any)
}
f.Responses[method] = response
}

func (f *FakeApiClient) addCallForMethod(method string, args []any) {
mutex.Lock()
defer mutex.Unlock()

if f.Calls == nil {
f.Calls = make(map[string][][]any)
}
f.Calls[method] = append(f.Calls[method], args)
}

func (f *FakeApiClient) addCall(params []any, op string) {
mutex.Lock()
defer mutex.Unlock()
Expand Down Expand Up @@ -62,7 +80,11 @@ func (f *FakeApiClient) GetCallParams(callNo int, op string) []any {
}

func (f *FakeApiClient) Clear() {
mutex.Lock()
defer mutex.Unlock()

f.Calls = map[string][][]any{}
f.Responses = map[string]any{}
}

func (f *FakeApiClient) GetAllCalls(op string) [][]any {
Expand All @@ -89,3 +111,14 @@ func (f *FakeApiClient) SastSettings() (SastResponse, error) {
AutofixEnabled: f.AutofixEnabled,
}, nil
}

func (f *FakeApiClient) FeatureFlagSettings(featureFlagType FeatureFlagType) (FFResponse, error) {
f.addCallForMethod("FeatureFlagSettings", []any{featureFlagType})

if resp, ok := f.Responses["FeatureFlagSettings"]; ok {
if ffResp, ok := resp.(FFResponse); ok {
return ffResp, nil
}
}
return FFResponse{}, nil
}
57 changes: 46 additions & 11 deletions infrastructure/snyk_api/snyk_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,19 @@ import (
"io"
"net/http"
"net/url"
"path"

"github.com/rs/zerolog/log"

"github.com/snyk/snyk-ls/application/config"
)

type FeatureFlagType string

const (
FeatureFlagSnykCodeConsistentIgnores FeatureFlagType = "snykCodeConsistentIgnores"
)

type SnykApiClientImpl struct {
httpClientFunc func() *http.Client
}
Expand All @@ -48,8 +55,14 @@ type SastResponse struct {
AutofixEnabled bool `json:"autofixEnabled"`
}

type FFResponse struct {
Ok bool `json:"ok"`
UserMessage *string `json:"userMessage,omitempty"`
}

type SnykApiClient interface {
SastSettings() (SastResponse, error)
FeatureFlagSettings(featureFlagType FeatureFlagType) (FFResponse, error)
}

type SnykApiError struct {
Expand Down Expand Up @@ -78,28 +91,38 @@ func NewSnykApiClient(client func() *http.Client) SnykApiClient {

func (s *SnykApiClientImpl) SastSettings() (SastResponse, error) {
method := "SastSettings"
var response SastResponse
log.Debug().Str("method", method).Msg("API: Getting SastEnabled")
path := "/cli-config/settings/sast"
organization := config.CurrentConfig().Organization()
if organization != "" {
path += "?org=" + url.QueryEscape(organization)
}
responseBody, err := s.doCall("GET", path, nil)

err := s.processApiResponse(method, path, &response)
if err != nil {
fmtErr := fmt.Errorf("%v: %v", err, responseBody)
log.Err(fmtErr).Str("method", method).Msg("error when calling sastEnabled endpoint")
log.Err(err).Str("method", method).Msg("error when calling sastEnabled endpoint")
return SastResponse{}, err
}
return response, err
}

var response SastResponse
unmarshalErr := json.Unmarshal(responseBody, &response)
if unmarshalErr != nil {
fmtErr := fmt.Errorf("%v: %v", err, responseBody)
log.Err(fmtErr).Str("method", method).Msg("couldn't unmarshal SastResponse")
return SastResponse{}, err
func (s *SnykApiClientImpl) FeatureFlagSettings(featureFlagType FeatureFlagType) (FFResponse, error) {
method := "FeatureFlagSettings"
var response FFResponse
log.Debug().Str("method", method).Msgf("API: Getting %s", featureFlagType)
path := path.Join("/cli-config/feature-flag/", string(featureFlagType))
organization := config.CurrentConfig().Organization()
if organization != "" {
path += "?org=" + url.QueryEscape(organization)
}

err := s.processApiResponse(method, path, &response)
if err != nil {
log.Err(err).Str("method", method).Msg("error when calling featureFlagSettings endpoint")
return FFResponse{}, err
}
log.Debug().Str("method", method).Msg("API: Done")
return response, nil
return response, err
}

func (s *SnykApiClientImpl) doCall(method string,
Expand Down Expand Up @@ -141,6 +164,18 @@ func (s *SnykApiClientImpl) doCall(method string,
return responseBody, nil
}

func (s *SnykApiClientImpl) processApiResponse(method string, path string, v interface{}) error {
responseBody, err := s.doCall("GET", path, nil)
if err != nil {
return fmt.Errorf("%s: %v: %v", method, err, responseBody)
}

if err := json.Unmarshal(responseBody, v); err != nil {
return fmt.Errorf("%s: couldn't unmarshal: %v", method, err)
}
return nil
}

func checkResponseCode(r *http.Response) *SnykApiError {
if r.StatusCode >= 200 && r.StatusCode <= 399 {
return nil
Expand Down
86 changes: 86 additions & 0 deletions infrastructure/snyk_api/snyk_api_pact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,92 @@ func TestSnykApiPact(t *testing.T) {

assert.NoError(t, err)
})

t.Run("Get feature flag status", func(t *testing.T) {
organization := orgUUID
config.CurrentConfig().SetOrganization(organization)
var featureFlagType FeatureFlagType = "snykCodeConsistentIgnores"

expectedResponse := FFResponse{
Ok: true,
UserMessage: nil,
}

matcher := dsl.MapMatcher{}
matcher["org"] = dsl.String(organization)

interaction := pact.AddInteraction().
WithRequest(dsl.Request{
Method: "GET",
Path: dsl.String("/cli-config/feature-flag/" + featureFlagType),
Query: matcher,
Headers: dsl.MapMatcher{
"Content-Type": dsl.String("application/json"),
"Authorization": dsl.Regex("token fc763eba-0905-41c5-a27f-3934ab26786c", `^token [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`),
},
}).WillRespondWith(dsl.Response{
Status: 200,
Headers: dsl.MapMatcher{
"Content-Type": dsl.String("application/json"),
},
Body: dsl.Match(expectedResponse),
})
interaction.Description = "feature flag with org as query param"

test := func() error {
_, err := client.FeatureFlagSettings("snykCodeConsistentIgnores")
if err != nil {
return err
}
return nil
}

err := pact.Verify(test)

assert.NoError(t, err)
})

t.Run("Get feature flag status when disabled for a ORG", func(t *testing.T) {
organization := "00000000-0000-0000-0000-000000000099"
config.CurrentConfig().SetOrganization(organization)
featureFlagType := FeatureFlagType("snykCodeConsistentIgnores")

message := "Org " + organization + " doesn't have '" + string(featureFlagType) + "' feature enabled"
disabledResponse := FFResponse{
Ok: false,
UserMessage: &message,
}

matcher := dsl.MapMatcher{}
matcher["org"] = dsl.String(organization)

interaction := pact.AddInteraction().
WithRequest(dsl.Request{
Method: "GET",
Path: dsl.String("/cli-config/feature-flag/" + featureFlagType),
Query: matcher,
Headers: dsl.MapMatcher{
"Content-Type": dsl.String("application/json"),
"Authorization": dsl.Regex("token fc763eba-0905-41c5-a27f-3934ab26786c", `^token [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`),
},
}).WillRespondWith(dsl.Response{
Status: 200,
Headers: dsl.MapMatcher{
"Content-Type": dsl.String("application/json"),
},
Body: dsl.Match(disabledResponse),
})
interaction.Description = fmt.Sprintf("feature flag '%s' disabled for org", featureFlagType)

test := func() error {
_, err := client.FeatureFlagSettings(featureFlagType)
t.Logf("err: %+v\n", err)
return err
}

err := pact.Verify(test)
assert.NoError(t, err)
})
}

func setupPact() {
Expand Down

0 comments on commit 888780b

Please sign in to comment.