owpengram-server/internal/rpc/inline_push_test.go

236 lines
6.6 KiB
Go

package rpc
import (
"context"
"sync"
"testing"
"time"
"github.com/gotd/td/clock"
"github.com/gotd/td/tg"
"go.uber.org/zap/zaptest"
botsapp "telesrv/internal/app/bots"
appusers "telesrv/internal/app/users"
"telesrv/internal/domain"
"telesrv/internal/store"
"telesrv/internal/store/memory"
)
func TestInlineBotQueryPublishesRemotePush(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
shared := newTestInlinePushBroker()
users := memory.NewUserStore()
botStore := memory.NewBotStore(users)
bots := botsapp.NewService(users, botStore, nil)
owner, err := users.Create(ctx, domain.User{AccessHash: 9101, Phone: "15550009101", FirstName: "Owner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
peer, err := users.Create(ctx, domain.User{AccessHash: 9102, Phone: "15550009102", FirstName: "Peer"})
if err != nil {
t.Fatalf("create peer: %v", err)
}
bot, _, err := bots.CreateBot(ctx, owner.ID, "Inline Push Bot", "inline_push_bot")
if err != nil {
t.Fatalf("create bot: %v", err)
}
if _, err := bots.SetInlinePlaceholder(ctx, bot.ID, "Search push"); err != nil {
t.Fatalf("set inline placeholder: %v", err)
}
localSessions := &captureSessions{}
remoteSessions := &captureSessions{}
local := New(Config{InstanceID: "node-a"}, Deps{
Users: appusers.NewService(users),
Bots: bots,
Inline: shared,
Sessions: localSessions,
}, zaptest.NewLogger(t), clock.System)
remote := New(Config{InstanceID: "node-b"}, Deps{
Inline: shared,
Sessions: remoteSessions,
}, zaptest.NewLogger(t), clock.System)
go local.RunInlineBotPushSubscriber(ctx)
go remote.RunInlineBotPushSubscriber(ctx)
shared.waitSubscribers(t, 2)
type inlineGetResult struct {
res *tg.MessagesBotResults
err error
}
gotCh := make(chan inlineGetResult, 1)
go func() {
res, err := local.onMessagesGetInlineBotResults(WithUserID(ctx, owner.ID), &tg.MessagesGetInlineBotResultsRequest{
Bot: inputUser(bot),
Peer: inputPeerUser(peer),
Query: "remote-shape",
})
gotCh <- inlineGetResult{res: res, err: err}
}()
queryID := waitInlineBotQuery(t, local)
event := shared.waitPublished(t)
if event.SourceID != "node-a" || event.QueryID != queryID || event.BotUserID != bot.ID || event.UserID != owner.ID {
t.Fatalf("published event = %+v, queryID=%d bot=%d owner=%d", event, queryID, bot.ID, owner.ID)
}
if event.Query != "remote-shape" || event.PeerType != store.InlineQueryPeerTypePM {
t.Fatalf("published query metadata = %+v", event)
}
localPushes := localSessions.pushedUserIDs()
if len(localPushes) != 1 || localPushes[0] != bot.ID {
t.Fatalf("local pushes = %+v, want exactly one direct push to bot", localPushes)
}
remoteUpdates := waitInlinePushUpdate(t, remoteSessions)
if remoteUpdates.Date == 0 || len(remoteUpdates.Updates) != 1 {
t.Fatalf("remote updates = %+v", remoteUpdates)
}
update, ok := remoteUpdates.Updates[0].(*tg.UpdateBotInlineQuery)
if !ok {
t.Fatalf("remote update type = %T", remoteUpdates.Updates[0])
}
if update.QueryID != queryID || update.UserID != owner.ID || update.Query != "remote-shape" {
t.Fatalf("remote inline update = %+v", update)
}
if _, ok := update.PeerType.(*tg.InlineQueryPeerTypePM); !ok {
t.Fatalf("remote peer type = %T", update.PeerType)
}
if ok, err := local.onMessagesSetInlineBotResults(WithUserID(ctx, bot.ID), &tg.MessagesSetInlineBotResultsRequest{
QueryID: queryID,
CacheTime: 30,
Results: []tg.InputBotInlineResultClass{
&tg.InputBotInlineResult{
ID: "remote-1",
Type: "article",
Title: "Remote",
SendMessage: &tg.InputBotInlineMessageText{Message: "remote ok"},
},
},
}); err != nil || !ok {
t.Fatalf("set inline results = %v,%v, want true,nil", ok, err)
}
select {
case got := <-gotCh:
if got.err != nil {
t.Fatalf("get inline results: %v", got.err)
}
if got.res.QueryID != queryID || len(got.res.Results) != 1 {
t.Fatalf("inline results = %+v", got.res)
}
case <-time.After(2 * time.Second):
t.Fatal("get inline results did not finish")
}
localPushes = localSessions.pushedUserIDs()
if len(localPushes) != 1 {
t.Fatalf("local self subscription duplicated push: %+v", localPushes)
}
}
type testInlinePushBroker struct {
*testInlineRegistryStore
mu sync.Mutex
subscribers map[chan store.BotInlineQueryPush]struct{}
published []store.BotInlineQueryPush
}
func newTestInlinePushBroker() *testInlinePushBroker {
return &testInlinePushBroker{
testInlineRegistryStore: newTestInlineRegistryStore(),
subscribers: make(map[chan store.BotInlineQueryPush]struct{}),
}
}
func (s *testInlinePushBroker) PublishBotInlineQuery(ctx context.Context, event store.BotInlineQueryPush) error {
s.mu.Lock()
s.published = append(s.published, event)
subscribers := make([]chan store.BotInlineQueryPush, 0, len(s.subscribers))
for ch := range s.subscribers {
subscribers = append(subscribers, ch)
}
s.mu.Unlock()
for _, ch := range subscribers {
select {
case ch <- event:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}
func (s *testInlinePushBroker) SubscribeBotInlineQueries(ctx context.Context, handle func(context.Context, store.BotInlineQueryPush)) error {
ch := make(chan store.BotInlineQueryPush, 16)
s.mu.Lock()
s.subscribers[ch] = struct{}{}
s.mu.Unlock()
defer func() {
s.mu.Lock()
delete(s.subscribers, ch)
s.mu.Unlock()
}()
for {
select {
case <-ctx.Done():
return ctx.Err()
case event := <-ch:
handle(ctx, event)
}
}
}
func (s *testInlinePushBroker) waitSubscribers(t *testing.T, want int) {
t.Helper()
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
s.mu.Lock()
got := len(s.subscribers)
s.mu.Unlock()
if got >= want {
return
}
time.Sleep(5 * time.Millisecond)
}
t.Fatalf("subscribers < %d", want)
}
func (s *testInlinePushBroker) waitPublished(t *testing.T) store.BotInlineQueryPush {
t.Helper()
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
s.mu.Lock()
if len(s.published) > 0 {
event := s.published[0]
s.mu.Unlock()
return event
}
s.mu.Unlock()
time.Sleep(5 * time.Millisecond)
}
t.Fatal("inline push event was not published")
return store.BotInlineQueryPush{}
}
func waitInlinePushUpdate(t *testing.T, sessions *captureSessions) *tg.Updates {
t.Helper()
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
snap := sessions.snapshot()
if snap.message != nil {
updates, ok := snap.message.(*tg.Updates)
if !ok {
t.Fatalf("pushed message type = %T", snap.message)
}
return updates
}
time.Sleep(5 * time.Millisecond)
}
t.Fatal("inline push update was not delivered")
return nil
}