fix for muting an account
This commit is contained in:
parent
aeaf3f4596
commit
87cebec9bd
12 changed files with 503 additions and 65 deletions
|
|
@ -0,0 +1,96 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// TestFirstContactMessageThenGetPeerDialogsReturnsUsableDialog reproduces the reported
|
||||
// client symptom (Android + desktop, both stock-derived): a first-ever message from a
|
||||
// sender with no prior dialog fires a notification, but the new dialog does not appear
|
||||
// in the recipient's dialog list until app restart. The client's live-update path
|
||||
// (updateInterfaceWithMessages) builds a dialog in-memory then confirms it via
|
||||
// messages.getPeerDialogs (DialogStore.ListByPeers server-side). This test drives that
|
||||
// exact server-side path end to end against real Postgres and dumps every field a stock
|
||||
// client needs to accept and render the dialog, to catch anything subtly missing/wrong
|
||||
// that unit tests against fakes wouldn't.
|
||||
func TestFirstContactMessageThenGetPeerDialogsReturnsUsableDialog(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
suffix := randomSuffix(t)
|
||||
|
||||
users := NewUserStore(pool)
|
||||
sender, err := users.Create(ctx, domain.User{AccessHash: 31, Phone: "+1667" + suffix + "01", FirstName: "Sender"})
|
||||
if err != nil {
|
||||
t.Fatalf("create sender: %v", err)
|
||||
}
|
||||
recipient, err := users.Create(ctx, domain.User{AccessHash: 32, Phone: "+1667" + suffix + "02", FirstName: "Recipient"})
|
||||
if err != nil {
|
||||
t.Fatalf("create recipient: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{sender.ID, recipient.ID})
|
||||
})
|
||||
|
||||
messages := NewMessageStore(pool)
|
||||
var originAuthKeyID [8]byte
|
||||
originAuthKeyID[0] = 9
|
||||
sendRes, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: sender.ID,
|
||||
RecipientUserID: recipient.ID,
|
||||
RandomID: 555111,
|
||||
Message: "hey, first message ever",
|
||||
Date: 1700005000,
|
||||
OriginAuthKeyID: originAuthKeyID,
|
||||
OriginSessionID: 41,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendPrivateText: %v", err)
|
||||
}
|
||||
t.Logf("sender box: %+v", sendRes.SenderMessage)
|
||||
t.Logf("recipient box: %+v", sendRes.RecipientMessage)
|
||||
|
||||
dialogs := NewDialogStore(pool)
|
||||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: sender.ID}
|
||||
list, err := dialogs.ListByPeers(ctx, recipient.ID, []domain.Peer{peer})
|
||||
if err != nil {
|
||||
t.Fatalf("ListByPeers (recipient's view of sender): %v", err)
|
||||
}
|
||||
t.Logf("dialog list: %+v", list)
|
||||
if len(list.Dialogs) != 1 {
|
||||
t.Fatalf("dialogs = %d, want exactly 1 (the freshly-created dialog with sender)", len(list.Dialogs))
|
||||
}
|
||||
d := list.Dialogs[0]
|
||||
t.Logf("dialog: %+v", d)
|
||||
if d.Peer != peer {
|
||||
t.Fatalf("dialog peer = %+v, want %+v", d.Peer, peer)
|
||||
}
|
||||
if d.TopMessage == 0 {
|
||||
t.Fatalf("dialog.TopMessage = 0, want the recipient's box id for the new message")
|
||||
}
|
||||
if d.TopMessage != sendRes.RecipientMessage.ID {
|
||||
t.Fatalf("dialog.TopMessage = %d, want recipient box id %d", d.TopMessage, sendRes.RecipientMessage.ID)
|
||||
}
|
||||
if len(list.Messages) != 1 {
|
||||
t.Fatalf("messages returned = %d, want 1 (top message content, required for stock client to accept the dialog)", len(list.Messages))
|
||||
}
|
||||
msg := list.Messages[0]
|
||||
t.Logf("message: %+v", msg)
|
||||
if msg.ID != d.TopMessage {
|
||||
t.Fatalf("message.ID = %d, does not match dialog.TopMessage = %d -- stock client discards a dialog whose top message it can't resolve", msg.ID, d.TopMessage)
|
||||
}
|
||||
if len(list.Users) == 0 {
|
||||
t.Fatalf("users returned = 0, want at least the sender's User object (client needs it to render the dialog title/avatar)")
|
||||
}
|
||||
foundSender := false
|
||||
for _, u := range list.Users {
|
||||
if u.ID == sender.ID {
|
||||
foundSender = true
|
||||
}
|
||||
}
|
||||
if !foundSender {
|
||||
t.Fatalf("users = %+v, want sender %d present", list.Users, sender.ID)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// TestNotifySettingsEventSurvivesPostgresRoundTrip is a regression test for a real production
|
||||
// incident: RecordNotifySettings set domain.UpdateEvent.NotifyPeerSettings only in memory, but
|
||||
// appendUserUpdateEvent/ListAfter never serialized that field to/from Postgres. The write
|
||||
// itself succeeded, but every later read (outbox dispatch batching, updates.getDifference)
|
||||
// got NotifyPeerSettings == nil back, so convert_updates.go's UpdateEventNotifySettings case
|
||||
// silently produced no TL update ("non-noop outbox event produced no update"). Worse: the
|
||||
// stuck outbox row blocked that account's entire dispatch lane, so unrelated updates (new
|
||||
// messages) stopped reaching other sessions until a full resync. This test appends a real
|
||||
// notify_settings event and reads it back through the same store, asserting the settings
|
||||
// survive the round trip.
|
||||
func TestNotifySettingsEventSurvivesPostgresRoundTrip(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
suffix := randomSuffix(t)
|
||||
|
||||
users := NewUserStore(pool)
|
||||
owner, err := users.Create(ctx, domain.User{AccessHash: 51, Phone: "+1668" + suffix + "01", FirstName: "Owner"})
|
||||
if err != nil {
|
||||
t.Fatalf("create owner: %v", err)
|
||||
}
|
||||
peerUser, err := users.Create(ctx, domain.User{AccessHash: 52, Phone: "+1668" + suffix + "02", FirstName: "Peer"})
|
||||
if err != nil {
|
||||
t.Fatalf("create peer: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, peerUser.ID})
|
||||
})
|
||||
|
||||
events := NewUpdateEventStore(pool)
|
||||
muteUntil := 2000000000
|
||||
showPreviews := false
|
||||
appended, err := events.AppendAllocatedWithDispatch(ctx, owner.ID, domain.UpdateEvent{
|
||||
Type: domain.UpdateEventNotifySettings,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: peerUser.ID},
|
||||
NotifyPeerSettings: &domain.PeerNotifySettings{
|
||||
MuteUntil: &muteUntil,
|
||||
ShowPreviews: &showPreviews,
|
||||
},
|
||||
PtsCount: 1,
|
||||
}, [8]byte{}, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("AppendAllocatedWithDispatch: %v", err)
|
||||
}
|
||||
t.Logf("appended event = %+v", appended)
|
||||
if appended.NotifyPeerSettings == nil {
|
||||
t.Fatalf("appended.NotifyPeerSettings = nil immediately after append, want the settings we sent")
|
||||
}
|
||||
|
||||
// Read it back exactly the way updates.getDifference does.
|
||||
replayed, err := events.ListAfter(ctx, owner.ID, appended.Pts-1, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("ListAfter: %v", err)
|
||||
}
|
||||
t.Logf("replayed events = %+v", replayed)
|
||||
var found *domain.UpdateEvent
|
||||
for i := range replayed {
|
||||
if replayed[i].Type == domain.UpdateEventNotifySettings && replayed[i].Pts == appended.Pts {
|
||||
found = &replayed[i]
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
t.Fatalf("notify_settings event at pts=%d not found in ListAfter result", appended.Pts)
|
||||
}
|
||||
if found.NotifyPeerSettings == nil {
|
||||
t.Fatalf("replayed event.NotifyPeerSettings = nil -- THE BUG: settings were lost across the Postgres round trip, so getDifference/outbox dispatch cannot build a TL update for this event")
|
||||
}
|
||||
if found.NotifyPeerSettings.MuteUntil == nil || *found.NotifyPeerSettings.MuteUntil != muteUntil {
|
||||
t.Fatalf("replayed MuteUntil = %+v, want %d", found.NotifyPeerSettings.MuteUntil, muteUntil)
|
||||
}
|
||||
if found.NotifyPeerSettings.ShowPreviews == nil || *found.NotifyPeerSettings.ShowPreviews != showPreviews {
|
||||
t.Fatalf("replayed ShowPreviews = %+v, want %v", found.NotifyPeerSettings.ShowPreviews, showPreviews)
|
||||
}
|
||||
if found.Peer != (domain.Peer{Type: domain.PeerTypeUser, ID: peerUser.ID}) {
|
||||
t.Fatalf("replayed Peer = %+v, want peer %d", found.Peer, peerUser.ID)
|
||||
}
|
||||
}
|
||||
|
|
@ -183,7 +183,12 @@ func appendUserUpdateEvent(ctx context.Context, db sqlcgen.DBTX, q *sqlcgen.Quer
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings, err := encodePeerSettings(event.Settings)
|
||||
var settings []byte
|
||||
if event.Type == domain.UpdateEventNotifySettings {
|
||||
settings, err = encodeNotifyPeerSettings(event.NotifyPeerSettings)
|
||||
} else {
|
||||
settings, err = encodePeerSettings(event.Settings)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -366,6 +371,13 @@ func (s *UpdateEventStore) ListAfter(ctx context.Context, userID int64, pts, lim
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("decode peer settings: %w", err)
|
||||
}
|
||||
var notifyPeerSettings *domain.PeerNotifySettings
|
||||
if domain.UpdateEventType(row.EventType) == domain.UpdateEventNotifySettings {
|
||||
notifyPeerSettings, err = decodeNotifyPeerSettings(row.PeerSettingsJson)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode notify peer settings: %w", err)
|
||||
}
|
||||
}
|
||||
messageIDs, err := decodeEventMessageIDs(row.MessageIdsJson)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode message ids: %w", err)
|
||||
|
|
@ -407,29 +419,30 @@ func (s *UpdateEventStore) ListAfter(ctx context.Context, userID int64, pts, lim
|
|||
return nil, fmt.Errorf("decode message rich message: %w", err)
|
||||
}
|
||||
event := domain.UpdateEvent{
|
||||
UserID: row.UserID,
|
||||
Type: domain.UpdateEventType(row.EventType),
|
||||
Pts: int(row.Pts),
|
||||
PtsCount: int(row.PtsCount),
|
||||
Date: int(row.Date),
|
||||
Peer: domain.Peer{Type: domain.PeerType(row.EventPeerType), ID: row.EventPeerID},
|
||||
Story: story,
|
||||
Peers: peers,
|
||||
Bool: row.EventBool,
|
||||
Phone: row.EventPhone,
|
||||
Settings: settings,
|
||||
MessageIDs: messageIDs,
|
||||
MaxID: int(row.MaxID),
|
||||
StillUnreadCount: int(row.StillUnreadCount),
|
||||
ChannelPts: int(row.ChannelPts),
|
||||
FilterID: int(row.FilterID),
|
||||
DialogFilter: dialogFilter,
|
||||
FilterOrder: filterOrder,
|
||||
FolderPeers: folderPeers,
|
||||
TagsEnabled: row.TagsEnabled,
|
||||
FolderID: int(row.FolderID),
|
||||
Reaction: reaction,
|
||||
EmojiStatus: emojiStatus,
|
||||
UserID: row.UserID,
|
||||
Type: domain.UpdateEventType(row.EventType),
|
||||
Pts: int(row.Pts),
|
||||
PtsCount: int(row.PtsCount),
|
||||
Date: int(row.Date),
|
||||
Peer: domain.Peer{Type: domain.PeerType(row.EventPeerType), ID: row.EventPeerID},
|
||||
Story: story,
|
||||
Peers: peers,
|
||||
Bool: row.EventBool,
|
||||
Phone: row.EventPhone,
|
||||
Settings: settings,
|
||||
NotifyPeerSettings: notifyPeerSettings,
|
||||
MessageIDs: messageIDs,
|
||||
MaxID: int(row.MaxID),
|
||||
StillUnreadCount: int(row.StillUnreadCount),
|
||||
ChannelPts: int(row.ChannelPts),
|
||||
FilterID: int(row.FilterID),
|
||||
DialogFilter: dialogFilter,
|
||||
FilterOrder: filterOrder,
|
||||
FolderPeers: folderPeers,
|
||||
TagsEnabled: row.TagsEnabled,
|
||||
FolderID: int(row.FolderID),
|
||||
Reaction: reaction,
|
||||
EmojiStatus: emojiStatus,
|
||||
Message: domain.Message{
|
||||
ID: int(row.MessageID),
|
||||
UID: row.PrivateMessageID,
|
||||
|
|
@ -566,6 +579,13 @@ func (s *UpdateEventStore) BatchByCursor(ctx context.Context, cursors []store.Ev
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("decode peer settings: %w", err)
|
||||
}
|
||||
var notifyPeerSettings *domain.PeerNotifySettings
|
||||
if domain.UpdateEventType(row.EventType) == domain.UpdateEventNotifySettings {
|
||||
notifyPeerSettings, err = decodeNotifyPeerSettings(row.PeerSettingsJson)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode notify peer settings: %w", err)
|
||||
}
|
||||
}
|
||||
messageIDs, err := decodeEventMessageIDs(row.MessageIdsJson)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode message ids: %w", err)
|
||||
|
|
@ -607,29 +627,30 @@ func (s *UpdateEventStore) BatchByCursor(ctx context.Context, cursors []store.Ev
|
|||
return nil, fmt.Errorf("decode message rich message: %w", err)
|
||||
}
|
||||
event := domain.UpdateEvent{
|
||||
UserID: row.UserID,
|
||||
Type: domain.UpdateEventType(row.EventType),
|
||||
Pts: int(row.Pts),
|
||||
PtsCount: int(row.PtsCount),
|
||||
Date: int(row.Date),
|
||||
Peer: domain.Peer{Type: domain.PeerType(row.EventPeerType), ID: row.EventPeerID},
|
||||
Story: story,
|
||||
Peers: peers,
|
||||
Bool: row.EventBool,
|
||||
Phone: row.EventPhone,
|
||||
Settings: settings,
|
||||
MessageIDs: messageIDs,
|
||||
MaxID: int(row.MaxID),
|
||||
StillUnreadCount: int(row.StillUnreadCount),
|
||||
ChannelPts: int(row.ChannelPts),
|
||||
FilterID: int(row.FilterID),
|
||||
DialogFilter: dialogFilter,
|
||||
FilterOrder: filterOrder,
|
||||
FolderPeers: folderPeers,
|
||||
TagsEnabled: row.TagsEnabled,
|
||||
FolderID: int(row.FolderID),
|
||||
Reaction: reaction,
|
||||
EmojiStatus: emojiStatus,
|
||||
UserID: row.UserID,
|
||||
Type: domain.UpdateEventType(row.EventType),
|
||||
Pts: int(row.Pts),
|
||||
PtsCount: int(row.PtsCount),
|
||||
Date: int(row.Date),
|
||||
Peer: domain.Peer{Type: domain.PeerType(row.EventPeerType), ID: row.EventPeerID},
|
||||
Story: story,
|
||||
Peers: peers,
|
||||
Bool: row.EventBool,
|
||||
Phone: row.EventPhone,
|
||||
Settings: settings,
|
||||
NotifyPeerSettings: notifyPeerSettings,
|
||||
MessageIDs: messageIDs,
|
||||
MaxID: int(row.MaxID),
|
||||
StillUnreadCount: int(row.StillUnreadCount),
|
||||
ChannelPts: int(row.ChannelPts),
|
||||
FilterID: int(row.FilterID),
|
||||
DialogFilter: dialogFilter,
|
||||
FilterOrder: filterOrder,
|
||||
FolderPeers: folderPeers,
|
||||
TagsEnabled: row.TagsEnabled,
|
||||
FolderID: int(row.FolderID),
|
||||
Reaction: reaction,
|
||||
EmojiStatus: emojiStatus,
|
||||
Message: domain.Message{
|
||||
ID: int(row.MessageID),
|
||||
UID: row.PrivateMessageID,
|
||||
|
|
@ -1072,6 +1093,53 @@ func decodePeerSettings(raw string) (domain.PeerSettings, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
type notifyPeerSettingsJSON struct {
|
||||
ShowPreviews *bool `json:"show_previews,omitempty"`
|
||||
Silent *bool `json:"silent,omitempty"`
|
||||
MuteUntil *int `json:"mute_until,omitempty"`
|
||||
StoriesMuted *bool `json:"stories_muted,omitempty"`
|
||||
StoriesHideSender *bool `json:"stories_hide_sender,omitempty"`
|
||||
}
|
||||
|
||||
// encodeNotifyPeerSettings/decodeNotifyPeerSettings reuse the peer_settings_json column:
|
||||
// UpdateEventNotifySettings rows never populate domain.UpdateEvent.Settings (that field is
|
||||
// PeerSettings, an unrelated type for a different event type), so there is no collision.
|
||||
// Adding a dedicated column would need a migration + sqlc regen; this avoids both for a
|
||||
// small, cleanly-scoped payload.
|
||||
func encodeNotifyPeerSettings(settings *domain.PeerNotifySettings) ([]byte, error) {
|
||||
if settings == nil {
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
raw, err := json.Marshal(notifyPeerSettingsJSON{
|
||||
ShowPreviews: settings.ShowPreviews,
|
||||
Silent: settings.Silent,
|
||||
MuteUntil: settings.MuteUntil,
|
||||
StoriesMuted: settings.StoriesMuted,
|
||||
StoriesHideSender: settings.StoriesHideSender,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal notify peer settings: %w", err)
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
func decodeNotifyPeerSettings(raw string) (*domain.PeerNotifySettings, error) {
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var wire notifyPeerSettingsJSON
|
||||
if err := json.Unmarshal([]byte(raw), &wire); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &domain.PeerNotifySettings{
|
||||
ShowPreviews: wire.ShowPreviews,
|
||||
Silent: wire.Silent,
|
||||
MuteUntil: wire.MuteUntil,
|
||||
StoriesMuted: wire.StoriesMuted,
|
||||
StoriesHideSender: wire.StoriesHideSender,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func maxInt(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue