[MM-66243] Omit sanitized last_viewed_at/last_update_at instead of returning -1 for other users (#37505)

* Omit sanitized channel member timestamps from JSON

The channel member sanitization introduced in #33835 replaced other
users' LastViewedAt and LastUpdateAt with -1, which clients decode as
Dec 31 1969. Serialize the sanitized sentinel as an absent field instead
so the API no longer returns an invalid timestamp for other users.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Add tests and API docs for omitted sanitized member timestamps

Verify at the JSON layer that last_viewed_at and last_update_at are
omitted for other users' memberships (across the channel and user
endpoints) while remaining present for the requester, including a
legitimate zero timestamp. Document the omission in the API spec.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Strengthen sanitized-timestamp test coverage

Cover the NDJSON streaming branch of getChannelMembersForUser and the
getChannelMembersForTeamForUser endpoint, assert the requester's own
timestamps are valid (not the sentinel), use the sanitizedTimestamp
constant, and note the ChannelMemberForExport marshaling footgun.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Marshal team data via a typed struct in ChannelMemberWithTeamData

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Avoid shadowing err in ChannelMemberWithTeamData.MarshalJSON

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Use omitzero tags to omit sanitized member timestamps

Replace the custom ChannelMember/ChannelMemberWithTeamData MarshalJSON
round-trip with the Go 1.24 omitzero tag on LastViewedAt/LastUpdateAt.
SanitizeForCurrentUser now zeroes another user's timestamps so they are
omitted from API responses, per reviewer feedback.

* Give current user a real last_viewed_at in sanitization test

With omitzero, a zero last_viewed_at is legitimately omitted. Have user2
post an unread message and the current user view the channel so the
current-user assertions verify a genuine timestamp survives sanitization.

* Use -1 sentinel for sanitized member timestamps with single-pass marshal

A last_viewed_at of 0 legitimately means "never viewed", so it cannot
double as the sanitization sentinel. Restore the -1 sentinel and omit it
during serialization via shadowing pointer fields, avoiding the previous
marshal/unmarshal/marshal round-trip.

* Clarify ChannelMember.MarshalJSON doc comment per review feedback

* Address PR feedback: 0 answered, 4 resolved, 0 declined

- Simplify sanitizedTimestamp and SanitizeForCurrentUser doc comments per review
- Document that new ChannelMemberWithTeamData fields must be added to MarshalJSON
- Add round-trip test guarding against fields dropped by MarshalJSON

* Address PR feedback: remove round-trip MarshalJSON test

The round-trip test did not guard against forgetting to add a new field to
MarshalJSON, since the same field would also be missing from the test.

* Address PR feedback: assert legitimate zero last_update_at is serialized

* Mark sanitized channel member timestamp fields as nullable in OpenAPI spec

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: mattermost-code <matty-code@mattermost.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
authored and GitHub committed 2026-08-18 16:46:56 +00:00
1 parent 6938cabac6
commit 44d12bef80
4 files changed
+346 -66

No files matched your search

+10 -2
View File
@@ -243,9 +243,13 @@ components:
roles:
type: string
last_viewed_at:
description: The time in milliseconds the channel was last viewed by the user
description: >-
The time in milliseconds the channel was last viewed by the user.
This field is omitted when the membership belongs to a user other
than the requester, as the value is private to that user.
type: integer
format: int64
nullable: true
msg_count:
type: integer
mention_count:
@@ -253,9 +257,13 @@ components:
notify_props:
$ref: "#/components/schemas/ChannelNotifyProps"
last_update_at:
description: The time in milliseconds the channel member was last updated
description: >-
The time in milliseconds the channel member was last updated. This
field is omitted when the membership belongs to a user other than
the requester, as the value is private to that user.
type: integer
format: int64
nullable: true
ChannelMemberWithTeamData:
allOf:
- $ref: "#/components/schemas/ChannelMember"
+144 -54
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"testing"
@@ -7505,73 +7506,162 @@ func TestChannelMemberSanitization(t *testing.T) {
_, _, err := client.AddChannelMember(context.Background(), channel.Id, user2.Id)
require.NoError(t, err)
t.Run("getChannelMembers sanitizes LastViewedAt and LastUpdateAt for other users", func(t *testing.T) {
members, _, err := client.GetChannelMembers(context.Background(), channel.Id, 0, 60, "")
require.NoError(t, err)
// Give the current user a real, non-zero last_viewed_at: user2 posts a
// message so the channel is unread for the current user, who then views it.
// This keeps the current-user assertions realistic; the requester's own
// timestamps are never sanitized, so a genuine 0 would still be serialized.
user2Client := th.CreateClient()
_, _, err = user2Client.Login(context.Background(), user2.Email, user2.Password)
require.NoError(t, err)
_, _, err = user2Client.CreatePost(context.Background(), &model.Post{ChannelId: channel.Id, Message: "unread message"})
require.NoError(t, err)
_, _, err = client.ViewChannel(context.Background(), user.Id, &model.ChannelView{ChannelId: channel.Id})
require.NoError(t, err)
// decodeRawMembers reads the raw JSON body of a channel member response so the
// test can assert whether the timestamp fields are present or omitted, which a
// typed model.ChannelMember cannot distinguish from a zero value.
decodeRawMembers := func(resp *http.Response, err error) []map[string]json.RawMessage {
require.NoError(t, err)
defer resp.Body.Close()
var raw json.RawMessage
require.NoError(t, json.NewDecoder(resp.Body).Decode(&raw))
var members []map[string]json.RawMessage
if decodeErr := json.Unmarshal(raw, &members); decodeErr != nil {
var single map[string]json.RawMessage
require.NoError(t, json.Unmarshal(raw, &single))
members = []map[string]json.RawMessage{single}
}
return members
}
// decodeNDJSONMembers reads a newline-delimited JSON stream, as returned by
// getChannelMembersForUser when page=-1.
decodeNDJSONMembers := func(resp *http.Response, err error) []map[string]json.RawMessage {
require.NoError(t, err)
defer resp.Body.Close()
var members []map[string]json.RawMessage
decoder := json.NewDecoder(resp.Body)
for {
var member map[string]json.RawMessage
decodeErr := decoder.Decode(&member)
if decodeErr == io.EOF {
break
}
require.NoError(t, decodeErr)
members = append(members, member)
}
return members
}
userIDOf := func(t *testing.T, member map[string]json.RawMessage) string {
t.Helper()
var id string
require.NoError(t, json.Unmarshal(member["user_id"], &id))
return id
}
// assertTimestamps verifies that the current user's memberships expose valid
// timestamps while other users' timestamps are omitted entirely.
assertTimestamps := func(t *testing.T, members []map[string]json.RawMessage) {
t.Helper()
for _, member := range members {
if member.UserId == user.Id {
// Current user should see their own timestamps
assert.NotEqual(t, int64(-1), member.LastViewedAt, "Current user should see their LastViewedAt")
assert.NotEqual(t, int64(-1), member.LastUpdateAt, "Current user should see their LastUpdateAt")
rawLastViewedAt, hasLastViewedAt := member["last_viewed_at"]
rawLastUpdateAt, hasLastUpdateAt := member["last_update_at"]
if userIDOf(t, member) == user.Id {
require.True(t, hasLastViewedAt, "Current user should see their last_viewed_at")
require.True(t, hasLastUpdateAt, "Current user should see their last_update_at")
var lastViewedAt, lastUpdateAt int64
require.NoError(t, json.Unmarshal(rawLastViewedAt, &lastViewedAt))
require.NoError(t, json.Unmarshal(rawLastUpdateAt, &lastUpdateAt))
assert.GreaterOrEqual(t, lastViewedAt, int64(0), "Current user's last_viewed_at should be a valid timestamp, not the sentinel")
assert.GreaterOrEqual(t, lastUpdateAt, int64(0), "Current user's last_update_at should be a valid timestamp, not the sentinel")
} else {
// Other users' timestamps should be sanitized
assert.Equal(t, int64(-1), member.LastViewedAt, "Other users' LastViewedAt should be sanitized")
assert.Equal(t, int64(-1), member.LastUpdateAt, "Other users' LastUpdateAt should be sanitized")
assert.False(t, hasLastViewedAt, "Other users' last_viewed_at should be omitted, not returned as an invalid value")
assert.False(t, hasLastUpdateAt, "Other users' last_update_at should be omitted, not returned as an invalid value")
}
}
})
}
t.Run("getChannelMember sanitizes LastViewedAt and LastUpdateAt for other users", func(t *testing.T) {
// Get other user's membership data
member, _, err := client.GetChannelMember(context.Background(), channel.Id, user2.Id, "")
require.NoError(t, err)
// Should be sanitized since it's not the current user
assert.Equal(t, int64(-1), member.LastViewedAt, "Other user's LastViewedAt should be sanitized")
assert.Equal(t, int64(-1), member.LastUpdateAt, "Other user's LastUpdateAt should be sanitized")
// Get current user's membership data
currentMember, _, err := client.GetChannelMember(context.Background(), channel.Id, user.Id, "")
require.NoError(t, err)
// Should not be sanitized since it's the current user
assert.NotEqual(t, int64(-1), currentMember.LastViewedAt, "Current user should see their LastViewedAt")
assert.NotEqual(t, int64(-1), currentMember.LastUpdateAt, "Current user should see their LastUpdateAt")
})
t.Run("getChannelMembersByIds sanitizes data appropriately", func(t *testing.T) {
userIds := []string{user.Id, user2.Id}
members, _, err := client.GetChannelMembersByIds(context.Background(), channel.Id, userIds)
require.NoError(t, err)
t.Run("getChannelMembers omits last_viewed_at and last_update_at for other users", func(t *testing.T) {
members := decodeRawMembers(client.DoAPIGet(context.Background(), "/channels/"+channel.Id+"/members?page=0&per_page=60", ""))
require.Len(t, members, 2)
for _, member := range members {
if member.UserId == user.Id {
// Current user should see their own timestamps
assert.NotEqual(t, int64(-1), member.LastViewedAt, "Current user should see their LastViewedAt")
assert.NotEqual(t, int64(-1), member.LastUpdateAt, "Current user should see their LastUpdateAt")
} else {
// Other users' timestamps should be sanitized
assert.Equal(t, int64(-1), member.LastViewedAt, "Other users' LastViewedAt should be sanitized")
assert.Equal(t, int64(-1), member.LastUpdateAt, "Other users' LastUpdateAt should be sanitized")
}
}
assertTimestamps(t, members)
})
t.Run("addChannelMember sanitizes returned member data", func(t *testing.T) {
t.Run("getChannelMember omits timestamps for other users but keeps them for the current user", func(t *testing.T) {
otherMembers := decodeRawMembers(client.DoAPIGet(context.Background(), "/channels/"+channel.Id+"/members/"+user2.Id, ""))
require.Len(t, otherMembers, 1)
_, hasLastViewedAt := otherMembers[0]["last_viewed_at"]
_, hasLastUpdateAt := otherMembers[0]["last_update_at"]
assert.False(t, hasLastViewedAt, "Other user's last_viewed_at should be omitted")
assert.False(t, hasLastUpdateAt, "Other user's last_update_at should be omitted")
currentMembers := decodeRawMembers(client.DoAPIGet(context.Background(), "/channels/"+channel.Id+"/members/"+user.Id, ""))
require.Len(t, currentMembers, 1)
assertTimestamps(t, currentMembers)
})
t.Run("getChannelMembersByIds omits timestamps for other users", func(t *testing.T) {
members := decodeRawMembers(client.DoAPIPostJSON(context.Background(), "/channels/"+channel.Id+"/members/ids", []string{user.Id, user2.Id}))
require.Len(t, members, 2)
assertTimestamps(t, members)
})
assertOtherUserMembersOmitted := func(t *testing.T, members []map[string]json.RawMessage, expectTeamData bool) {
t.Helper()
require.NotEmpty(t, members)
for _, member := range members {
assert.Equal(t, user2.Id, userIDOf(t, member))
assert.NotContains(t, member, "last_viewed_at", "Other user's last_viewed_at should be omitted")
assert.NotContains(t, member, "last_update_at", "Other user's last_update_at should be omitted")
if expectTeamData {
assert.Contains(t, member, "team_name", "Team data should still be present")
}
}
}
t.Run("getChannelMembersForUser (paginated) omits timestamps for other users", func(t *testing.T) {
// Querying another user's channel members requires the edit_other_users
// permission, so use the system admin client.
members := decodeRawMembers(th.SystemAdminClient.DoAPIGet(context.Background(), "/users/"+user2.Id+"/channel_members?page=0", ""))
assertOtherUserMembersOmitted(t, members, true)
})
t.Run("getChannelMembersForUser (NDJSON stream) omits timestamps for other users", func(t *testing.T) {
// page=-1 switches the endpoint to the newline-delimited streaming path,
// which sanitizes members through a separate code path.
members := decodeNDJSONMembers(th.SystemAdminClient.DoAPIGet(context.Background(), "/users/"+user2.Id+"/channel_members?page=-1", ""))
assertOtherUserMembersOmitted(t, members, true)
})
t.Run("getChannelMembersForTeamForUser omits timestamps for other users", func(t *testing.T) {
// Querying another user's memberships requires manage_system, so use the
// system admin client.
members := decodeRawMembers(th.SystemAdminClient.DoAPIGet(context.Background(), "/users/"+user2.Id+"/teams/"+th.BasicTeam.Id+"/channels/members", ""))
assertOtherUserMembersOmitted(t, members, false)
})
t.Run("addChannelMember omits timestamps in the returned member data", func(t *testing.T) {
newUser := th.CreateUser(t)
th.LinkUserToTeam(t, newUser, th.BasicTeam)
// Add new user and check returned member data
returnedMember, _, err := client.AddChannelMember(context.Background(), channel.Id, newUser.Id)
require.NoError(t, err)
members := decodeRawMembers(client.DoAPIPostJSON(context.Background(), "/channels/"+channel.Id+"/members", map[string]string{"user_id": newUser.Id}))
require.Len(t, members, 1)
// The returned member should be sanitized since it's not the current user
assert.Equal(t, int64(-1), returnedMember.LastViewedAt, "Returned member LastViewedAt should be sanitized")
assert.Equal(t, int64(-1), returnedMember.LastUpdateAt, "Returned member LastUpdateAt should be sanitized")
assert.Equal(t, newUser.Id, returnedMember.UserId, "UserId should be preserved")
assert.Equal(t, channel.Id, returnedMember.ChannelId, "ChannelId should be preserved")
assert.NotContains(t, members[0], "last_viewed_at", "Returned member last_viewed_at should be omitted")
assert.NotContains(t, members[0], "last_update_at", "Returned member last_update_at should be omitted")
assert.Equal(t, newUser.Id, userIDOf(t, members[0]), "UserId should be preserved")
var channelID string
require.NoError(t, json.Unmarshal(members[0]["channel_id"], &channelID))
assert.Equal(t, channel.Id, channelID, "ChannelId should be preserved")
})
}
+66 -6
View File
@@ -4,6 +4,7 @@
package model
import (
"encoding/json"
"fmt"
"net/http"
"strings"
@@ -90,19 +91,53 @@ func (o *ChannelMember) Auditable() map[string]any {
}
}
// SanitizeForCurrentUser sanitizes channel member data based on whether
// it's the current user's own membership or another user's membership
// sanitizedTimestamp marks a LastViewedAt/LastUpdateAt field that belongs to
// another user and must be hidden. MarshalJSON omits any field holding this
// sentinel rather than serializing an invalid value
const sanitizedTimestamp int64 = -1
// SanitizeForCurrentUser hides another user's private timestamp fields by
// marking them with the sanitized sentinel, which MarshalJSON then omits from
// API responses. The requester's own values are left untouched.
func (o *ChannelMember) SanitizeForCurrentUser(currentUserId string) {
// If this is not the current user's own membership,
// sanitize sensitive timestamp fields
if o.UserId != currentUserId {
o.LastViewedAt = -1
o.LastUpdateAt = -1
o.LastViewedAt = sanitizedTimestamp
o.LastUpdateAt = sanitizedTimestamp
}
}
// timestampOrNil returns nil for the sanitized sentinel so that the omitempty
// tag drops the field, and a pointer to the real value otherwise (including a
// legitimate 0).
func timestampOrNil(ts int64) *int64 {
if ts == sanitizedTimestamp {
return nil
}
return &ts
}
// MarshalJSON serializes the channel member in a single pass, omitting
// last_viewed_at and/or last_update_at when they hold the sanitized sentinel
// written by SanitizeForCurrentUser. The shadowing pointer fields allow for a
// direct marshal with the sanitized values removed if needed.
func (o ChannelMember) MarshalJSON() ([]byte, error) {
type alias ChannelMember
return json.Marshal(&struct {
*alias
LastViewedAt *int64 `json:"last_viewed_at,omitempty"`
LastUpdateAt *int64 `json:"last_update_at,omitempty"`
}{
alias: (*alias)(&o),
LastViewedAt: timestampOrNil(o.LastViewedAt),
LastUpdateAt: timestampOrNil(o.LastUpdateAt),
})
}
// ChannelMemberWithTeamData contains ChannelMember appended with extra team information
// as well.
//
// Any new non-embedded field added here must also be added to MarshalJSON below,
// otherwise it will be silently dropped from the JSON output.
type ChannelMemberWithTeamData struct {
ChannelMember
TeamDisplayName string `json:"team_display_name"`
@@ -110,10 +145,35 @@ type ChannelMemberWithTeamData struct {
TeamUpdateAt int64 `json:"team_update_at"`
}
// MarshalJSON flattens the embedded ChannelMember together with the team fields
// in a single pass. It is required because ChannelMember's MarshalJSON would
// otherwise be promoted and drop the team fields entirely.
func (o ChannelMemberWithTeamData) MarshalJSON() ([]byte, error) {
type alias ChannelMember
return json.Marshal(&struct {
*alias
LastViewedAt *int64 `json:"last_viewed_at,omitempty"`
LastUpdateAt *int64 `json:"last_update_at,omitempty"`
TeamDisplayName string `json:"team_display_name"`
TeamName string `json:"team_name"`
TeamUpdateAt int64 `json:"team_update_at"`
}{
alias: (*alias)(&o.ChannelMember),
LastViewedAt: timestampOrNil(o.LastViewedAt),
LastUpdateAt: timestampOrNil(o.LastUpdateAt),
TeamDisplayName: o.TeamDisplayName,
TeamName: o.TeamName,
TeamUpdateAt: o.TeamUpdateAt,
})
}
type ChannelMembers []ChannelMember
type ChannelMembersWithTeamData []ChannelMemberWithTeamData
// ChannelMemberForExport is only converted field-by-field for export and is
// never JSON-marshaled. If that changes, it must define its own MarshalJSON;
// otherwise ChannelMember's promoted MarshalJSON drops ChannelName and Username.
type ChannelMemberForExport struct {
ChannelMember
ChannelName string
+126 -4
View File
@@ -4,6 +4,7 @@
package model
import (
"encoding/json"
"strings"
"testing"
@@ -93,8 +94,8 @@ func TestChannelMemberSanitizeForCurrentUser(t *testing.T) {
member.SanitizeForCurrentUser(currentUserId)
assert.Equal(t, int64(-1), member.LastViewedAt, "LastViewedAt should be sanitized for other users")
assert.Equal(t, int64(-1), member.LastUpdateAt, "LastUpdateAt should be sanitized for other users")
assert.Equal(t, sanitizedTimestamp, member.LastViewedAt, "LastViewedAt should be marked sanitized for other users")
assert.Equal(t, sanitizedTimestamp, member.LastUpdateAt, "LastUpdateAt should be marked sanitized for other users")
})
t.Run("should preserve other fields when sanitizing", func(t *testing.T) {
@@ -120,8 +121,8 @@ func TestChannelMemberSanitizeForCurrentUser(t *testing.T) {
member.SanitizeForCurrentUser(currentUserId)
assert.Equal(t, int64(-1), member.LastViewedAt, "LastViewedAt should be sanitized")
assert.Equal(t, int64(-1), member.LastUpdateAt, "LastUpdateAt should be sanitized")
assert.Equal(t, sanitizedTimestamp, member.LastViewedAt, "LastViewedAt should be marked sanitized")
assert.Equal(t, sanitizedTimestamp, member.LastUpdateAt, "LastUpdateAt should be marked sanitized")
assert.Equal(t, originalRoles, member.Roles, "Roles should be preserved")
assert.Equal(t, originalMsgCount, member.MsgCount, "MsgCount should be preserved")
assert.Equal(t, originalMentionCount, member.MentionCount, "MentionCount should be preserved")
@@ -129,3 +130,124 @@ func TestChannelMemberSanitizeForCurrentUser(t *testing.T) {
assert.Equal(t, originalSchemeAdmin, member.SchemeAdmin, "SchemeAdmin should be preserved")
})
}
func TestChannelMemberMarshalJSON(t *testing.T) {
currentUserId := NewId()
otherUserId := NewId()
newMember := func(userId string) ChannelMember {
return ChannelMember{
ChannelId: NewId(),
UserId: userId,
Roles: "channel_user",
LastViewedAt: 1234567890000,
MsgCount: 100,
LastUpdateAt: 1234567890000,
NotifyProps: GetDefaultChannelNotifyProps(),
}
}
decode := func(t *testing.T, member ChannelMember) map[string]any {
t.Helper()
data, err := json.Marshal(member)
require.NoError(t, err)
fields := map[string]any{}
require.NoError(t, json.Unmarshal(data, &fields))
return fields
}
t.Run("keeps timestamps for the current user's own membership", func(t *testing.T) {
member := newMember(currentUserId)
member.SanitizeForCurrentUser(currentUserId)
fields := decode(t, member)
assert.EqualValues(t, 1234567890000, fields["last_viewed_at"])
assert.EqualValues(t, 1234567890000, fields["last_update_at"])
})
t.Run("keeps a legitimate zero timestamp for the requester", func(t *testing.T) {
member := newMember(currentUserId)
member.LastViewedAt = 0
member.LastUpdateAt = 0
member.SanitizeForCurrentUser(currentUserId)
fields := decode(t, member)
assert.Contains(t, fields, "last_viewed_at", "the requester's own last_viewed_at of 0 (never viewed) must be serialized")
assert.EqualValues(t, 0, fields["last_viewed_at"])
assert.Contains(t, fields, "last_update_at", "the requester's own last_update_at of 0 must be serialized")
assert.EqualValues(t, 0, fields["last_update_at"])
})
t.Run("omits sanitized timestamps for another user's membership", func(t *testing.T) {
member := newMember(otherUserId)
member.SanitizeForCurrentUser(currentUserId)
fields := decode(t, member)
assert.NotContains(t, fields, "last_viewed_at", "sanitized last_viewed_at must be omitted")
assert.NotContains(t, fields, "last_update_at", "sanitized last_update_at must be omitted")
assert.Equal(t, member.ChannelId, fields["channel_id"])
assert.Equal(t, otherUserId, fields["user_id"])
assert.Equal(t, "channel_user", fields["roles"])
assert.EqualValues(t, 100, fields["msg_count"])
assert.Contains(t, fields, "notify_props")
})
}
func TestChannelMemberWithTeamDataMarshalJSON(t *testing.T) {
currentUserId := NewId()
otherUserId := NewId()
newMember := func(userId string) ChannelMemberWithTeamData {
return ChannelMemberWithTeamData{
ChannelMember: ChannelMember{
ChannelId: NewId(),
UserId: userId,
Roles: "channel_user",
LastViewedAt: 1234567890000,
LastUpdateAt: 1234567890000,
NotifyProps: GetDefaultChannelNotifyProps(),
},
TeamDisplayName: "Test Team",
TeamName: "test-team",
TeamUpdateAt: 987654321,
}
}
decode := func(t *testing.T, member ChannelMemberWithTeamData) map[string]any {
t.Helper()
data, err := json.Marshal(member)
require.NoError(t, err)
fields := map[string]any{}
require.NoError(t, json.Unmarshal(data, &fields))
return fields
}
t.Run("preserves team data and timestamps for the current user", func(t *testing.T) {
member := newMember(currentUserId)
member.SanitizeForCurrentUser(currentUserId)
fields := decode(t, member)
assert.EqualValues(t, 1234567890000, fields["last_viewed_at"])
assert.EqualValues(t, 1234567890000, fields["last_update_at"])
assert.Equal(t, "Test Team", fields["team_display_name"])
assert.Equal(t, "test-team", fields["team_name"])
assert.EqualValues(t, 987654321, fields["team_update_at"])
})
t.Run("omits sanitized timestamps but keeps team data for another user", func(t *testing.T) {
member := newMember(otherUserId)
member.SanitizeForCurrentUser(currentUserId)
fields := decode(t, member)
assert.NotContains(t, fields, "last_viewed_at", "sanitized last_viewed_at must be omitted")
assert.NotContains(t, fields, "last_update_at", "sanitized last_update_at must be omitted")
assert.Equal(t, "Test Team", fields["team_display_name"])
assert.Equal(t, "test-team", fields["team_name"])
assert.EqualValues(t, 987654321, fields["team_update_at"])
assert.Equal(t, otherUserId, fields["user_id"])
})
}