owpengram-server/internal/rpc/ephemeral_push_test.go
2026-09-01 12:06:31 +03:00

221 lines
8.6 KiB
Go

package rpc
import (
"context"
"sync"
"testing"
"time"
"github.com/iamxvbaba/td/clock"
"github.com/iamxvbaba/td/proto"
"github.com/iamxvbaba/td/tg"
"github.com/iamxvbaba/td/tlprofile"
"go.uber.org/zap/zaptest"
"telesrv/internal/domain"
"telesrv/internal/store"
)
type ephemeralPushChannels struct {
ChannelsService
view domain.ChannelView
calls int
}
func (s *ephemeralPushChannels) ResolveChannel(context.Context, int64, int64) (domain.ChannelView, error) {
s.calls++
return s.view, nil
}
type ephemeralPushSessions struct {
SessionBinder
OnlineUserProvider
mu sync.Mutex
online bool
broadcasts []ephemeralPushCapture
targeted []ephemeralPushCapture
}
type ephemeralPushCapture struct {
userID int64
authKey [8]byte
semantic tlprofile.SemanticID
message tg.UpdatesClass
}
func (s *ephemeralPushSessions) IsUserOnline(int64) bool { return s.online }
func (s *ephemeralPushSessions) PushToUserTransientCompatible(_ context.Context, userID int64, semantic tlprofile.SemanticID, _ proto.MessageType, message tg.UpdatesClass, _ time.Duration) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.broadcasts = append(s.broadcasts, ephemeralPushCapture{userID: userID, semantic: semantic, message: message})
return 1, nil
}
func (s *ephemeralPushSessions) PushToUserAuthKeyTransientCompatible(_ context.Context, userID int64, authKey [8]byte, semantic tlprofile.SemanticID, _ proto.MessageType, message tg.UpdatesClass, _ time.Duration) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.targeted = append(s.targeted, ephemeralPushCapture{userID: userID, authKey: authKey, semantic: semantic, message: message})
return 1, nil
}
func (s *ephemeralPushSessions) counts() (int, int) {
s.mu.Lock()
defer s.mu.Unlock()
return len(s.broadcasts), len(s.targeted)
}
type inMemoryEphemeralBroker struct {
mu sync.Mutex
subscribers []func(context.Context, store.EphemeralPush)
registered chan struct{}
published []store.EphemeralPush
}
func newInMemoryEphemeralBroker() *inMemoryEphemeralBroker {
return &inMemoryEphemeralBroker{registered: make(chan struct{}, 8)}
}
func (b *inMemoryEphemeralBroker) PublishEphemeralPush(ctx context.Context, event store.EphemeralPush) error {
b.mu.Lock()
b.published = append(b.published, event)
handlers := append([]func(context.Context, store.EphemeralPush){}, b.subscribers...)
b.mu.Unlock()
for _, handler := range handlers {
handler(ctx, event)
}
return nil
}
func (b *inMemoryEphemeralBroker) SubscribeEphemeralPushes(ctx context.Context, handler func(context.Context, store.EphemeralPush)) error {
b.mu.Lock()
b.subscribers = append(b.subscribers, handler)
b.mu.Unlock()
b.registered <- struct{}{}
<-ctx.Done()
return ctx.Err()
}
func TestEphemeralPushMultiInstanceSourceDedupAndLayerRouting(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
broker := newInMemoryEphemeralBroker()
users := mapUsersService{users: map[int64]domain.User{
1001: {ID: 1001, FirstName: "Bot", Bot: true},
2001: {ID: 2001, FirstName: "Alice"},
}}
view := domain.ChannelView{
Channel: domain.Channel{ID: 3001, AccessHash: 7, Title: "Group", Megagroup: true},
Self: domain.ChannelMember{ChannelID: 3001, UserID: 2001, Status: domain.ChannelMemberActive},
}
channels1, channels2 := &ephemeralPushChannels{view: view}, &ephemeralPushChannels{view: view}
sessions1, sessions2 := &ephemeralPushSessions{online: true}, &ephemeralPushSessions{online: true}
r1 := New(Config{InstanceID: "one"}, Deps{Users: users, Channels: channels1, Sessions: sessions1, EphemeralPush: broker}, zaptest.NewLogger(t), clock.System)
r2 := New(Config{InstanceID: "two"}, Deps{Users: users, Channels: channels2, Sessions: sessions2, EphemeralPush: broker}, zaptest.NewLogger(t), clock.System)
go r1.RunEphemeralPushSubscriber(ctx)
go r2.RunEphemeralPushSubscriber(ctx)
for range 2 {
select {
case <-broker.registered:
case <-time.After(time.Second):
t.Fatal("subscriber did not register")
}
}
now := time.Now()
message := domain.EphemeralMessage{
ID: 77, Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 3001},
SenderUserID: 1001, ReceiverUserID: 2001, Date: int(now.Unix()), RandomID: 78,
Content: domain.EphemeralContent{Message: "private"}, PayloadHash: [32]byte{1}, Version: 1,
CreatedAt: now, ExpiresAt: now.Add(domain.EphemeralMessageRetention),
}
r1.publishEphemeralPush(ctx, store.EphemeralPush{Kind: store.EphemeralPushNew, TargetUserID: 2001, Message: message})
if broadcast, targeted := sessions1.counts(); broadcast != 1 || targeted != 0 {
t.Fatalf("source delivery broadcast=%d targeted=%d", broadcast, targeted)
}
if broadcast, targeted := sessions2.counts(); broadcast != 1 || targeted != 0 {
t.Fatalf("remote delivery broadcast=%d targeted=%d", broadcast, targeted)
}
if sessions1.broadcasts[0].semantic != tlprofile.SemanticTypeUpdateNewEphemeralMessage || sessions2.broadcasts[0].semantic != tlprofile.SemanticTypeUpdateNewEphemeralMessage {
t.Fatalf("semantics source=%#x remote=%#x", sessions1.broadcasts[0].semantic, sessions2.broadcasts[0].semantic)
}
if len(broker.published) != 1 || broker.published[0].SourceID != "one" {
t.Fatalf("published=%+v", broker.published)
}
key := [8]byte{9, 8, 7}
message.Deleted = true
message.Version++
message.Content = domain.EphemeralContent{}
r2.deliverEphemeralPushLocal(ctx, store.EphemeralPush{
Kind: store.EphemeralPushDelete, TargetUserID: 2001,
TargetBusinessAuthKey: key, Message: message, Date: int(time.Now().Unix()),
})
_, targeted := sessions2.counts()
if targeted != 1 || sessions2.targeted[0].authKey != key || sessions2.targeted[0].semantic != tlprofile.SemanticTypeUpdateDeleteEphemeralMessages {
t.Fatalf("targeted=%+v", sessions2.targeted)
}
deletedUpdates, ok := sessions2.targeted[0].message.(*tg.Updates)
if !ok || deletedUpdates.Seq != 0 || len(deletedUpdates.Updates) != 1 {
t.Fatalf("delete updates=%#v", sessions2.targeted[0].message)
}
deleted, ok := deletedUpdates.Updates[0].(*tg.UpdateDeleteEphemeralMessages)
if !ok || len(deleted.IDs) != 1 || deleted.IDs[0] != message.ID {
t.Fatalf("delete update=%#v", deletedUpdates.Updates[0])
}
}
func TestEphemeralMessageUpdatesAreTransientAndPtsFree(t *testing.T) {
now := time.Now()
router := New(Config{}, Deps{
Users: mapUsersService{users: map[int64]domain.User{
1001: {ID: 1001, FirstName: "Bot", Bot: true},
2001: {ID: 2001, FirstName: "Alice"},
}},
Channels: &ephemeralPushChannels{view: domain.ChannelView{
Channel: domain.Channel{ID: 3001, AccessHash: 7, Title: "Group", Megagroup: true},
Self: domain.ChannelMember{ChannelID: 3001, UserID: 2001, Status: domain.ChannelMemberActive},
}},
}, zaptest.NewLogger(t), clock.System)
message := domain.EphemeralMessage{
ID: 77, Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 3001},
SenderUserID: 1001, ReceiverUserID: 2001, Date: int(now.Unix()), RandomID: 78,
Content: domain.EphemeralContent{Message: "private"}, PayloadHash: [32]byte{1}, Version: 1,
CreatedAt: now, ExpiresAt: now.Add(domain.EphemeralMessageRetention),
}
updates, err := router.ephemeralMessageUpdates(context.Background(), 2001, message, false)
if err != nil || updates.Seq != 0 || len(updates.Updates) != 1 {
t.Fatalf("updates=%#v err=%v", updates, err)
}
if _, ok := updates.Updates[0].(*tg.UpdateNewEphemeralMessage); !ok {
t.Fatalf("update type=%T", updates.Updates[0])
}
deleted := ephemeralDeleteUpdates(domain.EphemeralMessage{ID: message.ID, Peer: message.Peer}, int(now.Unix()))
if deleted.Seq != 0 {
t.Fatalf("delete seq=%d", deleted.Seq)
}
}
func TestEphemeralPushOfflineSkipsHydration(t *testing.T) {
channels := &ephemeralPushChannels{view: domain.ChannelView{Channel: domain.Channel{ID: 3001}}}
sessions := &ephemeralPushSessions{online: false}
now := time.Now()
router := New(Config{InstanceID: "offline"}, Deps{
Users: mapUsersService{users: map[int64]domain.User{}}, Channels: channels, Sessions: sessions,
}, zaptest.NewLogger(t), clock.System)
router.deliverEphemeralPushLocal(context.Background(), store.EphemeralPush{
Kind: store.EphemeralPushNew, TargetUserID: 2001,
Message: domain.EphemeralMessage{
ID: 77, Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 3001},
SenderUserID: 1001, ReceiverUserID: 2001, Date: int(now.Unix()), RandomID: 78,
Content: domain.EphemeralContent{Message: "private"}, PayloadHash: [32]byte{1}, Version: 1,
CreatedAt: now, ExpiresAt: now.Add(domain.EphemeralMessageRetention),
},
})
if channels.calls != 0 {
t.Fatalf("offline push performed %d channel hydrations", channels.calls)
}
if broadcast, targeted := sessions.counts(); broadcast != 0 || targeted != 0 {
t.Fatalf("offline delivery broadcast=%d targeted=%d", broadcast, targeted)
}
}