package rpc import ( "context" "reflect" "testing" "time" "github.com/iamxvbaba/td/clock" "github.com/iamxvbaba/td/tg" "go.uber.org/zap/zaptest" appchannels "telesrv/internal/app/channels" appusers "telesrv/internal/app/users" "telesrv/internal/domain" "telesrv/internal/store/memory" ) type replayPreflightMessages struct { *captureMessages replays map[int64]domain.SendPrivateTextResult lookupRequests []domain.PrivateSendReplayRequest sendRequests []domain.SendPrivateTextRequest reserveRequests []domain.AlbumGroupReservationRequest reservedGroupedID int64 forceSendDuplicate bool } func newReplayPreflightMessages() *replayPreflightMessages { return &replayPreflightMessages{ captureMessages: &captureMessages{}, replays: make(map[int64]domain.SendPrivateTextResult), reservedGroupedID: 81001, } } func (s *replayPreflightMessages) LookupPrivateSendReplay(_ context.Context, _ int64, req domain.PrivateSendReplayRequest) (domain.SendPrivateTextResult, bool, error) { s.lookupRequests = append(s.lookupRequests, req) res, found := s.replays[req.RandomID] if found { res.Duplicate = true } return res, found, nil } func (s *replayPreflightMessages) SendPrivateText(_ context.Context, _ int64, req domain.SendPrivateTextRequest) (domain.SendPrivateTextResult, error) { s.sendRequests = append(s.sendRequests, req) res := privateReplayFixture(req.SenderUserID, req.RecipientUserID, req.RandomID, 100+len(s.sendRequests), req.GroupedID) res.Duplicate = s.forceSendDuplicate s.replays[req.RandomID] = res return res, nil } func (s *replayPreflightMessages) ReserveAlbumGroup(_ context.Context, _ int64, req domain.AlbumGroupReservationRequest) (int64, error) { s.reserveRequests = append(s.reserveRequests, req) return s.reservedGroupedID, nil } func privateReplayFixture(senderID, recipientID, randomID int64, messageID int, groupedID int64) domain.SendPrivateTextResult { msg := domain.Message{ ID: messageID, UID: int64(messageID) + 1000, RandomID: randomID, OwnerUserID: senderID, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: recipientID}, From: domain.Peer{Type: domain.PeerTypeUser, ID: senderID}, Date: 1700000000 + messageID, Out: true, Body: "committed", GroupedID: groupedID, Pts: messageID, } event := domain.UpdateEvent{ UserID: senderID, Type: domain.UpdateEventNewMessage, Pts: messageID, PtsCount: 1, Date: msg.Date, Message: msg, } return domain.SendPrivateTextResult{ SenderMessage: msg, SenderEvent: event, } } type replayCountingDialogs struct { *captureDialogs deleteDraftCalls int } func (s *replayCountingDialogs) DeleteDraft(_ context.Context, _ int64, peer domain.Peer, topMessageID int) (bool, error) { s.deleteDraftCalls++ s.deletedDraft.peer = peer s.deletedDraft.topMessageID = topMessageID return true, nil } type replaySelectiveFiles struct { *fakeFiles getPhotoIDs []int64 } func (f *replaySelectiveFiles) GetPhoto(ctx context.Context, id int64) (domain.Photo, bool, error) { f.getPhotoIDs = append(f.getPhotoIDs, id) return f.fakeFiles.GetPhoto(ctx, id) } func TestSendMessageExactReplayPrecedesSaturatedRateLimiter(t *testing.T) { const userID = int64(1001) messages := newReplayPreflightMessages() messages.replays[7001] = privateReplayFixture(userID, userID, 7001, 71, 0) limiter := &captureRateLimiter{block: true, retryAfter: 30} dialogs := &replayCountingDialogs{captureDialogs: &captureDialogs{}} r := New(Config{SendRateLimit: 1, SendRateWindow: time.Minute}, Deps{ Messages: messages, Dialogs: dialogs, Limiter: limiter, }, zaptest.NewLogger(t), clock.System) updates, err := r.onMessagesSendMessage(WithUserID(context.Background(), userID), &tg.MessagesSendMessageRequest{ Peer: &tg.InputPeerSelf{}, Message: "already committed", RandomID: 7001, ClearDraft: true, }) if err != nil { t.Fatalf("exact replay: %v", err) } if _, ok := updates.(*tg.Updates); !ok { t.Fatalf("updates = %T, want *tg.Updates", updates) } if len(limiter.calls) != 0 { t.Fatalf("limiter calls = %+v, want none for exact replay", limiter.calls) } if len(messages.sendRequests) != 0 { t.Fatalf("send requests = %d, want 0", len(messages.sendRequests)) } if dialogs.deleteDraftCalls != 0 { t.Fatalf("draft deletes = %d, want 0 for duplicate", dialogs.deleteDraftCalls) } } func TestSendMessageConcurrentReplayRaceDoesNotClearDraft(t *testing.T) { const userID = int64(1001) messages := newReplayPreflightMessages() // The read-only preflight misses, then the atomic store fence observes that another request // committed the same random_id first and returns Duplicate=true. messages.forceSendDuplicate = true dialogs := &replayCountingDialogs{captureDialogs: &captureDialogs{}} r := New(Config{}, Deps{Messages: messages, Dialogs: dialogs}, zaptest.NewLogger(t), clock.System) if _, err := r.onMessagesSendMessage(WithUserID(context.Background(), userID), &tg.MessagesSendMessageRequest{ Peer: &tg.InputPeerSelf{}, Message: "concurrent exact replay", RandomID: 7002, ClearDraft: true, }); err != nil { t.Fatalf("concurrent replay race: %v", err) } if len(messages.lookupRequests) != 1 || len(messages.sendRequests) != 1 { t.Fatalf("lookup/send calls = %d/%d, want preflight miss then one atomic send", len(messages.lookupRequests), len(messages.sendRequests)) } if dialogs.deleteDraftCalls != 0 { t.Fatalf("draft deletes = %d, want 0 when store race returns duplicate", dialogs.deleteDraftCalls) } } func TestSendMediaExactReplayPrecedesMediaResolvers(t *testing.T) { const userID = int64(1001) tests := []struct { name string media tg.InputMediaClass }{ { name: "uploaded photo", media: &tg.InputMediaUploadedPhoto{File: &tg.InputFile{ID: 11, Parts: 1, Name: "gone.jpg"}}, }, { name: "referenced photo", media: &tg.InputMediaPhoto{ID: &tg.InputPhoto{ID: 22, AccessHash: 220}}, }, { name: "poll", media: &tg.InputMediaPoll{Poll: tg.Poll{ Question: tg.TextWithEntities{Text: "question?", Entities: []tg.MessageEntityClass{}}, Answers: []tg.PollAnswerClass{ &tg.PollAnswer{Text: tg.TextWithEntities{Text: "yes", Entities: []tg.MessageEntityClass{}}, Option: []byte{0}}, &tg.PollAnswer{Text: tg.TextWithEntities{Text: "no", Entities: []tg.MessageEntityClass{}}, Option: []byte{1}}, }, }}, }, } for i, tc := range tests { t.Run(tc.name, func(t *testing.T) { randomID := int64(7100 + i) messages := newReplayPreflightMessages() messages.replays[randomID] = privateReplayFixture(userID, userID, randomID, 80+i, 0) limiter := &captureRateLimiter{block: true, retryAfter: 30} r := New(Config{SendRateLimit: 1, SendRateWindow: time.Minute}, Deps{ Messages: messages, Limiter: limiter, // Files and Polls are intentionally nil. Reaching any resolver would fail. }, zaptest.NewLogger(t), clock.System) req := &tg.MessagesSendMediaRequest{ Peer: &tg.InputPeerSelf{}, Media: tc.media, Message: "already committed", RandomID: randomID, } if fingerprint, err := sendMediaIdempotencyFingerprint(req); err != nil || len(fingerprint) != 32 { t.Fatalf("fingerprint len=%d err=%v, want SHA-256", len(fingerprint), err) } if _, err := r.onMessagesSendMedia(WithUserID(context.Background(), userID), req); err != nil { t.Fatalf("exact replay: %v", err) } if len(limiter.calls) != 0 { t.Fatalf("limiter calls = %+v, want none", limiter.calls) } if len(messages.sendRequests) != 0 || len(messages.lookupRequests) != 1 { t.Fatalf("lookup=%d send=%d, want 1/0", len(messages.lookupRequests), len(messages.sendRequests)) } }) } } func TestSendMultiMediaMixedReplayChargesAndResolvesOnlyAbsentItems(t *testing.T) { const userID = int64(1001) messages := newReplayPreflightMessages() messages.replays[7201] = privateReplayFixture(userID, userID, 7201, 91, messages.reservedGroupedID) limiter := &captureRateLimiter{} dialogs := &replayCountingDialogs{captureDialogs: &captureDialogs{}} files := &replaySelectiveFiles{fakeFiles: &fakeFiles{photos: map[int64]domain.Photo{ 222: {ID: 222, AccessHash: 2220, DCID: 2, Sizes: []domain.PhotoSize{{Kind: domain.PhotoSizeKindDefault, Type: "x", W: 100, H: 100}}}, }}} r := New(Config{SendRateLimit: 10, SendRateWindow: time.Minute}, Deps{ Messages: messages, Dialogs: dialogs, Files: files, Limiter: limiter, }, zaptest.NewLogger(t), clock.System) req := &tg.MessagesSendMultiMediaRequest{ Peer: &tg.InputPeerSelf{}, ClearDraft: true, MultiMedia: []tg.InputSingleMedia{ {Media: &tg.InputMediaPhoto{ID: &tg.InputPhoto{ID: 111, AccessHash: 1110}}, RandomID: 7201, Message: "duplicate"}, {Media: &tg.InputMediaPhoto{ID: &tg.InputPhoto{ID: 222, AccessHash: 2220}}, RandomID: 7202, Message: "new"}, }, } if _, err := r.onMessagesSendMultiMedia(WithUserID(context.Background(), userID), req); err != nil { t.Fatalf("mixed sendMultiMedia: %v", err) } if len(limiter.calls) != 1 || limiter.calls[0].cost != 1 { t.Fatalf("limiter calls = %+v, want one absent-item cost", limiter.calls) } if !reflect.DeepEqual(files.getPhotoIDs, []int64{222}) { t.Fatalf("resolved photo IDs = %v, want only absent item 222", files.getPhotoIDs) } if len(messages.sendRequests) != 1 || messages.sendRequests[0].RandomID != 7202 { t.Fatalf("send requests = %+v, want only random_id 7202", messages.sendRequests) } if len(messages.reserveRequests) != 1 { t.Fatalf("album reservations = %d, want 1", len(messages.reserveRequests)) } if dialogs.deleteDraftCalls != 1 { t.Fatalf("draft deletes = %d, want exactly one from first genuinely-new item", dialogs.deleteDraftCalls) } limiterCalls := len(limiter.calls) resolved := append([]int64(nil), files.getPhotoIDs...) reservations := len(messages.reserveRequests) if _, err := r.onMessagesSendMultiMedia(WithUserID(context.Background(), userID), req); err != nil { t.Fatalf("full duplicate sendMultiMedia: %v", err) } if len(limiter.calls) != limiterCalls { t.Fatalf("full duplicate added limiter calls: before=%d after=%d", limiterCalls, len(limiter.calls)) } if !reflect.DeepEqual(files.getPhotoIDs, resolved) { t.Fatalf("full duplicate resolved media: before=%v after=%v", resolved, files.getPhotoIDs) } if len(messages.reserveRequests) != reservations { t.Fatalf("full duplicate reservations: before=%d after=%d", reservations, len(messages.reserveRequests)) } if dialogs.deleteDraftCalls != 1 { t.Fatalf("full duplicate cleared draft again: calls=%d", dialogs.deleteDraftCalls) } } func TestForwardReplayPreflightSkipsCommittedSourcesAndLoadsOnlyAbsentIDs(t *testing.T) { const userID = int64(1001) t.Run("full duplicate tolerates deleted sources", func(t *testing.T) { messages := newReplayPreflightMessages() messages.replays[7301] = privateReplayFixture(userID, userID, 7301, 101, 0) messages.replays[7302] = privateReplayFixture(userID, userID, 7302, 102, 0) limiter := &captureRateLimiter{block: true, retryAfter: 30} r := New(Config{SendRateLimit: 1, SendRateWindow: time.Minute}, Deps{Messages: messages, Limiter: limiter}, zaptest.NewLogger(t), clock.System) if _, err := r.onMessagesForwardMessages(WithUserID(context.Background(), userID), &tg.MessagesForwardMessagesRequest{ FromPeer: &tg.InputPeerEmpty{}, ToPeer: &tg.InputPeerSelf{}, ID: []int{41, 42}, RandomID: []int64{7301, 7302}, }); err != nil { t.Fatalf("full duplicate forward with deleted sources: %v", err) } if messages.getMessagesCalls != 0 { t.Fatalf("GetMessages calls = %d, want 0", messages.getMessagesCalls) } if len(limiter.calls) != 0 || len(messages.sendRequests) != 0 { t.Fatalf("limiter=%v sends=%d, want no side effects", limiter.calls, len(messages.sendRequests)) } }) t.Run("mixed loads absent IDs only", func(t *testing.T) { messages := newReplayPreflightMessages() messages.replays[7311] = privateReplayFixture(userID, userID, 7311, 111, 0) messages.list = domain.MessageList{Messages: []domain.Message{{ ID: 52, OwnerUserID: userID, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 2002}, From: domain.Peer{Type: domain.PeerTypeUser, ID: 2002}, Date: 1700000100, Body: "still available", }}} limiter := &captureRateLimiter{} r := New(Config{SendRateLimit: 10, SendRateWindow: time.Minute}, Deps{Messages: messages, Limiter: limiter}, zaptest.NewLogger(t), clock.System) if _, err := r.onMessagesForwardMessages(WithUserID(context.Background(), userID), &tg.MessagesForwardMessagesRequest{ FromPeer: &tg.InputPeerEmpty{}, ToPeer: &tg.InputPeerSelf{}, ID: []int{51, 52}, RandomID: []int64{7311, 7312}, }); err != nil { t.Fatalf("mixed forward: %v", err) } if messages.getMessagesCalls != 1 || !reflect.DeepEqual(messages.getMessagesIDs, [][]int{{52}}) { t.Fatalf("GetMessages calls=%d ids=%v, want one [52]", messages.getMessagesCalls, messages.getMessagesIDs) } if len(limiter.calls) != 1 || limiter.calls[0].cost != 1 { t.Fatalf("limiter calls = %+v, want cost 1", limiter.calls) } if len(messages.sendRequests) != 1 || messages.sendRequests[0].RandomID != 7312 { t.Fatalf("send requests = %+v, want only random_id 7312", messages.sendRequests) } }) } func TestChannelAndMonoforumExactReplayPrecedesLimiterAndCurrentPermission(t *testing.T) { t.Run("channel replay survives sender ban", func(t *testing.T) { ctx := context.Background() users := memory.NewUserStore() owner, err := users.Create(ctx, domain.User{AccessHash: 11, Phone: "15550007001", FirstName: "Owner"}) if err != nil { t.Fatalf("create owner: %v", err) } member, err := users.Create(ctx, domain.User{AccessHash: 12, Phone: "15550007002", FirstName: "Member"}) if err != nil { t.Fatalf("create member: %v", err) } channels := appchannels.NewService(memory.NewChannelStore()) created, err := channels.CreateMegagroupFromCreateChat(ctx, owner.ID, domain.CreateChannelRequest{ Title: "Replay Group", MemberUserIDs: []int64{member.ID}, Date: 100, }) if err != nil { t.Fatalf("create group: %v", err) } limiter := &captureRateLimiter{} r := New(Config{SendRateLimit: 10, SendRateWindow: time.Minute}, Deps{ Users: appusers.NewService(users), Channels: channels, Limiter: limiter, }, zaptest.NewLogger(t), clock.System) req := &tg.MessagesSendMessageRequest{ Peer: &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash}, Message: "committed before ban", RandomID: 7401, } memberCtx := WithUserID(ctx, member.ID) if _, err := r.onMessagesSendMessage(memberCtx, req); err != nil { t.Fatalf("first channel send: %v", err) } callsBeforeReplay := len(limiter.calls) if _, err := channels.EditBanned(ctx, owner.ID, domain.EditChannelBannedRequest{ ChannelID: created.Channel.ID, Participant: domain.Peer{Type: domain.PeerTypeUser, ID: member.ID}, BannedRights: domain.ChannelBannedRights{ ViewMessages: true, UntilDate: 1000, }, Date: 101, }); err != nil { t.Fatalf("ban member: %v", err) } limiter.block = true if _, err := r.onMessagesSendMessage(memberCtx, req); err != nil { t.Fatalf("channel exact replay after ban: %v", err) } if len(limiter.calls) != callsBeforeReplay { t.Fatalf("replay limiter calls: before=%d after=%d", callsBeforeReplay, len(limiter.calls)) } }) t.Run("monoforum replay survives direct-message disable", func(t *testing.T) { ctx := context.Background() users := memory.NewUserStore() owner, err := users.Create(ctx, domain.User{AccessHash: 21, Phone: "15550007101", FirstName: "Owner"}) if err != nil { t.Fatalf("create owner: %v", err) } subscriber, err := users.Create(ctx, domain.User{AccessHash: 22, Phone: "15550007102", FirstName: "Subscriber"}) if err != nil { t.Fatalf("create subscriber: %v", err) } channelStore := memory.NewChannelStore() channels := appchannels.NewService(channelStore) created, err := channels.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{Title: "Replay DM", Broadcast: true, Date: 200}) if err != nil { t.Fatalf("create broadcast: %v", err) } enabled, err := channelStore.SetPaidMessagesPrice(ctx, owner.ID, created.Channel.ID, 0, true) if err != nil { t.Fatalf("enable direct messages: %v", err) } monoID := enabled.Channel.LinkedMonoforumID limiter := &captureRateLimiter{} r := New(Config{SendRateLimit: 10, SendRateWindow: time.Minute}, Deps{ Users: appusers.NewService(users), Channels: channels, Limiter: limiter, }, zaptest.NewLogger(t), clock.System) req := &tg.MessagesSendMessageRequest{ Peer: &tg.InputPeerChannel{ChannelID: monoID}, Message: "committed before disable", RandomID: 7411, } req.SetReplyTo(&tg.InputReplyToMonoForum{MonoforumPeerID: &tg.InputPeerUser{UserID: subscriber.ID}}) subscriberCtx := WithUserID(ctx, subscriber.ID) if _, err := r.onMessagesSendMessage(subscriberCtx, req); err != nil { t.Fatalf("first monoforum send: %v", err) } callsBeforeReplay := len(limiter.calls) if _, err := channelStore.SetPaidMessagesPrice(ctx, owner.ID, created.Channel.ID, 0, false); err != nil { t.Fatalf("disable direct messages: %v", err) } limiter.block = true if _, err := r.onMessagesSendMessage(subscriberCtx, req); err != nil { t.Fatalf("monoforum exact replay after disable: %v", err) } if len(limiter.calls) != callsBeforeReplay { t.Fatalf("replay limiter calls: before=%d after=%d", callsBeforeReplay, len(limiter.calls)) } }) }