package deepmerge_test import ( "encoding/json" "errors" "testing" "github.com/TwiN/deepmerge" ) func TestJSON(t *testing.T) { scenarios := []struct { name string config deepmerge.Config dst string src string expected string expectedErr error }{ { name: "invalid-dst", dst: ``, src: `{}`, expected: `{}`, expectedErr: errors.New("unexpected end of JSON input"), }, { name: "invalid-src", dst: `{}`, src: ``, expected: `{}`, expectedErr: errors.New("unexpected end of JSON input"), }, { name: "simple-endpoint-merge", dst: `{ "endpoints": [ { "name": "one", "url": "https://example.com", "client": { "timeout": "5s" }, "conditions": [ "[CONNECTED] == true", "[STATUS] == 200" ], "alerts": [ { "type": "slack", "failure-threshold": 5 } ] }, { "name": "two", "url": "https://example.org", "conditions": [ "len([BODY]) > 0" ] } ] }`, src: `{ "endpoints": [ { "name": "three", "url": "https://twin.sh/health", "conditions": [ "[STATUS] == 200", "[BODY].status == UP" ] } ] }`, expected: `{ "endpoints": [ { "name": "one", "url": "https://example.com", "client": { "timeout": "5s" }, "conditions": [ "[CONNECTED] == true", "[STATUS] == 200" ], "alerts": [ { "type": "slack", "failure-threshold": 5 } ] }, { "name": "two", "url": "https://example.org", "conditions": [ "len([BODY]) > 0" ] }, { "name": "three", "url": "https://twin.sh/health", "conditions": [ "[STATUS] == 200", "[BODY].status == UP" ] } ] }`, }, { name: "deep-merge-with-map-slice-and-primitive", dst: `{ "metrics": true, "alerting": { "slack": { "webhook-url": "https://hooks.slack.com/services/xxx/yyy/zzz", "default-alert": { "description": "health check failed", "send-on-resolved": true, "failure-threshold": 5, "success-threshold": 5 } } }, "endpoints": [ { "name": "example", "url": "https://example.org", "interval": "5s" } ] }`, src: `{ "debug": true, "alerting": { "discord": { "webhook-url": "https://discord.com/api/webhooks/xxx/yyy" } }, "endpoints": [ { "name": "frontend", "url": "https://example.com" } ] }`, expected: `{ "metrics": true, "debug": true, "alerting": { "discord": { "webhook-url": "https://discord.com/api/webhooks/xxx/yyy" }, "slack": { "webhook-url": "https://hooks.slack.com/services/xxx/yyy/zzz", "default-alert": { "description": "health check failed", "send-on-resolved": true, "failure-threshold": 5, "success-threshold": 5 } } }, "endpoints": [ { "interval": "5s", "name": "example", "url": "https://example.org" }, { "name": "frontend", "url": "https://example.com" } ] }`, }, { // only maps and slices can be merged. If there are duplicate keys that have a primitive value, then that's an error. name: "duplicate-key-with-primitive-value", config: deepmerge.Config{PreventMultipleDefinitionsOfKeysWithPrimitiveValue: true}, // NOTE: true is the default dst: `{"metrics": true}`, src: `{"metrics": false}`, expectedErr: deepmerge.ErrKeyWithPrimitiveValueDefinedMoreThanOnce, }, { name: "duplicate-key-with-primitive-value-with-PreventMultipleDefinitionsOfKeysWithPrimitiveValue-set-to-false", config: deepmerge.Config{PreventMultipleDefinitionsOfKeysWithPrimitiveValue: false}, dst: `{"metrics": true, "debug": true}`, src: `{"metrics": false}`, expected: `{"metrics": false, "debug": true}`, }, { name: "readme-example", dst: `{ "debug": true, "client": { "insecure": true }, "users": [ { "id": 1, "firstName": "John", "lastName": "Doe" }, { "id": 2, "firstName": "Jane", "lastName": "Doe" } ] }`, src: `{ "client": { "timeout": "5s" }, "users": [ { "id": 3, "firstName": "Bob", "lastName": "Smith" } ] }`, expected: `{ "client": { "insecure": true, "timeout": "5s" }, "debug": true, "users": [ { "firstName": "John", "id": 1, "lastName": "Doe" }, { "firstName": "Jane", "id": 2, "lastName": "Doe" }, { "firstName": "Bob", "id": 3, "lastName": "Smith" } ] }`, }, } for _, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { output, err := deepmerge.JSON([]byte(scenario.dst), []byte(scenario.src), scenario.config) if !errors.Is(err, scenario.expectedErr) && !(scenario.expectedErr != nil && err.Error() == scenario.expectedErr.Error()) { t.Errorf("[%s] expected error %v, got %v", scenario.name, scenario.expectedErr, err) } // Just so we don't have to worry about the formatting, we'll unmarshal the output and marshal it again. expectedAsMap, outputAsMap := make(map[string]interface{}), make(map[string]interface{}) if len(output) > 0 { if err := json.Unmarshal(output, &outputAsMap); err != nil { t.Errorf("[%s] failed to unmarshal output: %v", scenario.name, err) } } if len(scenario.expected) > 0 { if err := json.Unmarshal([]byte(scenario.expected), &expectedAsMap); err != nil { t.Errorf("[%s] failed to unmarshal expected: %v", scenario.name, err) } } formattedOutput, err := json.Marshal(outputAsMap) if err != nil { t.Errorf("[%s] should've been able to re-marshal output: %v", scenario.name, err) } formattedExpected, err := json.Marshal(expectedAsMap) if err != nil { t.Errorf("[%s] should've been able to re-marshal expected: %v", scenario.name, err) } // Compare what we got vs what we expected if string(formattedOutput) != string(formattedExpected) { t.Errorf("[%s] expected:\n%s\n\ngot:\n%s", scenario.name, string(formattedExpected), string(formattedOutput)) } }) } }