Skip to content

Commit a520093

Browse files
authored
fix: IAW conf key dep on org and fix SAST cache [IDE-1501] (#457)
IAW enabled config value was relying on the org, but did not have it as a key dependency, so was not invalidating the cache on org change. The fetching of the SAST settings was trying to cache itself instead of relying on the config to do the caching. Also made the SAST sub-settings use the main config for the SAST settings to prevent unnecessary re-fetching from the API. Note to the reviewer: The PR is split into 2 commits, if you don't like this change we can drop the second commit. Optimised the tests to use common code to verify that the cache is cleared when the org changes. The previous SAST tests were not actually testing this. This required moving the test utils to a sub-package, to prevent an import loop.
2 parents bd3e799 + c1a661a commit a520093

File tree

9 files changed

+393
-239
lines changed

9 files changed

+393
-239
lines changed

pkg/local_workflows/code_workflow.go

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,30 +43,23 @@ func GetCodeFlagSet() *pflag.FlagSet {
4343
}
4444

4545
// WORKFLOWID_CODE defines a new workflow identifier
46-
var WORKFLOWID_CODE workflow.Identifier = workflow.NewWorkflowIdentifier(codeWorkflowName)
46+
var WORKFLOWID_CODE = workflow.NewWorkflowIdentifier(codeWorkflowName)
4747

4848
func getSastSettings(engine workflow.Engine) (*sast_contract.SastResponse, error) {
4949
config := engine.GetConfiguration()
5050
org := config.GetString(configuration.ORGANIZATION)
5151
client := engine.GetNetworkAccess().GetHttpClient()
5252
url := config.GetString(configuration.API_URL)
5353
apiClient := api.NewApi(url, client)
54-
tmp, err := apiClient.GetSastSettings(org)
55-
if err != nil {
56-
engine.GetLogger().Err(err).Msg("Failed to access settings.")
57-
return nil, err
58-
}
59-
60-
engine.GetConfiguration().Set(code_workflow.ConfigurationSastSettings, tmp)
61-
return tmp, nil
54+
return apiClient.GetSastSettings(org)
6255
}
6356

6457
func getSastSettingsConfig(engine workflow.Engine) configuration.DefaultValueFunction {
6558
err := engine.GetConfiguration().AddKeyDependency(code_workflow.ConfigurationSastSettings, configuration.ORGANIZATION)
6659
if err != nil {
6760
engine.GetLogger().Err(err).Msg("Failed to add dependency for SAST settings.")
6861
}
69-
callback := func(_ configuration.Configuration, existingValue interface{}) (interface{}, error) {
62+
callback := func(_ configuration.Configuration, existingValue any) (any, error) {
7063
if existingValue != nil {
7164
return existingValue, nil
7265
}
@@ -82,44 +75,57 @@ func getSastSettingsConfig(engine workflow.Engine) configuration.DefaultValueFun
8275
return callback
8376
}
8477

78+
func getSastResponseFromConfig(config configuration.Configuration) (*sast_contract.SastResponse, error) {
79+
sastResponseGeneric, err := config.GetWithError(code_workflow.ConfigurationSastSettings)
80+
if err != nil {
81+
return nil, err
82+
}
83+
if sastResponseGeneric == nil {
84+
return nil, fmt.Errorf("SAST settings are nil")
85+
}
86+
sastResponse, ok := sastResponseGeneric.(*sast_contract.SastResponse)
87+
if !ok {
88+
return nil, fmt.Errorf("SAST settings are not a SastResponse")
89+
}
90+
return sastResponse, nil
91+
}
92+
8593
func getSastEnabled(engine workflow.Engine) configuration.DefaultValueFunction {
86-
err := engine.GetConfiguration().AddKeyDependency(code_workflow.ConfigurationSastEnabled, configuration.ORGANIZATION)
94+
err := engine.GetConfiguration().AddKeyDependency(code_workflow.ConfigurationSastEnabled, code_workflow.ConfigurationSastSettings)
8795
if err != nil {
8896
engine.GetLogger().Err(err).Msg("Failed to add dependency for SAST settings.")
8997
}
90-
callback := func(_ configuration.Configuration, existingValue interface{}) (interface{}, error) {
98+
callback := func(config configuration.Configuration, existingValue any) (any, error) {
9199
if existingValue != nil {
92100
return existingValue, nil
93101
}
94-
95-
response, err := getSastSettings(engine)
102+
sastResponse, err := getSastResponseFromConfig(config)
96103
if err != nil {
97104
engine.GetLogger().Err(err).Msg("Failed to access settings.")
98105
return false, err
99106
}
100107

101-
return response.SastEnabled, nil
108+
return sastResponse.SastEnabled, nil
102109
}
103110
return callback
104111
}
105112

106113
func getSlceEnabled(engine workflow.Engine) configuration.DefaultValueFunction {
107-
err := engine.GetConfiguration().AddKeyDependency(code_workflow.ConfigurarionSlceEnabled, configuration.ORGANIZATION)
114+
err := engine.GetConfiguration().AddKeyDependency(code_workflow.ConfigurarionSlceEnabled, code_workflow.ConfigurationSastSettings)
108115
if err != nil {
109116
engine.GetLogger().Err(err).Msg("Failed to add dependency for SAST settings.")
110117
}
111-
callback := func(_ configuration.Configuration, existingValue interface{}) (interface{}, error) {
118+
callback := func(config configuration.Configuration, existingValue any) (any, error) {
112119
if existingValue != nil {
113120
return existingValue, nil
114121
}
115-
116-
response, err := getSastSettings(engine)
122+
sastResponse, err := getSastResponseFromConfig(config)
117123
if err != nil {
118124
engine.GetLogger().Err(err).Msg("Failed to access settings.")
119125
return false, err
120126
}
121127

122-
return response.LocalCodeEngine.Enabled, nil
128+
return sastResponse.LocalCodeEngine.Enabled, nil
123129
}
124130
return callback
125131
}

pkg/local_workflows/code_workflow_test.go

Lines changed: 116 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"os"
1010
"strings"
1111
"testing"
12-
"time"
1312

1413
"github.com/golang/mock/gomock"
1514
"github.com/rs/zerolog"
@@ -25,6 +24,7 @@ import (
2524
"github.com/snyk/go-application-framework/pkg/local_workflows/code_workflow/sast_contract"
2625
"github.com/snyk/go-application-framework/pkg/local_workflows/content_type"
2726
"github.com/snyk/go-application-framework/pkg/local_workflows/json_schemas"
27+
testutils "github.com/snyk/go-application-framework/pkg/local_workflows/test_utils"
2828
"github.com/snyk/go-application-framework/pkg/mocks"
2929
"github.com/snyk/go-application-framework/pkg/networking"
3030
"github.com/snyk/go-application-framework/pkg/ui"
@@ -460,67 +460,6 @@ func Test_Code_UseNativeImplementation(t *testing.T) {
460460
})
461461
}
462462

463-
// Helper function to test key dependencies for configuration keys
464-
func testOrganizationDependency(
465-
t *testing.T,
466-
configKey string,
467-
dependencyKey string,
468-
configIsBoolean bool,
469-
) {
470-
t.Helper()
471-
472-
// Setup
473-
ctrl := gomock.NewController(t)
474-
defer ctrl.Finish()
475-
476-
config := configuration.NewWithOpts(configuration.WithCachingEnabled(10 * time.Minute))
477-
config.Set(dependencyKey, "value1")
478-
479-
// Track how many times the callback is invoked
480-
callCount := 0
481-
testCallback := func(c configuration.Configuration, existingValue interface{}) (interface{}, error) {
482-
callCount++
483-
// Return a value that changes on each call. This allows us to check whether the returned value comes from
484-
// the cache or this callback function.
485-
if configIsBoolean {
486-
// Alternate between true and false
487-
return callCount%2 == 1, nil
488-
}
489-
return fmt.Sprintf("value-%d", callCount), nil
490-
}
491-
492-
// Register the dependency
493-
err := config.AddKeyDependency(configKey, dependencyKey)
494-
assert.NoError(t, err)
495-
config.AddDefaultValue(configKey, testCallback)
496-
497-
// First Get - should invoke callback
498-
result1 := getValue(config, configKey, configIsBoolean)
499-
assert.NotNil(t, result1)
500-
assert.Equal(t, 1, callCount, "Callback should be invoked on first read")
501-
502-
// Second Get - should use cached value
503-
result2 := getValue(config, configKey, configIsBoolean)
504-
assert.Equal(t, result1, result2, "Cached value should be used on second read")
505-
assert.Equal(t, 1, callCount, "Callback should not be called on second read")
506-
507-
// Change the value of the dependency - this should clear the cached value.
508-
config.Set(dependencyKey, "value2")
509-
510-
// Third Get - should invoke callback again since cache was cleared
511-
result3 := getValue(config, configKey, configIsBoolean)
512-
assert.NotNil(t, result3)
513-
assert.NotEqual(t, result1, result3, "Cached value should not be used after dependency changed")
514-
assert.Equal(t, 2, callCount, "Callback should be called again after dependency changed")
515-
}
516-
517-
func getValue(config configuration.Configuration, key string, isBoolean bool) interface{} {
518-
if isBoolean {
519-
return config.GetBool(key)
520-
}
521-
return config.Get(key)
522-
}
523-
524463
// setupMockEngine creates a mock engine with basic expectations
525464
func setupMockEngine(t *testing.T) *mocks.MockEngine {
526465
t.Helper()
@@ -591,16 +530,7 @@ func setupMockEngineWithServer(t *testing.T, server *httptest.Server) (*mocks.Mo
591530
return mockEngine, config
592531
}
593532

594-
func Test_GetSastSettingsConfig(t *testing.T) {
595-
t.Run("adds organization dependency and clears cache on org change", func(t *testing.T) {
596-
testOrganizationDependency(
597-
t,
598-
code_workflow.ConfigurationSastSettings,
599-
configuration.ORGANIZATION,
600-
false,
601-
)
602-
})
603-
533+
func Test_getSastSettingsConfig(t *testing.T) {
604534
t.Run("callback returns existing value when provided", func(t *testing.T) {
605535
existingValue := &sast_contract.SastResponse{SastEnabled: true}
606536

@@ -625,6 +555,28 @@ func Test_GetSastSettingsConfig(t *testing.T) {
625555
assert.True(t, sastResponse.LocalCodeEngine.Enabled)
626556
})
627557

558+
t.Run("adds organization dependency and clears cache on org change", func(t *testing.T) {
559+
testutils.CheckCacheRespectOrgDependency(
560+
t,
561+
code_workflow.ConfigurationSastSettings,
562+
func(isFirstCall bool) any {
563+
return &sast_contract.SastResponse{
564+
SastEnabled: isFirstCall,
565+
LocalCodeEngine: sast_contract.LocalCodeEngine{Enabled: isFirstCall},
566+
}
567+
},
568+
InitCodeWorkflow,
569+
&sast_contract.SastResponse{
570+
SastEnabled: true,
571+
LocalCodeEngine: sast_contract.LocalCodeEngine{Enabled: true},
572+
},
573+
&sast_contract.SastResponse{
574+
SastEnabled: false,
575+
LocalCodeEngine: sast_contract.LocalCodeEngine{Enabled: false},
576+
},
577+
)
578+
})
579+
628580
t.Run("callback returns error when API call fails and existing value is nil", func(t *testing.T) {
629581
server := setupMockServerWithError(t)
630582
defer server.Close()
@@ -637,16 +589,7 @@ func Test_GetSastSettingsConfig(t *testing.T) {
637589
})
638590
}
639591

640-
func Test_GetSastEnabled(t *testing.T) {
641-
t.Run("adds organization dependency and clears cache on org change", func(t *testing.T) {
642-
testOrganizationDependency(
643-
t,
644-
code_workflow.ConfigurationSastEnabled,
645-
configuration.ORGANIZATION,
646-
true,
647-
)
648-
})
649-
592+
func Test_getSastEnabled(t *testing.T) {
650593
t.Run("callback function returns existing value when provided", func(t *testing.T) {
651594
mockEngine := setupMockEngine(t)
652595
result, err := getSastEnabled(mockEngine)(mockEngine.GetConfiguration(), true)
@@ -656,17 +599,57 @@ func Test_GetSastEnabled(t *testing.T) {
656599
assert.True(t, boolResult, "Should return existing value when provided")
657600
})
658601

659-
t.Run("callback fetches settings when existing value is nil", func(t *testing.T) {
660-
server := setupMockServerForSastSettings(t, true, false)
661-
defer server.Close()
602+
t.Run("callback reads from ConfigurationSastSettings (pre-cached) when existing value is nil", func(t *testing.T) {
603+
mockEngine := setupMockEngine(t)
604+
config := mockEngine.GetConfiguration()
662605

663-
mockEngine, config := setupMockEngineWithServer(t, server)
606+
// Set ConfigurationSastSettings in config
607+
sastSettings := &sast_contract.SastResponse{
608+
SastEnabled: true,
609+
LocalCodeEngine: sast_contract.LocalCodeEngine{Enabled: false},
610+
}
611+
config.Set(code_workflow.ConfigurationSastSettings, sastSettings)
664612

665613
result, err := getSastEnabled(mockEngine)(config, nil)
666614
assert.NoError(t, err)
667615
boolResult, ok := result.(bool)
668616
assert.True(t, ok, "result should be of type bool")
669-
assert.True(t, boolResult, "Should return SastEnabled from API response")
617+
assert.True(t, boolResult, "Should return SastEnabled from ConfigurationSastSettings")
618+
})
619+
620+
t.Run("depends on ConfigurationSastSettings", func(t *testing.T) {
621+
testutils.CheckConfigCachesDependency(
622+
t,
623+
code_workflow.ConfigurationSastEnabled,
624+
code_workflow.ConfigurationSastSettings,
625+
getSastEnabled,
626+
&sast_contract.SastResponse{
627+
SastEnabled: true,
628+
LocalCodeEngine: sast_contract.LocalCodeEngine{Enabled: false},
629+
},
630+
&sast_contract.SastResponse{
631+
SastEnabled: false,
632+
LocalCodeEngine: sast_contract.LocalCodeEngine{Enabled: false},
633+
},
634+
true,
635+
false,
636+
)
637+
})
638+
639+
t.Run("respects organization changes (full chain)", func(t *testing.T) {
640+
testutils.CheckCacheRespectOrgDependency(
641+
t,
642+
code_workflow.ConfigurationSastEnabled,
643+
func(isFirstCall bool) any {
644+
return &sast_contract.SastResponse{
645+
SastEnabled: isFirstCall,
646+
LocalCodeEngine: sast_contract.LocalCodeEngine{Enabled: false},
647+
}
648+
},
649+
InitCodeWorkflow,
650+
true,
651+
false,
652+
)
670653
})
671654

672655
t.Run("callback returns false and error when API call fails and existing value is nil", func(t *testing.T) {
@@ -683,16 +666,7 @@ func Test_GetSastEnabled(t *testing.T) {
683666
})
684667
}
685668

686-
func Test_GetSlceEnabled(t *testing.T) {
687-
t.Run("adds organization dependency and clears cache on org change", func(t *testing.T) {
688-
testOrganizationDependency(
689-
t,
690-
code_workflow.ConfigurarionSlceEnabled,
691-
configuration.ORGANIZATION,
692-
true,
693-
)
694-
})
695-
669+
func Test_getSlceEnabled(t *testing.T) {
696670
t.Run("callback function returns existing value when provided", func(t *testing.T) {
697671
mockEngine := setupMockEngine(t)
698672
result, err := getSlceEnabled(mockEngine)(mockEngine.GetConfiguration(), true)
@@ -702,17 +676,57 @@ func Test_GetSlceEnabled(t *testing.T) {
702676
assert.True(t, boolResult, "Should return existing value when provided")
703677
})
704678

705-
t.Run("callback fetches settings when existing value is nil", func(t *testing.T) {
706-
server := setupMockServerForSastSettings(t, false, true)
707-
defer server.Close()
679+
t.Run("callback reads from ConfigurationSastSettings (pre-cached) when existing value is nil", func(t *testing.T) {
680+
mockEngine := setupMockEngine(t)
681+
config := mockEngine.GetConfiguration()
708682

709-
mockEngine, config := setupMockEngineWithServer(t, server)
683+
// Set ConfigurationSastSettings in config
684+
sastSettings := &sast_contract.SastResponse{
685+
SastEnabled: false,
686+
LocalCodeEngine: sast_contract.LocalCodeEngine{Enabled: true},
687+
}
688+
config.Set(code_workflow.ConfigurationSastSettings, sastSettings)
710689

711690
result, err := getSlceEnabled(mockEngine)(config, nil)
712691
assert.NoError(t, err)
713692
boolResult, ok := result.(bool)
714693
assert.True(t, ok, "result should be of type bool")
715-
assert.True(t, boolResult, "Should return LocalCodeEngine.Enabled from API response")
694+
assert.True(t, boolResult, "Should return LocalCodeEngine.Enabled from ConfigurationSastSettings")
695+
})
696+
697+
t.Run("depends on ConfigurationSastSettings", func(t *testing.T) {
698+
testutils.CheckConfigCachesDependency(
699+
t,
700+
code_workflow.ConfigurarionSlceEnabled,
701+
code_workflow.ConfigurationSastSettings,
702+
getSlceEnabled,
703+
&sast_contract.SastResponse{
704+
SastEnabled: false,
705+
LocalCodeEngine: sast_contract.LocalCodeEngine{Enabled: true},
706+
},
707+
&sast_contract.SastResponse{
708+
SastEnabled: false,
709+
LocalCodeEngine: sast_contract.LocalCodeEngine{Enabled: false},
710+
},
711+
true,
712+
false,
713+
)
714+
})
715+
716+
t.Run("respects organization changes (full chain)", func(t *testing.T) {
717+
testutils.CheckCacheRespectOrgDependency(
718+
t,
719+
code_workflow.ConfigurarionSlceEnabled,
720+
func(isFirstCall bool) any {
721+
return &sast_contract.SastResponse{
722+
SastEnabled: false,
723+
LocalCodeEngine: sast_contract.LocalCodeEngine{Enabled: isFirstCall},
724+
}
725+
},
726+
InitCodeWorkflow,
727+
true,
728+
false,
729+
)
716730
})
717731

718732
t.Run("callback returns false and error when API call fails and existing value is nil", func(t *testing.T) {

0 commit comments

Comments
 (0)