owpengram-server/internal/store/postgres/secretchat_integration_test.go

210 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package postgres
import (
"context"
"errors"
"testing"
"telesrv/internal/domain"
)
// TestSecretChatStorePostgres 验证密聊握手状态机 PG 实现的行为契约(与 memory 实现
// 同构create/get、幂等去重、accept CAS、double-accept、discard 幂等、
// accept-after-discard、部分唯一索引discard 后同 random_id 可重建)。
// 门控于 TELESRV_TEST_POSTGRES_DSN。
func TestSecretChatStorePostgres(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
store := NewSecretChatStore(pool)
const (
adminUser = int64(770001)
partUser = int64(770002)
adminKey = int64(0xA1)
partKey = int64(0xB2)
base = 7700001
)
// 隔离:清理本测试用到的行(测试库持久累积)。
cleanup := func() {
_, _ = pool.Exec(ctx, `DELETE FROM secret_chats WHERE admin_user_id = $1`, adminUser)
}
cleanup()
t.Cleanup(cleanup)
mk := func(id int, randomID int32) domain.SecretChat {
return domain.SecretChat{
ID: id,
AdminAccessHash: 111,
ParticipantAccessHash: 222,
AdminUserID: adminUser,
AdminAuthKeyID: adminKey,
ParticipantUserID: partUser,
State: domain.SecretChatStateRequested,
GA: []byte{0x0a, 0x0b, 0x0c},
RandomID: randomID,
Date: 1000,
}
}
// create + get round-trip。
chat := mk(base, 555)
if err := store.CreateSecretChat(ctx, chat); err != nil {
t.Fatalf("create: %v", err)
}
got, found, err := store.GetSecretChat(ctx, base)
if err != nil || !found {
t.Fatalf("get: found=%v err=%v", found, err)
}
if got.AdminUserID != adminUser || got.RandomID != 555 || string(got.GA) != string(chat.GA) {
t.Fatalf("round-trip mismatch: %+v", got)
}
// 重复 chat_id → ID conflict。
if err := store.CreateSecretChat(ctx, mk(base, 556)); !errors.Is(err, domain.ErrSecretChatIDConflict) {
t.Fatalf("duplicate chat_id err = %v, want ErrSecretChatIDConflict", err)
}
// 幂等查询(非终态)。
idem, found, err := store.GetByAdminRandom(ctx, adminKey, 555)
if err != nil || !found || idem.ID != base {
t.Fatalf("GetByAdminRandom: found=%v id=%d err=%v", found, idem.ID, err)
}
// accept CAS。
accepted, err := store.AcceptSecretChat(ctx, base, partKey, []byte{0x0d, 0x0e}, 0x99)
if err != nil {
t.Fatalf("accept: %v", err)
}
if accepted.State != domain.SecretChatStateNormal || accepted.ParticipantAuthKeyID != partKey ||
accepted.KeyFingerprint != 0x99 || string(accepted.GB) != string([]byte{0x0d, 0x0e}) {
t.Fatalf("accepted = %+v", accepted)
}
// double accept → already accepted。
if _, err := store.AcceptSecretChat(ctx, base, int64(0xC3), []byte{0x01}, 1); !errors.Is(err, domain.ErrSecretChatAlreadyAccepted) {
t.Fatalf("double accept err = %v, want ErrSecretChatAlreadyAccepted", err)
}
// discard。
_, already, err := store.DiscardSecretChat(ctx, base, true)
if err != nil || already {
t.Fatalf("discard: already=%v err=%v", already, err)
}
// 幂等 discard。
cur, already, err := store.DiscardSecretChat(ctx, base, false)
if err != nil || !already || cur.State != domain.SecretChatStateDiscarded {
t.Fatalf("idempotent discard: already=%v state=%v err=%v", already, cur.State, err)
}
// accept after discard → already declined。
if _, err := store.AcceptSecretChat(ctx, base, partKey, []byte{0x01}, 1); !errors.Is(err, domain.ErrSecretChatAlreadyDeclined) {
t.Fatalf("accept after discard err = %v, want ErrSecretChatAlreadyDeclined", err)
}
// 部分唯一索引discarded 旧 chat 不阻塞同 (admin_auth_key_id, random_id) 重建。
if err := store.CreateSecretChat(ctx, mk(base+1, 555)); err != nil {
t.Fatalf("recreate after discard with same random_id: %v", err)
}
// MaxSecretChatID 反映最大 chat_id≥ base+1
maxID, err := store.MaxSecretChatID(ctx)
if err != nil || maxID < base+1 {
t.Fatalf("max chat id = %d err = %v, want >= %d", maxID, err, base+1)
}
// 不存在 chataccept/discard 返回 not found。
if _, err := store.AcceptSecretChat(ctx, base+999, partKey, []byte{0x01}, 1); !errors.Is(err, domain.ErrSecretChatNotFound) {
t.Fatalf("accept missing err = %v, want ErrSecretChatNotFound", err)
}
if _, _, err := store.DiscardSecretChat(ctx, base+999, false); !errors.Is(err, domain.ErrSecretChatNotFound) {
t.Fatalf("discard missing err = %v, want ErrSecretChatNotFound", err)
}
}
// TestSecretChatListActiveByAuthKeyPostgres 验证 ListActiveSecretChatsByAuthKey登出/撤销
// 级联 discard 的查询基础P1 修复):匹配 admin 或 participant auth_key、排除 discarded、
// 按 chat_id 升序;与 memory 实现同构。
func TestSecretChatListActiveByAuthKeyPostgres(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
store := NewSecretChatStore(pool)
const (
adminUser = int64(770101)
partUser = int64(770102)
keyA = int64(0xAA01) // admin 设备 key
keyB = int64(0xBB02) // participant 设备 key
keyOther = int64(0xCC03)
base = 7701001
)
cleanup := func() { _, _ = pool.Exec(ctx, `DELETE FROM secret_chats WHERE admin_user_id IN ($1, $2)`, adminUser, partUser) }
cleanup()
t.Cleanup(cleanup)
mk := func(id int, randomID int32, adminUID, adminKey, partUID, partKeyV int64, state domain.SecretChatState) domain.SecretChat {
return domain.SecretChat{
ID: id, AdminAccessHash: 1, ParticipantAccessHash: 2,
AdminUserID: adminUID, AdminAuthKeyID: adminKey,
ParticipantUserID: partUID, ParticipantAuthKeyID: partKeyV,
State: state, GA: []byte{0x01}, RandomID: randomID, Date: 1,
}
}
idsOf := func(chats []domain.SecretChat) []int {
out := make([]int, len(chats))
for i, c := range chats {
out[i] = c.ID
}
return out
}
eq := func(got, want []int) bool {
if len(got) != len(want) {
return false
}
for i := range got {
if got[i] != want[i] {
return false
}
}
return true
}
// chat1 admin=keyA participant=keyB normalchat2 admin=keyA 未绑定 requested
// chat3 admin=keyOther participant=keyB normalchat4 admin=keyA participant=keyB 已 discard。
for _, c := range []domain.SecretChat{
mk(base+1, 1, adminUser, keyA, partUser, keyB, domain.SecretChatStateNormal),
mk(base+2, 2, adminUser, keyA, partUser, 0, domain.SecretChatStateRequested),
mk(base+3, 3, partUser, keyOther, adminUser, keyB, domain.SecretChatStateNormal),
mk(base+4, 4, adminUser, keyA, partUser, keyB, domain.SecretChatStateNormal),
} {
if err := store.CreateSecretChat(ctx, c); err != nil {
t.Fatalf("create %d: %v", c.ID, err)
}
}
if _, _, err := store.DiscardSecretChat(ctx, base+4, false); err != nil {
t.Fatalf("discard chat4: %v", err)
}
// keyA作为 admin→ chat1, chat2chat4 已 discard 排除)。
got, err := store.ListActiveSecretChatsByAuthKey(ctx, keyA)
if err != nil {
t.Fatalf("list keyA: %v", err)
}
if ids := idsOf(got); !eq(ids, []int{base + 1, base + 2}) {
t.Fatalf("keyA active = %v, want [%d %d]", ids, base+1, base+2)
}
// keyB作为 participant→ chat1, chat3chat4 已 discard 排除)。
got, err = store.ListActiveSecretChatsByAuthKey(ctx, keyB)
if err != nil {
t.Fatalf("list keyB: %v", err)
}
if ids := idsOf(got); !eq(ids, []int{base + 1, base + 3}) {
t.Fatalf("keyB active = %v, want [%d %d]", ids, base+1, base+3)
}
// 未知 key → 空authKeyID 0 → nil。
if got, err := store.ListActiveSecretChatsByAuthKey(ctx, 0x9999); err != nil || len(got) != 0 {
t.Fatalf("unknown key = %d (err=%v), want 0", len(got), err)
}
if got, err := store.ListActiveSecretChatsByAuthKey(ctx, 0); err != nil || got != nil {
t.Fatalf("zero key = %v (err=%v), want nil", got, err)
}
}