Files
fleet/server/platform/endpointer/json_key_rewriter_test.go
T
Scott GressandCopilot Autofix powered by AI 4daed869ad Add API param linter (#44045)
Adds a linter to ensure we don't add new instances of `team` or `query`
in API params. This will be used incrementally, but this PR also adds
`nolint` directives to places that still have these terms, both to avoid
false-positives later and to help with full migration away from these
terms in in Fleet 5.

Example:

```
server/fleet/campaigns.go:51:16: json tag "team_id": uses deprecated "team"/"teams" — use "fleet"/"fleets" instead (apiparamcheck)
        Team  *uint   `json:"team_id,omitempty"`
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a new static analyzer (apiparamcheck) to flag deprecated API
parameter names ("team/teams") and improper usages of "query/queries".

* **Chores**
  * Integrated the new check into CI tooling and configuration.
  * Added analyzer tests and plugin registration.
* Applied targeted lint-suppression annotations across code and tests
where legacy parameter names must remain.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-11 08:46:02 -05:00

503 lines
17 KiB
Go

package endpointer
import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJSONKeyRewriteReader_OldKeyPassThrough(t *testing.T) {
// Old (deprecated) key should pass through as-is and be tracked.
input := `{"team_id": 42, "name": "hello"}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Equal(t, float64(42), result["team_id"])
assert.Equal(t, "hello", result["name"])
assert.Nil(t, result["fleet_id"], "new key should not appear in output")
// Verify deprecated key was tracked.
assert.Equal(t, []string{"team_id"}, r.UsedDeprecatedKeys())
}
func TestJSONKeyRewriteReader_NewKeyRewritten(t *testing.T) {
// New key should be rewritten to old key for struct deserialization.
input := `{"fleet_id": 42, "name": "hello"}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Equal(t, float64(42), result["team_id"])
assert.Nil(t, result["fleet_id"], "new key should be rewritten to old")
assert.Empty(t, r.UsedDeprecatedKeys())
}
func TestJSONKeyRewriteReader_NoRewriteNeeded(t *testing.T) {
// Unrelated keys should pass through unchanged.
input := `{"other_field": 42, "name": "hello"}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Equal(t, float64(42), result["other_field"])
assert.Empty(t, r.UsedDeprecatedKeys())
}
func TestJSONKeyRewriteReader_AliasConflict(t *testing.T) {
input := `{"team_id": 42, "fleet_id": 99}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
_, err := io.ReadAll(r)
require.Error(t, err)
var ace *AliasConflictError
require.True(t, errors.As(err, &ace))
assert.Equal(t, "team_id", ace.Old)
assert.Equal(t, "fleet_id", ace.New)
}
func TestJSONKeyRewriteReader_AliasConflictNewThenOld(t *testing.T) {
// New key first, then deprecated key.
input := `{"fleet_id": 99, "team_id": 42}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
_, err := io.ReadAll(r)
require.Error(t, err)
var ace *AliasConflictError
require.True(t, errors.As(err, &ace))
}
func TestJSONKeyRewriteReader_NestedObjects(t *testing.T) {
input := `{"outer": {"team_id": 1}, "team_id": 2}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
// Old keys should pass through as-is.
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Equal(t, float64(2), result["team_id"])
inner := result["outer"].(map[string]any)
assert.Equal(t, float64(1), inner["team_id"])
assert.Contains(t, r.UsedDeprecatedKeys(), "team_id")
}
func TestJSONKeyRewriteReader_NestedNewKeys(t *testing.T) {
input := `{"outer": {"fleet_id": 1}, "fleet_id": 2}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
// New keys should be rewritten to old keys.
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Equal(t, float64(2), result["team_id"])
inner := result["outer"].(map[string]any)
assert.Equal(t, float64(1), inner["team_id"])
assert.Empty(t, r.UsedDeprecatedKeys())
}
func TestJSONKeyRewriteReader_NestedConflictDoesNotAffectOuter(t *testing.T) {
// Conflict in nested object should be detected, even though outer is fine.
input := `{"name": "ok", "inner": {"team_id": 1, "fleet_id": 2}}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
_, err := io.ReadAll(r)
require.Error(t, err)
var ace *AliasConflictError
require.True(t, errors.As(err, &ace))
}
func TestJSONKeyRewriteReader_NoConflictAcrossScopes(t *testing.T) {
// team_id in outer, fleet_id in inner — no conflict (different scopes).
input := `{"team_id": 1, "inner": {"fleet_id": 2}}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Equal(t, float64(1), result["team_id"])
inner := result["inner"].(map[string]any)
assert.Equal(t, float64(2), inner["team_id"]) // fleet_id rewritten to team_id
}
func TestJSONKeyRewriteReader_StringValuesNotRewritten(t *testing.T) {
// "team_id" as a string value (not a key) should NOT be rewritten.
input := `{"name": "team_id", "description": "the team_id field"}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
// String values should not be rewritten, only keys.
assert.Equal(t, "team_id", result["name"], "string value should not be rewritten")
assert.Equal(t, "the team_id field", result["description"])
// Make sure it didn't accidentally transform the team_id string value into a new fleet_id key.
assert.Empty(t, result["fleet_id"], "new key should not appear in output")
assert.Empty(t, r.UsedDeprecatedKeys())
}
func TestJSONKeyRewriteReader_ArrayValues(t *testing.T) {
input := `{"team_id": [1, 2, 3], "items": ["a", "b"]}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.NotNil(t, result["team_id"])
assert.Contains(t, r.UsedDeprecatedKeys(), "team_id")
}
func TestJSONKeyRewriteReader_ArrayOfObjects(t *testing.T) {
input := `{"items": [{"team_id": 1}, {"team_id": 2}]}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
items := result["items"].([]any)
for _, item := range items {
obj := item.(map[string]any)
assert.NotNil(t, obj["team_id"])
assert.Nil(t, obj["fleet_id"])
}
}
func TestJSONKeyRewriteReader_MultipleRules(t *testing.T) {
input := `{"team_id": 1, "team_name": "Engineering"}`
rules := []AliasRule{
{OldKey: "team_id", NewKey: "fleet_id"},
{OldKey: "team_name", NewKey: "fleet_name"},
}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Equal(t, float64(1), result["team_id"])
assert.Equal(t, "Engineering", result["team_name"])
assert.Nil(t, result["fleet_id"])
assert.Nil(t, result["fleet_name"])
deprecated := r.UsedDeprecatedKeys()
assert.Len(t, deprecated, 2)
assert.Contains(t, deprecated, "team_id")
assert.Contains(t, deprecated, "team_name")
}
func TestJSONKeyRewriteReader_MultipleRulesNewKeys(t *testing.T) {
// New keys should be rewritten to old keys.
input := `{"fleet_id": 1, "fleet_name": "Engineering"}`
rules := []AliasRule{
{OldKey: "team_id", NewKey: "fleet_id"},
{OldKey: "team_name", NewKey: "fleet_name"},
}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Equal(t, float64(1), result["team_id"])
assert.Equal(t, "Engineering", result["team_name"])
assert.Nil(t, result["fleet_id"])
assert.Nil(t, result["fleet_name"])
assert.Empty(t, r.UsedDeprecatedKeys())
}
func TestJSONKeyRewriteReader_EmptyObject(t *testing.T) {
input := `{}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
assert.JSONEq(t, `{}`, string(out))
assert.Empty(t, r.UsedDeprecatedKeys())
}
func TestJSONKeyRewriteReader_NullValues(t *testing.T) {
input := `{"team_id": null}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Contains(t, result, "team_id")
assert.Nil(t, result["team_id"])
}
func TestJSONKeyRewriteReader_BooleanValues(t *testing.T) {
input := `{"team_id": true}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Equal(t, true, result["team_id"])
}
func TestJSONKeyRewriteReader_NoRules(t *testing.T) {
input := `{"team_id": 42}`
var rules []AliasRule
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
// With no rules, output should be identical to input.
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Equal(t, float64(42), result["team_id"])
assert.Empty(t, r.UsedDeprecatedKeys())
}
func TestJSONKeyRewriteReader_LargePayload(t *testing.T) {
// Build a large JSON payload that exceeds the internal buffer size (4096 bytes).
var sb strings.Builder
sb.WriteString(`{"team_id": 1`)
for i := range 500 {
sb.WriteString(fmt.Sprintf(`, "field_%04d": "value"`, i))
}
sb.WriteString(`}`)
input := sb.String()
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Equal(t, float64(1), result["team_id"])
assert.Nil(t, result["fleet_id"])
assert.Contains(t, r.UsedDeprecatedKeys(), "team_id")
}
func TestJSONKeyRewriteReader_WithJSONDecoderOldKey(t *testing.T) {
// Simulate the real usage: json.NewDecoder reading from the rewriter
// with old (deprecated) key in the request. The struct uses old key names.
input := `{"team_id": 42, "name": "test"}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
rewriter := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
type request struct {
TeamID int `json:"team_id"` //nolint:apiparamcheck // rename handled centrally by spec.DeprecatedGitOpsKeyMappings
Name string `json:"name"`
}
var req request
err := json.NewDecoder(rewriter).Decode(&req)
require.NoError(t, err)
assert.Equal(t, 42, req.TeamID)
assert.Equal(t, "test", req.Name)
assert.Contains(t, rewriter.UsedDeprecatedKeys(), "team_id")
}
func TestJSONKeyRewriteReader_WithJSONDecoderNewKey(t *testing.T) {
// Simulate the real usage: json.NewDecoder reading from the rewriter
// with new key in the request. Should be rewritten to old key.
input := `{"fleet_id": 42, "name": "test"}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
rewriter := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
type request struct {
TeamID int `json:"team_id"` //nolint:apiparamcheck // rename handled centrally by spec.DeprecatedGitOpsKeyMappings
Name string `json:"name"`
}
var req request
err := json.NewDecoder(rewriter).Decode(&req)
require.NoError(t, err)
assert.Equal(t, 42, req.TeamID)
assert.Equal(t, "test", req.Name)
assert.Empty(t, rewriter.UsedDeprecatedKeys())
}
func TestJSONKeyRewriteReader_AliasConflictWithJSONDecoder(t *testing.T) {
input := `{"team_id": 42, "fleet_id": 99}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
rewriter := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
type request struct {
TeamID int `json:"team_id"` //nolint:apiparamcheck // rename handled centrally by spec.DeprecatedGitOpsKeyMappings
}
var req request
err := json.NewDecoder(rewriter).Decode(&req)
require.Error(t, err)
var ace *AliasConflictError
require.True(t, errors.As(err, &ace))
assert.Equal(t, "team_id", ace.Old)
assert.Equal(t, "fleet_id", ace.New)
}
func TestJSONKeyRewriteReader_DeeplyNestedObjectsOldKeys(t *testing.T) {
input := `{"a": {"b": {"c": {"team_id": 99}}}}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
inner := result["a"].(map[string]any)["b"].(map[string]any)["c"].(map[string]any)
assert.Equal(t, float64(99), inner["team_id"])
assert.Contains(t, r.UsedDeprecatedKeys(), "team_id")
}
func TestJSONKeyRewriteReader_DeeplyNestedObjectsNewKeys(t *testing.T) {
input := `{"a": {"b": {"c": {"fleet_id": 99}}}}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
inner := result["a"].(map[string]any)["b"].(map[string]any)["c"].(map[string]any)
assert.Equal(t, float64(99), inner["team_id"])
}
func TestJSONKeyRewriteReader_TopLevelArrayOldKeys(t *testing.T) {
input := `[{"team_id": 1}, {"team_id": 2}]`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result []map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Len(t, result, 2)
assert.Equal(t, float64(1), result[0]["team_id"])
assert.Equal(t, float64(2), result[1]["team_id"])
assert.Contains(t, r.UsedDeprecatedKeys(), "team_id")
}
func TestJSONKeyRewriteReader_TopLevelArrayNewKeys(t *testing.T) {
input := `[{"fleet_id": 1}, {"fleet_id": 2}]`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result []map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Len(t, result, 2)
assert.Equal(t, float64(1), result[0]["team_id"])
assert.Equal(t, float64(2), result[1]["team_id"])
}
func TestJSONKeyRewriteReader_NestedObjectStringValue(t *testing.T) {
// Object as value with keys that need tracking.
input := `{"config": {"team_id": 5, "enabled": true}}`
rules := []AliasRule{{OldKey: "team_id", NewKey: "fleet_id"}}
r := NewJSONKeyRewriteReader(strings.NewReader(input), rules)
out, err := io.ReadAll(r)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
config := result["config"].(map[string]any)
assert.Equal(t, float64(5), config["team_id"])
assert.Equal(t, true, config["enabled"])
assert.Equal(t, []string{"team_id"}, r.UsedDeprecatedKeys())
}
func TestAliasConflictError_ErrorMessage(t *testing.T) {
err := &AliasConflictError{Old: "team_id", New: "fleet_id"}
assert.Contains(t, err.Error(), "team_id")
assert.Contains(t, err.Error(), "fleet_id")
}
func TestRewriteOldToNewKeys(t *testing.T) {
rules := []AliasRule{
{OldKey: "team_id", NewKey: "fleet_id"},
{OldKey: "team", NewKey: "fleet"},
{OldKey: "custom_settings", NewKey: "configuration_profiles"},
}
t.Run("rewrites old keys to new", func(t *testing.T) {
input := `{"team_id":42,"name":"hello","team":"engineering"}`
out, err := RewriteOldToNewKeys([]byte(input), rules)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Equal(t, float64(42), result["fleet_id"])
assert.Equal(t, "engineering", result["fleet"])
assert.Equal(t, "hello", result["name"])
assert.Nil(t, result["team_id"])
assert.Nil(t, result["team"])
})
t.Run("new keys pass through unchanged", func(t *testing.T) {
input := `{"fleet_id":42}`
out, err := RewriteOldToNewKeys([]byte(input), rules)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Equal(t, float64(42), result["fleet_id"])
})
}