owpengram-server/internal/store/redisstore/bot_callback_integration_test.go
A bf965f610c feat: sync bot keyboards and callbacks
Sync telesrv b96f2dd (feat(bot): complete keyboards callbacks and durable delivery).

Skipped private docs and preserved public README files per sync rules; normalized the appearance seed log label for public naming.
2026-07-19 20:38:48 +08:00

76 lines
2.6 KiB
Go

package redisstore
import (
"context"
"os"
"testing"
"time"
"telesrv/internal/domain"
"telesrv/internal/store"
)
func TestRedisBotCallbackRegistryCrossInstanceCASAndPubSub(t *testing.T) {
addr := os.Getenv("TELESRV_TEST_REDIS_ADDR")
if addr == "" {
t.Skip("set TELESRV_TEST_REDIS_ADDR to run redis integration test")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
clientA, err := Open(ctx, addr, "", 0)
if err != nil {
t.Fatal(err)
}
defer clientA.Close()
clientB, err := Open(ctx, addr, "", 0)
if err != nil {
t.Fatal(err)
}
defer clientB.Close()
a, b := NewBotCallbackRegistryStore(clientA), NewBotCallbackRegistryStore(clientB)
queryID := time.Now().UnixNano()
defer a.DeleteBotCallbackPending(context.Background(), 1001, queryID)
pushes := make(chan store.BotCallbackAnswerPush, 1)
subscribed := make(chan struct{})
go func() {
_ = b.SubscribeBotCallbackAnswers(ctx, func(_ context.Context, push store.BotCallbackAnswerPush) {
select {
case pushes <- push:
default:
}
})
}()
// Subscribe uses Redis' acknowledgement before consuming Channel. Give that
// acknowledgement one bounded scheduling turn before publishing.
time.AfterFunc(50*time.Millisecond, func() { close(subscribed) })
<-subscribed
created, err := a.PutBotCallbackPending(ctx, store.BotCallbackPending{QueryID: queryID, BotUserID: 1001, UserID: 2001}, time.Second)
if err != nil || !created {
t.Fatalf("put created=%v err=%v", created, err)
}
if duplicate, err := b.PutBotCallbackPending(ctx, store.BotCallbackPending{QueryID: queryID, BotUserID: 1001, UserID: 2002}, time.Second); err != nil || duplicate {
t.Fatalf("duplicate=%v err=%v", duplicate, err)
}
answer := domain.BotCallbackAnswer{Message: "done", CacheTime: 3}
if resolved, err := b.ResolveBotCallback(ctx, 9999, queryID, answer); err != nil || resolved {
t.Fatalf("foreign resolve=%v err=%v", resolved, err)
}
if resolved, err := b.ResolveBotCallback(ctx, 1001, queryID, answer); err != nil || !resolved {
t.Fatalf("owner resolve=%v err=%v", resolved, err)
}
if second, err := a.ResolveBotCallback(ctx, 1001, queryID, domain.BotCallbackAnswer{Message: "second"}); err != nil || second {
t.Fatalf("second resolve=%v err=%v", second, err)
}
stored, found, err := a.GetBotCallbackAnswer(ctx, 1001, queryID)
if err != nil || !found || stored.Message != "done" {
t.Fatalf("stored=%#v found=%v err=%v", stored, found, err)
}
select {
case push := <-pushes:
if push.QueryID != queryID || push.BotUserID != 1001 || push.Answer.Message != "done" {
t.Fatalf("push=%#v", push)
}
case <-ctx.Done():
t.Fatal("missing cross-instance callback pubsub")
}
}