475 lines
18 KiB
Go
475 lines
18 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/store"
|
|
)
|
|
|
|
func TestLoginCodeDeliveryPostgresAtomicFactsAndReplay(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
user := createLoginCodeDeliveryTestUser(t, ctx, pool, "basic")
|
|
req := domain.LoginCodeDeliveryRequest{
|
|
UserID: user.ID,
|
|
PhoneCodeHash: "pg-login-code-basic-" + randomSuffix(t),
|
|
Code: "12345",
|
|
Date: 1700001000,
|
|
ExpiresAt: 1700001300,
|
|
}
|
|
|
|
first, err := NewMessageStore(pool).DeliverLoginCodeMessage(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("DeliverLoginCodeMessage: %v", err)
|
|
}
|
|
if !first.Created || first.Message.ID != 1 || first.Message.Pts != 1 || first.Message.UID <= 0 || first.Message.Out ||
|
|
first.Message.OwnerUserID != user.ID || first.Message.Peer.ID != domain.OfficialSystemUserID || first.Message.From.ID != domain.OfficialSystemUserID {
|
|
t.Fatalf("first delivery = %+v, want first incoming 777000 message", first)
|
|
}
|
|
|
|
assertLoginCodeDeliveryFacts(t, ctx, pool, user.ID, first.Message, 1)
|
|
var senderUserID, recipientUserID, randomID int64
|
|
var delivered bool
|
|
var senderBoxID, senderPts, recipientBoxID, recipientPts int32
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT sender_user_id,
|
|
recipient_user_id,
|
|
random_id,
|
|
recipient_delivered,
|
|
sender_box_id,
|
|
sender_pts,
|
|
recipient_box_id,
|
|
recipient_pts
|
|
FROM private_messages
|
|
WHERE sender_user_id = $1 AND id = $2`, domain.OfficialSystemUserID, first.Message.UID).Scan(
|
|
&senderUserID,
|
|
&recipientUserID,
|
|
&randomID,
|
|
&delivered,
|
|
&senderBoxID,
|
|
&senderPts,
|
|
&recipientBoxID,
|
|
&recipientPts,
|
|
); err != nil {
|
|
t.Fatalf("load private message receipt: %v", err)
|
|
}
|
|
if senderUserID != domain.OfficialSystemUserID || recipientUserID != user.ID || randomID != 0 || !delivered ||
|
|
senderBoxID != 0 || senderPts != 0 || int(recipientBoxID) != first.Message.ID || int(recipientPts) != first.Message.Pts {
|
|
t.Fatalf("private receipt sender=%d recipient=%d random=%d delivered=%v sender=%d/%d recipient=%d/%d",
|
|
senderUserID, recipientUserID, randomID, delivered, senderBoxID, senderPts, recipientBoxID, recipientPts)
|
|
}
|
|
var officialSenderBoxes int
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM message_boxes WHERE owner_user_id = $1 AND private_message_id = $2`, domain.OfficialSystemUserID, first.Message.UID).Scan(&officialSenderBoxes); err != nil {
|
|
t.Fatalf("count official sender boxes: %v", err)
|
|
}
|
|
if officialSenderBoxes != 0 {
|
|
t.Fatalf("official sender boxes = %d, want recipient-only login notification", officialSenderBoxes)
|
|
}
|
|
|
|
var deliveryKey, codeFingerprint []byte
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT delivery_key, code_fingerprint
|
|
FROM login_code_message_deliveries
|
|
WHERE user_id = $1 AND message_box_id = $2`, user.ID, first.Message.ID).Scan(&deliveryKey, &codeFingerprint); err != nil {
|
|
t.Fatalf("load compact receipt: %v", err)
|
|
}
|
|
if len(deliveryKey) != 32 || len(codeFingerprint) != 32 || string(deliveryKey) == req.PhoneCodeHash {
|
|
t.Fatalf("compact receipt key/fingerprint lengths = %d/%d", len(deliveryKey), len(codeFingerprint))
|
|
}
|
|
|
|
replayReq := req
|
|
replayReq.Date += 99
|
|
replay, err := NewMessageStore(pool).DeliverLoginCodeMessage(ctx, replayReq)
|
|
if err != nil {
|
|
t.Fatalf("replay DeliverLoginCodeMessage: %v", err)
|
|
}
|
|
if replay.Created || !reflect.DeepEqual(replay.Message, first.Message) {
|
|
t.Fatalf("replay = %+v, want immutable first result %+v", replay, first)
|
|
}
|
|
assertLoginCodeDeliveryFacts(t, ctx, pool, user.ID, first.Message, 1)
|
|
|
|
changedCode := req
|
|
changedCode.Code = "54321"
|
|
if _, err := NewMessageStore(pool).DeliverLoginCodeMessage(ctx, changedCode); !errors.Is(err, domain.ErrLoginCodeDeliveryConflict) {
|
|
t.Fatalf("changed-code replay err = %v, want ErrLoginCodeDeliveryConflict", err)
|
|
}
|
|
assertLoginCodeDeliveryFacts(t, ctx, pool, user.ID, first.Message, 1)
|
|
|
|
otherUser := createLoginCodeDeliveryTestUser(t, ctx, pool, "conflict")
|
|
changedUser := req
|
|
changedUser.UserID = otherUser.ID
|
|
if _, err := NewMessageStore(pool).DeliverLoginCodeMessage(ctx, changedUser); !errors.Is(err, domain.ErrLoginCodeDeliveryConflict) {
|
|
t.Fatalf("changed-user replay err = %v, want ErrLoginCodeDeliveryConflict", err)
|
|
}
|
|
var otherFacts int
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM message_boxes WHERE owner_user_id = $1`, otherUser.ID).Scan(&otherFacts); err != nil {
|
|
t.Fatalf("count changed-user facts: %v", err)
|
|
}
|
|
if otherFacts != 0 {
|
|
t.Fatalf("changed-user replay created %d message boxes", otherFacts)
|
|
}
|
|
}
|
|
|
|
func TestLoginCodeDeliveryPostgresConcurrentExactlyOnce(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
user := createLoginCodeDeliveryTestUser(t, ctx, pool, "concurrent")
|
|
req := domain.LoginCodeDeliveryRequest{
|
|
UserID: user.ID,
|
|
PhoneCodeHash: "pg-login-code-concurrent-" + randomSuffix(t),
|
|
Code: "24680",
|
|
Date: 1700001100,
|
|
ExpiresAt: 1700001400,
|
|
}
|
|
|
|
const workers = 24
|
|
var created atomic.Int32
|
|
results := make(chan domain.LoginCodeDeliveryResult, workers)
|
|
errs := make(chan error, workers)
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < workers; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
got, err := NewMessageStore(pool).DeliverLoginCodeMessage(ctx, req)
|
|
if err != nil {
|
|
errs <- err
|
|
return
|
|
}
|
|
if got.Created {
|
|
created.Add(1)
|
|
}
|
|
results <- got
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
close(results)
|
|
for err := range errs {
|
|
t.Fatalf("concurrent delivery: %v", err)
|
|
}
|
|
if created.Load() != 1 {
|
|
t.Fatalf("created calls = %d, want exactly 1", created.Load())
|
|
}
|
|
var first domain.Message
|
|
for got := range results {
|
|
if first.ID == 0 {
|
|
first = got.Message
|
|
continue
|
|
}
|
|
if !reflect.DeepEqual(got.Message, first) {
|
|
t.Fatalf("concurrent result = %+v, want %+v", got.Message, first)
|
|
}
|
|
}
|
|
if first.ID == 0 {
|
|
t.Fatal("no successful concurrent result")
|
|
}
|
|
assertLoginCodeDeliveryFacts(t, ctx, pool, user.ID, first, 1)
|
|
}
|
|
|
|
func TestLoginCodeDeliveryPostgresCommitAckLossRecoversFromReceipt(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
user := createLoginCodeDeliveryTestUser(t, ctx, pool, "commit-ack-loss")
|
|
req := domain.LoginCodeDeliveryRequest{
|
|
UserID: user.ID,
|
|
PhoneCodeHash: "pg-login-code-commit-ack-loss-" + randomSuffix(t),
|
|
Code: "86420",
|
|
Date: int(time.Now().Unix()),
|
|
ExpiresAt: time.Now().Add(5 * time.Minute).Unix(),
|
|
}
|
|
|
|
got, err := NewMessageStore(&commitAckLossDB{Pool: pool}).DeliverLoginCodeMessage(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("DeliverLoginCodeMessage with lost commit ACK: %v", err)
|
|
}
|
|
if got.Created {
|
|
t.Fatalf("commit-ACK recovery Created = true, want conservative replay result")
|
|
}
|
|
assertLoginCodeDeliveryFacts(t, ctx, pool, user.ID, got.Message, 1)
|
|
|
|
replay, err := NewMessageStore(pool).DeliverLoginCodeMessage(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("replay after lost commit ACK: %v", err)
|
|
}
|
|
if replay.Created || !reflect.DeepEqual(replay.Message, got.Message) {
|
|
t.Fatalf("replay = %+v, want recovered snapshot %+v", replay, got)
|
|
}
|
|
}
|
|
|
|
func TestLoginCodeDeliveryPostgresDifferentUsersDoNotRewriteOfficialUser(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
firstUser := createLoginCodeDeliveryTestUser(t, ctx, pool, "official-row-first")
|
|
now := int(time.Now().Unix())
|
|
if _, err := NewMessageStore(pool).DeliverLoginCodeMessage(ctx, domain.LoginCodeDeliveryRequest{
|
|
UserID: firstUser.ID, PhoneCodeHash: "official-row-first-" + randomSuffix(t), Code: "12345", Date: now, ExpiresAt: int64(now + 300),
|
|
}); err != nil {
|
|
t.Fatalf("first delivery: %v", err)
|
|
}
|
|
var xminBefore string
|
|
if err := pool.QueryRow(ctx, `SELECT xmin::text FROM users WHERE id = $1`, domain.OfficialSystemUserID).Scan(&xminBefore); err != nil {
|
|
t.Fatalf("load official user xmin: %v", err)
|
|
}
|
|
|
|
const workers = 12
|
|
users := make([]domain.User, workers)
|
|
hashes := make([]string, workers)
|
|
for i := range users {
|
|
users[i] = createLoginCodeDeliveryTestUser(t, ctx, pool, fmt.Sprintf("official-row-%02d", i))
|
|
hashes[i] = fmt.Sprintf("official-row-concurrent-%d-%s", i, randomSuffix(t))
|
|
}
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, workers)
|
|
for i, user := range users {
|
|
wg.Add(1)
|
|
go func(i int, user domain.User) {
|
|
defer wg.Done()
|
|
_, err := NewMessageStore(pool).DeliverLoginCodeMessage(ctx, domain.LoginCodeDeliveryRequest{
|
|
UserID: user.ID, PhoneCodeHash: hashes[i], Code: "12345", Date: now, ExpiresAt: int64(now + 300),
|
|
})
|
|
if err != nil {
|
|
errs <- err
|
|
}
|
|
}(i, user)
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
for err := range errs {
|
|
t.Fatalf("different-user delivery: %v", err)
|
|
}
|
|
var xminAfter string
|
|
if err := pool.QueryRow(ctx, `SELECT xmin::text FROM users WHERE id = $1`, domain.OfficialSystemUserID).Scan(&xminAfter); err != nil {
|
|
t.Fatalf("reload official user xmin: %v", err)
|
|
}
|
|
if xminAfter != xminBefore {
|
|
t.Fatalf("official system user row was rewritten: xmin %s -> %s", xminBefore, xminAfter)
|
|
}
|
|
}
|
|
|
|
func TestLoginCodeDeliveryPostgresReceiptRetentionIsBoundedAndSeekOrdered(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
now := time.Now().Truncate(time.Second)
|
|
users := []domain.User{
|
|
createLoginCodeDeliveryTestUser(t, ctx, pool, "expiry-old-1"),
|
|
createLoginCodeDeliveryTestUser(t, ctx, pool, "expiry-old-2"),
|
|
createLoginCodeDeliveryTestUser(t, ctx, pool, "expiry-future"),
|
|
}
|
|
for i, user := range users {
|
|
expiresAt := now.Add(time.Hour).Unix()
|
|
if i < 2 {
|
|
expiresAt = now.Add(time.Duration(i-2) * time.Minute).Unix()
|
|
}
|
|
if _, err := NewMessageStore(pool).DeliverLoginCodeMessage(ctx, domain.LoginCodeDeliveryRequest{
|
|
UserID: user.ID, PhoneCodeHash: fmt.Sprintf("expiry-%d-%s", i, randomSuffix(t)), Code: "12345", Date: int(now.Add(-time.Hour).Unix()), ExpiresAt: expiresAt,
|
|
}); err != nil {
|
|
t.Fatalf("seed expiry receipt %d: %v", i, err)
|
|
}
|
|
}
|
|
store := NewMessageStore(pool)
|
|
deleted, err := store.DeleteExpiredLoginCodeDeliveries(ctx, now, 1)
|
|
if err != nil || deleted != 1 {
|
|
t.Fatalf("first bounded retention = %d, %v; want 1", deleted, err)
|
|
}
|
|
deleted, err = store.DeleteExpiredLoginCodeDeliveries(ctx, now, 10)
|
|
if err != nil || deleted != 1 {
|
|
t.Fatalf("second bounded retention = %d, %v; want 1", deleted, err)
|
|
}
|
|
var receipts, messages, events int
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM login_code_message_deliveries WHERE user_id = ANY($1)`, []int64{users[0].ID, users[1].ID, users[2].ID}).Scan(&receipts); err != nil {
|
|
t.Fatalf("count retained receipts: %v", err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM message_boxes WHERE owner_user_id = ANY($1) AND peer_id = $2`, []int64{users[0].ID, users[1].ID, users[2].ID}, domain.OfficialSystemUserID).Scan(&messages); err != nil {
|
|
t.Fatalf("count retained messages: %v", err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM user_update_events WHERE user_id = ANY($1) AND event_type = 'new_message'`, []int64{users[0].ID, users[1].ID, users[2].ID}).Scan(&events); err != nil {
|
|
t.Fatalf("count retained events: %v", err)
|
|
}
|
|
if receipts != 1 || messages != 3 || events != 3 {
|
|
t.Fatalf("after receipt GC receipts/messages/events = %d/%d/%d, want 1/3/3", receipts, messages, events)
|
|
}
|
|
}
|
|
|
|
func TestLoginCodeDeliveryPostgresRollsBackEveryFact(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
user := createLoginCodeDeliveryTestUser(t, ctx, pool, "rollback")
|
|
first, err := NewMessageStore(pool).DeliverLoginCodeMessage(ctx, domain.LoginCodeDeliveryRequest{
|
|
UserID: user.ID,
|
|
PhoneCodeHash: "pg-login-code-rollback-first-" + randomSuffix(t),
|
|
Code: "11111",
|
|
Date: 1700001200,
|
|
ExpiresAt: 1700001500,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("seed first delivery: %v", err)
|
|
}
|
|
if first.Message.ID != 1 || first.Message.Pts != 1 {
|
|
t.Fatalf("first allocation = id %d pts %d, want 1/1", first.Message.ID, first.Message.Pts)
|
|
}
|
|
|
|
failedReq := domain.LoginCodeDeliveryRequest{
|
|
UserID: user.ID,
|
|
PhoneCodeHash: "pg-login-code-rollback-failed-" + randomSuffix(t),
|
|
Code: "22222",
|
|
Date: 1700001201,
|
|
ExpiresAt: 1700001501,
|
|
}
|
|
failing := NewMessageStore(pool, WithMessageAllocators(loginCodeFixedBoxAllocator{boxID: first.Message.ID}))
|
|
if _, err := failing.DeliverLoginCodeMessage(ctx, failedReq); err == nil {
|
|
t.Fatal("duplicate box allocator delivery succeeded, want rollback")
|
|
}
|
|
assertLoginCodeDeliveryFacts(t, ctx, pool, user.ID, first.Message, 1)
|
|
failedKey, err := store.LoginCodeDeliveryKey(failedReq.PhoneCodeHash)
|
|
if err != nil {
|
|
t.Fatalf("failed delivery key: %v", err)
|
|
}
|
|
var failedReceipts, failedBodies int
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM login_code_message_deliveries WHERE delivery_key = $1`, failedKey[:]).Scan(&failedReceipts); err != nil {
|
|
t.Fatalf("count failed receipts: %v", err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM private_messages WHERE sender_user_id = $1 AND recipient_user_id = $2 AND body LIKE '%22222%'`, domain.OfficialSystemUserID, user.ID).Scan(&failedBodies); err != nil {
|
|
t.Fatalf("count failed private messages: %v", err)
|
|
}
|
|
if failedReceipts != 0 || failedBodies != 0 {
|
|
t.Fatalf("failed transaction leaked receipts=%d private_messages=%d", failedReceipts, failedBodies)
|
|
}
|
|
|
|
third, err := NewMessageStore(pool).DeliverLoginCodeMessage(ctx, domain.LoginCodeDeliveryRequest{
|
|
UserID: user.ID,
|
|
PhoneCodeHash: "pg-login-code-rollback-third-" + randomSuffix(t),
|
|
Code: "33333",
|
|
Date: 1700001202,
|
|
ExpiresAt: 1700001502,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("delivery after rollback: %v", err)
|
|
}
|
|
if third.Message.ID != 2 || third.Message.Pts != 2 {
|
|
t.Fatalf("allocation after rollback = id %d pts %d, want contiguous 2/2", third.Message.ID, third.Message.Pts)
|
|
}
|
|
}
|
|
|
|
type loginCodeFixedBoxAllocator struct {
|
|
boxID int
|
|
}
|
|
|
|
type commitAckLossDB struct {
|
|
*pgxpool.Pool
|
|
}
|
|
|
|
func (d *commitAckLossDB) Begin(ctx context.Context) (pgx.Tx, error) {
|
|
tx, err := d.Pool.Begin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &commitAckLossTx{Tx: tx}, nil
|
|
}
|
|
|
|
type commitAckLossTx struct {
|
|
pgx.Tx
|
|
}
|
|
|
|
func (t *commitAckLossTx) Commit(ctx context.Context) error {
|
|
if err := t.Tx.Commit(ctx); err != nil {
|
|
return err
|
|
}
|
|
return errors.New("synthetic lost commit acknowledgement")
|
|
}
|
|
|
|
func (a loginCodeFixedBoxAllocator) NextBoxID(context.Context, int64) (int, error) {
|
|
return a.boxID, nil
|
|
}
|
|
|
|
func (a loginCodeFixedBoxAllocator) CurrentBoxID(context.Context, int64) (int, error) {
|
|
return a.boxID, nil
|
|
}
|
|
|
|
func createLoginCodeDeliveryTestUser(t *testing.T, ctx context.Context, pool *pgxpool.Pool, label string) domain.User {
|
|
t.Helper()
|
|
user, err := NewUserStore(pool).Create(ctx, domain.User{
|
|
AccessHash: 8100000000,
|
|
Phone: "+1888" + randomSuffix(t),
|
|
FirstName: "LoginCode" + label,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create login code test user: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_, _ = pool.Exec(context.Background(), `DELETE FROM users WHERE id = $1`, user.ID)
|
|
})
|
|
return user
|
|
}
|
|
|
|
func assertLoginCodeDeliveryFacts(t *testing.T, ctx context.Context, pool *pgxpool.Pool, userID int64, msg domain.Message, want int) {
|
|
t.Helper()
|
|
queries := []struct {
|
|
name string
|
|
sql string
|
|
args []any
|
|
}{
|
|
{"private_messages", `SELECT count(*) FROM private_messages WHERE sender_user_id = $1 AND recipient_user_id = $2`, []any{domain.OfficialSystemUserID, userID}},
|
|
{"message_boxes", `SELECT count(*) FROM message_boxes WHERE owner_user_id = $1 AND peer_type = 'user' AND peer_id = $2`, []any{userID, domain.OfficialSystemUserID}},
|
|
{"dialogs", `SELECT count(*) FROM dialogs WHERE user_id = $1 AND peer_type = 'user' AND peer_id = $2`, []any{userID, domain.OfficialSystemUserID}},
|
|
{"user_update_events", `SELECT count(*) FROM user_update_events WHERE user_id = $1 AND event_type = 'new_message'`, []any{userID}},
|
|
{"dispatch_outbox", `SELECT count(*) FROM dispatch_outbox WHERE target_user_id = $1 AND event_type = 'new_message'`, []any{userID}},
|
|
{"delivery_receipts", `SELECT count(*) FROM login_code_message_deliveries WHERE user_id = $1`, []any{userID}},
|
|
}
|
|
for _, query := range queries {
|
|
var got int
|
|
if err := pool.QueryRow(ctx, query.sql, query.args...).Scan(&got); err != nil {
|
|
t.Fatalf("count %s: %v", query.name, err)
|
|
}
|
|
if got != want {
|
|
t.Fatalf("%s count = %d, want %d", query.name, got, want)
|
|
}
|
|
}
|
|
|
|
var boxPts, eventPts, eventBoxID, outboxPts int32
|
|
var eventType, outboxEventType string
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT b.pts,
|
|
e.pts,
|
|
e.message_box_id,
|
|
e.event_type,
|
|
o.pts,
|
|
o.event_type
|
|
FROM message_boxes b
|
|
JOIN user_update_events e
|
|
ON e.user_id = b.owner_user_id
|
|
AND e.message_box_id = b.box_id
|
|
JOIN dispatch_outbox o
|
|
ON o.target_user_id = e.user_id
|
|
AND o.pts = e.pts
|
|
WHERE b.owner_user_id = $1
|
|
AND b.box_id = $2`, userID, msg.ID).Scan(&boxPts, &eventPts, &eventBoxID, &eventType, &outboxPts, &outboxEventType); err != nil {
|
|
t.Fatalf("load login message/event/outbox chain: %v", err)
|
|
}
|
|
if int(boxPts) != msg.Pts || eventPts != boxPts || eventBoxID != int32(msg.ID) || eventType != string(domain.UpdateEventNewMessage) ||
|
|
outboxPts != eventPts || outboxEventType != eventType {
|
|
t.Fatalf("box/event/outbox chain = box_pts %d event %d/%d/%s outbox %d/%s, message=%+v",
|
|
boxPts, eventPts, eventBoxID, eventType, outboxPts, outboxEventType, msg)
|
|
}
|
|
var topMessageID, unreadCount int32
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT top_message_id, unread_count
|
|
FROM dialogs
|
|
WHERE user_id = $1 AND peer_type = 'user' AND peer_id = $2`, userID, domain.OfficialSystemUserID).Scan(&topMessageID, &unreadCount); err != nil {
|
|
t.Fatalf("load login code dialog: %v", err)
|
|
}
|
|
if int(topMessageID) != msg.ID || int(unreadCount) != want {
|
|
t.Fatalf("dialog top/unread = %d/%d, want %d/%d", topMessageID, unreadCount, msg.ID, want)
|
|
}
|
|
}
|