perf: sync protocol and core hardening updates
This commit is contained in:
parent
152fed3b87
commit
4390ebf5a9
283 changed files with 29231 additions and 2295 deletions
475
internal/store/postgres/login_code_delivery_integration_test.go
Normal file
475
internal/store/postgres/login_code_delivery_integration_test.go
Normal file
|
|
@ -0,0 +1,475 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue