fix: improve chat audit log descriptions and diff rendering (#25728)

Chat ACL audit diffs rendered as `[object Object]` because the diff
viewer called `.toString()` on object values. Common chat operations
(archive, share) showed generic "updated chat" descriptions instead of
semantic ones.

Add `chatAuditLogDescription` to derive semantic descriptions from the
audit diff for successful chat writes: "archived/unarchived chat" for
archive toggles, "updated sharing for chat" for ACL-only changes.
Extract diff value formatting into `formatAuditDiffValue`, which renders
object values as deterministic compact JSON with sorted keys, fixing the
`[object Object]` rendering for chat ACLs and any other object-valued
fields. The previous `determineIdPSyncMappingDiff` workaround for IdP
sync mappings was removed because the generic formatting handles it.

Closes CODAGT-513

> Generated by Coder Agents on behalf of @johnstcn
This commit is contained in:
Cian Johnston
2026-05-28 18:37:57 +01:00
committed by GitHub
parent ebf56ebd12
commit 7ea0eff94e
7 changed files with 381 additions and 73 deletions
+103
View File
@@ -3,6 +3,7 @@ package coderd
import (
"context"
"database/sql"
"encoding/json"
"testing"
"github.com/google/uuid"
@@ -14,6 +15,7 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbmock"
"github.com/coder/coder/v2/codersdk"
)
func TestAuditLogIsResourceDeleted(t *testing.T) {
@@ -111,6 +113,91 @@ func TestAuditLogDescription(t *testing.T) {
},
want: "{user} deleted the git ssh key",
},
{
name: "chat_archived",
alog: chatAuditLogRow(t, codersdk.AuditDiff{
"archived": {Old: false, New: true},
}),
want: "{user} archived chat {target}",
},
{
name: "chat_unarchived",
alog: chatAuditLogRow(t, codersdk.AuditDiff{
"archived": {Old: true, New: false},
}),
want: "{user} unarchived chat {target}",
},
{
name: "chat_sharing_user_acl",
alog: chatAuditLogRow(t, codersdk.AuditDiff{
"user_acl": {Old: map[string]any{}, New: map[string]any{"user-1": map[string]any{"permissions": []string{"read"}}}},
}),
want: "{user} updated sharing for chat {target}",
},
{
name: "chat_sharing_group_acl",
alog: chatAuditLogRow(t, codersdk.AuditDiff{
"group_acl": {Old: map[string]any{}, New: map[string]any{"group-1": map[string]any{"permissions": []string{"read"}}}},
}),
want: "{user} updated sharing for chat {target}",
},
{
name: "chat_sharing_both_acls",
alog: chatAuditLogRow(t, codersdk.AuditDiff{
"user_acl": {Old: map[string]any{}, New: map[string]any{"user-1": map[string]any{"permissions": []string{"read"}}}},
"group_acl": {Old: map[string]any{}, New: map[string]any{"group-1": map[string]any{"permissions": []string{"read"}}}},
}),
want: "{user} updated sharing for chat {target}",
},
{
name: "chat_mixed_diff_falls_through",
alog: chatAuditLogRow(t, codersdk.AuditDiff{
"archived": {Old: false, New: true},
"pin_order": {Old: 1, New: 0},
}),
want: "{user} updated chat {target}",
},
{
name: "chat_acl_with_extra_field_falls_through",
alog: chatAuditLogRow(t, codersdk.AuditDiff{
"user_acl": {Old: map[string]any{}, New: map[string]any{}},
"pin_order": {Old: 1, New: 0},
}),
want: "{user} updated chat {target}",
},
{
name: "chat_failed_write_no_override",
alog: func() database.GetAuditLogsOffsetRow {
row := chatAuditLogRow(t, codersdk.AuditDiff{
"archived": {Old: false, New: true},
})
row.AuditLog.StatusCode = 400
return row
}(),
want: "{user} unsuccessfully attempted to write chat {target}",
},
{
name: "chat_redirect_no_override",
alog: func() database.GetAuditLogsOffsetRow {
row := chatAuditLogRow(t, codersdk.AuditDiff{
"archived": {Old: false, New: true},
})
row.AuditLog.StatusCode = 303
return row
}(),
want: "{user} was redirected attempting to write chat {target}",
},
{
name: "chat_non_write_action_no_override",
alog: func() database.GetAuditLogsOffsetRow {
row := chatAuditLogRow(t, codersdk.AuditDiff{
"user_acl": {Old: map[string]any{}, New: map[string]any{"user-1": map[string]any{"permissions": []string{"read"}}}},
})
row.AuditLog.Action = database.AuditActionCreate
return row
}(),
want: "{user} created chat {target}",
},
}
// nolint: paralleltest // no longer need to reinitialize loop vars in go 1.22
for _, tc := range testCases {
@@ -121,3 +208,19 @@ func TestAuditLogDescription(t *testing.T) {
})
}
}
// chatAuditLogRow builds a GetAuditLogsOffsetRow for a successful chat write
// with the given diff, suitable for testing auditLogDescription.
func chatAuditLogRow(t *testing.T, diff codersdk.AuditDiff) database.GetAuditLogsOffsetRow {
t.Helper()
rawDiff, err := json.Marshal(diff)
require.NoError(t, err)
return database.GetAuditLogsOffsetRow{
AuditLog: database.AuditLog{
Action: database.AuditActionWrite,
StatusCode: 200,
ResourceType: database.ResourceTypeChat,
Diff: rawDiff,
},
}
}