221 lines
8.6 KiB
Go
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)
|
|
}
|
|
}
|