diff --git a/resources/default.json b/resources/default.json deleted file mode 100644 index 0769846..0000000 --- a/resources/default.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "domain": { - "group": [ - { - "name": "Release 1", - "description": "Showcase configuration", - "activated": true, - "config": [ - { - "key": "MY_SWITCHER_1", - "description": "My first switcher", - "activated": true, - "strategies": [ - { - "strategy": "VALUE_VALIDATION", - "activated": false, - "operation": "EXIST", - "values": [ - "user_1" - ] - } - ], - "components": [ - "switcher-playground" - ] - }, - { - "key": "MY_SWITCHER_2", - "description": "", - "activated": false, - "strategies": [], - "components": [ - "switcher-playground" - ] - }, - { - "key": "MY_SWITCHER_3", - "description": "", - "activated": true, - "strategies": [], - "components": [ - "benchmark" - ] - } - ] - } - ] - } - } \ No newline at end of file diff --git a/resources/fixtures/changed_config.json b/resources/fixtures/changed_config.json new file mode 100644 index 0000000..c9eed93 --- /dev/null +++ b/resources/fixtures/changed_config.json @@ -0,0 +1,49 @@ +{ + "domain": { + "group": [ + { + "name": "Release 1", + "description": "Showcase configuration", + "activated": true, + "config": [ + { + "key": "MY_SWITCHER_1", + "description": "My first switcher", + "activated": true, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": false, + "operation": "EXIST", + "values": [ + "user_1" + ] + } + ], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_2", + "description": "New description", + "activated": true, + "strategies": [], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_3", + "description": "", + "activated": true, + "strategies": [], + "components": [ + "benchmark" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/resources/fixtures/changed_group.json b/resources/fixtures/changed_group.json new file mode 100644 index 0000000..4873c93 --- /dev/null +++ b/resources/fixtures/changed_group.json @@ -0,0 +1,49 @@ +{ + "domain": { + "group": [ + { + "name": "Release 1", + "description": "New description", + "activated": false, + "config": [ + { + "key": "MY_SWITCHER_1", + "description": "My first switcher", + "activated": true, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": false, + "operation": "EXIST", + "values": [ + "user_1" + ] + } + ], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_2", + "description": "", + "activated": false, + "strategies": [], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_3", + "description": "", + "activated": true, + "strategies": [], + "components": [ + "benchmark" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/resources/fixtures/changed_strategy.json b/resources/fixtures/changed_strategy.json new file mode 100644 index 0000000..97b369b --- /dev/null +++ b/resources/fixtures/changed_strategy.json @@ -0,0 +1,49 @@ +{ + "domain": { + "group": [ + { + "name": "Release 1", + "description": "Showcase configuration", + "activated": true, + "config": [ + { + "key": "MY_SWITCHER_1", + "description": "My first switcher", + "activated": true, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": true, + "operation": "EXIST", + "values": [ + "user_1" + ] + } + ], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_2", + "description": "", + "activated": false, + "strategies": [], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_3", + "description": "", + "activated": true, + "strategies": [], + "components": [ + "benchmark" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/resources/fixtures/default.json b/resources/fixtures/default.json new file mode 100644 index 0000000..fff0302 --- /dev/null +++ b/resources/fixtures/default.json @@ -0,0 +1,49 @@ +{ + "domain": { + "group": [ + { + "name": "Release 1", + "description": "Showcase configuration", + "activated": true, + "config": [ + { + "key": "MY_SWITCHER_1", + "description": "My first switcher", + "activated": true, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": false, + "operation": "EXIST", + "values": [ + "user_1" + ] + } + ], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_2", + "description": "", + "activated": false, + "strategies": [], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_3", + "description": "", + "activated": true, + "strategies": [], + "components": [ + "benchmark" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/resources/fixtures/default_empty.json b/resources/fixtures/default_empty.json new file mode 100644 index 0000000..93c62a8 --- /dev/null +++ b/resources/fixtures/default_empty.json @@ -0,0 +1,5 @@ +{ + "domain": { + "group": [] + } +} \ No newline at end of file diff --git a/resources/fixtures/default_empty_config.json b/resources/fixtures/default_empty_config.json new file mode 100644 index 0000000..185959b --- /dev/null +++ b/resources/fixtures/default_empty_config.json @@ -0,0 +1,12 @@ +{ + "domain": { + "group": [ + { + "name": "Release 1", + "description": "Showcase configuration", + "activated": true, + "config": [] + } + ] + } +} \ No newline at end of file diff --git a/resources/fixtures/deleted_component.json b/resources/fixtures/deleted_component.json new file mode 100644 index 0000000..9110e1b --- /dev/null +++ b/resources/fixtures/deleted_component.json @@ -0,0 +1,47 @@ +{ + "domain": { + "group": [ + { + "name": "Release 1", + "description": "Showcase configuration", + "activated": true, + "config": [ + { + "key": "MY_SWITCHER_1", + "description": "My first switcher", + "activated": true, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": false, + "operation": "EXIST", + "values": [ + "user_1" + ] + } + ], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_2", + "description": "", + "activated": false, + "strategies": [], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_3", + "description": "", + "activated": true, + "strategies": [], + "components": [] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/resources/fixtures/deleted_config.json b/resources/fixtures/deleted_config.json new file mode 100644 index 0000000..9128076 --- /dev/null +++ b/resources/fixtures/deleted_config.json @@ -0,0 +1,40 @@ +{ + "domain": { + "group": [ + { + "name": "Release 1", + "description": "Showcase configuration", + "activated": true, + "config": [ + { + "key": "MY_SWITCHER_1", + "description": "My first switcher", + "activated": true, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": false, + "operation": "EXIST", + "values": [ + "user_1" + ] + } + ], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_2", + "description": "", + "activated": false, + "strategies": [], + "components": [ + "switcher-playground" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/resources/fixtures/deleted_group.json b/resources/fixtures/deleted_group.json new file mode 100644 index 0000000..93c62a8 --- /dev/null +++ b/resources/fixtures/deleted_group.json @@ -0,0 +1,5 @@ +{ + "domain": { + "group": [] + } +} \ No newline at end of file diff --git a/resources/fixtures/deleted_strategy.json b/resources/fixtures/deleted_strategy.json new file mode 100644 index 0000000..cbfce03 --- /dev/null +++ b/resources/fixtures/deleted_strategy.json @@ -0,0 +1,40 @@ +{ + "domain": { + "group": [ + { + "name": "Release 1", + "description": "Showcase configuration", + "activated": true, + "config": [ + { + "key": "MY_SWITCHER_1", + "description": "My first switcher", + "activated": true, + "strategies": [], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_2", + "description": "", + "activated": false, + "strategies": [], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_3", + "description": "", + "activated": true, + "strategies": [], + "components": [ + "benchmark" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/resources/fixtures/deleted_strategy_value.json b/resources/fixtures/deleted_strategy_value.json new file mode 100644 index 0000000..a81bfd9 --- /dev/null +++ b/resources/fixtures/deleted_strategy_value.json @@ -0,0 +1,47 @@ +{ + "domain": { + "group": [ + { + "name": "Release 1", + "description": "Showcase configuration", + "activated": true, + "config": [ + { + "key": "MY_SWITCHER_1", + "description": "My first switcher", + "activated": true, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": false, + "operation": "EXIST", + "values": [] + } + ], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_2", + "description": "", + "activated": false, + "strategies": [], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_3", + "description": "", + "activated": true, + "strategies": [], + "components": [ + "benchmark" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/resources/fixtures/new_component.json b/resources/fixtures/new_component.json new file mode 100644 index 0000000..2b9a6e5 --- /dev/null +++ b/resources/fixtures/new_component.json @@ -0,0 +1,50 @@ +{ + "domain": { + "group": [ + { + "name": "Release 1", + "description": "Showcase configuration", + "activated": true, + "config": [ + { + "key": "MY_SWITCHER_1", + "description": "My first switcher", + "activated": true, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": false, + "operation": "EXIST", + "values": [ + "user_1" + ] + } + ], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_2", + "description": "", + "activated": false, + "strategies": [], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_3", + "description": "", + "activated": true, + "strategies": [], + "components": [ + "benchmark", + "new_component" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/resources/fixtures/new_config.json b/resources/fixtures/new_config.json new file mode 100644 index 0000000..64b6d91 --- /dev/null +++ b/resources/fixtures/new_config.json @@ -0,0 +1,58 @@ +{ + "domain": { + "group": [ + { + "name": "Release 1", + "description": "Showcase configuration", + "activated": true, + "config": [ + { + "key": "MY_SWITCHER_1", + "description": "My first switcher", + "activated": true, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": false, + "operation": "EXIST", + "values": [ + "user_1" + ] + } + ], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_2", + "description": "", + "activated": false, + "strategies": [], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_3", + "description": "", + "activated": true, + "strategies": [], + "components": [ + "benchmark" + ] + }, + { + "key": "MY_SWITCHER_4", + "description": "", + "activated": true, + "strategies": [], + "components": [ + "benchmark" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/resources/fixtures/new_group.json b/resources/fixtures/new_group.json new file mode 100644 index 0000000..3f2ee26 --- /dev/null +++ b/resources/fixtures/new_group.json @@ -0,0 +1,63 @@ +{ + "domain": { + "group": [ + { + "name": "Release 1", + "description": "Showcase configuration", + "activated": true, + "config": [ + { + "key": "MY_SWITCHER_1", + "description": "My first switcher", + "activated": true, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": false, + "operation": "EXIST", + "values": [ + "user_1" + ] + } + ], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_2", + "description": "", + "activated": false, + "strategies": [], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_3", + "description": "", + "activated": true, + "strategies": [], + "components": [ + "benchmark" + ] + } + ] + }, + { + "name": "Release 2", + "description": "Showcase configuration 2", + "activated": true, + "config": [ + { + "key": "MY_SWITCHER_4", + "activated": false, + "components": [ + "switcher-playground" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/resources/fixtures/new_strategy.json b/resources/fixtures/new_strategy.json new file mode 100644 index 0000000..7004b38 --- /dev/null +++ b/resources/fixtures/new_strategy.json @@ -0,0 +1,58 @@ +{ + "domain": { + "group": [ + { + "name": "Release 1", + "description": "Showcase configuration", + "activated": true, + "config": [ + { + "key": "MY_SWITCHER_1", + "description": "My first switcher", + "activated": true, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": false, + "operation": "EXIST", + "values": [ + "user_1" + ] + } + ], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_2", + "description": "", + "activated": false, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": true, + "operation": "EXIST", + "values": [ + "user_2" + ] + } + ], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_3", + "description": "", + "activated": true, + "strategies": [], + "components": [ + "benchmark" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/resources/fixtures/new_strategy_value.json b/resources/fixtures/new_strategy_value.json new file mode 100644 index 0000000..c2be7dc --- /dev/null +++ b/resources/fixtures/new_strategy_value.json @@ -0,0 +1,50 @@ +{ + "domain": { + "group": [ + { + "name": "Release 1", + "description": "Showcase configuration", + "activated": true, + "config": [ + { + "key": "MY_SWITCHER_1", + "description": "My first switcher", + "activated": true, + "strategies": [ + { + "strategy": "VALUE_VALIDATION", + "activated": false, + "operation": "EXIST", + "values": [ + "user_1", + "user_2" + ] + } + ], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_2", + "description": "", + "activated": false, + "strategies": [], + "components": [ + "switcher-playground" + ] + }, + { + "key": "MY_SWITCHER_3", + "description": "", + "activated": true, + "strategies": [], + "components": [ + "benchmark" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/src/core/comparator.go b/src/core/comparator.go new file mode 100644 index 0000000..ef08040 --- /dev/null +++ b/src/core/comparator.go @@ -0,0 +1,201 @@ +package core + +import ( + "encoding/json" + "slices" + + "github.com/switcherapi/switcher-gitops/src/model" +) + +type DiffType string +type DiffResult string + +const ( + NEW DiffType = "NEW" + CHANGED DiffType = "CHANGED" + DELETED DiffType = "DELETED" + + GROUP DiffResult = "GROUP" + CONFIG DiffResult = "CONFIG" + STRATEGY DiffResult = "STRATEGY" + STRATEGY_VALUE DiffResult = "STRATEGY_VALUE" + COMPONENT DiffResult = "COMPONENT" +) + +func NewSnapshotFromJson(jsonData []byte) model.Snapshot { + var snapshot model.Snapshot + json.Unmarshal(jsonData, &snapshot) + return snapshot +} + +func CheckSnapshotDiff(left model.Snapshot, right model.Snapshot, diffType DiffType) model.DiffResult { + diffResult := model.DiffResult{} + return checkGroupDiff(left, right, diffType, diffResult) +} + +func MergeResults(diffResults []model.DiffResult) model.DiffResult { + var result model.DiffResult + + for _, diffResult := range diffResults { + result.Changes = append(result.Changes, diffResult.Changes...) + } + + return result +} + +func checkGroupDiff(left model.Snapshot, right model.Snapshot, diffType DiffType, diffResult model.DiffResult) model.DiffResult { + for _, leftGroup := range left.Domain.Group { + if !slices.Contains(model.GroupNames(right.Domain.Group), leftGroup.Name) { + if diffType == NEW { + appendDiffResults(string(diffType), string(GROUP), []string{}, leftGroup, &diffResult) + } else if diffType == DELETED { + appendDiffResults(string(diffType), string(GROUP), []string{leftGroup.Name}, nil, &diffResult) + } + } else { + rightGroup := model.GetGroupByName(right.Domain.Group, leftGroup.Name) + modelDiffFound := model.Group{} + + diffFound := false + if diffType == CHANGED { + diffFound = compareAndUpdateBool(leftGroup.Activated, rightGroup.Activated, diffFound, &modelDiffFound.Activated) + diffFound = compareAndUpdateString(leftGroup.Description, rightGroup.Description, diffFound, &modelDiffFound.Description) + } + + checkConfigDiff(leftGroup, rightGroup, &diffResult, diffType) + + if diffFound { + appendDiffResults(string(diffType), string(GROUP), []string{leftGroup.Name}, modelDiffFound, &diffResult) + } + } + } + + return diffResult +} + +func checkConfigDiff(leftGroup model.Group, rightGroup model.Group, diffResult *model.DiffResult, diffType DiffType) { + if len(leftGroup.Config) == 0 { + return + } + + for _, leftConfig := range leftGroup.Config { + if !slices.Contains(model.ConfigKeys(rightGroup.Config), leftConfig.Key) { + if diffType == NEW { + appendDiffResults(string(diffType), string(CONFIG), []string{leftGroup.Name}, leftConfig, diffResult) + } else if diffType == DELETED { + appendDiffResults(string(diffType), string(CONFIG), []string{leftGroup.Name, leftConfig.Key}, nil, diffResult) + } + } else { + rightConfig := model.GetConfigByKey(rightGroup.Config, leftConfig.Key) + modelDiffFound := model.Config{} + + diffFound := false + if diffType == CHANGED { + diffFound = compareAndUpdateBool(leftConfig.Activated, rightConfig.Activated, diffFound, &modelDiffFound.Activated) + diffFound = compareAndUpdateString(leftConfig.Description, rightConfig.Description, diffFound, &modelDiffFound.Description) + } + + checkStrategyDiff(leftConfig, rightConfig, leftGroup, diffResult, diffType) + checkComponentsDiff(leftConfig, rightConfig, leftGroup, diffResult, diffType) + + if diffFound { + appendDiffResults(string(diffType), string(CONFIG), []string{leftGroup.Name, leftConfig.Key}, modelDiffFound, diffResult) + } + } + } +} + +func checkStrategyDiff(leftConfig model.Config, rightConfig model.Config, leftGroup model.Group, diffResult *model.DiffResult, diffType DiffType) { + if len(leftConfig.Strategies) == 0 { + return + } + + for _, leftStrategy := range leftConfig.Strategies { + if !slices.Contains(model.StrategyNames(rightConfig.Strategies), leftStrategy.Strategy) { + if diffType == NEW { + appendDiffResults(string(diffType), string(STRATEGY), []string{leftGroup.Name, leftConfig.Key}, leftStrategy, diffResult) + } else if diffType == DELETED { + appendDiffResults(string(diffType), string(STRATEGY), []string{leftGroup.Name, leftConfig.Key, leftStrategy.Strategy}, nil, diffResult) + } + } else { + rightStrategy := model.GetStrategyByName(rightConfig.Strategies, leftStrategy.Strategy) + modelDiffFound := model.Strategy{} + + diffFound := false + if diffType == CHANGED { + diffFound = compareAndUpdateBool(leftStrategy.Activated, rightStrategy.Activated, diffFound, &modelDiffFound.Activated) + diffFound = compareAndUpdateString(leftStrategy.Operation, rightStrategy.Operation, diffFound, &modelDiffFound.Operation) + } + + checkValuesDiff(leftStrategy, rightStrategy, leftGroup, leftConfig, diffResult, diffType) + + if diffFound { + appendDiffResults(string(diffType), string(STRATEGY), + []string{leftGroup.Name, leftConfig.Key, leftStrategy.Strategy}, modelDiffFound, diffResult) + } + } + } +} + +func checkValuesDiff(leftStrategy model.Strategy, rightStrategy model.Strategy, leftGroup model.Group, leftConfig model.Config, + diffResult *model.DiffResult, diffType DiffType) { + + if len(leftStrategy.Values) == 0 { + return + } + + var diff []string + for _, leftValue := range leftStrategy.Values { + if (diffType == NEW || diffType == DELETED) && !slices.Contains(rightStrategy.Values, leftValue) { + diff = append(diff, leftValue) + } + } + + if len(diff) > 0 { + appendDiffResults(string(diffType), string(STRATEGY_VALUE), + []string{leftGroup.Name, leftConfig.Key, leftStrategy.Strategy}, diff, diffResult) + } +} + +func checkComponentsDiff(leftConfig model.Config, rightConfig model.Config, leftGroup model.Group, + diffResult *model.DiffResult, diffType DiffType) { + + if len(leftConfig.Components) == 0 { + return + } + + var diff []string + for _, leftComponent := range leftConfig.Components { + if (diffType == NEW || diffType == DELETED) && !slices.Contains(rightConfig.Components, leftComponent) { + diff = append(diff, leftComponent) + } + } + + if len(diff) > 0 { + appendDiffResults(string(diffType), string(COMPONENT), []string{leftGroup.Name, leftConfig.Key}, diff, diffResult) + } +} + +func compareAndUpdateBool(left *bool, right *bool, diffFound bool, modelDiffFound **bool) bool { + if *left != *right { + diffFound = true + *modelDiffFound = right + } + return diffFound +} + +func compareAndUpdateString(left string, right string, diffFound bool, modelDiffFound *string) bool { + if left != right { + diffFound = true + *modelDiffFound = right + } + return diffFound +} + +func appendDiffResults(action string, diff string, path []string, content any, diffResult *model.DiffResult) { + diffResult.Changes = append(diffResult.Changes, model.DiffDetails{ + Action: action, + Diff: diff, + Path: path, + Content: content, + }) +} diff --git a/src/core/comparator_test.go b/src/core/comparator_test.go new file mode 100644 index 0000000..7e4c2d5 --- /dev/null +++ b/src/core/comparator_test.go @@ -0,0 +1,479 @@ +package core + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/switcherapi/switcher-gitops/src/model" + "github.com/switcherapi/switcher-gitops/src/utils" +) + +const DEFAULT_JSON = "../../resources/fixtures/default.json" + +func TestCheckSnapshotDiffGroupChange(t *testing.T) { + // Given + jsonLeft := utils.ReadJsonFromFile(DEFAULT_JSON) + jsonRight := utils.ReadJsonFromFile("../../resources/fixtures/changed_group.json") + snapshotLeft := NewSnapshotFromJson([]byte(jsonLeft)) + snapshotRight := NewSnapshotFromJson([]byte(jsonRight)) + + // Test Check/Merge changes + diffChanged := CheckSnapshotDiff(snapshotLeft, snapshotRight, CHANGED) + diffNew := CheckSnapshotDiff(snapshotRight, snapshotLeft, NEW) + diffDeleted := CheckSnapshotDiff(snapshotLeft, snapshotRight, DELETED) + actual := MergeResults([]model.DiffResult{diffChanged, diffNew, diffDeleted}) + + AssertNotNil(t, actual) + assert.JSONEq(t, `{ + "changes": [ + { + "action": "CHANGED", + "diff": "GROUP", + "path": [ + "Release 1" + ], + "content": { + "activated": false, + "description": "New description" + } + } + ] + }`, utils.ToJsonFromObject(actual)) +} + +func TestCheckSnapshotDiffNewGroup(t *testing.T) { + // Given + jsonLeft := utils.ReadJsonFromFile(DEFAULT_JSON) + jsonRight := utils.ReadJsonFromFile("../../resources/fixtures/new_group.json") + snapshotLeft := NewSnapshotFromJson([]byte(jsonLeft)) + snapshotRight := NewSnapshotFromJson([]byte(jsonRight)) + + // Test Check/Merge changes + diffChanged := CheckSnapshotDiff(snapshotLeft, snapshotRight, CHANGED) + diffNew := CheckSnapshotDiff(snapshotRight, snapshotLeft, NEW) + diffDeleted := CheckSnapshotDiff(snapshotLeft, snapshotRight, DELETED) + actual := MergeResults([]model.DiffResult{diffChanged, diffNew, diffDeleted}) + + AssertNotNil(t, actual) + assert.JSONEq(t, `{ + "changes": [ + { + "action": "NEW", + "diff": "GROUP", + "path": [], + "content": { + "name": "Release 2", + "description": "Showcase configuration 2", + "activated": true, + "config": [ + { + "key": "MY_SWITCHER_4", + "activated": false, + "components": [ + "switcher-playground" + ] + } + ] + } + } + ] + }`, utils.ToJsonFromObject(actual)) +} + +func TestCheckSnapshotDiffNewGroupFromEmptyGroup(t *testing.T) { + // Given + jsonLeft := utils.ReadJsonFromFile("../../resources/fixtures/default_empty.json") + jsonRight := utils.ReadJsonFromFile(DEFAULT_JSON) + snapshotLeft := NewSnapshotFromJson([]byte(jsonLeft)) + snapshotRight := NewSnapshotFromJson([]byte(jsonRight)) + + // Test Check/Merge changes + diffChanged := CheckSnapshotDiff(snapshotLeft, snapshotRight, CHANGED) + diffNew := CheckSnapshotDiff(snapshotRight, snapshotLeft, NEW) + diffDeleted := CheckSnapshotDiff(snapshotLeft, snapshotRight, DELETED) + actual := MergeResults([]model.DiffResult{diffChanged, diffNew, diffDeleted}) + + AssertNotNil(t, actual) + assert.Equal(t, "NEW", actual.Changes[0].Action) + assert.Equal(t, "GROUP", actual.Changes[0].Diff) +} + +func TestCheckSnapshotDiffNewGroupFromEmptyConfig(t *testing.T) { + // Given + jsonLeft := utils.ReadJsonFromFile("../../resources/fixtures/default_empty_config.json") + jsonRight := utils.ReadJsonFromFile(DEFAULT_JSON) + snapshotLeft := NewSnapshotFromJson([]byte(jsonLeft)) + snapshotRight := NewSnapshotFromJson([]byte(jsonRight)) + + // Test Check/Merge changes + diffChanged := CheckSnapshotDiff(snapshotLeft, snapshotRight, CHANGED) + diffNew := CheckSnapshotDiff(snapshotRight, snapshotLeft, NEW) + diffDeleted := CheckSnapshotDiff(snapshotLeft, snapshotRight, DELETED) + actual := MergeResults([]model.DiffResult{diffChanged, diffNew, diffDeleted}) + + AssertNotNil(t, actual) + assert.Equal(t, "NEW", actual.Changes[0].Action) + assert.Equal(t, "CONFIG", actual.Changes[0].Diff) +} + +func TestCheckSnapshotDiffDeletedGroup(t *testing.T) { + // Given + jsonLeft := utils.ReadJsonFromFile(DEFAULT_JSON) + jsonRight := utils.ReadJsonFromFile("../../resources/fixtures/deleted_group.json") + snapshotLeft := NewSnapshotFromJson([]byte(jsonLeft)) + snapshotRight := NewSnapshotFromJson([]byte(jsonRight)) + + // Test Check/Merge changes + diffChanged := CheckSnapshotDiff(snapshotLeft, snapshotRight, CHANGED) + diffNew := CheckSnapshotDiff(snapshotRight, snapshotLeft, NEW) + diffDeleted := CheckSnapshotDiff(snapshotLeft, snapshotRight, DELETED) + actual := MergeResults([]model.DiffResult{diffChanged, diffNew, diffDeleted}) + + AssertNotNil(t, actual) + assert.JSONEq(t, `{ + "changes": [ + { + "action": "DELETED", + "diff": "GROUP", + "path": [ + "Release 1" + ], + "content": null + } + ] + }`, utils.ToJsonFromObject(actual)) +} + +func TestCheckSnapshotDiffConfigChange(t *testing.T) { + // Given + jsonLeft := utils.ReadJsonFromFile(DEFAULT_JSON) + jsonRight := utils.ReadJsonFromFile("../../resources/fixtures/changed_config.json") + snapshotLeft := NewSnapshotFromJson([]byte(jsonLeft)) + snapshotRight := NewSnapshotFromJson([]byte(jsonRight)) + + // Test Check/Merge changes + diffChanged := CheckSnapshotDiff(snapshotLeft, snapshotRight, CHANGED) + diffNew := CheckSnapshotDiff(snapshotRight, snapshotLeft, NEW) + diffDeleted := CheckSnapshotDiff(snapshotLeft, snapshotRight, DELETED) + actual := MergeResults([]model.DiffResult{diffChanged, diffNew, diffDeleted}) + + AssertNotNil(t, actual) + assert.JSONEq(t, `{ + "changes": [ + { + "action": "CHANGED", + "diff": "CONFIG", + "path": [ + "Release 1", + "MY_SWITCHER_2" + ], + "content": { + "activated": true, + "description": "New description" + } + } + ] + }`, utils.ToJsonFromObject(actual)) +} + +func TestCheckSnapshotDiffNewConfig(t *testing.T) { + // Given + jsonLeft := utils.ReadJsonFromFile(DEFAULT_JSON) + jsonRight := utils.ReadJsonFromFile("../../resources/fixtures/new_config.json") + snapshotLeft := NewSnapshotFromJson([]byte(jsonLeft)) + snapshotRight := NewSnapshotFromJson([]byte(jsonRight)) + + // Test Check/Merge changes + diffChanged := CheckSnapshotDiff(snapshotLeft, snapshotRight, CHANGED) + diffNew := CheckSnapshotDiff(snapshotRight, snapshotLeft, NEW) + diffDeleted := CheckSnapshotDiff(snapshotLeft, snapshotRight, DELETED) + actual := MergeResults([]model.DiffResult{diffChanged, diffNew, diffDeleted}) + + AssertNotNil(t, actual) + assert.JSONEq(t, `{ + "changes": [ + { + "action": "NEW", + "diff": "CONFIG", + "path": [ + "Release 1" + ], + "content": { + "key": "MY_SWITCHER_4", + "activated": true, + "components": [ + "benchmark" + ] + } + } + ] + }`, utils.ToJsonFromObject(actual)) +} + +func TestCheckSnapshotDiffDeletedConfig(t *testing.T) { + // Given + jsonLeft := utils.ReadJsonFromFile(DEFAULT_JSON) + jsonRight := utils.ReadJsonFromFile("../../resources/fixtures/deleted_config.json") + snapshotLeft := NewSnapshotFromJson([]byte(jsonLeft)) + snapshotRight := NewSnapshotFromJson([]byte(jsonRight)) + + // Test Check/Merge changes + diffChanged := CheckSnapshotDiff(snapshotLeft, snapshotRight, CHANGED) + diffNew := CheckSnapshotDiff(snapshotRight, snapshotLeft, NEW) + diffDeleted := CheckSnapshotDiff(snapshotLeft, snapshotRight, DELETED) + actual := MergeResults([]model.DiffResult{diffChanged, diffNew, diffDeleted}) + + AssertNotNil(t, actual) + assert.JSONEq(t, `{ + "changes": [ + { + "action": "DELETED", + "diff": "CONFIG", + "path": [ + "Release 1", + "MY_SWITCHER_3" + ], + "content": null + } + ] + }`, utils.ToJsonFromObject(actual)) +} + +func TestCheckSnapshotDiffStrategyChange(t *testing.T) { + // Given + jsonLeft := utils.ReadJsonFromFile(DEFAULT_JSON) + jsonRight := utils.ReadJsonFromFile("../../resources/fixtures/changed_strategy.json") + snapshotLeft := NewSnapshotFromJson([]byte(jsonLeft)) + snapshotRight := NewSnapshotFromJson([]byte(jsonRight)) + + // Test Check/Merge changes + diffChanged := CheckSnapshotDiff(snapshotLeft, snapshotRight, CHANGED) + diffNew := CheckSnapshotDiff(snapshotRight, snapshotLeft, NEW) + diffDeleted := CheckSnapshotDiff(snapshotLeft, snapshotRight, DELETED) + actual := MergeResults([]model.DiffResult{diffChanged, diffNew, diffDeleted}) + + AssertNotNil(t, actual) + assert.JSONEq(t, `{ + "changes": [ + { + "action": "CHANGED", + "diff": "STRATEGY", + "path": [ + "Release 1", + "MY_SWITCHER_1", + "VALUE_VALIDATION" + ], + "content": { + "activated": true + } + } + ] + }`, utils.ToJsonFromObject(actual)) +} + +func TestCheckSnapshotDiffNewStrategy(t *testing.T) { + // Given + jsonLeft := utils.ReadJsonFromFile(DEFAULT_JSON) + jsonRight := utils.ReadJsonFromFile("../../resources/fixtures/new_strategy.json") + snapshotLeft := NewSnapshotFromJson([]byte(jsonLeft)) + snapshotRight := NewSnapshotFromJson([]byte(jsonRight)) + + // Test Check/Merge changes + diffChanged := CheckSnapshotDiff(snapshotLeft, snapshotRight, CHANGED) + diffNew := CheckSnapshotDiff(snapshotRight, snapshotLeft, NEW) + diffDeleted := CheckSnapshotDiff(snapshotLeft, snapshotRight, DELETED) + actual := MergeResults([]model.DiffResult{diffChanged, diffNew, diffDeleted}) + + AssertNotNil(t, actual) + assert.JSONEq(t, `{ + "changes": [ + { + "action": "NEW", + "diff": "STRATEGY", + "path": [ + "Release 1", + "MY_SWITCHER_2" + ], + "content": { + "strategy": "VALUE_VALIDATION", + "activated": true, + "operation": "EXIST", + "values": [ + "user_2" + ] + } + } + ] + }`, utils.ToJsonFromObject(actual)) +} + +func TestCheckSnapshotDiffDeletedStrategy(t *testing.T) { + // Given + jsonLeft := utils.ReadJsonFromFile(DEFAULT_JSON) + jsonRight := utils.ReadJsonFromFile("../../resources/fixtures/deleted_strategy.json") + snapshotLeft := NewSnapshotFromJson([]byte(jsonLeft)) + snapshotRight := NewSnapshotFromJson([]byte(jsonRight)) + + // Test Check/Merge changes + diffChanged := CheckSnapshotDiff(snapshotLeft, snapshotRight, CHANGED) + diffNew := CheckSnapshotDiff(snapshotRight, snapshotLeft, NEW) + diffDeleted := CheckSnapshotDiff(snapshotLeft, snapshotRight, DELETED) + actual := MergeResults([]model.DiffResult{diffChanged, diffNew, diffDeleted}) + + AssertNotNil(t, actual) + assert.JSONEq(t, `{ + "changes": [ + { + "action": "DELETED", + "diff": "STRATEGY", + "path": [ + "Release 1", + "MY_SWITCHER_1", + "VALUE_VALIDATION" + ], + "content": null + } + ] + }`, utils.ToJsonFromObject(actual)) +} + +func TestCheckSnapshotDiffStrategyValueChange(t *testing.T) { + // Given + jsonLeft := utils.ReadJsonFromFile(DEFAULT_JSON) + jsonRight := utils.ReadJsonFromFile("../../resources/fixtures/new_strategy_value.json") + snapshotLeft := NewSnapshotFromJson([]byte(jsonLeft)) + snapshotRight := NewSnapshotFromJson([]byte(jsonRight)) + + // Test Check/Merge changes + diffChanged := CheckSnapshotDiff(snapshotLeft, snapshotRight, CHANGED) + diffNew := CheckSnapshotDiff(snapshotRight, snapshotLeft, NEW) + diffDeleted := CheckSnapshotDiff(snapshotLeft, snapshotRight, DELETED) + actual := MergeResults([]model.DiffResult{diffChanged, diffNew, diffDeleted}) + + AssertNotNil(t, actual) + assert.JSONEq(t, `{ + "changes": [ + { + "action": "NEW", + "diff": "STRATEGY_VALUE", + "path": [ + "Release 1", + "MY_SWITCHER_1", + "VALUE_VALIDATION" + ], + "content": [ + "user_2" + ] + } + ] + }`, utils.ToJsonFromObject(actual)) +} + +func TestCheckSnapshotDiffDeletedStrategyValue(t *testing.T) { + // Given + jsonLeft := utils.ReadJsonFromFile(DEFAULT_JSON) + jsonRight := utils.ReadJsonFromFile("../../resources/fixtures/deleted_strategy_value.json") + snapshotLeft := NewSnapshotFromJson([]byte(jsonLeft)) + snapshotRight := NewSnapshotFromJson([]byte(jsonRight)) + + // Test Check/Merge changes + diffChanged := CheckSnapshotDiff(snapshotLeft, snapshotRight, CHANGED) + diffNew := CheckSnapshotDiff(snapshotRight, snapshotLeft, NEW) + diffDeleted := CheckSnapshotDiff(snapshotLeft, snapshotRight, DELETED) + actual := MergeResults([]model.DiffResult{diffChanged, diffNew, diffDeleted}) + + AssertNotNil(t, actual) + assert.JSONEq(t, `{ + "changes": [ + { + "action": "DELETED", + "diff": "STRATEGY_VALUE", + "path": [ + "Release 1", + "MY_SWITCHER_1", + "VALUE_VALIDATION" + ], + "content": [ + "user_1" + ] + } + ] + }`, utils.ToJsonFromObject(actual)) +} + +func TestCheckSnapshotDiffNewComponent(t *testing.T) { + // Given + jsonLeft := utils.ReadJsonFromFile(DEFAULT_JSON) + jsonRight := utils.ReadJsonFromFile("../../resources/fixtures/new_component.json") + snapshotLeft := NewSnapshotFromJson([]byte(jsonLeft)) + snapshotRight := NewSnapshotFromJson([]byte(jsonRight)) + + // Test Check/Merge changes + diffChanged := CheckSnapshotDiff(snapshotLeft, snapshotRight, CHANGED) + diffNew := CheckSnapshotDiff(snapshotRight, snapshotLeft, NEW) + diffDeleted := CheckSnapshotDiff(snapshotLeft, snapshotRight, DELETED) + actual := MergeResults([]model.DiffResult{diffChanged, diffNew, diffDeleted}) + + AssertNotNil(t, actual) + assert.JSONEq(t, `{ + "changes": [ + { + "action": "NEW", + "diff": "COMPONENT", + "path": [ + "Release 1", + "MY_SWITCHER_3" + ], + "content": [ + "new_component" + ] + } + ] + }`, utils.ToJsonFromObject(actual)) +} + +func TestCheckSnapshotDiffDeletedComponent(t *testing.T) { + // Given + jsonLeft := utils.ReadJsonFromFile(DEFAULT_JSON) + jsonRight := utils.ReadJsonFromFile("../../resources/fixtures/deleted_component.json") + snapshotLeft := NewSnapshotFromJson([]byte(jsonLeft)) + snapshotRight := NewSnapshotFromJson([]byte(jsonRight)) + + // Test Check/Merge changes + diffChanged := CheckSnapshotDiff(snapshotLeft, snapshotRight, CHANGED) + diffNew := CheckSnapshotDiff(snapshotRight, snapshotLeft, NEW) + diffDeleted := CheckSnapshotDiff(snapshotLeft, snapshotRight, DELETED) + actual := MergeResults([]model.DiffResult{diffChanged, diffNew, diffDeleted}) + + AssertNotNil(t, actual) + assert.JSONEq(t, `{ + "changes": [ + { + "action": "DELETED", + "diff": "COMPONENT", + "path": [ + "Release 1", + "MY_SWITCHER_3" + ], + "content": [ + "benchmark" + ] + } + ] + }`, utils.ToJsonFromObject(actual)) +} + +// Helpers + +func AssertNotNil(t *testing.T, object interface{}) { + if object == nil { + t.Errorf("Object is nil") + } +} + +func AssertContains(t *testing.T, actual string, expected string) { + if !strings.Contains(actual, expected) { + t.Errorf("Expected %v to contain %v", actual, expected) + } +} diff --git a/src/model/diff.go b/src/model/diff.go new file mode 100644 index 0000000..2a7d79d --- /dev/null +++ b/src/model/diff.go @@ -0,0 +1,12 @@ +package model + +type DiffResult struct { + Changes []DiffDetails `json:"changes"` +} + +type DiffDetails struct { + Action string `json:"action"` + Diff string `json:"diff"` + Path []string `json:"path"` + Content any `json:"content"` +} diff --git a/src/model/snapshot.go b/src/model/snapshot.go new file mode 100644 index 0000000..0dfb9a8 --- /dev/null +++ b/src/model/snapshot.go @@ -0,0 +1,86 @@ +package model + +type Domain struct { + Group []Group `json:"group,omitempty"` +} + +type Group struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Activated *bool `json:"activated,omitempty"` + Config []Config `json:"config,omitempty"` +} + +type Config struct { + Key string `json:"key,omitempty"` + Description string `json:"description,omitempty"` + Activated *bool `json:"activated,omitempty"` + Strategies []Strategy `json:"strategies,omitempty"` + Components []string `json:"components,omitempty"` +} + +type Strategy struct { + Strategy string `json:"strategy,omitempty"` + Activated *bool `json:"activated,omitempty"` + Operation string `json:"operation,omitempty"` + Values []string `json:"values,omitempty"` +} + +type Snapshot struct { + Domain Domain `json:"domain,omitempty"` +} + +type Data struct { + Snapshot Snapshot `json:"data,omitempty"` +} + +func GroupNames(groups []Group) []string { + names := make([]string, len(groups)) + for i, group := range groups { + names[i] = group.Name + } + return names +} + +func ConfigKeys(configs []Config) []string { + keys := make([]string, len(configs)) + for i, config := range configs { + keys[i] = config.Key + } + return keys +} + +func StrategyNames(strategies []Strategy) []string { + names := make([]string, len(strategies)) + for i, strategy := range strategies { + names[i] = strategy.Strategy + } + return names +} + +func GetStrategyByName(strategies []Strategy, name string) Strategy { + for _, s := range strategies { + if s.Strategy == name { + return s + } + } + return Strategy{} +} + +func GetConfigByKey(configs []Config, key string) Config { + for _, c := range configs { + if c.Key == key { + return c + } + } + return Config{} +} + +func GetGroupByName(groups []Group, name string) Group { + for _, g := range groups { + if g.Name == name { + return g + } + } + return Group{} +} diff --git a/src/utils/util.go b/src/utils/util.go index 1a5a08a..0706812 100644 --- a/src/utils/util.go +++ b/src/utils/util.go @@ -3,6 +3,7 @@ package utils import ( "bytes" "encoding/json" + "os" ) func FormatJSON(jsonString string) string { @@ -14,6 +15,16 @@ func FormatJSON(jsonString string) string { return string(prettyJSON.String()) } +func ReadJsonFromFile(path string) string { + file, _ := os.Open(path) + defer file.Close() + + stat, _ := file.Stat() + bs := make([]byte, stat.Size()) + file.Read(bs) + return string(bs) +} + func ToJsonFromObject(object interface{}) string { json, _ := json.MarshalIndent(object, "", " ") return string(json) diff --git a/src/utils/util_test.go b/src/utils/util_test.go index 9307fc8..399398f 100644 --- a/src/utils/util_test.go +++ b/src/utils/util_test.go @@ -26,6 +26,12 @@ func TestFormatJSONError(t *testing.T) { AssertNotNil(t, actual) } +func TestReadJsonFileToObject(t *testing.T) { + json := ReadJsonFromFile("../../resources/fixtures/default.json") + AssertNotNil(t, json) + AssertContains(t, json, "Release 1") +} + // Fixtures func givenAccount(active bool) model.Account {