<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** For #39344 # Details This PR builds on the previous PR (https://github.com/fleetdm/fleet/pull/39847) which added `renameto` tags to certain API parameters to mark them as deprecated. How this is used: ### In requests * When decoding requests, log a warning if a `json` or `query` param is used that has a `renameto` tag, e.g. if a `team_id` param is sent but the related struct has `renameto:"fleet_id"` in it. * If the `renamedto` version (e.g. `fleet_id`) is sent in the request, rewrite it to the deprecated name so that it can be unmarshalled into the struct * If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an error and quit * URLs with deprecated terms have new aliases using `WithAltPaths` -- warning on using old URLSs a TODO that will be handled in a subsequent PR. ### In responses * Output _both_ the deprecated and new names for fields that have `renameto` tags, so that we don't break existing workflows expecting the old keys. Uses a shared `DuplicateJSONKeys` to do the duplication. * Most API responses are handled in `EncodeCommonResponse`. Exceptions are activities, failing policy webhooks and the streaming "list hosts" endpoints which call the function directly. ### In fleetctl * Similar to requests, log warnings when deprecated keys are used and rewrite the new keys internally so that they can be unmarshalled. * For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the new names * The set of keys to replace is hardcoded in `fleetctl` rather than being dynamically generated as it is for API endpoints. Given the mixture of typed and untyped data and the level of nesting, dynamic map generation was very fragile and error-prone. ### Performance considerations * The biggest performance hit is the addition of the JSON key rewriter to the request pipeline. The rewriter buffers the entire request into memory before eventually passing it to the decoder than unmarshals the data into structs. I tried implementing this as a true streaming rewriter but encountered issues where the request would hang if the downstream reader (the decoder) encountered any errors. It's possible we could implement this in a streaming fashion if we replace our [current request decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108) with the v2 version, which is a bigger change requiring more thoughtful discussion in the engineering team. As it stands, memory usage for requests with deprecated fields will double while the request is being decoded. * The "alias rules" used to determine the old and new key names are cached per struct type and for most endpoints are generated on server start, so no performance impact is expected. * Some `fleetctl` commands may have an extra unmarshal/marshal step but as these are user-initiated and not performed in tight loops, the impact should be minimal. ### TODO * Log deprecation warnings when old URLs like "/fleet/teams" are used * Update API fields that the front-end uses to avoid deprecation warnings * Update `fleetctl apply` to accept/return `kind: fleet` rather than `kind: team` * Find/update any fleet server config vars with old language * Update all error messages that use old language # Checklist for submitter If some of the following don't apply, delete the relevant line. - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [X] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [X] Added/updated automated tests - [X] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [X] QA'd all new/changed functionality manually * Clicking around the front-end, no broken pages due to request ingestion errors or bad responses * Looking in network tab to verify that responses have both the old and new keys * Running `fleetctl generate-gitops` and verifying that the output looks correct and can be ingested by `fleetctl gitops` * Running `fleetctl get` and `fleetctl apply` --------- Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
506 lines
15 KiB
Go
506 lines
15 KiB
Go
package endpointer
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestDuplicateJSONKeys(t *testing.T) {
|
|
rules := []AliasRule{
|
|
{OldKey: "team_id", NewKey: "fleet_id"},
|
|
{OldKey: "team_ids", NewKey: "fleet_ids"},
|
|
{OldKey: "team_name", NewKey: "fleet_name"},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
rules []AliasRule
|
|
validate func(t *testing.T, result []byte)
|
|
}{
|
|
{
|
|
name: "BasicDuplication",
|
|
input: `{"team_id": 42, "name": "hello"}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Equal(t, float64(42), m["team_id"])
|
|
assert.Equal(t, float64(42), m["fleet_id"])
|
|
assert.Equal(t, "hello", m["name"])
|
|
},
|
|
},
|
|
{
|
|
name: "NoDuplicationNeeded",
|
|
input: `{"name": "hello", "count": 5}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
assert.JSONEq(t, `{"name": "hello", "count": 5}`, string(result))
|
|
},
|
|
},
|
|
{
|
|
name: "MultipleRules",
|
|
input: `{"team_id": 1, "team_name": "test"}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Equal(t, float64(1), m["team_id"])
|
|
assert.Equal(t, float64(1), m["fleet_id"])
|
|
assert.Equal(t, "test", m["team_name"])
|
|
assert.Equal(t, "test", m["fleet_name"])
|
|
},
|
|
},
|
|
{
|
|
name: "NewKeyAlreadyExists",
|
|
input: `{"team_id": 1, "fleet_id": 2}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
// When new key already exists, no duplication should happen.
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Equal(t, float64(1), m["team_id"])
|
|
assert.Equal(t, float64(2), m["fleet_id"])
|
|
},
|
|
},
|
|
{
|
|
name: "StringValue",
|
|
input: `{"team_name": "my fleet"}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Equal(t, "my fleet", m["team_name"])
|
|
assert.Equal(t, "my fleet", m["fleet_name"])
|
|
},
|
|
},
|
|
{
|
|
name: "NullValue",
|
|
input: `{"team_id": null}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Nil(t, m["team_id"])
|
|
assert.Nil(t, m["fleet_id"])
|
|
_, hasFleetID := m["fleet_id"]
|
|
assert.True(t, hasFleetID)
|
|
},
|
|
},
|
|
{
|
|
name: "BooleanValue",
|
|
input: `{"team_id": true}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Equal(t, true, m["team_id"])
|
|
assert.Equal(t, true, m["fleet_id"])
|
|
},
|
|
},
|
|
{
|
|
name: "ArrayValue",
|
|
input: `{"team_ids": [1, 2, 3]}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Equal(t, []any{float64(1), float64(2), float64(3)}, m["team_ids"])
|
|
assert.Equal(t, []any{float64(1), float64(2), float64(3)}, m["fleet_ids"])
|
|
},
|
|
},
|
|
{
|
|
name: "ObjectValue",
|
|
input: `{"team_id": {"sub": "value"}}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
expected := map[string]any{"sub": "value"}
|
|
assert.Equal(t, expected, m["team_id"])
|
|
assert.Equal(t, expected, m["fleet_id"])
|
|
},
|
|
},
|
|
{
|
|
name: "NestedObjects",
|
|
input: `{"outer": {"team_id": 10}}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
inner := m["outer"].(map[string]any)
|
|
assert.Equal(t, float64(10), inner["team_id"])
|
|
assert.Equal(t, float64(10), inner["fleet_id"])
|
|
},
|
|
},
|
|
{
|
|
name: "DeeplyNested",
|
|
input: `{"a": {"b": {"c": {"team_id": 99}}}}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
c := m["a"].(map[string]any)["b"].(map[string]any)["c"].(map[string]any)
|
|
assert.Equal(t, float64(99), c["team_id"])
|
|
assert.Equal(t, float64(99), c["fleet_id"])
|
|
},
|
|
},
|
|
{
|
|
// This simulates the ABM tokens response pattern where a duplicated
|
|
// outer key (e.g., ios_team→ios_fleet) has an object value that itself
|
|
// contains keys needing duplication (e.g., team_id→fleet_id).
|
|
name: "DuplicatedKeyWithNestedDuplicatableKeys",
|
|
input: `{"ios_team": {"name": "Default", "team_id": 5}}`,
|
|
rules: []AliasRule{
|
|
{OldKey: "team_id", NewKey: "fleet_id"},
|
|
{OldKey: "ios_team", NewKey: "ios_fleet"},
|
|
},
|
|
validate: func(t *testing.T, result []byte) {
|
|
assert.True(t, json.Valid(result), "result should be valid JSON: %s", string(result))
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
// Both ios_team and ios_fleet should exist.
|
|
iosTeam := m["ios_team"].(map[string]any)
|
|
iosFleet := m["ios_fleet"].(map[string]any)
|
|
// Both should have team_id AND fleet_id.
|
|
assert.Equal(t, "Default", iosTeam["name"])
|
|
assert.Equal(t, float64(5), iosTeam["team_id"])
|
|
assert.Equal(t, float64(5), iosTeam["fleet_id"])
|
|
assert.Equal(t, "Default", iosFleet["name"])
|
|
assert.Equal(t, float64(5), iosFleet["team_id"])
|
|
assert.Equal(t, float64(5), iosFleet["fleet_id"])
|
|
},
|
|
},
|
|
{
|
|
name: "ArrayOfObjects",
|
|
input: `[{"team_id": 1}, {"team_id": 2}]`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
var arr []map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &arr))
|
|
require.Len(t, arr, 2)
|
|
assert.Equal(t, float64(1), arr[0]["team_id"])
|
|
assert.Equal(t, float64(1), arr[0]["fleet_id"])
|
|
assert.Equal(t, float64(2), arr[1]["team_id"])
|
|
assert.Equal(t, float64(2), arr[1]["fleet_id"])
|
|
},
|
|
},
|
|
{
|
|
name: "ScopeIsolation",
|
|
input: `{"team_id": 1, "child": {"fleet_id": 5}}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
// Top level: team_id should be duplicated (no fleet_id at top level).
|
|
assert.Equal(t, float64(1), m["team_id"])
|
|
assert.Equal(t, float64(1), m["fleet_id"])
|
|
// Child: fleet_id exists but team_id doesn't; no duplication
|
|
// (we only duplicate old->new, not new->old).
|
|
child := m["child"].(map[string]any)
|
|
assert.Equal(t, float64(5), child["fleet_id"])
|
|
_, hasTeamIDInChild := child["team_id"]
|
|
assert.False(t, hasTeamIDInChild)
|
|
},
|
|
},
|
|
{
|
|
name: "EmptyObject",
|
|
input: `{}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
assert.JSONEq(t, `{}`, string(result))
|
|
},
|
|
},
|
|
{
|
|
name: "EmptyArray",
|
|
input: `[]`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
assert.JSONEq(t, `[]`, string(result))
|
|
},
|
|
},
|
|
{
|
|
name: "NoRules",
|
|
input: `{"team_id": 42}`,
|
|
rules: nil,
|
|
validate: func(t *testing.T, result []byte) {
|
|
assert.Equal(t, `{"team_id": 42}`, string(result))
|
|
},
|
|
},
|
|
{
|
|
name: "EmptyData",
|
|
input: ``,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
assert.Equal(t, ``, string(result))
|
|
},
|
|
},
|
|
{
|
|
name: "StringValueNotDuplicated",
|
|
input: `{"value": "team_id"}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
// String values that happen to match a key name should NOT trigger duplication.
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Equal(t, "team_id", m["value"])
|
|
_, hasFleetID := m["fleet_id"]
|
|
assert.False(t, hasFleetID)
|
|
},
|
|
},
|
|
{
|
|
name: "NumberWithExponent",
|
|
input: `{"team_id": 1.5e2}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Equal(t, float64(150), m["team_id"])
|
|
assert.Equal(t, float64(150), m["fleet_id"])
|
|
},
|
|
},
|
|
{
|
|
name: "NegativeNumber",
|
|
input: `{"team_id": -7}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Equal(t, float64(-7), m["team_id"])
|
|
assert.Equal(t, float64(-7), m["fleet_id"])
|
|
},
|
|
},
|
|
{
|
|
name: "EscapedQuotesInStringValue",
|
|
input: `{"team_name": "he said \"hi\""}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Equal(t, `he said "hi"`, m["team_name"])
|
|
assert.Equal(t, `he said "hi"`, m["fleet_name"])
|
|
},
|
|
},
|
|
{
|
|
name: "PrettyPrintedJSON",
|
|
input: "{\n \"team_id\": 42,\n \"name\": \"test\"\n}",
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Equal(t, float64(42), m["team_id"])
|
|
assert.Equal(t, float64(42), m["fleet_id"])
|
|
assert.Equal(t, "test", m["name"])
|
|
},
|
|
},
|
|
{
|
|
name: "ValidJSON",
|
|
input: `{"team_id": 42, "nested": {"team_name": "x"}, "arr": [{"team_ids": [1]}]}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
// Ensure the result is valid JSON.
|
|
assert.True(t, json.Valid(result), "result should be valid JSON: %s", string(result))
|
|
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Equal(t, float64(42), m["team_id"])
|
|
assert.Equal(t, float64(42), m["fleet_id"])
|
|
|
|
nested := m["nested"].(map[string]any)
|
|
assert.Equal(t, "x", nested["team_name"])
|
|
assert.Equal(t, "x", nested["fleet_name"])
|
|
|
|
arr := m["arr"].([]any)
|
|
arrObj := arr[0].(map[string]any)
|
|
assert.Equal(t, []any{float64(1)}, arrObj["team_ids"])
|
|
assert.Equal(t, []any{float64(1)}, arrObj["fleet_ids"])
|
|
},
|
|
},
|
|
{
|
|
name: "LargePayload",
|
|
input: func() string {
|
|
var items []string
|
|
for i := range 100 {
|
|
items = append(items, fmt.Sprintf(`{"team_id": %d, "field_%04d": "val"}`, i, i))
|
|
}
|
|
return "[" + strings.Join(items, ",") + "]"
|
|
}(),
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
assert.True(t, json.Valid(result), "result should be valid JSON")
|
|
var arr []map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &arr))
|
|
require.Len(t, arr, 100)
|
|
for i, obj := range arr {
|
|
assert.Equal(t, float64(i), obj["team_id"])
|
|
assert.Equal(t, float64(i), obj["fleet_id"])
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "OnlyNewKeyPresent",
|
|
input: `{"fleet_id": 5}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
// The new key is present but not the old key. We only duplicate
|
|
// old->new, not new->old. So no duplication should happen.
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Equal(t, float64(5), m["fleet_id"])
|
|
_, hasTeamID := m["team_id"]
|
|
assert.False(t, hasTeamID)
|
|
},
|
|
},
|
|
{
|
|
name: "MixedKeysAcrossScopes",
|
|
input: `{"team_id": 1, "child": {"team_id": 2, "fleet_id": 3}}`,
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
// Top level: team_id duplicated (no fleet_id at top).
|
|
assert.Equal(t, float64(1), m["team_id"])
|
|
assert.Equal(t, float64(1), m["fleet_id"])
|
|
// Child: both keys exist, no duplication.
|
|
child := m["child"].(map[string]any)
|
|
assert.Equal(t, float64(2), child["team_id"])
|
|
assert.Equal(t, float64(3), child["fleet_id"])
|
|
},
|
|
},
|
|
{
|
|
name: "TrailingNewline",
|
|
input: "{\"team_id\": 1}\n",
|
|
rules: rules,
|
|
validate: func(t *testing.T, result []byte) {
|
|
// json.Encoder appends a newline; ensure it still works.
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Equal(t, float64(1), m["team_id"])
|
|
assert.Equal(t, float64(1), m["fleet_id"])
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := DuplicateJSONKeys([]byte(tc.input), tc.rules)
|
|
tc.validate(t, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDuplicateJSONKeysWithEncoder tests that the duplicator works correctly
|
|
// with the output of json.Encoder (which adds pretty-printing and a trailing newline).
|
|
func TestDuplicateJSONKeysWithEncoder(t *testing.T) {
|
|
rules := []AliasRule{
|
|
{OldKey: "team_id", NewKey: "fleet_id"},
|
|
}
|
|
|
|
type response struct {
|
|
TeamID int `json:"team_id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
data, err := json.MarshalIndent(response{TeamID: 42, Name: "test"}, "", " ")
|
|
require.NoError(t, err)
|
|
|
|
result := DuplicateJSONKeys(data, rules)
|
|
assert.True(t, json.Valid(result), "result should be valid JSON: %s", string(result))
|
|
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Equal(t, float64(42), m["team_id"])
|
|
assert.Equal(t, float64(42), m["fleet_id"])
|
|
assert.Equal(t, "test", m["name"])
|
|
}
|
|
|
|
// TestDuplicateJSONKeysCompact tests that the Compact option disables
|
|
// pretty-printing and that the option propagates to recursive calls
|
|
// (nested objects whose values are themselves duplicated).
|
|
func TestDuplicateJSONKeysCompact(t *testing.T) {
|
|
rules := []AliasRule{
|
|
{OldKey: "team_id", NewKey: "fleet_id"},
|
|
{OldKey: "ios_team", NewKey: "ios_fleet"},
|
|
}
|
|
opts := DuplicateJSONKeysOpts{Compact: true}
|
|
|
|
t.Run("flat object is compact", func(t *testing.T) {
|
|
input := `{"team_id": 42, "name": "hello"}`
|
|
result := DuplicateJSONKeys([]byte(input), rules, opts)
|
|
|
|
// Compact output should have no newlines (other than a possible
|
|
// trailing one from the encoder) or multi-space indentation.
|
|
trimmed := strings.TrimRight(string(result), "\n")
|
|
assert.NotContains(t, trimmed, "\n")
|
|
assert.NotContains(t, trimmed, " ")
|
|
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
assert.Equal(t, float64(42), m["team_id"])
|
|
assert.Equal(t, float64(42), m["fleet_id"])
|
|
assert.Equal(t, "hello", m["name"])
|
|
})
|
|
|
|
t.Run("nested duplicated key value is also compact", func(t *testing.T) {
|
|
// ios_team's value contains team_id, which triggers a recursive
|
|
// DuplicateJSONKeys call. The Compact option must propagate so the
|
|
// recursively-processed value is also compact.
|
|
input := `{"ios_team": {"team_id": 5, "name": "Default"}}`
|
|
result := DuplicateJSONKeys([]byte(input), rules, opts)
|
|
|
|
trimmed := strings.TrimRight(string(result), "\n")
|
|
assert.NotContains(t, trimmed, "\n")
|
|
assert.NotContains(t, trimmed, " ")
|
|
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal(result, &m))
|
|
|
|
iosTeam := m["ios_team"].(map[string]any)
|
|
assert.Equal(t, float64(5), iosTeam["team_id"])
|
|
assert.Equal(t, float64(5), iosTeam["fleet_id"])
|
|
|
|
iosFleet := m["ios_fleet"].(map[string]any)
|
|
assert.Equal(t, float64(5), iosFleet["team_id"])
|
|
assert.Equal(t, float64(5), iosFleet["fleet_id"])
|
|
})
|
|
|
|
t.Run("default (no opts) is indented", func(t *testing.T) {
|
|
input := `{"team_id": 42}`
|
|
expected := `{
|
|
"team_id": 42,
|
|
"fleet_id": 42
|
|
}
|
|
`
|
|
result := DuplicateJSONKeys([]byte(input), rules)
|
|
assert.Equal(t, expected, string(result))
|
|
})
|
|
}
|
|
|
|
// TestDuplicateJSONKeysIdempotent ensures that running the duplicator twice
|
|
// doesn't add more keys (since after the first run the new key exists).
|
|
func TestDuplicateJSONKeysIdempotent(t *testing.T) {
|
|
rules := []AliasRule{
|
|
{OldKey: "team_id", NewKey: "fleet_id"},
|
|
}
|
|
|
|
input := `{"team_id": 42}`
|
|
expected := `{
|
|
"team_id": 42,
|
|
"fleet_id": 42
|
|
}
|
|
`
|
|
|
|
first := DuplicateJSONKeys([]byte(input), rules)
|
|
|
|
assert.Equal(t, expected, string(first))
|
|
|
|
// Second pass should not add anything new.
|
|
second := DuplicateJSONKeys(first, rules)
|
|
assert.Equal(t, expected, string(second))
|
|
}
|