chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,479 @@
package phone
import (
"context"
"crypto/sha256"
"errors"
"sync"
"testing"
"time"
"github.com/gotd/td/clock"
"telesrv/internal/domain"
)
type testClock struct {
mu sync.Mutex
now time.Time
}
func newTestClock() *testClock {
return &testClock{now: time.Unix(1_700_000_000, 0)}
}
func (c *testClock) Now() time.Time {
c.mu.Lock()
defer c.mu.Unlock()
return c.now
}
func (c *testClock) Advance(d time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.now = c.now.Add(d)
}
func (c *testClock) Timer(d time.Duration) clock.Timer { return clock.System.Timer(d) }
func (c *testClock) Ticker(d time.Duration) clock.Ticker { return clock.System.Ticker(d) }
func testProtocol(versions ...string) domain.PhoneCallProtocol {
if len(versions) == 0 {
versions = []string{"11.0.0", "10.0.0"}
}
return domain.PhoneCallProtocol{
UDPP2P: true,
UDPReflector: true,
MinLayer: 65,
MaxLayer: 92,
LibraryVersions: versions,
}
}
func testGA() ([]byte, []byte) {
ga := make([]byte, 256)
for i := range ga {
ga[i] = byte(i + 1)
}
hash := sha256.Sum256(ga)
return ga, hash[:]
}
func testGB() []byte {
gb := make([]byte, 256)
for i := range gb {
gb[i] = byte(255 - i%200)
}
return gb
}
func newTestService(clk clock.Clock, mutate ...func(*Config)) *Service {
cfg := Config{
RingTimeout: 90 * time.Second,
TombstoneTTL: 60 * time.Second,
MaxActivePerUser: 4,
SignalingRatePerSecond: 50,
}
for _, fn := range mutate {
fn(&cfg)
}
return NewService(cfg, WithClock(clk))
}
func mustRequest(t *testing.T, s *Service, caller, callee int64, gaHash []byte) domain.PhoneCall {
t.Helper()
call, err := s.RequestCall(context.Background(), caller, domain.PhoneCallRequest{
CalleeID: callee,
RandomID: caller*1000 + callee,
GAHash: gaHash,
Protocol: testProtocol(),
PrivacyP2P: true, // rpc 层算定的 phone_p2p 双向放行P3 起参与 p2p_allowed AND
})
if err != nil {
t.Fatalf("RequestCall: %v", err)
}
return call
}
func TestPhoneCallHappyPath(t *testing.T) {
clk := newTestClock()
s := newTestService(clk)
ctx := context.Background()
ga, gaHash := testGA()
gb := testGB()
call := mustRequest(t, s, 1, 2, gaHash)
if call.State != domain.PhoneCallStateRequested || call.AdminID != 1 || call.ParticipantID != 2 {
t.Fatalf("requested call = %+v", call)
}
clk.Advance(2 * time.Second)
ringing, transitioned, err := s.ReceivedCall(ctx, 2, call.ID, call.AccessHash)
if err != nil || !transitioned || ringing.State != domain.PhoneCallStateRinging || ringing.ReceiveDate == 0 {
t.Fatalf("ReceivedCall = %+v transitioned=%v err=%v", ringing, transitioned, err)
}
if _, again, err := s.ReceivedCall(ctx, 2, call.ID, call.AccessHash); err != nil || again {
t.Fatalf("second ReceivedCall transitioned=%v err=%v, want idempotent", again, err)
}
accepted, err := s.AcceptCall(ctx, 2, call.ID, call.AccessHash, gb, testProtocol(), domain.SessionRef{SessionID: 22})
if err != nil || accepted.State != domain.PhoneCallStateAccepted {
t.Fatalf("AcceptCall = %+v err=%v", accepted, err)
}
if string(accepted.GB) != string(gb) {
t.Fatalf("accepted.GB mismatch")
}
confirmed, forced, err := s.ConfirmCall(ctx, 1, call.ID, call.AccessHash, ga, 0x1234, testProtocol())
if err != nil || forced || confirmed.State != domain.PhoneCallStateConfirmed {
t.Fatalf("ConfirmCall = %+v forced=%v err=%v", confirmed, forced, err)
}
if !confirmed.P2PAllowed || confirmed.KeyFingerprint != 0x1234 || confirmed.StartDate == 0 {
t.Fatalf("confirmed snapshot = %+v", confirmed)
}
clk.Advance(30 * time.Second)
discarded, already, err := s.DiscardCall(ctx, 2, call.ID, call.AccessHash, domain.PhoneCallDiscardReasonHangup, 30)
if err != nil || already || discarded.State != domain.PhoneCallStateDiscarded || discarded.Duration != 30 {
t.Fatalf("DiscardCall = %+v already=%v err=%v", discarded, already, err)
}
// 双方同时挂断后到者幂等拿快照reason 由先到者决定。
again, already, err := s.DiscardCall(ctx, 1, call.ID, call.AccessHash, domain.PhoneCallDiscardReasonBusy, 0)
if err != nil || !already || again.DiscardReason != domain.PhoneCallDiscardReasonHangup {
t.Fatalf("second DiscardCall = %+v already=%v err=%v", again, already, err)
}
}
func TestPhoneCallStateErrors(t *testing.T) {
clk := newTestClock()
s := newTestService(clk)
ctx := context.Background()
ga, gaHash := testGA()
gb := testGB()
call := mustRequest(t, s, 1, 2, gaHash)
// confirm 前置必须 Accepted。
if _, _, err := s.ConfirmCall(ctx, 1, call.ID, call.AccessHash, ga, 1, testProtocol()); !errors.Is(err, ErrPeerInvalid) {
t.Fatalf("confirm before accept err = %v, want ErrPeerInvalid", err)
}
// 非被叫不能 accept / receivedCall。
if _, err := s.AcceptCall(ctx, 1, call.ID, call.AccessHash, gb, testProtocol(), domain.SessionRef{}); !errors.Is(err, ErrPeerInvalid) {
t.Fatalf("accept by caller err = %v, want ErrPeerInvalid", err)
}
if _, _, err := s.ReceivedCall(ctx, 1, call.ID, call.AccessHash); !errors.Is(err, ErrPeerInvalid) {
t.Fatalf("receivedCall by caller err = %v, want ErrPeerInvalid", err)
}
// access_hash 不符。
if _, err := s.AcceptCall(ctx, 2, call.ID, call.AccessHash+1, gb, testProtocol(), domain.SessionRef{}); !errors.Is(err, ErrPeerInvalid) {
t.Fatalf("wrong access hash err = %v, want ErrPeerInvalid", err)
}
if _, err := s.AcceptCall(ctx, 2, call.ID, call.AccessHash, gb, testProtocol(), domain.SessionRef{}); err != nil {
t.Fatalf("accept: %v", err)
}
if _, err := s.AcceptCall(ctx, 2, call.ID, call.AccessHash, gb, testProtocol(), domain.SessionRef{}); !errors.Is(err, ErrAlreadyAccepted) {
t.Fatalf("double accept err = %v, want ErrAlreadyAccepted", err)
}
if _, _, err := s.DiscardCall(ctx, 1, call.ID, call.AccessHash, domain.PhoneCallDiscardReasonHangup, 0); err != nil {
t.Fatalf("discard: %v", err)
}
if _, err := s.AcceptCall(ctx, 2, call.ID, call.AccessHash, gb, testProtocol(), domain.SessionRef{}); !errors.Is(err, ErrAlreadyDeclined) {
t.Fatalf("accept after discard err = %v, want ErrAlreadyDeclined", err)
}
}
func TestPhoneCallGAHashMismatchForcesDiscard(t *testing.T) {
clk := newTestClock()
s := newTestService(clk)
ctx := context.Background()
_, gaHash := testGA()
call := mustRequest(t, s, 1, 2, gaHash)
if _, err := s.AcceptCall(ctx, 2, call.ID, call.AccessHash, testGB(), testProtocol(), domain.SessionRef{}); err != nil {
t.Fatalf("accept: %v", err)
}
wrong := make([]byte, 256)
wrong[0] = 0x7
snap, forced, err := s.ConfirmCall(ctx, 1, call.ID, call.AccessHash, wrong, 1, testProtocol())
if !errors.Is(err, ErrGAHashMismatch) || !forced {
t.Fatalf("confirm with wrong ga: forced=%v err=%v", forced, err)
}
if snap.State != domain.PhoneCallStateDiscarded || snap.DiscardReason != domain.PhoneCallDiscardReasonDisconnect {
t.Fatalf("forced discard snapshot = %+v", snap)
}
}
func TestPhoneCallConcurrentAcceptSingleWinner(t *testing.T) {
clk := newTestClock()
s := newTestService(clk)
ctx := context.Background()
_, gaHash := testGA()
call := mustRequest(t, s, 1, 2, gaHash)
const devices = 8
var wg sync.WaitGroup
wins := make(chan int64, devices)
losses := make(chan error, devices)
for i := 0; i < devices; i++ {
wg.Add(1)
go func(sessionID int64) {
defer wg.Done()
_, err := s.AcceptCall(ctx, 2, call.ID, call.AccessHash, testGB(), testProtocol(), domain.SessionRef{SessionID: sessionID})
if err == nil {
wins <- sessionID
} else {
losses <- err
}
}(int64(100 + i))
}
wg.Wait()
close(wins)
close(losses)
if len(wins) != 1 {
t.Fatalf("winners = %d, want exactly 1", len(wins))
}
for err := range losses {
if !errors.Is(err, ErrAlreadyAccepted) {
t.Fatalf("loser err = %v, want ErrAlreadyAccepted", err)
}
}
winner := <-wins
snap, ok := s.Lookup(ctx, call.ID, call.AccessHash)
if !ok || snap.CalleeDevice.SessionID != winner {
t.Fatalf("callee device = %+v, want session %d", snap.CalleeDevice, winner)
}
}
func TestPhoneCallRandomIDIdempotent(t *testing.T) {
clk := newTestClock()
s := newTestService(clk)
ctx := context.Background()
_, gaHash := testGA()
req := domain.PhoneCallRequest{CalleeID: 2, RandomID: 777, GAHash: gaHash, Protocol: testProtocol()}
first, err := s.RequestCall(ctx, 1, req)
if err != nil {
t.Fatalf("request: %v", err)
}
second, err := s.RequestCall(ctx, 1, req)
if err != nil || second.ID != first.ID {
t.Fatalf("retry id = %d err=%v, want %d", second.ID, err, first.ID)
}
// 终结后同 random_id 重新可用(新通话)。
if _, _, err := s.DiscardCall(ctx, 1, first.ID, first.AccessHash, domain.PhoneCallDiscardReasonHangup, 0); err != nil {
t.Fatalf("discard: %v", err)
}
third, err := s.RequestCall(ctx, 1, req)
if err != nil || third.ID == first.ID {
t.Fatalf("post-discard request id = %d err=%v, want fresh call", third.ID, err)
}
}
func TestPhoneCallQuotaAndSweep(t *testing.T) {
clk := newTestClock()
s := newTestService(clk, func(c *Config) { c.MaxActivePerUser = 2 })
ctx := context.Background()
_, gaHash := testGA()
for i := int64(0); i < 2; i++ {
if _, err := s.RequestCall(ctx, 1, domain.PhoneCallRequest{CalleeID: 10 + i, RandomID: i, GAHash: gaHash, Protocol: testProtocol()}); err != nil {
t.Fatalf("request %d: %v", i, err)
}
}
if _, err := s.RequestCall(ctx, 1, domain.PhoneCallRequest{CalleeID: 99, RandomID: 99, GAHash: gaHash, Protocol: testProtocol()}); !errors.Is(err, ErrOccupyFailed) {
t.Fatalf("over quota err = %v, want ErrOccupyFailed", err)
}
// 双端崩溃兜底:超过 2×RingTimeout 的僵尸通话被纯年龄 GC 回收,配额释放。
clk.Advance(181 * time.Second)
if _, err := s.RequestCall(ctx, 1, domain.PhoneCallRequest{CalleeID: 99, RandomID: 99, GAHash: gaHash, Protocol: testProtocol()}); err != nil {
t.Fatalf("request after sweep: %v", err)
}
}
func TestPhoneCallTombstoneGC(t *testing.T) {
clk := newTestClock()
s := newTestService(clk)
ctx := context.Background()
_, gaHash := testGA()
call := mustRequest(t, s, 1, 2, gaHash)
if _, _, err := s.DiscardCall(ctx, 1, call.ID, call.AccessHash, domain.PhoneCallDiscardReasonHangup, 0); err != nil {
t.Fatalf("discard: %v", err)
}
if _, ok := s.Lookup(ctx, call.ID, call.AccessHash); !ok {
t.Fatalf("tombstone should be visible before TTL")
}
clk.Advance(61 * time.Second)
mustRequest(t, s, 3, 4, gaHash) // 触发 sweep
if _, ok := s.Lookup(ctx, call.ID, call.AccessHash); ok {
t.Fatalf("tombstone should be collected after TTL")
}
}
func TestPhoneCallDurationOnlyWhenConfirmed(t *testing.T) {
clk := newTestClock()
s := newTestService(clk)
ctx := context.Background()
_, gaHash := testGA()
call := mustRequest(t, s, 1, 2, gaHash)
snap, _, err := s.DiscardCall(ctx, 2, call.ID, call.AccessHash, domain.PhoneCallDiscardReasonBusy, 55)
if err != nil || snap.Duration != 0 {
t.Fatalf("unconfirmed discard duration = %d err=%v, want 0", snap.Duration, err)
}
}
func TestPhoneCallSignal(t *testing.T) {
clk := newTestClock()
s := newTestService(clk, func(c *Config) { c.SignalingRatePerSecond = 2 })
ctx := context.Background()
_, gaHash := testGA()
call := mustRequest(t, s, 1, 2, gaHash)
// Accepted 前不可转发。
if _, err := s.Signal(ctx, 1, call.ID, call.AccessHash, func(int64, domain.SessionRef) {}); !errors.Is(err, ErrPeerInvalid) {
t.Fatalf("signal before accept err = %v, want ErrPeerInvalid", err)
}
if _, err := s.AcceptCall(ctx, 2, call.ID, call.AccessHash, testGB(), testProtocol(), domain.SessionRef{}); err != nil {
t.Fatalf("accept: %v", err)
}
var forwarded []int64
forward := func(peer int64, _ domain.SessionRef) { forwarded = append(forwarded, peer) }
for i := 0; i < 3; i++ {
if _, err := s.Signal(ctx, 1, call.ID, call.AccessHash, forward); err != nil {
t.Fatalf("signal %d: %v", i, err)
}
}
// 限速 2/s第三条被静默丢弃。
if len(forwarded) != 2 || forwarded[0] != 2 || forwarded[1] != 2 {
t.Fatalf("forwarded = %v, want [2 2]", forwarded)
}
clk.Advance(time.Second)
if drop, err := s.Signal(ctx, 2, call.ID, call.AccessHash, forward); err != nil || drop {
t.Fatalf("signal new window drop=%v err=%v", drop, err)
}
if forwarded[len(forwarded)-1] != 1 {
t.Fatalf("callee→caller forward peer = %d, want 1", forwarded[len(forwarded)-1])
}
// 终态尾包静默吞掉。
if _, _, err := s.DiscardCall(ctx, 1, call.ID, call.AccessHash, domain.PhoneCallDiscardReasonHangup, 0); err != nil {
t.Fatalf("discard: %v", err)
}
if drop, err := s.Signal(ctx, 1, call.ID, call.AccessHash, forward); err != nil || !drop {
t.Fatalf("signal after discard drop=%v err=%v, want drop", drop, err)
}
}
func TestNegotiateProtocol(t *testing.T) {
base := func(min, max int, versions ...string) domain.PhoneCallProtocol {
return domain.PhoneCallProtocol{UDPP2P: true, UDPReflector: true, MinLayer: min, MaxLayer: max, LibraryVersions: versions}
}
t.Run("layer intersection", func(t *testing.T) {
out, err := negotiateProtocol(base(65, 92, "9.0.0"), base(70, 110, "9.0.0"))
if err != nil || out.MinLayer != 70 || out.MaxLayer != 92 {
t.Fatalf("negotiated = %+v err=%v", out, err)
}
})
t.Run("layer disjoint", func(t *testing.T) {
if _, err := negotiateProtocol(base(65, 70, "9.0.0"), base(80, 92, "9.0.0")); !errors.Is(err, ErrProtocolCompatLayerInvalid) {
t.Fatalf("err = %v, want ErrProtocolCompatLayerInvalid", err)
}
})
t.Run("best common version is semver max", func(t *testing.T) {
out, err := negotiateProtocol(base(65, 92, "11.0.0", "9.0.0", "2.4.4"), base(65, 92, "2.4.4", "9.0.0"))
if err != nil || len(out.LibraryVersions) != 1 || out.LibraryVersions[0] != "9.0.0" {
t.Fatalf("versions = %v err=%v, want [9.0.0]", out.LibraryVersions, err)
}
})
t.Run("preferred version beats semver max", func(t *testing.T) {
// ⚠ "9.0.0" 优先于更高版本DrKLO 视频 gate 是字符串字典序比较
//"1x.0.0" < "2.7.7" 会判不支持视频),且 12/13 走 V3 SCTP 信令。
out, err := negotiateProtocol(base(65, 92, "13.0.0", "10.0.0", "9.0.0"), base(65, 92, "9.0.0", "10.0.0", "13.0.0"))
if err != nil || out.LibraryVersions[0] != "9.0.0" {
t.Fatalf("versions = %v err=%v, want preferred [9.0.0]", out.LibraryVersions, err)
}
})
t.Run("numeric compare fallback not lexicographic", func(t *testing.T) {
// 交集无 preferred 版本时退化为语义化最高(数值比较,非字典序)。
out, err := negotiateProtocol(base(65, 92, "10.0.0", "11.0.0"), base(65, 92, "11.0.0", "10.0.0"))
if err != nil || out.LibraryVersions[0] != "11.0.0" {
t.Fatalf("versions = %v err=%v, want [11.0.0]", out.LibraryVersions, err)
}
})
t.Run("no common versions passes callee list through", func(t *testing.T) {
// ⚠ P1-3版本无交集绝不拒绝通话透传被叫列表。
out, err := negotiateProtocol(base(65, 92, "11.0.0"), base(65, 92, "2.4.4", "3.0.0"))
if err != nil || len(out.LibraryVersions) != 2 || out.LibraryVersions[0] != "2.4.4" {
t.Fatalf("versions = %v err=%v, want callee passthrough", out.LibraryVersions, err)
}
})
}
func TestValidateProtocol(t *testing.T) {
cases := []struct {
name string
p domain.PhoneCallProtocol
want error
}{
{"min over max", domain.PhoneCallProtocol{UDPP2P: true, MinLayer: 93, MaxLayer: 92, LibraryVersions: []string{"9.0.0"}}, ErrProtocolLayerInvalid},
{"max below 65", domain.PhoneCallProtocol{UDPP2P: true, MinLayer: 60, MaxLayer: 64, LibraryVersions: []string{"9.0.0"}}, ErrProtocolCompatLayerInvalid},
{"no transport flags", domain.PhoneCallProtocol{MinLayer: 65, MaxLayer: 92, LibraryVersions: []string{"9.0.0"}}, ErrProtocolFlagsInvalid},
{"no versions", domain.PhoneCallProtocol{UDPP2P: true, MinLayer: 65, MaxLayer: 92}, ErrProtocolFlagsInvalid},
{"ok", testProtocol(), nil},
}
for _, tc := range cases {
if err := validateProtocol(tc.p); !errors.Is(err, tc.want) {
t.Fatalf("%s: err = %v, want %v", tc.name, err, tc.want)
}
}
}
func TestPhoneCallExpireDue(t *testing.T) {
clk := newTestClock()
s := newTestService(clk)
ctx := context.Background()
ga, gaHash := testGA()
// 三通通话振铃中→missed、Accepted 悬挂→disconnect、Confirmed不回收
ringingCall := mustRequest(t, s, 1, 2, gaHash)
acceptedCall, err := s.RequestCall(ctx, 3, domain.PhoneCallRequest{CalleeID: 4, RandomID: 1, GAHash: gaHash, Protocol: testProtocol()})
if err != nil {
t.Fatalf("request accepted call: %v", err)
}
if _, err := s.AcceptCall(ctx, 4, acceptedCall.ID, acceptedCall.AccessHash, testGB(), testProtocol(), domain.SessionRef{}); err != nil {
t.Fatalf("accept: %v", err)
}
confirmedCall, err := s.RequestCall(ctx, 5, domain.PhoneCallRequest{CalleeID: 6, RandomID: 2, GAHash: gaHash, Protocol: testProtocol()})
if err != nil {
t.Fatalf("request confirmed call: %v", err)
}
if _, err := s.AcceptCall(ctx, 6, confirmedCall.ID, confirmedCall.AccessHash, testGB(), testProtocol(), domain.SessionRef{}); err != nil {
t.Fatalf("accept confirmed: %v", err)
}
if _, _, err := s.ConfirmCall(ctx, 5, confirmedCall.ID, confirmedCall.AccessHash, ga, 1, testProtocol()); err != nil {
t.Fatalf("confirm: %v", err)
}
if got := s.ExpireDue(ctx, clk.Now()); len(got) != 0 {
t.Fatalf("nothing should expire yet, got %d", len(got))
}
clk.Advance(91 * time.Second)
expired := s.ExpireDue(ctx, clk.Now())
if len(expired) != 2 {
t.Fatalf("expired = %d, want 2 (ringing+accepted)", len(expired))
}
reasons := map[int64]domain.PhoneCallDiscardReason{}
for _, c := range expired {
reasons[c.ID] = c.DiscardReason
}
if reasons[ringingCall.ID] != domain.PhoneCallDiscardReasonMissed {
t.Fatalf("ringing call reason = %s, want missed", reasons[ringingCall.ID])
}
if reasons[acceptedCall.ID] != domain.PhoneCallDiscardReasonDisconnect {
t.Fatalf("accepted call reason = %s, want disconnect", reasons[acceptedCall.ID])
}
// Confirmed 通话不受服务端时长限制。
if snap, ok := s.Lookup(ctx, confirmedCall.ID, confirmedCall.AccessHash); !ok || snap.State != domain.PhoneCallStateConfirmed {
t.Fatalf("confirmed call = %+v ok=%v, want untouched", snap, ok)
}
// 幂等:再跑一轮无新增。
if got := s.ExpireDue(ctx, clk.Now()); len(got) != 0 {
t.Fatalf("second ExpireDue = %d, want 0", len(got))
}
}