Files
coder/enterprise/audit/diff_internal_test.go
T
Michael Suchacz 299e72ad30 feat: audit MCP server config changes (#27943)
Adds enterprise audit logging for MCP server config create, update, and
delete, with strict secret redaction. MCP configs hold credentials
(OAuth2 client secrets, API keys, custom headers), so admin changes to
them need an audit trail.

## Summary

- `enterprise/audit/table.go` gains an `MCPServerConfig` entry
enumerating every column: `oauth2_client_secret`, `api_key_value`, and
`custom_headers` are `ActionSecret` (never appear in diffs); dbcrypt
`*_key_id` bookkeeping, IDs, and timestamps are ignored; the remaining
config fields, including the endpoint URL fields, are tracked so
auditors can see which endpoints a config points at.
- Type registration in `coderd/audit` (diff, request, resource target
with org attribution), `codersdk/audit.go`, and a `resource_type` enum
migration.
- Handlers wire `audit.InitRequest`: create records `New`; update and
delete record `Old` from the param middleware before the
write-authorization check, so a readable-but-not-writable caller
produces an audited 403 while read-denied callers stay concealed as
unaudited 404s.
- Tests: create/update/delete audit entries, write-denied and
delete-denied 403 auditing, cross-org concealment producing zero
entries, and a serializer-level regression test proving none of the
three secret classes can reach a serialized diff.
- Review round: MCP config audit entries link to
`/ai/settings/mcp-servers/{id}`, audit table comments are trimmed per
review, and a fault-injection test pins that a config row surviving a
failed post-discovery credential update still gets its creation audit
entry.

Stacked on #27942 (org-scoped MCP configs). Part of the MCP
org-separation stack.

Closes https://linear.app/codercom/issue/CODAGT-717

UAT: verified on a trial-licensed dogfood instance: audit entries for
the full CRUD lifecycle with correct actor/org/target, redacted secrets
in the update and OAuth2 create diffs, and a full plaintext scan of the
audit dump finding zero secret leaks.

> Mux (AI agent) authored this PR on Mike's behalf.

<!-- mux-attribution: model=claude-fable-5 thinking=high -->
2026-08-19 19:11:52 +00:00

706 lines
21 KiB
Go

package audit
import (
"database/sql"
"encoding/json"
"reflect"
"testing"
"time"
"github.com/google/uuid"
"github.com/lib/pq"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/util/ptr"
)
func Test_diffValues(t *testing.T) {
t.Parallel()
t.Run("Normal", func(t *testing.T) {
t.Parallel()
type foo struct {
Bar string `json:"bar"`
Baz int `json:"baz"`
}
table := auditMap(map[any]map[string]Action{
&foo{}: {
"bar": ActionTrack,
"baz": ActionTrack,
},
})
runDiffValuesTests(t, table, []diffTest{
{
name: "LeftEmpty",
left: foo{Bar: "", Baz: 0}, right: foo{Bar: "bar", Baz: 10},
exp: audit.Map{
"bar": audit.OldNew{Old: "", New: "bar"},
"baz": audit.OldNew{Old: 0, New: 10},
},
},
{
name: "RightEmpty",
left: foo{Bar: "Bar", Baz: 10}, right: foo{Bar: "", Baz: 0},
exp: audit.Map{
"bar": audit.OldNew{Old: "Bar", New: ""},
"baz": audit.OldNew{Old: 10, New: 0},
},
},
{
name: "NoChange",
left: foo{Bar: "", Baz: 0}, right: foo{Bar: "", Baz: 0},
exp: audit.Map{},
},
{
name: "SingleFieldChange",
left: foo{Bar: "", Baz: 0}, right: foo{Bar: "Bar", Baz: 0},
exp: audit.Map{
"bar": audit.OldNew{Old: "", New: "Bar"},
},
},
})
})
t.Run("PointerField", func(t *testing.T) {
t.Parallel()
type foo struct {
Bar *string `json:"bar"`
}
table := auditMap(map[any]map[string]Action{
&foo{}: {
"bar": ActionTrack,
},
})
runDiffValuesTests(t, table, []diffTest{
{
name: "LeftNil",
left: foo{Bar: nil}, right: foo{Bar: ptr.Ref("baz")},
exp: audit.Map{
"bar": audit.OldNew{Old: "", New: "baz"},
},
},
{
name: "RightNil",
left: foo{Bar: ptr.Ref("baz")}, right: foo{Bar: nil},
exp: audit.Map{
"bar": audit.OldNew{Old: "baz", New: ""},
},
},
})
})
//nolint:revive
t.Run("EmbeddedStruct", func(t *testing.T) {
t.Parallel()
type Bar struct {
Baz int `json:"baz"`
Buzz string `json:"buzz"`
}
type PtrBar struct {
Qux string `json:"qux"`
}
type foo struct {
Bar
*PtrBar
TopLevel string `json:"top_level"`
}
table := auditMap(map[any]map[string]Action{
&foo{}: {
"baz": ActionTrack,
"buzz": ActionTrack,
"qux": ActionTrack,
"top_level": ActionTrack,
},
})
runDiffValuesTests(t, table, []diffTest{
{
name: "SingleFieldChange",
left: foo{TopLevel: "top-before", Bar: Bar{Baz: 1, Buzz: "before"}, PtrBar: &PtrBar{Qux: "qux-before"}},
right: foo{TopLevel: "top-after", Bar: Bar{Baz: 0, Buzz: "after"}, PtrBar: &PtrBar{Qux: "qux-after"}},
exp: audit.Map{
"baz": audit.OldNew{Old: 1, New: 0},
"buzz": audit.OldNew{Old: "before", New: "after"},
"qux": audit.OldNew{Old: "qux-before", New: "qux-after"},
"top_level": audit.OldNew{Old: "top-before", New: "top-after"},
},
},
{
name: "Empty",
left: foo{},
right: foo{},
exp: audit.Map{},
},
{
name: "NoChange",
left: foo{TopLevel: "top-before", Bar: Bar{Baz: 1, Buzz: "before"}, PtrBar: &PtrBar{Qux: "qux-before"}},
right: foo{TopLevel: "top-before", Bar: Bar{Baz: 1, Buzz: "before"}, PtrBar: &PtrBar{Qux: "qux-before"}},
exp: audit.Map{},
},
{
name: "LeftEmpty",
left: foo{},
right: foo{TopLevel: "top-after", Bar: Bar{Baz: 1, Buzz: "after"}, PtrBar: &PtrBar{Qux: "qux-after"}},
exp: audit.Map{
"baz": audit.OldNew{Old: 0, New: 1},
"buzz": audit.OldNew{Old: "", New: "after"},
"qux": audit.OldNew{Old: "", New: "qux-after"},
"top_level": audit.OldNew{Old: "", New: "top-after"},
},
},
{
name: "RightNil",
left: foo{TopLevel: "top-before", Bar: Bar{Baz: 1, Buzz: "before"}, PtrBar: &PtrBar{Qux: "qux-before"}},
right: foo{},
exp: audit.Map{
"baz": audit.OldNew{Old: 1, New: 0},
"buzz": audit.OldNew{Old: "before", New: ""},
"qux": audit.OldNew{Old: "qux-before", New: ""},
"top_level": audit.OldNew{Old: "top-before", New: ""},
},
},
})
})
// We currently don't support nested structs.
// t.Run("NestedStruct", func(t *testing.T) {
// t.Parallel()
// type bar struct {
// Baz string `json:"baz"`
// }
// type foo struct {
// Bar *bar `json:"bar"`
// }
// table := auditMap(map[any]map[string]Action{
// &foo{}: {
// "bar": ActionTrack,
// },
// &bar{}: {
// "baz": ActionTrack,
// },
// })
// runDiffValuesTests(t, table, []diffTest{
// {
// name: "LeftEmpty",
// left: foo{Bar: &bar{}}, right: foo{Bar: &bar{Baz: "baz"}},
// exp: audit.Map{
// "bar": audit.Map{
// "baz": audit.OldNew{Old: "", New: "baz"},
// },
// },
// },
// {
// name: "RightEmpty",
// left: foo{Bar: &bar{Baz: "baz"}}, right: foo{Bar: &bar{}},
// exp: audit.Map{
// "bar": audit.Map{
// "baz": audit.OldNew{Old: "baz", New: ""},
// },
// },
// },
// {
// name: "LeftNil",
// left: foo{Bar: nil}, right: foo{Bar: &bar{}},
// exp: audit.Map{
// "bar": audit.Map{},
// },
// },
// {
// name: "RightNil",
// left: foo{Bar: &bar{Baz: "baz"}}, right: foo{Bar: nil},
// exp: audit.Map{
// "bar": audit.Map{
// "baz": audit.OldNew{Old: "baz", New: ""},
// },
// },
// },
// })
// })
}
type diffTest struct {
name string
left, right any
exp any
}
func runDiffValuesTests(t *testing.T, table Table, tests []diffTest) {
t.Helper()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t,
test.exp,
diffValues(test.left, test.right, table),
)
})
}
}
func Test_diff(t *testing.T) {
t.Parallel()
runDiffTests(t, []diffTest{
{
name: "Create",
left: audit.Empty[database.GitSSHKey](),
right: database.GitSSHKey{
UserID: uuid.UUID{1},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
PrivateKey: "a very secret private key",
PublicKey: "a very public public key",
},
exp: audit.Map{
"user_id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()},
"private_key": audit.OldNew{Old: "", New: "", Secret: true},
"public_key": audit.OldNew{Old: "", New: "a very public public key"},
},
},
})
runDiffTests(t, []diffTest{
{
name: "Create",
left: audit.Empty[database.Template](),
right: database.Template{
ID: uuid.UUID{1},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
OrganizationID: uuid.UUID{2},
Deleted: false,
Name: "rust",
Provisioner: database.ProvisionerTypeTerraform,
ActiveVersionID: uuid.UUID{3},
DefaultTTL: int64(time.Hour),
CreatedBy: uuid.UUID{4},
},
exp: audit.Map{
"id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()},
"name": audit.OldNew{Old: "", New: "rust"},
"provisioner": audit.OldNew{Old: database.ProvisionerType(""), New: database.ProvisionerTypeTerraform},
"active_version_id": audit.OldNew{Old: "", New: uuid.UUID{3}.String()},
"default_ttl": audit.OldNew{Old: int64(0), New: int64(time.Hour)},
"created_by": audit.OldNew{Old: "", New: uuid.UUID{4}.String()},
},
},
})
runDiffTests(t, []diffTest{
{
name: "Create",
left: audit.Empty[database.TemplateVersion](),
right: database.TemplateVersion{
ID: uuid.UUID{1},
TemplateID: uuid.NullUUID{UUID: uuid.UUID{2}, Valid: true},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
OrganizationID: uuid.UUID{3},
Name: "rust",
CreatedBy: uuid.UUID{4},
},
exp: audit.Map{
"id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()},
"template_id": audit.OldNew{Old: "null", New: uuid.UUID{2}.String()},
"created_by": audit.OldNew{Old: "", New: uuid.UUID{4}.String()},
"name": audit.OldNew{Old: "", New: "rust"},
},
},
{
name: "CreateNullTemplateID",
left: audit.Empty[database.TemplateVersion](),
right: database.TemplateVersion{
ID: uuid.UUID{1},
TemplateID: uuid.NullUUID{},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
OrganizationID: uuid.UUID{3},
Name: "rust",
CreatedBy: uuid.UUID{4},
},
exp: audit.Map{
"id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()},
"created_by": audit.OldNew{Old: "", New: uuid.UUID{4}.String()},
"name": audit.OldNew{Old: "", New: "rust"},
},
},
})
runDiffTests(t, []diffTest{
{
name: "Create",
left: audit.Empty[database.User](),
right: database.User{
ID: uuid.UUID{1},
Email: "colin@coder.com",
Username: "colin",
HashedPassword: []byte("hunter2ButHashed"),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Status: database.UserStatusActive,
RBACRoles: []string{"omega admin"},
},
exp: audit.Map{
"id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()},
"email": audit.OldNew{Old: "", New: "colin@coder.com"},
"username": audit.OldNew{Old: "", New: "colin"},
"hashed_password": audit.OldNew{Old: ([]byte)(nil), New: ([]byte)(nil), Secret: true},
"status": audit.OldNew{Old: database.UserStatus(""), New: database.UserStatusActive},
"rbac_roles": audit.OldNew{Old: (pq.StringArray)(nil), New: pq.StringArray{"omega admin"}},
},
},
})
runDiffTests(t, []diffTest{
{
// Chat titles can contain sensitive content, so they must be
// masked in audit diffs via ActionSecret. This case guards
// against a regression where title is flipped back to
// ActionTrack in enterprise/audit/table.go.
name: "TitleMasked",
left: audit.Empty[database.Chat](),
right: database.Chat{
ID: uuid.UUID{1},
OwnerID: uuid.UUID{2},
WorkspaceID: uuid.NullUUID{UUID: uuid.UUID{3}, Valid: true},
Title: "a very secret chat title",
},
exp: audit.Map{
"id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()},
"owner_id": audit.OldNew{Old: "", New: uuid.UUID{2}.String()},
"workspace_id": audit.OldNew{Old: "null", New: uuid.UUID{3}.String()},
"title": audit.OldNew{Old: "", New: "", Secret: true},
},
},
})
runDiffTests(t, []diffTest{
{
// User skill content is user-authored instruction text, not secret
// material, so audit diffs can include the content change.
name: "UserSkillContentTracked",
left: audit.Empty[database.UserSkill](),
right: database.UserSkill{
ID: uuid.UUID{1},
UserID: uuid.UUID{2},
Name: "review-guidance",
Description: "How to review private projects",
Content: "review markdown",
},
exp: audit.Map{
"id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()},
"user_id": audit.OldNew{Old: "", New: uuid.UUID{2}.String()},
"name": audit.OldNew{Old: "", New: "review-guidance"},
"description": audit.OldNew{Old: "", New: "How to review private projects"},
"content": audit.OldNew{Old: "", New: "review markdown"},
},
},
})
runDiffTests(t, []diffTest{
{
name: "Create",
left: audit.Empty[database.WorkspaceTable](),
right: database.WorkspaceTable{
ID: uuid.UUID{1},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
OwnerID: uuid.UUID{2},
TemplateID: uuid.UUID{3},
Name: "rust workspace",
AutostartSchedule: sql.NullString{String: "0 12 * * 1-5", Valid: true},
Ttl: sql.NullInt64{Int64: int64(8 * time.Hour), Valid: true},
},
exp: audit.Map{
"id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()},
"owner_id": audit.OldNew{Old: "", New: uuid.UUID{2}.String()},
"template_id": audit.OldNew{Old: "", New: uuid.UUID{3}.String()},
"name": audit.OldNew{Old: "", New: "rust workspace"},
"autostart_schedule": audit.OldNew{Old: "null", New: "0 12 * * 1-5"},
"ttl": audit.OldNew{Old: int64(0), New: int64(8 * time.Hour)}, // XXX: pq still does not support time.Duration
},
},
{
name: "NullSchedules",
left: audit.Empty[database.WorkspaceTable](),
right: database.WorkspaceTable{
ID: uuid.UUID{1},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
OwnerID: uuid.UUID{2},
TemplateID: uuid.UUID{3},
Name: "rust workspace",
AutostartSchedule: sql.NullString{},
Ttl: sql.NullInt64{},
},
exp: audit.Map{
"id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()},
"owner_id": audit.OldNew{Old: "", New: uuid.UUID{2}.String()},
"template_id": audit.OldNew{Old: "", New: uuid.UUID{3}.String()},
"name": audit.OldNew{Old: "", New: "rust workspace"},
},
},
})
runDiffTests(t, []diffTest{
{
name: "PropertyChange",
left: database.AIProvider{
ID: uuid.UUID{1},
Type: database.AIProviderTypeOpenai,
Name: "primary-openai",
DisplayName: sql.NullString{String: "Primary", Valid: true},
Enabled: true,
BaseUrl: "https://api.openai.com/v1",
},
right: database.AIProvider{
ID: uuid.UUID{1},
Type: database.AIProviderTypeOpenai,
Name: "primary-openai",
DisplayName: sql.NullString{String: "Renamed", Valid: true},
Enabled: false,
BaseUrl: "https://api.openai.com/v2",
},
exp: audit.Map{
"display_name": audit.OldNew{Old: "Primary", New: "Renamed"},
"enabled": audit.OldNew{Old: true, New: false},
"base_url": audit.OldNew{Old: "https://api.openai.com/v1", New: "https://api.openai.com/v2"},
},
},
})
runDiffTests(t, []diffTest{
{
// api_key is tracked, but callers must pre-mask before the
// row reaches the audit pipeline. The pre-masked rendering
// (sk-prefix...suffix) is what flows into the diff.
name: "PreMaskedKeyFlowsThrough",
left: audit.Empty[database.AIProviderKey](),
right: database.AIProviderKey{
ID: uuid.UUID{1},
ProviderID: uuid.UUID{2},
APIKey: "sk-a...wxyz",
},
exp: audit.Map{
"id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()},
"provider_id": audit.OldNew{Old: "", New: uuid.UUID{2}.String()},
"api_key": audit.OldNew{Old: "", New: "sk-a...wxyz"},
},
},
})
runDiffTests(t, []diffTest{
{
// Prompt text is tracked, not secret: reviewers must see what
// agents are told to do (CODAGT-719). The system-prompt
// endpoint populates only its two fields; the untouched
// plan-mode field stays zero on both sides and never diffs.
name: "SystemPromptChangeTracked",
left: database.ChatInstructionSettings{
SystemPrompt: "old instructions",
IncludeDefaultSystemPrompt: true,
},
right: database.ChatInstructionSettings{
ID: uuid.UUID{1},
SystemPrompt: "new instructions",
IncludeDefaultSystemPrompt: false,
},
exp: audit.Map{
"system_prompt": audit.OldNew{Old: "old instructions", New: "new instructions"},
"include_default_system_prompt": audit.OldNew{Old: true, New: false},
},
},
{
// The plan-mode endpoint populates only its own field; the
// system-prompt fields stay zero on both sides, so a
// plan-mode change diffs exactly one field.
name: "PlanModeInstructionsChangeTracked",
left: database.ChatInstructionSettings{
PlanModeInstructions: "old plan guidance",
},
right: database.ChatInstructionSettings{
ID: uuid.UUID{1},
PlanModeInstructions: "new plan guidance",
},
exp: audit.Map{
"plan_mode_instructions": audit.OldNew{Old: "old plan guidance", New: "new plan guidance"},
},
},
{
// The artificial ID is ignored, so a value-identical write
// would diff empty. Handlers additionally suppress the entry
// entirely by leaving both resource IDs nil.
name: "ArtificialIDIgnored",
left: database.ChatInstructionSettings{
SystemPrompt: "same",
IncludeDefaultSystemPrompt: true,
},
right: database.ChatInstructionSettings{
ID: uuid.UUID{1},
SystemPrompt: "same",
IncludeDefaultSystemPrompt: true,
},
exp: audit.Map{},
},
})
runDiffTests(t, []diffTest{
{
name: "CreateEmptyStoredValues",
left: audit.Empty[database.MCPServerConfig](),
right: database.MCPServerConfig{
CustomHeaders: "{}",
ToolAllowList: []string{},
ToolDenyList: []string{},
},
exp: audit.Map{},
},
{
name: "Create",
left: audit.Empty[database.MCPServerConfig](),
right: database.MCPServerConfig{
ID: uuid.UUID{1},
DisplayName: "GitHub MCP",
Slug: "github",
Url: "https://mcp.example.com/v1",
AuthType: "api_key",
APIKeyHeader: "X-Api-Key",
APIKeyValue: "plaintext-api-key",
CustomHeaders: `{"Authorization":"Bearer plaintext-header"}`,
ToolAllowList: []string{"issues"},
ToolDenyList: []string{"delete_repository"},
OAuth2ClientSecret: "plaintext-oauth-secret",
Enabled: true,
CreatedBy: uuid.NullUUID{UUID: uuid.UUID{2}, Valid: true},
UpdatedBy: uuid.NullUUID{UUID: uuid.UUID{2}, Valid: true},
OrganizationID: uuid.UUID{4},
},
exp: audit.Map{
"display_name": audit.OldNew{Old: "", New: "GitHub MCP"},
"slug": audit.OldNew{Old: "", New: "github"},
"url": audit.OldNew{Old: "", New: "https://mcp.example.com/v1"},
"auth_type": audit.OldNew{Old: "", New: "api_key"},
"api_key_header": audit.OldNew{Old: "", New: "X-Api-Key"},
"api_key_value": audit.OldNew{Old: "", New: "", Secret: true},
"custom_headers": audit.OldNew{Old: "", New: "", Secret: true},
"tool_allow_list": audit.OldNew{Old: []string(nil), New: []string{"issues"}},
"tool_deny_list": audit.OldNew{Old: []string(nil), New: []string{"delete_repository"}},
"oauth2_client_secret": audit.OldNew{Old: "", New: "", Secret: true},
"enabled": audit.OldNew{Old: false, New: true},
"created_by": audit.OldNew{Old: "null", New: uuid.UUID{2}.String()},
"updated_by": audit.OldNew{Old: "null", New: uuid.UUID{2}.String()},
},
},
{
name: "CustomHeadersAdded",
left: database.MCPServerConfig{
CustomHeaders: "{}",
},
right: database.MCPServerConfig{
CustomHeaders: `{"Authorization":"Bearer plaintext-header"}`,
},
exp: audit.Map{
"custom_headers": audit.OldNew{Old: "", New: "", Secret: true},
},
},
{
name: "CustomHeadersRemoved",
left: database.MCPServerConfig{
CustomHeaders: `{"Authorization":"Bearer plaintext-header"}`,
},
right: database.MCPServerConfig{
CustomHeaders: "{}",
},
exp: audit.Map{
"custom_headers": audit.OldNew{Old: "", New: "", Secret: true},
},
},
{
name: "SecretRotationRedacted",
left: database.MCPServerConfig{
ID: uuid.UUID{1},
DisplayName: "GitHub MCP",
AuthType: "api_key",
APIKeyValue: "old-plaintext-api-key",
APIKeyValueKeyID: sql.NullString{String: "key-1", Valid: true},
CustomHeaders: `{"Authorization":"Bearer old-plaintext"}`,
CustomHeadersKeyID: sql.NullString{String: "key-1", Valid: true},
OrganizationID: uuid.UUID{4},
},
right: database.MCPServerConfig{
ID: uuid.UUID{1},
DisplayName: "Renamed MCP",
AuthType: "api_key",
APIKeyValue: "new-plaintext-api-key",
CustomHeaders: `{"Authorization":"Bearer new-plaintext"}`,
OrganizationID: uuid.UUID{4},
},
exp: audit.Map{
"display_name": audit.OldNew{Old: "GitHub MCP", New: "Renamed MCP"},
"api_key_value": audit.OldNew{Old: "", New: "", Secret: true},
"custom_headers": audit.OldNew{Old: "", New: "", Secret: true},
},
},
})
}
func Test_mcpServerConfigSecretsNeverSerialized(t *testing.T) {
t.Parallel()
secrets := []string{
"plaintext-oauth-secret",
"plaintext-api-key",
"Bearer plaintext-header",
}
left := audit.Empty[database.MCPServerConfig]()
right := database.MCPServerConfig{
ID: uuid.UUID{1},
DisplayName: "GitHub MCP",
AuthType: "oauth2",
OAuth2ClientID: "client-id",
OAuth2ClientSecret: secrets[0],
APIKeyValue: secrets[1],
CustomHeaders: `{"Authorization":"` + secrets[2] + `"}`,
OrganizationID: uuid.UUID{4},
}
raw, err := json.Marshal(diffValues(left, right, AuditableResources))
require.NoError(t, err)
for _, secret := range secrets {
require.NotContains(t, string(raw), secret)
}
require.Contains(t, string(raw), "client-id")
}
func runDiffTests(t *testing.T, tests []diffTest) {
t.Helper()
for _, test := range tests {
typName := reflect.TypeOf(test.left).Name()
t.Run(typName+"/"+test.name, func(t *testing.T) {
t.Parallel()
require.Equal(t,
test.exp,
diffValues(test.left, test.right, AuditableResources),
)
})
}
}