diff --git a/client.go b/client.go index 5e17083..c9bbbcd 100644 --- a/client.go +++ b/client.go @@ -14,6 +14,7 @@ type Client struct { mu sync.RWMutex context Context switchers map[string]*Switcher + snapshot *Snapshot authMu sync.Mutex authToken string @@ -79,6 +80,45 @@ func (c *Client) Context() Context { return c.context } +func LoadSnapshot(options *LoadSnapshotOptions) (int, error) { + return defaultClient().LoadSnapshot(options) +} + +func (c *Client) LoadSnapshot(options *LoadSnapshotOptions) (int, error) { + snapshot, err := loadSnapshotFromFile(c.Context()) + if err != nil { + return 0, err + } + + c.mu.Lock() + c.snapshot = snapshot + c.mu.Unlock() + + return c.SnapshotVersion(), nil +} + +func SnapshotVersion() int { + return defaultClient().SnapshotVersion() +} + +func (c *Client) SnapshotVersion() int { + c.mu.RLock() + defer c.mu.RUnlock() + + if c.snapshot == nil { + return 0 + } + + return c.snapshot.Domain.Version +} + +func (c *Client) snapshotState() *Snapshot { + c.mu.RLock() + defer c.mu.RUnlock() + + return c.snapshot +} + func defaultClient() *Client { if client := globalClient.Load(); client != nil { return client diff --git a/errors.go b/errors.go index 18b0a2f..b9c019b 100644 --- a/errors.go +++ b/errors.go @@ -18,6 +18,14 @@ type RemoteCriteriaError struct { RemoteError } +type LocalCriteriaError struct { + message string +} + +func (e *LocalCriteriaError) Error() string { + return e.message +} + func newRemoteAuthError(format string, args ...any) error { return &RemoteAuthError{RemoteError: RemoteError{message: fmt.Sprintf(format, args...)}} } @@ -25,3 +33,7 @@ func newRemoteAuthError(format string, args ...any) error { func newRemoteCriteriaError(format string, args ...any) error { return &RemoteCriteriaError{RemoteError: RemoteError{message: fmt.Sprintf(format, args...)}} } + +func newLocalCriteriaError(format string, args ...any) error { + return &LocalCriteriaError{message: fmt.Sprintf(format, args...)} +} diff --git a/local_test.go b/local_test.go new file mode 100644 index 0000000..c0687c6 --- /dev/null +++ b/local_test.go @@ -0,0 +1,341 @@ +package client + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSnapshotLoading(t *testing.T) { + t.Run("should load snapshot version from a local file", func(t *testing.T) { + BuildContext(Context{ + Domain: "My Domain", + Options: ContextOptions{ + Local: true, + SnapshotLocation: snapshotFixtureDir(), + }, + }) + + assert.Equal(t, 0, SnapshotVersion()) + + version, err := LoadSnapshot(nil) + + assert.NoError(t, err) + assert.Equal(t, 1, version) + assert.Equal(t, 1, SnapshotVersion()) + }) + + t.Run("should return an error when the snapshot file is malformed", func(t *testing.T) { + BuildContext(Context{ + Domain: "My Domain", + Options: ContextOptions{ + Local: true, + SnapshotLocation: snapshotFixtureDir(), + }, + Environment: "default_malformed", + }) + + version, err := LoadSnapshot(nil) + + assert.Error(t, err) + assert.Zero(t, version) + }) + + t.Run("should return an error when the snapshot file is not accessible", func(t *testing.T) { + snapshotLocation := filepath.Join(t.TempDir(), "snapshot-location-file") + writeErr := os.WriteFile(snapshotLocation, []byte("not-a-directory"), 0o644) + assert.NoError(t, writeErr) + + BuildContext(Context{ + Domain: "My Domain", + Options: ContextOptions{ + Local: true, + SnapshotLocation: snapshotLocation, + }, + Environment: "default", + }) + + version, err := LoadSnapshot(nil) + + assert.Error(t, err) + assert.Zero(t, version) + }) + + t.Run("should return an error when the snapshot file path cannot be created", func(t *testing.T) { + BuildContext(Context{ + Domain: "My Domain", + Options: ContextOptions{ + Local: true, + SnapshotLocation: t.TempDir(), + }, + Environment: filepath.Join("nested", "missing"), + }) + + version, err := LoadSnapshot(nil) + + assert.Error(t, err) + assert.Zero(t, version) + }) + + t.Run("should create a clean snapshot when no file exists", func(t *testing.T) { + snapshotDir := t.TempDir() + + BuildContext(Context{ + Domain: "My Domain", + Environment: "generated-clean", + Options: ContextOptions{ + Local: true, + SnapshotLocation: snapshotDir, + }, + }) + + version, err := LoadSnapshot(nil) + + assert.NoError(t, err) + assert.Equal(t, 0, version) + assert.Equal(t, 0, SnapshotVersion()) + + content, readErr := os.ReadFile(filepath.Join(snapshotDir, "generated-clean.json")) + assert.NoError(t, readErr) + + var snapshot Snapshot + unmarshalErr := json.Unmarshal(content, &snapshot) + assert.NoError(t, unmarshalErr) + assert.Equal(t, 0, snapshot.Domain.Version) + }) +} + +func TestSwitcherLocalEvaluation(t *testing.T) { + t.Run("should use local snapshot to evaluate a switcher without strategies", func(t *testing.T) { + useLocalSnapshotFixture(t, "default") + + got, err := GetSwitcher("FF2FOR2022").IsOn() + + assert.NoError(t, err) + assert.True(t, got) + }) + + t.Run("should use local snapshot to evaluate a switcher with value validation", func(t *testing.T) { + useLocalSnapshotFixture(t, "default_value_only") + + got, err := GetSwitcher("FF2FOR2020").CheckValue("Japan").IsOn() + + assert.NoError(t, err) + assert.True(t, got) + }) + + t.Run("should return disabled when a value strategy does not receive any input", func(t *testing.T) { + useLocalSnapshotFixture(t, "default_value_only") + + got, err := GetSwitcher("FF2FOR2020").IsOnWithDetails() + + assert.NoError(t, err) + assert.False(t, got.Result) + assert.Equal(t, "Strategy 'VALUE_VALIDATION' did not receive any input", got.Reason) + }) + + t.Run("should return enabled when value EXIST matches the input", func(t *testing.T) { + useLocalSnapshotFixture(t, "default_value_only") + + got, err := GetSwitcher("VALUE_EXIST").CheckValue("guest").IsOn() + + assert.NoError(t, err) + assert.True(t, got) + }) + + t.Run("should return disabled when value EXIST does not match the input", func(t *testing.T) { + useLocalSnapshotFixture(t, "default_value_only") + + got, err := GetSwitcher("VALUE_EXIST").CheckValue("anonymous").IsOnWithDetails() + + assert.NoError(t, err) + assert.False(t, got.Result) + assert.Equal(t, "Strategy 'VALUE_VALIDATION' does not agree", got.Reason) + }) + + t.Run("should return disabled when operation is invalid for the strategy", func(t *testing.T) { + useLocalSnapshotFixture(t, "default_value_only") + + got, err := GetSwitcher("HAS_ALL").CheckValue("anonymous").IsOnWithDetails() + + assert.NoError(t, err) + assert.False(t, got.Result) + assert.Equal(t, "Strategy 'VALUE_VALIDATION' does not agree", got.Reason) + }) + + t.Run("should return disabled when strategy input does not match snapshot settings", func(t *testing.T) { + useLocalSnapshotFixture(t, "default_value_only") + + got, err := GetSwitcher("FF2FOR2020").CheckNetwork("10.0.0.3").IsOnWithDetails() + + assert.NoError(t, err) + assert.False(t, got.Result) + assert.Equal(t, "Strategy 'VALUE_VALIDATION' does not agree", got.Reason) + }) + + t.Run("should return enabled when value EQUAL matches the input", func(t *testing.T) { + useLocalSnapshotFixture(t, "default_value_only") + + got, err := GetSwitcher("VALUE_EQUAL").CheckValue("pro-user").IsOn() + + assert.NoError(t, err) + assert.True(t, got) + }) + + t.Run("should return disabled when the domain is deactivated", func(t *testing.T) { + useLocalSnapshotFixture(t, "default_disabled") + + got, err := GetSwitcher("FEATURE").IsOnWithDetails() + + assert.NoError(t, err) + assert.False(t, got.Result) + assert.Equal(t, "Domain is disabled", got.Reason) + }) + + t.Run("should return disabled when the group is deactivated", func(t *testing.T) { + useLocalSnapshotFixture(t, "default") + + got, err := GetSwitcher("FF2FOR2040").IsOnWithDetails() + + assert.NoError(t, err) + assert.False(t, got.Result) + assert.Equal(t, "Group disabled", got.Reason) + }) + + t.Run("should return disabled when the config is deactivated", func(t *testing.T) { + useLocalSnapshotFixture(t, "default") + + got, err := GetSwitcher("FF2FOR2031").IsOnWithDetails() + + assert.NoError(t, err) + assert.False(t, got.Result) + assert.Equal(t, "Config disabled", got.Reason) + }) + + t.Run("should return enabled when the strategy is deactivated", func(t *testing.T) { + useLocalSnapshotFixture(t, "default") + + got, err := GetSwitcher("FF2FOR2021").CheckNetwork("10.0.0.3").IsOn() + + assert.NoError(t, err) + assert.True(t, got) + }) + + t.Run("should return enabled when the network input is inside a CIDR range", func(t *testing.T) { + useLocalSnapshotFixture(t, "default_network_only") + + got, err := GetSwitcher("NET_EXIST_CIDR").CheckNetwork("10.0.0.3").IsOn() + + assert.NoError(t, err) + assert.True(t, got) + }) + + t.Run("should return disabled when the network input is outside a CIDR range", func(t *testing.T) { + useLocalSnapshotFixture(t, "default_network_only") + + got, err := GetSwitcher("NET_EXIST_CIDR").CheckNetwork("192.168.1.2").IsOnWithDetails() + + assert.NoError(t, err) + assert.False(t, got.Result) + assert.Equal(t, "Strategy 'NETWORK_VALIDATION' does not agree", got.Reason) + }) + + t.Run("should return enabled when the network input exactly matches an IP value", func(t *testing.T) { + useLocalSnapshotFixture(t, "default_network_only") + + got, err := GetSwitcher("NET_EXIST_IP").CheckNetwork("10.0.0.3").IsOn() + + assert.NoError(t, err) + assert.True(t, got) + }) + + t.Run("should return disabled when the network input is invalid", func(t *testing.T) { + useLocalSnapshotFixture(t, "default_network_only") + + got, err := GetSwitcher("NET_EXIST_CIDR").CheckNetwork("not-an-ip").IsOnWithDetails() + + assert.NoError(t, err) + assert.False(t, got.Result) + assert.Equal(t, "Strategy 'NETWORK_VALIDATION' does not agree", got.Reason) + }) + + t.Run("should return enabled when NOT_EXIST network strategy does not match the input", func(t *testing.T) { + useLocalSnapshotFixture(t, "default_network_only") + + got, err := GetSwitcher("NET_NOT_EXIST_CIDR").CheckNetwork("192.168.1.10").IsOn() + + assert.NoError(t, err) + assert.True(t, got) + }) + + t.Run("should return disabled when NOT_EXIST network strategy matches the input", func(t *testing.T) { + useLocalSnapshotFixture(t, "default_network_only") + + got, err := GetSwitcher("NET_NOT_EXIST_CIDR").CheckNetwork("10.0.0.3").IsOnWithDetails() + + assert.NoError(t, err) + assert.False(t, got.Result) + assert.Equal(t, "Strategy 'NETWORK_VALIDATION' does not agree", got.Reason) + }) + + t.Run("should return disabled when relay is enabled and relay restriction is active", func(t *testing.T) { + useLocalSnapshotFixture(t, "default") + + got, err := GetSwitcher("USECASE103").IsOnWithDetails() + + assert.NoError(t, err) + assert.False(t, got.Result) + assert.Equal(t, "Config has relay enabled", got.Reason) + }) + + t.Run("should return an error when the key is not found in the snapshot", func(t *testing.T) { + useLocalSnapshotFixture(t, "default") + + _, err := GetSwitcher("INVALID_KEY").IsOn() + + assert.Error(t, err) + var localCriteriaErr *LocalCriteriaError + assert.ErrorAs(t, err, &localCriteriaErr) + assert.EqualError(t, err, "Config with key 'INVALID_KEY' not found in the snapshot") + }) + + t.Run("should return an error when no snapshot has been loaded", func(t *testing.T) { + BuildContext(Context{ + Domain: "My Domain", + Options: ContextOptions{ + Local: true, + SnapshotLocation: filepath.Join(t.TempDir(), "missing"), + }, + }) + + _, err := GetSwitcher("FF2FOR2022").IsOn() + + assert.Error(t, err) + var localCriteriaErr *LocalCriteriaError + assert.ErrorAs(t, err, &localCriteriaErr) + assert.EqualError(t, err, "Snapshot not loaded. Try to use 'Client.load_snapshot()'") + }) +} + +func useLocalSnapshotFixture(t *testing.T, environment string) { + t.Helper() + + BuildContext(Context{ + Domain: "My Domain", + Environment: environment, + Options: ContextOptions{ + Local: true, + SnapshotLocation: snapshotFixtureDir(), + }, + }) + + _, err := LoadSnapshot(nil) + assert.NoError(t, err) +} + +func snapshotFixtureDir() string { + return filepath.Join("tests", "snapshots") +} diff --git a/remote.go b/remote.go index 335afc9..a195bac 100644 --- a/remote.go +++ b/remote.go @@ -15,7 +15,8 @@ import ( ) const ( - StrategyValue = "VALUE_VALIDATION" + StrategyValue = "VALUE_VALIDATION" + StrategyNetwork = "NETWORK_VALIDATION" ) type criteriaEntry struct { diff --git a/resolver.go b/resolver.go new file mode 100644 index 0000000..e835562 --- /dev/null +++ b/resolver.go @@ -0,0 +1,133 @@ +package client + +import ( + "net" + "slices" +) + +func checkLocalCriteria(snapshot *Snapshot, switcher *Switcher) (ResultDetail, error) { + if snapshot == nil { + return ResultDetail{}, newLocalCriteriaError("Snapshot not loaded. Try to use 'Client.load_snapshot()'") + } + + return checkLocalDomain(snapshot, switcher) +} + +func checkLocalDomain(snapshot *Snapshot, switcher *Switcher) (ResultDetail, error) { + if !snapshot.Domain.Activated { + return ResultDetail{Result: false, Reason: "Domain is disabled", Metadata: map[string]any{}}, nil + } + + return checkLocalGroup(snapshot.Domain.Groups, switcher) +} + +func checkLocalGroup(groups []SnapshotGroup, switcher *Switcher) (ResultDetail, error) { + key := switcher.key + + for _, group := range groups { + for _, config := range group.Configs { + if config.Key != key { + continue + } + + if !group.Activated { + return ResultDetail{Result: false, Reason: "Group disabled", Metadata: map[string]any{}}, nil + } + + return checkLocalConfig(config, switcher) + } + } + + return ResultDetail{}, newLocalCriteriaError("Config with key '%s' not found in the snapshot", switcher.key) +} + +func checkLocalConfig(config SnapshotConfig, switcher *Switcher) (ResultDetail, error) { + if !config.Activated { + return ResultDetail{Result: false, Reason: "Config disabled", Metadata: map[string]any{}}, nil + } + + if config.Relay != nil && config.Relay.Activated && switcher.client.Context().Options.RestrictRelay { + return ResultDetail{Result: false, Reason: "Config has relay enabled", Metadata: map[string]any{}}, nil + } + + return checkLocalStrategies(config.Strategies, switcher.entries) +} + +func checkLocalStrategies(strategies []SnapshotStrategy, entries []criteriaEntry) (ResultDetail, error) { + activeStrategies := 0 + + for _, strategy := range strategies { + if !strategy.Activated { + continue + } + + activeStrategies++ + if len(entries) == 0 { + return ResultDetail{Result: false, Reason: "Strategy '" + strategy.Strategy + "' did not receive any input", Metadata: map[string]any{}}, nil + } + + entry, ok := findCriteriaEntry(entries, strategy.Strategy) + if !ok || !evaluateLocalStrategy(strategy, entry.Input) { + return ResultDetail{Result: false, Reason: "Strategy '" + strategy.Strategy + "' does not agree", Metadata: map[string]any{}}, nil + } + } + + return ResultDetail{Result: true, Reason: "Success", Metadata: map[string]any{}}, nil +} + +func findCriteriaEntry(entries []criteriaEntry, strategy string) (criteriaEntry, bool) { + for _, entry := range entries { + if entry.Strategy == strategy { + return entry, true + } + } + + return criteriaEntry{}, false +} + +func evaluateLocalStrategy(strategy SnapshotStrategy, input string) bool { + switch strategy.Strategy { + case StrategyValue: + switch strategy.Operation { + case "EXIST", "EQUAL": + return containsString(strategy.Values, input) + case "NOT_EXIST", "NOT_EQUAL": + return !containsString(strategy.Values, input) + } + case StrategyNetwork: + switch strategy.Operation { + case "EXIST": + return networkExists(strategy.Values, input) + case "NOT_EXIST": + return !networkExists(strategy.Values, input) + } + } + + return false +} + +func containsString(values []string, target string) bool { + return slices.Contains(values, target) +} + +func networkExists(values []string, input string) bool { + ip := net.ParseIP(input) + if ip == nil { + return false + } + + for _, value := range values { + if _, network, err := net.ParseCIDR(value); err == nil { + if network.Contains(ip) { + return true + } + continue + } + + if parsed := net.ParseIP(value); parsed != nil && parsed.Equal(ip) { + return true + } + } + + return false +} diff --git a/snapshot.go b/snapshot.go new file mode 100644 index 0000000..f145da0 --- /dev/null +++ b/snapshot.go @@ -0,0 +1,88 @@ +package client + +import ( + "encoding/json" + "os" + "path/filepath" +) + +type LoadSnapshotOptions struct { + FetchRemote bool + WatchSnapshot bool +} + +type Snapshot struct { + Domain SnapshotDomain `json:"domain"` +} + +type SnapshotDomain struct { + Name string `json:"name"` + Activated bool `json:"activated"` + Version int `json:"version"` + Groups []SnapshotGroup `json:"group"` + Description string `json:"description"` +} + +type SnapshotGroup struct { + Name string `json:"name"` + Activated bool `json:"activated"` + Configs []SnapshotConfig `json:"config"` + Description string `json:"description"` +} + +type SnapshotConfig struct { + Key string `json:"key"` + Activated bool `json:"activated"` + Strategies []SnapshotStrategy `json:"strategies"` + Relay *SnapshotRelay `json:"relay"` + Description string `json:"description"` +} + +type SnapshotStrategy struct { + Strategy string `json:"strategy"` + Activated bool `json:"activated"` + Operation string `json:"operation"` + Values []string `json:"values"` +} + +type SnapshotRelay struct { + Type string `json:"type"` + Activated bool `json:"activated"` +} + +func createDefaultSnapshot(ctx Context, snapshotFile string) (*Snapshot, error) { + snapshot := &Snapshot{ + Domain: SnapshotDomain{ + Version: 0, + }, + } + + if ctx.Options.SnapshotLocation != "" { + if err := os.MkdirAll(ctx.Options.SnapshotLocation, 0o755); err != nil { + return nil, err + } + + content, _ := json.MarshalIndent(snapshot, "", " ") + if err := os.WriteFile(snapshotFile, content, 0o644); err != nil { + return nil, err + } + } + + return snapshot, nil +} + +func loadSnapshotFromFile(ctx Context) (*Snapshot, error) { + snapshotFile := filepath.Join(ctx.Options.SnapshotLocation, ctx.Environment+".json") + if _, err := os.Stat(snapshotFile); err != nil { + return createDefaultSnapshot(ctx, snapshotFile) + } + + content, _ := os.ReadFile(snapshotFile) + + var snapshot Snapshot + if err := json.Unmarshal(content, &snapshot); err != nil { + return nil, err + } + + return &snapshot, nil +} diff --git a/switcher.go b/switcher.go index 48a1b73..3a2dcea 100644 --- a/switcher.go +++ b/switcher.go @@ -42,12 +42,19 @@ func (s *Switcher) Validate() error { } func (s *Switcher) CheckValue(input string) *Switcher { - s.entries = []criteriaEntry{ - { - Strategy: StrategyValue, - Input: input, - }, - } + s.entries = upsertEntry(s.entries, criteriaEntry{ + Strategy: StrategyValue, + Input: input, + }) + + return s +} + +func (s *Switcher) CheckNetwork(input string) *Switcher { + s.entries = upsertEntry(s.entries, criteriaEntry{ + Strategy: StrategyNetwork, + Input: input, + }) return s } @@ -83,6 +90,10 @@ func (s *Switcher) IsOnWithDetails() (ResultDetail, error) { } func (s *Switcher) submit(showDetails bool) (ResultDetail, error) { + if s.client.Context().Options.Local { + return checkLocalCriteria(s.client.snapshotState(), s) + } + if err := s.Validate(); err != nil { return ResultDetail{}, err } @@ -98,3 +109,14 @@ func (s *Switcher) submit(showDetails bool) (ResultDetail, error) { return s.client.checkCriteria(token, s, showDetails) } + +func upsertEntry(entries []criteriaEntry, next criteriaEntry) []criteriaEntry { + for i := range entries { + if entries[i].Strategy == next.Strategy { + entries[i] = next + return entries + } + } + + return append(entries, next) +} diff --git a/tests/snapshots/default.json b/tests/snapshots/default.json new file mode 100644 index 0000000..b896e47 --- /dev/null +++ b/tests/snapshots/default.json @@ -0,0 +1,163 @@ +{ + "domain": { + "name": "Business", + "description": "Business description", + "activated": true, + "version": 1, + "group": [ + { + "name": "Rollout 2020", + "description": "Changes that will be applied during the rollout", + "activated": true, + "config": [ + { + "key": "FF2FOR2020", + "description": "Feature Flag", + "activated": true, + "strategies": [ + { + "strategy": "NETWORK_VALIDATION", + "activated": true, + "operation": "EXIST", + "values": [ + "10.0.0.3/24" + ] + }, + { + "strategy": "VALUE_VALIDATION", + "activated": true, + "operation": "NOT_EXIST", + "values": [ + "USA", + "Canada", + "Australia", + "Africa" + ] + } + ], + "components": [] + }, + { + "key": "FF2FOR2021", + "description": "Strategy disabled", + "activated": true, + "strategies": [ + { + "strategy": "NETWORK_VALIDATION", + "activated": false, + "operation": "EXIST", + "values": [ + "10.0.0.3/24" + ] + } + ], + "components": [] + }, + { + "key": "FF2FOR2022", + "description": "No strategies", + "activated": true, + "components": [] + }, + { + "key": "FF2FOR2023", + "description": "Feature Flag - Payload Strategy", + "activated": true, + "strategies": [ + { + "strategy": "PAYLOAD_VALIDATION", + "activated": true, + "operation": "HAS_ALL", + "values": [ + "id", + "user", + "user.login", + "user.role" + ] + } + ], + "components": [] + }, + { + "key": "FF2FOR2024", + "description": "reDOS safe test", + "activated": true, + "strategies": [ + { + "strategy": "REGEX_VALIDATION", + "activated": true, + "operation": "EXIST", + "values": [ + "^(([a-z])+.)+[A-Z]([a-z])+$" + ] + } + ], + "components": [] + } + ] + }, + { + "name": "Rollout 2030", + "description": "Changes that will be applied during the rollout", + "activated": true, + "config": [ + { + "key": "FF2FOR2030", + "description": "Feature Flag", + "activated": true, + "strategies": [], + "components": [] + }, + { + "key": "FF2FOR2031", + "description": "Feature Flag disabled", + "activated": false, + "strategies": [], + "components": [] + } + ] + }, + { + "name": "Rollout 2040", + "description": "Project is disabled", + "activated": false, + "config": [ + { + "key": "FF2FOR2040", + "description": "Feature Flag", + "activated": true, + "strategies": [], + "components": [] + } + ] + }, + { + "name": "Relay test", + "description": "Relay group", + "activated": true, + "config": [ + { + "key": "USECASE103", + "description": "Relay enabled", + "activated": true, + "relay": { + "type": "VALIDATOR", + "activated": true + }, + "components": [] + }, + { + "key": "USECASE104", + "description": "Relay disabled", + "relay": { + "type": "VALIDATOR", + "activated": false + }, + "activated": true, + "components": [] + } + ] + } + ] + } +} diff --git a/tests/snapshots/default_disabled.json b/tests/snapshots/default_disabled.json new file mode 100644 index 0000000..5cd9e37 --- /dev/null +++ b/tests/snapshots/default_disabled.json @@ -0,0 +1,9 @@ +{ + "domain": { + "name": "Business", + "description": "Business description", + "activated": false, + "version": 1, + "group": [] + } +} diff --git a/tests/snapshots/default_malformed.json b/tests/snapshots/default_malformed.json new file mode 100644 index 0000000..2490329 --- /dev/null +++ b/tests/snapshots/default_malformed.json @@ -0,0 +1,7 @@ +{ + "domain": { + "name": "Business", + "activated": true, + "version": + } +} diff --git a/tests/snapshots/default_network_only.json b/tests/snapshots/default_network_only.json new file mode 100644 index 0000000..73951d2 --- /dev/null +++ b/tests/snapshots/default_network_only.json @@ -0,0 +1,51 @@ +{ + "domain": { + "name": "Business", + "activated": true, + "version": 1, + "group": [ + { + "name": "Network tests", + "activated": true, + "config": [ + { + "key": "NET_EXIST_CIDR", + "activated": true, + "strategies": [ + { + "strategy": "NETWORK_VALIDATION", + "activated": true, + "operation": "EXIST", + "values": ["10.0.0.0/24"] + } + ] + }, + { + "key": "NET_EXIST_IP", + "activated": true, + "strategies": [ + { + "strategy": "NETWORK_VALIDATION", + "activated": true, + "operation": "EXIST", + "values": ["10.0.0.3"] + } + ] + }, + { + "key": "NET_NOT_EXIST_CIDR", + "activated": true, + "strategies": [ + { + "strategy": "NETWORK_VALIDATION", + "activated": true, + "operation": "NOT_EXIST", + "values": ["10.0.0.0/24"] + } + ] + } + ] + } + ] + } +} diff --git a/tests/snapshots/default_value_only.json b/tests/snapshots/default_value_only.json new file mode 100644 index 0000000..b6744ea --- /dev/null +++ b/tests/snapshots/default_value_only.json @@ -0,0 +1,85 @@ +{ + "domain": { + "name": "Business", + "description": "Business description", + "activated": true, + "version": 1, + "group": [ + { + "name": "Rollout 2020", + "description": "Value-only checks for current Go local slice", + "activated": true, + "config": [ + { + "key": "FF2FOR2020", + "description": "Feature Flag", + "activated": true, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": true, + "operation": "NOT_EXIST", + "values": [ + "USA", + "Canada", + "Australia", + "Africa" + ] + } + ], + "components": [] + }, + { + "key": "VALUE_EXIST", + "description": "Value EXIST strategy", + "activated": true, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": true, + "operation": "EXIST", + "values": [ + "guest", + "admin" + ] + } + ], + "components": [] + }, + { + "key": "VALUE_EQUAL", + "description": "Value EQUAL strategy", + "activated": true, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": true, + "operation": "EQUAL", + "values": [ + "pro-user" + ] + } + ], + "components": [] + }, + { + "key": "HAS_ALL", + "description": "Invalid operation HAS_ALL strategy", + "activated": true, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": true, + "operation": "HAS_ALL", + "values": [ + "pro-user" + ] + } + ], + "components": [] + } + ] + } + ] + } +}