updateChannelParticipant carries the account's qts per the MTProto spec, but the server always sent Qts: 0, so real clients silently discarded it as a stale duplicate -- the banned/kicked user's channel never vanished locally and no correct "removed by admin" message showed, even though the update was delivered successfully at the transport layer. Add a durable per-device qts queue (channel_participant_event_queue) sharing its qts number space with the existing secret-chat queue (one qts sequence per device, per spec), and use it to stamp a correct, monotonically increasing qts on the update for every device of the affected user -- for channel bans/kicks, admin promotion/demotion, and ownership transfer. A device offline when it happened can now recover the event via updates.getDifference instead of missing it permanently.
514 lines
18 KiB
Go
514 lines
18 KiB
Go
package secretchat
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"sync"
|
||
"testing"
|
||
|
||
"telesrv/internal/domain"
|
||
"telesrv/internal/store/memory"
|
||
)
|
||
|
||
// validGA 返回一个落在合法 DH 区间的 256 字节 g_a(首字节 0x55 ≈ 2^2046,
|
||
// 既 > 2^1984 又 < p≈0xc7..)。
|
||
func validGA() []byte {
|
||
b := make([]byte, 256)
|
||
for i := range b {
|
||
b[i] = 0x42
|
||
}
|
||
b[0] = 0x55
|
||
return b
|
||
}
|
||
|
||
func newTestService() (*Service, *memory.SecretChatStore) {
|
||
st := memory.NewSecretChatStore()
|
||
queue := memory.NewEncryptedQueueStore()
|
||
return NewService(st, queue, queue), st
|
||
}
|
||
|
||
const (
|
||
adminUser = int64(1001)
|
||
partUser = int64(2002)
|
||
adminAuthKey = int64(0x1111)
|
||
partAuthKey = int64(0x2222)
|
||
otherAuthKey = int64(0x3333)
|
||
keyFP = int64(0x0123456789abcdef)
|
||
)
|
||
|
||
func requestFixture() domain.SecretChatRequest {
|
||
return domain.SecretChatRequest{
|
||
AdminUserID: adminUser,
|
||
AdminAuthKeyID: adminAuthKey,
|
||
ParticipantUserID: partUser,
|
||
RandomID: 12345,
|
||
GA: validGA(),
|
||
Date: 1000,
|
||
}
|
||
}
|
||
|
||
func TestRequestEncryption(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
chat, err := svc.RequestEncryption(ctx, requestFixture())
|
||
if err != nil {
|
||
t.Fatalf("RequestEncryption: %v", err)
|
||
}
|
||
if chat.ID != int(requestFixture().RandomID) {
|
||
t.Fatalf("chat id = %d, want request random_id %d", chat.ID, requestFixture().RandomID)
|
||
}
|
||
if chat.State != domain.SecretChatStateRequested {
|
||
t.Fatalf("state = %q, want requested", chat.State)
|
||
}
|
||
if len(chat.GA) != dhPubSize {
|
||
t.Fatalf("g_a length = %d, want %d (left-padded)", len(chat.GA), dhPubSize)
|
||
}
|
||
if chat.AdminAccessHash == 0 || chat.ParticipantAccessHash == 0 {
|
||
t.Fatal("access hashes must be non-zero")
|
||
}
|
||
if chat.AdminAccessHash == chat.ParticipantAccessHash {
|
||
t.Fatal("admin/participant access hashes must differ (per-viewer)")
|
||
}
|
||
if chat.ParticipantAuthKeyID != 0 {
|
||
t.Fatalf("participant auth key must be unbound before accept, got %d", chat.ParticipantAuthKeyID)
|
||
}
|
||
}
|
||
|
||
func TestRequestEncryptionIdempotent(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
first, err := svc.RequestEncryption(ctx, requestFixture())
|
||
if err != nil {
|
||
t.Fatalf("first request: %v", err)
|
||
}
|
||
second, err := svc.RequestEncryption(ctx, requestFixture())
|
||
if err != nil {
|
||
t.Fatalf("second request: %v", err)
|
||
}
|
||
if first.ID != second.ID {
|
||
t.Fatalf("idempotent re-request must return same chat: %d vs %d", first.ID, second.ID)
|
||
}
|
||
}
|
||
|
||
func TestRequestEncryptionConcurrentExactRetry(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
results := make([]domain.SecretChat, 2)
|
||
errs := make([]error, 2)
|
||
var wg sync.WaitGroup
|
||
for i := range results {
|
||
wg.Add(1)
|
||
go func(i int) {
|
||
defer wg.Done()
|
||
results[i], errs[i] = svc.RequestEncryption(ctx, requestFixture())
|
||
}(i)
|
||
}
|
||
wg.Wait()
|
||
for i, err := range errs {
|
||
if err != nil {
|
||
t.Fatalf("concurrent request %d: %v", i, err)
|
||
}
|
||
}
|
||
if results[0].ID != int(requestFixture().RandomID) || results[1].ID != results[0].ID ||
|
||
results[1].AdminAccessHash != results[0].AdminAccessHash ||
|
||
results[1].ParticipantAccessHash != results[0].ParticipantAccessHash {
|
||
t.Fatalf("concurrent exact retry diverged: first=%+v second=%+v", results[0], results[1])
|
||
}
|
||
}
|
||
|
||
func TestRequestEncryptionPreservesNegativeRandomID(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
req := requestFixture()
|
||
req.RandomID = -12345
|
||
chat, err := svc.RequestEncryption(context.Background(), req)
|
||
if err != nil {
|
||
t.Fatalf("request negative random_id: %v", err)
|
||
}
|
||
if chat.ID != int(req.RandomID) || chat.RandomID != req.RandomID {
|
||
t.Fatalf("chat id/random_id = %d/%d, want %d", chat.ID, chat.RandomID, req.RandomID)
|
||
}
|
||
}
|
||
|
||
func TestRequestEncryptionRejectsChangedIntentAndGlobalCollision(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
if _, err := svc.RequestEncryption(ctx, requestFixture()); err != nil {
|
||
t.Fatalf("first request: %v", err)
|
||
}
|
||
|
||
changedPeer := requestFixture()
|
||
changedPeer.ParticipantUserID++
|
||
if _, err := svc.RequestEncryption(ctx, changedPeer); !errors.Is(err, domain.ErrSecretChatRandomIDDuplicate) {
|
||
t.Fatalf("changed peer err = %v, want ErrSecretChatRandomIDDuplicate", err)
|
||
}
|
||
|
||
changedGA := requestFixture()
|
||
changedGA.GA = validGA()
|
||
changedGA.GA[1] ^= 0x01
|
||
if _, err := svc.RequestEncryption(ctx, changedGA); !errors.Is(err, domain.ErrSecretChatRandomIDDuplicate) {
|
||
t.Fatalf("changed g_a err = %v, want ErrSecretChatRandomIDDuplicate", err)
|
||
}
|
||
|
||
otherAuthKey := requestFixture()
|
||
otherAuthKey.AdminUserID++
|
||
otherAuthKey.AdminAuthKeyID++
|
||
if _, err := svc.RequestEncryption(ctx, otherAuthKey); !errors.Is(err, domain.ErrSecretChatRandomIDDuplicate) {
|
||
t.Fatalf("global collision err = %v, want ErrSecretChatRandomIDDuplicate", err)
|
||
}
|
||
}
|
||
|
||
func TestRequestEncryptionRejectsZeroAndDiscardedReuse(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
zero := requestFixture()
|
||
zero.RandomID = 0
|
||
if _, err := svc.RequestEncryption(ctx, zero); !errors.Is(err, domain.ErrSecretChatRandomIDDuplicate) {
|
||
t.Fatalf("zero random_id err = %v, want ErrSecretChatRandomIDDuplicate", err)
|
||
}
|
||
|
||
chat, err := svc.RequestEncryption(ctx, requestFixture())
|
||
if err != nil {
|
||
t.Fatalf("request: %v", err)
|
||
}
|
||
if _, _, err := svc.DiscardEncryption(ctx, chat.ID, adminUser, adminAuthKey, true); err != nil {
|
||
t.Fatalf("discard: %v", err)
|
||
}
|
||
if _, err := svc.RequestEncryption(ctx, requestFixture()); !errors.Is(err, domain.ErrSecretChatRandomIDDuplicate) {
|
||
t.Fatalf("discarded reuse err = %v, want ErrSecretChatRandomIDDuplicate", err)
|
||
}
|
||
}
|
||
|
||
func TestRequestEncryptionInvalidGA(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
req := requestFixture()
|
||
req.GA = []byte{0x01} // value 1 → 不在 (1, p-1)
|
||
if _, err := svc.RequestEncryption(context.Background(), req); !errors.Is(err, ErrGAInvalid) {
|
||
t.Fatalf("err = %v, want ErrGAInvalid", err)
|
||
}
|
||
}
|
||
|
||
func TestAcceptEncryption(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
chat, err := svc.RequestEncryption(ctx, requestFixture())
|
||
if err != nil {
|
||
t.Fatalf("request: %v", err)
|
||
}
|
||
accepted, err := svc.AcceptEncryption(ctx, chat.ID, partUser, partAuthKey, chat.ParticipantAccessHash, validGA(), keyFP)
|
||
if err != nil {
|
||
t.Fatalf("accept: %v", err)
|
||
}
|
||
if accepted.State != domain.SecretChatStateNormal {
|
||
t.Fatalf("state = %q, want normal", accepted.State)
|
||
}
|
||
if accepted.KeyFingerprint != keyFP {
|
||
t.Fatalf("key fingerprint not relayed byte-for-byte: got %x want %x", accepted.KeyFingerprint, keyFP)
|
||
}
|
||
if accepted.ParticipantAuthKeyID != partAuthKey {
|
||
t.Fatalf("participant auth key not bound: %d", accepted.ParticipantAuthKeyID)
|
||
}
|
||
if len(accepted.GB) != dhPubSize {
|
||
t.Fatalf("g_b length = %d, want %d", len(accepted.GB), dhPubSize)
|
||
}
|
||
}
|
||
|
||
func TestAcceptEncryptionWrongAccessHash(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
chat, _ := svc.RequestEncryption(ctx, requestFixture())
|
||
_, err := svc.AcceptEncryption(ctx, chat.ID, partUser, partAuthKey, chat.ParticipantAccessHash+1, validGA(), keyFP)
|
||
if !errors.Is(err, domain.ErrSecretChatNotFound) {
|
||
t.Fatalf("err = %v, want ErrSecretChatNotFound", err)
|
||
}
|
||
}
|
||
|
||
func TestAcceptEncryptionWrongUser(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
chat, _ := svc.RequestEncryption(ctx, requestFixture())
|
||
// admin 自己冒充接受方。
|
||
_, err := svc.AcceptEncryption(ctx, chat.ID, adminUser, adminAuthKey, chat.ParticipantAccessHash, validGA(), keyFP)
|
||
if !errors.Is(err, domain.ErrSecretChatNotFound) {
|
||
t.Fatalf("err = %v, want ErrSecretChatNotFound", err)
|
||
}
|
||
}
|
||
|
||
func TestAcceptEncryptionDoubleAccept(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
chat, _ := svc.RequestEncryption(ctx, requestFixture())
|
||
if _, err := svc.AcceptEncryption(ctx, chat.ID, partUser, partAuthKey, chat.ParticipantAccessHash, validGA(), keyFP); err != nil {
|
||
t.Fatalf("first accept: %v", err)
|
||
}
|
||
// 第二台设备 accept:CAS 落空 → ENCRYPTION_ALREADY_ACCEPTED。
|
||
_, err := svc.AcceptEncryption(ctx, chat.ID, partUser, int64(0x3333), chat.ParticipantAccessHash, validGA(), keyFP)
|
||
if !errors.Is(err, domain.ErrSecretChatAlreadyAccepted) {
|
||
t.Fatalf("err = %v, want ErrSecretChatAlreadyAccepted", err)
|
||
}
|
||
}
|
||
|
||
func TestAcceptEncryptionConcurrentDevicesSingleWinner(t *testing.T) {
|
||
svc, st := newTestService()
|
||
ctx := context.Background()
|
||
chat, err := svc.RequestEncryption(ctx, requestFixture())
|
||
if err != nil {
|
||
t.Fatalf("request: %v", err)
|
||
}
|
||
|
||
authKeys := []int64{partAuthKey, otherAuthKey}
|
||
errs := make([]error, len(authKeys))
|
||
var wg sync.WaitGroup
|
||
for i, authKeyID := range authKeys {
|
||
wg.Add(1)
|
||
go func(i int, authKeyID int64) {
|
||
defer wg.Done()
|
||
_, errs[i] = svc.AcceptEncryption(ctx, chat.ID, partUser, authKeyID, chat.ParticipantAccessHash, validGA(), keyFP)
|
||
}(i, authKeyID)
|
||
}
|
||
wg.Wait()
|
||
|
||
winners := 0
|
||
losers := 0
|
||
for _, err := range errs {
|
||
switch {
|
||
case err == nil:
|
||
winners++
|
||
case errors.Is(err, domain.ErrSecretChatAlreadyAccepted):
|
||
losers++
|
||
default:
|
||
t.Fatalf("concurrent accept err = %v", err)
|
||
}
|
||
}
|
||
if winners != 1 || losers != 1 {
|
||
t.Fatalf("concurrent accepts winners=%d losers=%d, want 1/1", winners, losers)
|
||
}
|
||
|
||
stored, ok, err := st.GetSecretChat(ctx, chat.ID)
|
||
if err != nil || !ok {
|
||
t.Fatalf("get accepted chat: ok=%v err=%v", ok, err)
|
||
}
|
||
if stored.State != domain.SecretChatStateNormal ||
|
||
(stored.ParticipantAuthKeyID != partAuthKey && stored.ParticipantAuthKeyID != otherAuthKey) {
|
||
t.Fatalf("accepted chat = %+v, want normal bound to one participant device", stored)
|
||
}
|
||
loserAuthKeyID := partAuthKey
|
||
if stored.ParticipantAuthKeyID == partAuthKey {
|
||
loserAuthKeyID = otherAuthKey
|
||
}
|
||
if _, _, err := svc.DiscardEncryption(ctx, chat.ID, partUser, loserAuthKeyID, true); !errors.Is(err, domain.ErrSecretChatNotFound) {
|
||
t.Fatalf("loser discard err = %v, want ErrSecretChatNotFound", err)
|
||
}
|
||
stored, ok, err = st.GetSecretChat(ctx, chat.ID)
|
||
if err != nil || !ok || stored.State != domain.SecretChatStateNormal {
|
||
t.Fatalf("chat after loser discard = %+v ok=%v err=%v, want normal", stored, ok, err)
|
||
}
|
||
}
|
||
|
||
func TestAcceptEncryptionInvalidGB(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
chat, _ := svc.RequestEncryption(ctx, requestFixture())
|
||
_, err := svc.AcceptEncryption(ctx, chat.ID, partUser, partAuthKey, chat.ParticipantAccessHash, []byte{0x01}, keyFP)
|
||
if !errors.Is(err, ErrGAInvalid) {
|
||
t.Fatalf("err = %v, want ErrGAInvalid", err)
|
||
}
|
||
}
|
||
|
||
func TestDiscardEncryption(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
chat, _ := svc.RequestEncryption(ctx, requestFixture())
|
||
got, already, err := svc.DiscardEncryption(ctx, chat.ID, adminUser, adminAuthKey, true)
|
||
if err != nil {
|
||
t.Fatalf("discard: %v", err)
|
||
}
|
||
if already {
|
||
t.Fatal("first discard must not report already")
|
||
}
|
||
if got.State != domain.SecretChatStateDiscarded || !got.HistoryDeleted {
|
||
t.Fatalf("discarded chat = %+v", got)
|
||
}
|
||
// 幂等:再 discard 返回 already=true。
|
||
_, already, err = svc.DiscardEncryption(ctx, chat.ID, partUser, partAuthKey, false)
|
||
if err != nil || !already {
|
||
t.Fatalf("idempotent discard: already=%v err=%v", already, err)
|
||
}
|
||
}
|
||
|
||
func TestDiscardEncryptionNonParticipant(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
chat, _ := svc.RequestEncryption(ctx, requestFixture())
|
||
_, _, err := svc.DiscardEncryption(ctx, chat.ID, int64(9999), int64(9999), false)
|
||
if !errors.Is(err, domain.ErrSecretChatNotFound) {
|
||
t.Fatalf("err = %v, want ErrSecretChatNotFound", err)
|
||
}
|
||
}
|
||
|
||
// acceptedChat 跑完 request→accept,返回 normal 态密聊。
|
||
func acceptedChat(t *testing.T, svc *Service) domain.SecretChat {
|
||
t.Helper()
|
||
ctx := context.Background()
|
||
chat, err := svc.RequestEncryption(ctx, requestFixture())
|
||
if err != nil {
|
||
t.Fatalf("request: %v", err)
|
||
}
|
||
accepted, err := svc.AcceptEncryption(ctx, chat.ID, partUser, partAuthKey, chat.ParticipantAccessHash, dhParamGB(), keyFP)
|
||
if err != nil {
|
||
t.Fatalf("accept: %v", err)
|
||
}
|
||
return accepted
|
||
}
|
||
|
||
func dhParamGB() []byte {
|
||
b := make([]byte, 256)
|
||
for i := range b {
|
||
b[i] = 0x42
|
||
}
|
||
b[0] = 0x66
|
||
return b
|
||
}
|
||
|
||
func TestSendEncryptedQtsAllocation(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
chat := acceptedChat(t, svc)
|
||
|
||
// admin 发 → 投给 participant 设备(partAuthKey),qts 从 1 起。
|
||
_, m1, err := svc.SendEncrypted(ctx, chat.ID, adminUser, adminAuthKey, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: 111, Bytes: []byte{1, 2, 3}, Date: 2000})
|
||
if err != nil {
|
||
t.Fatalf("send 1: %v", err)
|
||
}
|
||
if m1.Qts != 1 || m1.ReceiverAuthKeyID != partAuthKey || m1.ReceiverUserID != partUser {
|
||
t.Fatalf("msg1 = %+v (want qts=1, receiver=participant device)", m1)
|
||
}
|
||
_, m2, err := svc.SendEncrypted(ctx, chat.ID, adminUser, adminAuthKey, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: 222, Bytes: []byte{4}, Date: 2001})
|
||
if err != nil || m2.Qts != 2 {
|
||
t.Fatalf("msg2 qts = %d err=%v, want 2", m2.Qts, err)
|
||
}
|
||
|
||
// 幂等重发同 random_id → 返回首次 qts/date,不分配新 qts。
|
||
_, dup, err := svc.SendEncrypted(ctx, chat.ID, adminUser, adminAuthKey, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: 111, Bytes: []byte{1, 2, 3}, Date: 9999})
|
||
if err != nil {
|
||
t.Fatalf("dup send: %v", err)
|
||
}
|
||
if dup.Qts != 1 || dup.Date != 2000 {
|
||
t.Fatalf("idempotent resend = %+v, want qts=1 date=2000 (首次落库值)", dup)
|
||
}
|
||
|
||
// participant 发 → 投给 admin 设备(adminAuthKey),独立 qts 序列从 1 起。
|
||
_, pm, err := svc.SendEncrypted(ctx, chat.ID, partUser, partAuthKey, chat.ParticipantAccessHash, domain.SecretMessageDelivery{RandomID: 333, Bytes: []byte{9}, Date: 2002})
|
||
if err != nil {
|
||
t.Fatalf("participant send: %v", err)
|
||
}
|
||
if pm.Qts != 1 || pm.ReceiverAuthKeyID != adminAuthKey || pm.ReceiverUserID != adminUser {
|
||
t.Fatalf("participant msg = %+v (want qts=1, receiver=admin device)", pm)
|
||
}
|
||
}
|
||
|
||
func TestSendEncryptedWrongAccessHash(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
chat := acceptedChat(t, svc)
|
||
_, _, err := svc.SendEncrypted(ctx, chat.ID, adminUser, adminAuthKey, chat.AdminAccessHash+1, domain.SecretMessageDelivery{RandomID: 1, Bytes: []byte{1}, Date: 2000})
|
||
if !errors.Is(err, domain.ErrSecretChatNotFound) {
|
||
t.Fatalf("err = %v, want ErrSecretChatNotFound", err)
|
||
}
|
||
}
|
||
|
||
func TestSendEncryptedRejectsUnboundAccountDevice(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
chat := acceptedChat(t, svc)
|
||
|
||
for _, tc := range []struct {
|
||
name string
|
||
userID int64
|
||
accessHash int64
|
||
}{
|
||
{name: "admin", userID: adminUser, accessHash: chat.AdminAccessHash},
|
||
{name: "participant", userID: partUser, accessHash: chat.ParticipantAccessHash},
|
||
} {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
_, _, err := svc.SendEncrypted(ctx, chat.ID, tc.userID, otherAuthKey, tc.accessHash, domain.SecretMessageDelivery{
|
||
RandomID: 991, Bytes: []byte{1}, Date: 2000,
|
||
})
|
||
if !errors.Is(err, domain.ErrSecretChatNotFound) {
|
||
t.Fatalf("err = %v, want ErrSecretChatNotFound", err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestDiscardEncryptionRejectsUnboundAccountDeviceAfterAccept(t *testing.T) {
|
||
svc, st := newTestService()
|
||
ctx := context.Background()
|
||
chat := acceptedChat(t, svc)
|
||
|
||
if _, _, err := svc.DiscardEncryption(ctx, chat.ID, partUser, otherAuthKey, true); !errors.Is(err, domain.ErrSecretChatNotFound) {
|
||
t.Fatalf("unbound discard err = %v, want ErrSecretChatNotFound", err)
|
||
}
|
||
stored, ok, err := st.GetSecretChat(ctx, chat.ID)
|
||
if err != nil || !ok || stored.State != domain.SecretChatStateNormal {
|
||
t.Fatalf("chat after rejected discard = %+v ok=%v err=%v", stored, ok, err)
|
||
}
|
||
}
|
||
|
||
func TestSendEncryptedNonNormal(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
chat, _ := svc.RequestEncryption(ctx, requestFixture()) // requested, 未 accept
|
||
_, _, err := svc.SendEncrypted(ctx, chat.ID, adminUser, adminAuthKey, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: 1, Bytes: []byte{1}, Date: 2000})
|
||
if !errors.Is(err, domain.ErrSecretChatNotFound) {
|
||
t.Fatalf("err = %v, want ErrSecretChatNotFound (未成型不能发)", err)
|
||
}
|
||
}
|
||
|
||
func TestListNewMessagesAndAck(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
chat := acceptedChat(t, svc)
|
||
for i := 0; i < 3; i++ {
|
||
if _, _, err := svc.SendEncrypted(ctx, chat.ID, adminUser, adminAuthKey, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: int64(1000 + i), Bytes: []byte{byte(i)}, Date: 2000 + i}); err != nil {
|
||
t.Fatalf("send %d: %v", i, err)
|
||
}
|
||
}
|
||
// 接收设备(participant)补差分:qts>0 全部 3 条。
|
||
msgs, err := svc.ListNewMessages(ctx, partAuthKey, 0, 0)
|
||
if err != nil || len(msgs) != 3 {
|
||
t.Fatalf("list since 0 = %d msgs err=%v, want 3", len(msgs), err)
|
||
}
|
||
if msgs[0].Qts != 1 || msgs[2].Qts != 3 {
|
||
t.Fatalf("qts sequence broken: %d..%d", msgs[0].Qts, msgs[2].Qts)
|
||
}
|
||
// qts>1 → 剩 2 条。
|
||
msgs, _ = svc.ListNewMessages(ctx, partAuthKey, 1, 0)
|
||
if len(msgs) != 2 || msgs[0].Qts != 2 {
|
||
t.Fatalf("list since 1 = %+v, want qts 2,3", msgs)
|
||
}
|
||
// reserved qts = 3。
|
||
if q, _ := svc.DeviceReservedQts(ctx, partAuthKey); q != 3 {
|
||
t.Fatalf("reserved qts = %d, want 3", q)
|
||
}
|
||
// ack 到 3:不报错(confirmed 推进)。
|
||
if err := svc.AckQueue(ctx, partAuthKey, 3); err != nil {
|
||
t.Fatalf("ack: %v", err)
|
||
}
|
||
// 未参与设备 qts=0。
|
||
if q, _ := svc.DeviceReservedQts(ctx, int64(0xDEAD)); q != 0 {
|
||
t.Fatalf("unrelated device reserved qts = %d, want 0", q)
|
||
}
|
||
}
|
||
|
||
func TestAcceptAfterDiscard(t *testing.T) {
|
||
svc, _ := newTestService()
|
||
ctx := context.Background()
|
||
chat, _ := svc.RequestEncryption(ctx, requestFixture())
|
||
if _, _, err := svc.DiscardEncryption(ctx, chat.ID, adminUser, adminAuthKey, false); err != nil {
|
||
t.Fatalf("discard: %v", err)
|
||
}
|
||
_, err := svc.AcceptEncryption(ctx, chat.ID, partUser, partAuthKey, chat.ParticipantAccessHash, validGA(), keyFP)
|
||
if !errors.Is(err, domain.ErrSecretChatAlreadyDeclined) {
|
||
t.Fatalf("err = %v, want ErrSecretChatAlreadyDeclined", err)
|
||
}
|
||
}
|