merged from gramsrv upstream

This commit is contained in:
onysd 2026-09-01 12:06:31 +03:00
parent 79c64ee916
commit 21a0856587
651 changed files with 54774 additions and 4590 deletions

View file

@ -3,30 +3,13 @@ package secretchat
import (
"context"
"errors"
"sync"
"testing"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
)
// fakeChatIDAllocator 是单调自增的测试分配器(无 Redis
type fakeChatIDAllocator struct{ n int }
func (a *fakeChatIDAllocator) NextSecretChatID(context.Context) (int, error) {
a.n++
return a.n, nil
}
func (a *fakeChatIDAllocator) NextSecretChatIDAtLeast(_ context.Context, floor int) (int, error) {
if a.n < floor {
a.n = floor
}
a.n++
return a.n, nil
}
func (a *fakeChatIDAllocator) CurrentSecretChatID(context.Context) (int, error) { return a.n, nil }
// validGA 返回一个落在合法 DH 区间的 256 字节 g_a首字节 0x55 ≈ 2^2046
// 既 > 2^1984 又 < p≈0xc7..)。
func validGA() []byte {
@ -40,7 +23,7 @@ func validGA() []byte {
func newTestService() (*Service, *memory.SecretChatStore) {
st := memory.NewSecretChatStore()
return NewService(st, memory.NewEncryptedQueueStore(), &fakeChatIDAllocator{}), st
return NewService(st, memory.NewEncryptedQueueStore()), st
}
const (
@ -48,6 +31,7 @@ const (
partUser = int64(2002)
adminAuthKey = int64(0x1111)
partAuthKey = int64(0x2222)
otherAuthKey = int64(0x3333)
keyFP = int64(0x0123456789abcdef)
)
@ -69,8 +53,8 @@ func TestRequestEncryption(t *testing.T) {
if err != nil {
t.Fatalf("RequestEncryption: %v", err)
}
if chat.ID <= 0 || chat.ID > 0x7fffffff {
t.Fatalf("chat id out of int32 range: %d", chat.ID)
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)
@ -105,6 +89,94 @@ func TestRequestEncryptionIdempotent(t *testing.T) {
}
}
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()
@ -174,6 +246,63 @@ func TestAcceptEncryptionDoubleAccept(t *testing.T) {
}
}
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()
@ -188,7 +317,7 @@ func TestDiscardEncryption(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
chat, _ := svc.RequestEncryption(ctx, requestFixture())
got, already, err := svc.DiscardEncryption(ctx, chat.ID, adminUser, true)
got, already, err := svc.DiscardEncryption(ctx, chat.ID, adminUser, adminAuthKey, true)
if err != nil {
t.Fatalf("discard: %v", err)
}
@ -199,7 +328,7 @@ func TestDiscardEncryption(t *testing.T) {
t.Fatalf("discarded chat = %+v", got)
}
// 幂等:再 discard 返回 already=true。
_, already, err = svc.DiscardEncryption(ctx, chat.ID, partUser, false)
_, already, err = svc.DiscardEncryption(ctx, chat.ID, partUser, partAuthKey, false)
if err != nil || !already {
t.Fatalf("idempotent discard: already=%v err=%v", already, err)
}
@ -209,7 +338,7 @@ func TestDiscardEncryptionNonParticipant(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
chat, _ := svc.RequestEncryption(ctx, requestFixture())
_, _, err := svc.DiscardEncryption(ctx, chat.ID, int64(9999), false)
_, _, err := svc.DiscardEncryption(ctx, chat.ID, int64(9999), int64(9999), false)
if !errors.Is(err, domain.ErrSecretChatNotFound) {
t.Fatalf("err = %v, want ErrSecretChatNotFound", err)
}
@ -245,20 +374,20 @@ func TestSendEncryptedQtsAllocation(t *testing.T) {
chat := acceptedChat(t, svc)
// admin 发 → 投给 participant 设备partAuthKeyqts 从 1 起。
_, m1, err := svc.SendEncrypted(ctx, chat.ID, adminUser, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: 111, Bytes: []byte{1, 2, 3}, Date: 2000})
_, 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, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: 222, Bytes: []byte{4}, Date: 2001})
_, 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, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: 111, Bytes: []byte{1, 2, 3}, Date: 9999})
_, 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)
}
@ -267,7 +396,7 @@ func TestSendEncryptedQtsAllocation(t *testing.T) {
}
// participant 发 → 投给 admin 设备adminAuthKey独立 qts 序列从 1 起。
_, pm, err := svc.SendEncrypted(ctx, chat.ID, partUser, chat.ParticipantAccessHash, domain.SecretMessageDelivery{RandomID: 333, Bytes: []byte{9}, Date: 2002})
_, 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)
}
@ -280,17 +409,55 @@ func TestSendEncryptedWrongAccessHash(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
chat := acceptedChat(t, svc)
_, _, err := svc.SendEncrypted(ctx, chat.ID, adminUser, chat.AdminAccessHash+1, domain.SecretMessageDelivery{RandomID: 1, Bytes: []byte{1}, Date: 2000})
_, _, 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, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: 1, Bytes: []byte{1}, Date: 2000})
_, _, 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)
}
@ -301,7 +468,7 @@ func TestListNewMessagesAndAck(t *testing.T) {
ctx := context.Background()
chat := acceptedChat(t, svc)
for i := 0; i < 3; i++ {
if _, _, err := svc.SendEncrypted(ctx, chat.ID, adminUser, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: int64(1000 + i), Bytes: []byte{byte(i)}, Date: 2000 + i}); err != nil {
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)
}
}
@ -336,7 +503,7 @@ func TestAcceptAfterDiscard(t *testing.T) {
svc, _ := newTestService()
ctx := context.Background()
chat, _ := svc.RequestEncryption(ctx, requestFixture())
if _, _, err := svc.DiscardEncryption(ctx, chat.ID, adminUser, false); err != nil {
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)