890 lines
36 KiB
Go
890 lines
36 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gotd/td/clock"
|
|
"github.com/gotd/td/tg"
|
|
"github.com/gotd/td/tgerr"
|
|
"go.uber.org/zap/zaptest"
|
|
|
|
appgroupcalls "telesrv/internal/app/groupcalls"
|
|
appmessages "telesrv/internal/app/messages"
|
|
appphone "telesrv/internal/app/phone"
|
|
appusers "telesrv/internal/app/users"
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/store/memory"
|
|
)
|
|
|
|
type conferenceFixture struct {
|
|
ctx context.Context
|
|
router *Router
|
|
group *appgroupcalls.Service
|
|
messages *appmessages.Service
|
|
sessions *groupCallSessions
|
|
alice domain.User
|
|
bob domain.User
|
|
carol domain.User
|
|
clock *phoneTestClock
|
|
}
|
|
|
|
func newConferenceFixture(t *testing.T) *conferenceFixture {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
users := memory.NewUserStore()
|
|
dialogs := memory.NewDialogStore()
|
|
messageStore := memory.NewMessageStore(dialogs)
|
|
groupStore := memory.NewGroupCallStore()
|
|
sessions := &groupCallSessions{}
|
|
clk := &phoneTestClock{now: time.Unix(1_700_000_000, 0)}
|
|
groupSvc := appgroupcalls.NewService(groupStore)
|
|
messageSvc := appmessages.NewService(messageStore, dialogs)
|
|
router := New(Config{GroupCallMaxParticipants: 8}, Deps{
|
|
Users: appusers.NewService(users),
|
|
Messages: messageSvc,
|
|
GroupCalls: groupSvc,
|
|
Phone: appphone.NewService(appphone.Config{}, appphone.WithClock(clk)),
|
|
Sessions: sessions,
|
|
}, zaptest.NewLogger(t), clk)
|
|
mk := func(hash int64, phone, name string) domain.User {
|
|
u, err := users.Create(ctx, domain.User{AccessHash: hash, Phone: phone, FirstName: name})
|
|
if err != nil {
|
|
t.Fatalf("create user %s: %v", name, err)
|
|
}
|
|
return u
|
|
}
|
|
f := &conferenceFixture{ctx: ctx, router: router, group: groupSvc, messages: messageSvc, sessions: sessions, clock: clk}
|
|
f.alice = mk(3001, "13700000001", "Alice")
|
|
f.bob = mk(3002, "13700000002", "Bob")
|
|
f.carol = mk(3003, "13700000003", "Carol")
|
|
f.sessions.online = []int64{f.alice.ID, f.bob.ID, f.carol.ID}
|
|
return f
|
|
}
|
|
|
|
func (f *conferenceFixture) userCtx(u domain.User, session int64) context.Context {
|
|
return WithSessionID(WithUserID(f.ctx, u.ID), session)
|
|
}
|
|
|
|
func joinConferenceForTest(t *testing.T, f *conferenceFixture, ctx context.Context, call tg.InputGroupCallClass, blockSuffix string, ssrc int32) {
|
|
t.Helper()
|
|
block := conferenceTestBlock(conferenceChainBlockConstructor, blockSuffix)
|
|
req := &tg.PhoneJoinGroupCallRequest{
|
|
Call: call,
|
|
JoinAs: &tg.InputPeerSelf{},
|
|
Params: groupCallJoinParams(t, ssrc),
|
|
}
|
|
req.SetBlock(block)
|
|
if _, err := f.router.onPhoneJoinGroupCall(ctx, req); err != nil {
|
|
t.Fatalf("join conference %s: %v", blockSuffix, err)
|
|
}
|
|
}
|
|
|
|
func optionalUpdate[T tg.UpdateClass](updates tg.UpdatesClass) (T, bool) {
|
|
box, ok := updates.(*tg.Updates)
|
|
if !ok {
|
|
var zero T
|
|
return zero, false
|
|
}
|
|
for _, u := range box.Updates {
|
|
if v, ok := u.(T); ok {
|
|
return v, true
|
|
}
|
|
}
|
|
var zero T
|
|
return zero, false
|
|
}
|
|
|
|
func pushedDiscardedGroupCallUsers(records []phonePushRecord, callID int64) map[int64]bool {
|
|
seen := map[int64]bool{}
|
|
for _, rec := range records {
|
|
box, ok := rec.msg.(*tg.Updates)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, raw := range box.Updates {
|
|
update, ok := raw.(*tg.UpdateGroupCall)
|
|
if !ok {
|
|
continue
|
|
}
|
|
discarded, ok := update.Call.(*tg.GroupCallDiscarded)
|
|
if ok && discarded.ID == callID {
|
|
seen[rec.userID] = true
|
|
}
|
|
}
|
|
}
|
|
return seen
|
|
}
|
|
|
|
func TestConferenceCreateLinkAndGetBySlug(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
ctx := f.userCtx(f.alice, 11)
|
|
res, err := f.router.onPhoneCreateConferenceCall(ctx, &tg.PhoneCreateConferenceCallRequest{RandomID: 42})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
update := findUpdate[*tg.UpdateGroupCall](t, res)
|
|
call, ok := update.Call.(*tg.GroupCall)
|
|
if !ok || !call.Conference || call.InviteLink == "" || !strings.Contains(call.InviteLink, "slug=") || !strings.HasPrefix(call.InviteLink, "https://telesrv.net/call/") {
|
|
t.Fatalf("created call = %#v", update.Call)
|
|
}
|
|
slug := conferenceSlugFromLink(t, call.InviteLink)
|
|
got, err := f.router.onPhoneGetGroupCall(ctx, &tg.PhoneGetGroupCallRequest{
|
|
Call: &tg.InputGroupCallSlug{Slug: slug},
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("get by slug: %v", err)
|
|
}
|
|
if got.Call.(*tg.GroupCall).ID != call.ID {
|
|
t.Fatalf("get by slug id = %d, want %d", got.Call.(*tg.GroupCall).ID, call.ID)
|
|
}
|
|
again, err := f.router.onPhoneCreateConferenceCall(ctx, &tg.PhoneCreateConferenceCallRequest{RandomID: 42})
|
|
if err != nil {
|
|
t.Fatalf("repeat create conference: %v", err)
|
|
}
|
|
if findUpdate[*tg.UpdateGroupCall](t, again).Call.(*tg.GroupCall).ID != call.ID {
|
|
t.Fatalf("random_id must be idempotent")
|
|
}
|
|
}
|
|
|
|
func TestConferenceExportGroupCallInviteReturnsConferenceLink(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
ctx := f.userCtx(f.alice, 11)
|
|
create, err := f.router.onPhoneCreateConferenceCall(ctx, &tg.PhoneCreateConferenceCallRequest{RandomID: 43})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
call := findUpdate[*tg.UpdateGroupCall](t, create).Call.(*tg.GroupCall)
|
|
exported, err := f.router.onPhoneExportGroupCallInvite(ctx, &tg.PhoneExportGroupCallInviteRequest{
|
|
Call: &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("export invite: %v", err)
|
|
}
|
|
if exported.Link != call.InviteLink || !strings.Contains(exported.Link, "slug=") {
|
|
t.Fatalf("exported link = %q, want conference invite link %q", exported.Link, call.InviteLink)
|
|
}
|
|
}
|
|
|
|
func TestConferenceExportGroupCallInviteReturnsPathSlug(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
ctx := WithClientInfo(f.userCtx(f.alice, 11), ClientInfo{Type: ClientTypeAndroid, AppVersion: "12.8.1"})
|
|
create, err := f.router.onPhoneCreateConferenceCall(ctx, &tg.PhoneCreateConferenceCallRequest{RandomID: 44})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
call := findUpdate[*tg.UpdateGroupCall](t, create).Call.(*tg.GroupCall)
|
|
slug := conferenceSlugFromLink(t, call.InviteLink)
|
|
exported, err := f.router.onPhoneExportGroupCallInvite(ctx, &tg.PhoneExportGroupCallInviteRequest{
|
|
Call: &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("export invite: %v", err)
|
|
}
|
|
if exported.Link != call.InviteLink {
|
|
t.Fatalf("exported link = %q, want stored canonical invite link %q", exported.Link, call.InviteLink)
|
|
}
|
|
if got := lastPathSegmentFromLink(t, exported.Link); got != slug {
|
|
t.Fatalf("export path slug = %q, want %q (link %q)", got, slug, exported.Link)
|
|
}
|
|
if got := conferenceSlugFromLink(t, exported.Link); got != slug {
|
|
t.Fatalf("export query slug = %q, want %q (link %q)", got, slug, exported.Link)
|
|
}
|
|
}
|
|
|
|
func TestConferenceLinksNormalizeLegacyTMeLink(t *testing.T) {
|
|
const slug = "legacy_slug-1"
|
|
legacy := domain.GroupCall{
|
|
ID: 1,
|
|
AccessHash: 2,
|
|
Kind: domain.GroupCallKindConference,
|
|
State: domain.GroupCallStateActive,
|
|
Version: 1,
|
|
InviteSlug: slug,
|
|
InviteLink: "https://t.me/call?slug=" + slug,
|
|
}
|
|
want := "https://telesrv.net/call/" + slug + "?slug=" + slug
|
|
if got := conferenceExportInviteLink(legacy); got != want {
|
|
t.Fatalf("export link = %q, want %q", got, want)
|
|
}
|
|
call := tgGroupCall(legacy, 0, false).(*tg.GroupCall)
|
|
if call.InviteLink != want {
|
|
t.Fatalf("tg group call invite link = %q, want %q", call.InviteLink, want)
|
|
}
|
|
}
|
|
|
|
func TestConferenceJoinBroadcastAndGetChainBlocks(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
ctx := f.userCtx(f.alice, 11)
|
|
create, err := f.router.onPhoneCreateConferenceCall(ctx, &tg.PhoneCreateConferenceCallRequest{RandomID: 77})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
call := findUpdate[*tg.UpdateGroupCall](t, create).Call.(*tg.GroupCall)
|
|
input := &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash}
|
|
join, err := f.router.onPhoneJoinGroupCall(ctx, &tg.PhoneJoinGroupCallRequest{
|
|
Call: input,
|
|
JoinAs: &tg.InputPeerSelf{},
|
|
Params: groupCallJoinParams(t, 9001),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("join conference: %v", err)
|
|
}
|
|
findUpdate[*tg.UpdateGroupCallConnection](t, join)
|
|
participants := findUpdate[*tg.UpdateGroupCallParticipants](t, join)
|
|
if len(participants.Participants) != 1 || !participants.Participants[0].Self {
|
|
t.Fatalf("participants update = %+v", participants.Participants)
|
|
}
|
|
block := conferenceTestBlock(conferenceBroadcastCommitConstructor, "opaque-chain-block")
|
|
broadcast, err := f.router.onPhoneSendConferenceCallBroadcast(ctx, &tg.PhoneSendConferenceCallBroadcastRequest{
|
|
Call: input,
|
|
Block: block,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("broadcast: %v", err)
|
|
}
|
|
chain := findUpdate[*tg.UpdateGroupCallChainBlocks](t, broadcast)
|
|
if chain.SubChainID != 1 || chain.NextOffset != 1 || len(chain.Blocks) != 1 || conferenceTestConstructor(chain.Blocks[0]) != conferenceBroadcastCommitServerConstructor {
|
|
t.Fatalf("chain update = %+v", chain)
|
|
}
|
|
list, err := f.router.onPhoneGetGroupCallChainBlocks(ctx, &tg.PhoneGetGroupCallChainBlocksRequest{
|
|
Call: input, SubChainID: 1, Offset: 0, Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("get chain blocks: %v", err)
|
|
}
|
|
got := findUpdate[*tg.UpdateGroupCallChainBlocks](t, list)
|
|
if got.SubChainID != 1 || len(got.Blocks) != 1 || conferenceTestConstructor(got.Blocks[0]) != conferenceBroadcastCommitServerConstructor {
|
|
t.Fatalf("get chain blocks = %+v", got)
|
|
}
|
|
nextBlock := conferenceTestBlock(conferenceBroadcastRevealConstructor, "opaque-chain-block-2")
|
|
nextBroadcast, err := f.router.onPhoneSendConferenceCallBroadcast(ctx, &tg.PhoneSendConferenceCallBroadcastRequest{
|
|
Call: input,
|
|
Block: nextBlock,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("second broadcast: %v", err)
|
|
}
|
|
nextChain := findUpdate[*tg.UpdateGroupCallChainBlocks](t, nextBroadcast)
|
|
if nextChain.SubChainID != 1 || nextChain.NextOffset != 2 || len(nextChain.Blocks) != 1 || conferenceTestConstructor(nextChain.Blocks[0]) != conferenceBroadcastRevealServerConstructor {
|
|
t.Fatalf("second chain update = %+v", nextChain)
|
|
}
|
|
latest, err := f.router.onPhoneGetGroupCallChainBlocks(ctx, &tg.PhoneGetGroupCallChainBlocksRequest{
|
|
Call: input, SubChainID: 1, Offset: domain.GroupCallChainBlockLatestOffset, Limit: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("get latest chain block: %v", err)
|
|
}
|
|
gotLatest := findUpdate[*tg.UpdateGroupCallChainBlocks](t, latest)
|
|
if gotLatest.SubChainID != 1 || gotLatest.NextOffset != 2 || len(gotLatest.Blocks) != 1 || conferenceTestConstructor(gotLatest.Blocks[0]) != conferenceBroadcastRevealServerConstructor {
|
|
t.Fatalf("latest chain block = %+v", gotLatest)
|
|
}
|
|
}
|
|
|
|
func TestConferenceBroadcastRequiresActiveParticipant(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
aliceCtx := f.userCtx(f.alice, 11)
|
|
create, err := f.router.onPhoneCreateConferenceCall(aliceCtx, &tg.PhoneCreateConferenceCallRequest{RandomID: 770})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
call := findUpdate[*tg.UpdateGroupCall](t, create).Call.(*tg.GroupCall)
|
|
input := &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash}
|
|
|
|
f.sessions.reset()
|
|
block := conferenceTestBlock(conferenceBroadcastCommitConstructor, "creator-not-joined-broadcast")
|
|
res, err := f.router.onPhoneSendConferenceCallBroadcast(aliceCtx, &tg.PhoneSendConferenceCallBroadcastRequest{
|
|
Call: input,
|
|
Block: block,
|
|
})
|
|
if res != nil || !tgerr.Is(err, "GROUPCALL_JOIN_MISSING") {
|
|
t.Fatalf("broadcast before join = %+v err=%v, want GROUPCALL_JOIN_MISSING", res, err)
|
|
}
|
|
if got := f.sessions.records(); len(got) != 0 {
|
|
t.Fatalf("broadcast before join must not push updates, got %+v", got)
|
|
}
|
|
list, err := f.router.onPhoneGetGroupCallChainBlocks(aliceCtx, &tg.PhoneGetGroupCallChainBlocksRequest{
|
|
Call: input, SubChainID: 1, Offset: 0, Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("get chain blocks: %v", err)
|
|
}
|
|
chain := findUpdate[*tg.UpdateGroupCallChainBlocks](t, list)
|
|
if chain.NextOffset != 0 || len(chain.Blocks) != 0 {
|
|
t.Fatalf("chain after forbidden broadcast = %+v, want empty", chain)
|
|
}
|
|
}
|
|
|
|
func TestConferenceJoinBlockSeedsChainAndDuplicateJoinIsRejected(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
aliceCtx := f.userCtx(f.alice, 11)
|
|
bobCtx := f.userCtx(f.bob, 22)
|
|
create, err := f.router.onPhoneCreateConferenceCall(aliceCtx, &tg.PhoneCreateConferenceCallRequest{RandomID: 78})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
call := findUpdate[*tg.UpdateGroupCall](t, create).Call.(*tg.GroupCall)
|
|
input := &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash}
|
|
joinBlock := conferenceTestBlock(conferenceChainBlockConstructor, "join-chain-block")
|
|
joinReq := &tg.PhoneJoinGroupCallRequest{
|
|
Call: input,
|
|
JoinAs: &tg.InputPeerSelf{},
|
|
Params: groupCallJoinParams(t, 9002),
|
|
}
|
|
joinReq.SetBlock(joinBlock)
|
|
join, err := f.router.onPhoneJoinGroupCall(aliceCtx, joinReq)
|
|
if err != nil {
|
|
t.Fatalf("join conference: %v", err)
|
|
}
|
|
findUpdate[*tg.UpdateGroupCallConnection](t, join)
|
|
joinChain := findUpdate[*tg.UpdateGroupCallChainBlocks](t, join)
|
|
if joinChain.SubChainID != 0 || joinChain.NextOffset != 1 || len(joinChain.Blocks) != 1 || conferenceTestConstructor(joinChain.Blocks[0]) != conferenceChainBlockServerConstructor {
|
|
t.Fatalf("join chain update = %+v", joinChain)
|
|
}
|
|
|
|
dupJoinReq := &tg.PhoneJoinGroupCallRequest{
|
|
Call: &tg.InputGroupCallSlug{Slug: conferenceSlugFromLink(t, call.InviteLink)},
|
|
JoinAs: &tg.InputPeerSelf{},
|
|
Params: groupCallJoinParams(t, 9003),
|
|
}
|
|
dupJoinReq.SetBlock(append([]byte(nil), joinBlock...))
|
|
dupJoin, err := f.router.onPhoneJoinGroupCall(bobCtx, dupJoinReq)
|
|
if dupJoin != nil || !tgerr.Is(err, "CONF_WRITE_CHAIN_INVALID") {
|
|
t.Fatalf("duplicate join = %+v err=%v, want CONF_WRITE_CHAIN_INVALID", dupJoin, err)
|
|
}
|
|
list, err := f.router.onPhoneGetGroupCallChainBlocks(aliceCtx, &tg.PhoneGetGroupCallChainBlocksRequest{
|
|
Call: input, Offset: 0, Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("get chain blocks: %v", err)
|
|
}
|
|
got := findUpdate[*tg.UpdateGroupCallChainBlocks](t, list)
|
|
if got.NextOffset != 1 || len(got.Blocks) != 1 || conferenceTestConstructor(got.Blocks[0]) != conferenceChainBlockServerConstructor {
|
|
t.Fatalf("get chain blocks after duplicate = %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestConferenceJoinReturnsSubmittedBlockOnly(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
aliceCtx := f.userCtx(f.alice, 11)
|
|
bobCtx := f.userCtx(f.bob, 22)
|
|
create, err := f.router.onPhoneCreateConferenceCall(aliceCtx, &tg.PhoneCreateConferenceCallRequest{RandomID: 790})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
call := findUpdate[*tg.UpdateGroupCall](t, create).Call.(*tg.GroupCall)
|
|
input := &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash}
|
|
aliceBlock := conferenceTestBlock(conferenceChainBlockConstructor, "alice-join-chain-block")
|
|
aliceJoin := &tg.PhoneJoinGroupCallRequest{
|
|
Call: input,
|
|
JoinAs: &tg.InputPeerSelf{},
|
|
Params: groupCallJoinParams(t, 9031),
|
|
}
|
|
aliceJoin.SetBlock(aliceBlock)
|
|
if _, err := f.router.onPhoneJoinGroupCall(aliceCtx, aliceJoin); err != nil {
|
|
t.Fatalf("alice join: %v", err)
|
|
}
|
|
|
|
bobBlock := conferenceTestBlock(conferenceChainBlockConstructor, "bob-join-chain-block")
|
|
bobJoin := &tg.PhoneJoinGroupCallRequest{
|
|
Call: &tg.InputGroupCallSlug{Slug: conferenceSlugFromLink(t, call.InviteLink)},
|
|
JoinAs: &tg.InputPeerSelf{},
|
|
Params: groupCallJoinParams(t, 9032),
|
|
}
|
|
bobJoin.SetBlock(bobBlock)
|
|
join, err := f.router.onPhoneJoinGroupCall(bobCtx, bobJoin)
|
|
if err != nil {
|
|
t.Fatalf("bob join: %v", err)
|
|
}
|
|
joinChain := findUpdate[*tg.UpdateGroupCallChainBlocks](t, join)
|
|
if joinChain.SubChainID != 0 || joinChain.NextOffset != 2 || len(joinChain.Blocks) != 1 {
|
|
t.Fatalf("bob join chain update = %+v, want only submitted block at next_offset=2", joinChain)
|
|
}
|
|
if conferenceTestConstructor(joinChain.Blocks[0]) != conferenceChainBlockServerConstructor || string(joinChain.Blocks[0][4:]) != string(bobBlock[4:]) {
|
|
t.Fatalf("bob join block = %x, want server-form submitted block %x", joinChain.Blocks[0], bobBlock)
|
|
}
|
|
list, err := f.router.onPhoneGetGroupCallChainBlocks(aliceCtx, &tg.PhoneGetGroupCallChainBlocksRequest{
|
|
Call: input, Offset: 0, Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("get chain blocks: %v", err)
|
|
}
|
|
got := findUpdate[*tg.UpdateGroupCallChainBlocks](t, list)
|
|
if got.NextOffset != 2 || len(got.Blocks) != 2 {
|
|
t.Fatalf("persisted chain blocks = %+v, want full history through getGroupCallChainBlocks", got)
|
|
}
|
|
}
|
|
|
|
func TestConferenceDuplicateJoinBlockDoesNotLeaveParticipantActive(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
aliceCtx := f.userCtx(f.alice, 11)
|
|
bobCtx := f.userCtx(f.bob, 22)
|
|
create, err := f.router.onPhoneCreateConferenceCall(aliceCtx, &tg.PhoneCreateConferenceCallRequest{RandomID: 79})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
call := findUpdate[*tg.UpdateGroupCall](t, create).Call.(*tg.GroupCall)
|
|
input := &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash}
|
|
joinBlock := conferenceTestBlock(conferenceChainBlockConstructor, "alice-join-chain-block")
|
|
aliceJoin := &tg.PhoneJoinGroupCallRequest{
|
|
Call: input,
|
|
JoinAs: &tg.InputPeerSelf{},
|
|
Params: groupCallJoinParams(t, 9011),
|
|
}
|
|
aliceJoin.SetBlock(joinBlock)
|
|
if _, err := f.router.onPhoneJoinGroupCall(aliceCtx, aliceJoin); err != nil {
|
|
t.Fatalf("alice join: %v", err)
|
|
}
|
|
|
|
bobJoin := &tg.PhoneJoinGroupCallRequest{
|
|
Call: &tg.InputGroupCallSlug{Slug: conferenceSlugFromLink(t, call.InviteLink)},
|
|
JoinAs: &tg.InputPeerSelf{},
|
|
Params: groupCallJoinParams(t, 9022),
|
|
}
|
|
bobJoin.SetBlock(append([]byte(nil), joinBlock...))
|
|
if res, err := f.router.onPhoneJoinGroupCall(bobCtx, bobJoin); res != nil || !tgerr.Is(err, "CONF_WRITE_CHAIN_INVALID") {
|
|
t.Fatalf("bob duplicate join = %+v err=%v, want CONF_WRITE_CHAIN_INVALID", res, err)
|
|
}
|
|
if p, found, err := f.group.Participant(f.ctx, call.ID, f.bob.ID); err != nil || (found && !p.Left) {
|
|
t.Fatalf("bob participant after duplicate join = %+v found=%v err=%v, want absent or left", p, found, err)
|
|
}
|
|
}
|
|
|
|
func TestConferenceDeleteParticipantsReturnsSubmittedBlock(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
aliceCtx := f.userCtx(f.alice, 11)
|
|
bobCtx := f.userCtx(f.bob, 22)
|
|
create, err := f.router.onPhoneCreateConferenceCall(aliceCtx, &tg.PhoneCreateConferenceCallRequest{RandomID: 791})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
call := findUpdate[*tg.UpdateGroupCall](t, create).Call.(*tg.GroupCall)
|
|
input := &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash}
|
|
aliceBlock := conferenceTestBlock(conferenceChainBlockConstructor, "alice-delete-seed-block")
|
|
aliceJoin := &tg.PhoneJoinGroupCallRequest{Call: input, JoinAs: &tg.InputPeerSelf{}, Params: groupCallJoinParams(t, 9041)}
|
|
aliceJoin.SetBlock(aliceBlock)
|
|
if _, err := f.router.onPhoneJoinGroupCall(aliceCtx, aliceJoin); err != nil {
|
|
t.Fatalf("alice join: %v", err)
|
|
}
|
|
bobBlock := conferenceTestBlock(conferenceChainBlockConstructor, "bob-delete-seed-block")
|
|
bobJoin := &tg.PhoneJoinGroupCallRequest{Call: &tg.InputGroupCallSlug{Slug: conferenceSlugFromLink(t, call.InviteLink)}, JoinAs: &tg.InputPeerSelf{}, Params: groupCallJoinParams(t, 9042)}
|
|
bobJoin.SetBlock(bobBlock)
|
|
if _, err := f.router.onPhoneJoinGroupCall(bobCtx, bobJoin); err != nil {
|
|
t.Fatalf("bob join: %v", err)
|
|
}
|
|
|
|
removeBlock := conferenceTestBlock(conferenceChainBlockConstructor, "alice-remove-bob-block")
|
|
res, err := f.router.onPhoneDeleteConferenceCallParticipants(aliceCtx, &tg.PhoneDeleteConferenceCallParticipantsRequest{
|
|
Call: input,
|
|
IDs: []int64{f.bob.ID},
|
|
Kick: true,
|
|
Block: removeBlock,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("delete conference participants: %v", err)
|
|
}
|
|
chain := findUpdate[*tg.UpdateGroupCallChainBlocks](t, res)
|
|
if chain.SubChainID != 0 || chain.NextOffset != 3 || len(chain.Blocks) != 1 || conferenceTestConstructor(chain.Blocks[0]) != conferenceChainBlockServerConstructor {
|
|
t.Fatalf("delete participants chain update = %+v", chain)
|
|
}
|
|
}
|
|
|
|
func TestConferenceOnlyLeftRemoveIsIdempotentAndAllowedForParticipant(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
aliceCtx := f.userCtx(f.alice, 11)
|
|
bobCtx := f.userCtx(f.bob, 22)
|
|
carolCtx := f.userCtx(f.carol, 33)
|
|
create, err := f.router.onPhoneCreateConferenceCall(aliceCtx, &tg.PhoneCreateConferenceCallRequest{RandomID: 792})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
call := findUpdate[*tg.UpdateGroupCall](t, create).Call.(*tg.GroupCall)
|
|
input := &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash}
|
|
joinConferenceForTest(t, f, aliceCtx, input, "alice-only-left-join", 9051)
|
|
joinConferenceForTest(t, f, bobCtx, &tg.InputGroupCallSlug{Slug: conferenceSlugFromLink(t, call.InviteLink)}, "bob-only-left-join", 9052)
|
|
joinConferenceForTest(t, f, carolCtx, &tg.InputGroupCallSlug{Slug: conferenceSlugFromLink(t, call.InviteLink)}, "carol-only-left-join", 9053)
|
|
if _, err := f.router.onPhoneLeaveGroupCall(carolCtx, &tg.PhoneLeaveGroupCallRequest{Call: input}); err != nil {
|
|
t.Fatalf("carol leave: %v", err)
|
|
}
|
|
|
|
aliceRemove := conferenceTestBlock(conferenceChainBlockConstructor, "alice-remove-left-carol")
|
|
first, err := f.router.onPhoneDeleteConferenceCallParticipants(aliceCtx, &tg.PhoneDeleteConferenceCallParticipantsRequest{
|
|
Call: input,
|
|
IDs: []int64{f.carol.ID},
|
|
OnlyLeft: true,
|
|
Block: aliceRemove,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("alice only_left remove: %v", err)
|
|
}
|
|
firstChain := findUpdate[*tg.UpdateGroupCallChainBlocks](t, first)
|
|
if firstChain.SubChainID != 0 || firstChain.NextOffset != 4 || len(firstChain.Blocks) != 1 {
|
|
t.Fatalf("first only_left chain update = %+v", firstChain)
|
|
}
|
|
|
|
bobRemove := conferenceTestBlock(conferenceChainBlockConstructor, "bob-stale-remove-left-carol")
|
|
second, err := f.router.onPhoneDeleteConferenceCallParticipants(bobCtx, &tg.PhoneDeleteConferenceCallParticipantsRequest{
|
|
Call: input,
|
|
IDs: []int64{f.carol.ID},
|
|
OnlyLeft: true,
|
|
Block: bobRemove,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("bob duplicate only_left remove: %v", err)
|
|
}
|
|
if chain, ok := optionalUpdate[*tg.UpdateGroupCallChainBlocks](second); ok {
|
|
t.Fatalf("duplicate only_left must not append stale chain block, got %+v", chain)
|
|
}
|
|
list, err := f.router.onPhoneGetGroupCallChainBlocks(aliceCtx, &tg.PhoneGetGroupCallChainBlocksRequest{
|
|
Call: input, Offset: 0, Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("get chain blocks: %v", err)
|
|
}
|
|
got := findUpdate[*tg.UpdateGroupCallChainBlocks](t, list)
|
|
if got.NextOffset != 4 || len(got.Blocks) != 4 {
|
|
t.Fatalf("chain after duplicate only_left = %+v, want exactly 3 joins + 1 remove", got)
|
|
}
|
|
}
|
|
|
|
func TestConferenceForbiddenKickDoesNotAppendChainBlock(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
aliceCtx := f.userCtx(f.alice, 11)
|
|
bobCtx := f.userCtx(f.bob, 22)
|
|
carolCtx := f.userCtx(f.carol, 33)
|
|
create, err := f.router.onPhoneCreateConferenceCall(aliceCtx, &tg.PhoneCreateConferenceCallRequest{RandomID: 793})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
call := findUpdate[*tg.UpdateGroupCall](t, create).Call.(*tg.GroupCall)
|
|
input := &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash}
|
|
joinConferenceForTest(t, f, aliceCtx, input, "alice-forbidden-kick-join", 9061)
|
|
joinConferenceForTest(t, f, bobCtx, &tg.InputGroupCallSlug{Slug: conferenceSlugFromLink(t, call.InviteLink)}, "bob-forbidden-kick-join", 9062)
|
|
joinConferenceForTest(t, f, carolCtx, &tg.InputGroupCallSlug{Slug: conferenceSlugFromLink(t, call.InviteLink)}, "carol-forbidden-kick-join", 9063)
|
|
|
|
staleBlock := conferenceTestBlock(conferenceChainBlockConstructor, "bob-forbidden-kick-carol")
|
|
res, err := f.router.onPhoneDeleteConferenceCallParticipants(bobCtx, &tg.PhoneDeleteConferenceCallParticipantsRequest{
|
|
Call: input,
|
|
IDs: []int64{f.carol.ID},
|
|
Kick: true,
|
|
Block: staleBlock,
|
|
})
|
|
if res != nil || !tgerr.Is(err, "GROUPCALL_FORBIDDEN") {
|
|
t.Fatalf("bob forbidden kick = %+v err=%v, want GROUPCALL_FORBIDDEN", res, err)
|
|
}
|
|
list, err := f.router.onPhoneGetGroupCallChainBlocks(aliceCtx, &tg.PhoneGetGroupCallChainBlocksRequest{
|
|
Call: input, Offset: 0, Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("get chain blocks: %v", err)
|
|
}
|
|
got := findUpdate[*tg.UpdateGroupCallChainBlocks](t, list)
|
|
if got.NextOffset != 3 || len(got.Blocks) != 3 {
|
|
t.Fatalf("chain after forbidden kick = %+v, want only join blocks", got)
|
|
}
|
|
}
|
|
|
|
func TestConferenceLastLeaveDiscardsCall(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
aliceCtx := f.userCtx(f.alice, 11)
|
|
bobCtx := f.userCtx(f.bob, 22)
|
|
create, err := f.router.onPhoneCreateConferenceCall(aliceCtx, &tg.PhoneCreateConferenceCallRequest{RandomID: 794})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
call := findUpdate[*tg.UpdateGroupCall](t, create).Call.(*tg.GroupCall)
|
|
input := &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash}
|
|
joinConferenceForTest(t, f, aliceCtx, input, "alice-last-leave-join", 9071)
|
|
joinConferenceForTest(t, f, bobCtx, &tg.InputGroupCallSlug{Slug: conferenceSlugFromLink(t, call.InviteLink)}, "bob-last-leave-join", 9072)
|
|
|
|
bobLeave, err := f.router.onPhoneLeaveGroupCall(bobCtx, &tg.PhoneLeaveGroupCallRequest{Call: input})
|
|
if err != nil {
|
|
t.Fatalf("bob leave: %v", err)
|
|
}
|
|
if update, ok := optionalUpdate[*tg.UpdateGroupCall](bobLeave); ok {
|
|
if _, discarded := update.Call.(*tg.GroupCallDiscarded); discarded {
|
|
t.Fatalf("first leave must keep conference active, got %+v", update.Call)
|
|
}
|
|
}
|
|
aliceLeave, err := f.router.onPhoneLeaveGroupCall(aliceCtx, &tg.PhoneLeaveGroupCallRequest{Call: input})
|
|
if err != nil {
|
|
t.Fatalf("alice last leave: %v", err)
|
|
}
|
|
discardUpdate := findUpdate[*tg.UpdateGroupCall](t, aliceLeave)
|
|
if discarded, ok := discardUpdate.Call.(*tg.GroupCallDiscarded); !ok || discarded.ID != call.ID {
|
|
t.Fatalf("last leave update = %+v, want groupCallDiscarded %d", discardUpdate.Call, call.ID)
|
|
}
|
|
_, err = f.router.onPhoneJoinGroupCall(bobCtx, &tg.PhoneJoinGroupCallRequest{
|
|
Call: input,
|
|
JoinAs: &tg.InputPeerSelf{},
|
|
Params: groupCallJoinParams(t, 9073),
|
|
})
|
|
if !tgerr.Is(err, "GROUPCALL_ALREADY_DISCARDED") {
|
|
t.Fatalf("join after last leave err = %v, want GROUPCALL_ALREADY_DISCARDED", err)
|
|
}
|
|
}
|
|
|
|
func TestConferenceDiscardRequiresCreator(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
aliceCtx := f.userCtx(f.alice, 11)
|
|
bobCtx := f.userCtx(f.bob, 22)
|
|
create, err := f.router.onPhoneCreateConferenceCall(aliceCtx, &tg.PhoneCreateConferenceCallRequest{RandomID: 794})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
call := findUpdate[*tg.UpdateGroupCall](t, create).Call.(*tg.GroupCall)
|
|
input := &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash}
|
|
joinConferenceForTest(t, f, aliceCtx, input, "alice-discard-permission-join", 9071)
|
|
joinConferenceForTest(t, f, bobCtx, &tg.InputGroupCallSlug{Slug: conferenceSlugFromLink(t, call.InviteLink)}, "bob-discard-permission-join", 9072)
|
|
|
|
f.sessions.reset()
|
|
res, err := f.router.onPhoneDiscardGroupCall(bobCtx, input)
|
|
if res != nil || !tgerr.Is(err, "CHAT_ADMIN_REQUIRED") {
|
|
t.Fatalf("bob discard = %+v err=%v, want CHAT_ADMIN_REQUIRED", res, err)
|
|
}
|
|
if got := f.sessions.records(); len(got) != 0 {
|
|
t.Fatalf("forbidden discard must not push updates, got %+v", got)
|
|
}
|
|
got, err := f.router.onPhoneGetGroupCall(aliceCtx, &tg.PhoneGetGroupCallRequest{Call: input, Limit: 10})
|
|
if err != nil {
|
|
t.Fatalf("get group call after forbidden discard: %v", err)
|
|
}
|
|
if _, ok := got.Call.(*tg.GroupCall); !ok {
|
|
t.Fatalf("call after forbidden discard = %T, want active GroupCall", got.Call)
|
|
}
|
|
}
|
|
|
|
func TestConferenceDiscardFanoutIncludesSlugJoinedParticipants(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
aliceCtx := f.userCtx(f.alice, 11)
|
|
bobCtx := f.userCtx(f.bob, 22)
|
|
carolCtx := f.userCtx(f.carol, 33)
|
|
create, err := f.router.onPhoneCreateConferenceCall(aliceCtx, &tg.PhoneCreateConferenceCallRequest{RandomID: 795})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
call := findUpdate[*tg.UpdateGroupCall](t, create).Call.(*tg.GroupCall)
|
|
input := &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash}
|
|
joinConferenceForTest(t, f, aliceCtx, input, "alice-discard-fanout-join", 9081)
|
|
joinConferenceForTest(t, f, bobCtx, &tg.InputGroupCallSlug{Slug: conferenceSlugFromLink(t, call.InviteLink)}, "bob-discard-fanout-join", 9082)
|
|
joinConferenceForTest(t, f, carolCtx, &tg.InputGroupCallSlug{Slug: conferenceSlugFromLink(t, call.InviteLink)}, "carol-discard-fanout-join", 9083)
|
|
|
|
f.sessions.reset()
|
|
discard, err := f.router.onPhoneDiscardGroupCall(aliceCtx, input)
|
|
if err != nil {
|
|
t.Fatalf("discard conference: %v", err)
|
|
}
|
|
if _, ok := findUpdate[*tg.UpdateGroupCall](t, discard).Call.(*tg.GroupCallDiscarded); !ok {
|
|
t.Fatalf("discard response missing groupCallDiscarded")
|
|
}
|
|
seen := pushedDiscardedGroupCallUsers(f.sessions.records(), call.ID)
|
|
for _, user := range []domain.User{f.alice, f.bob, f.carol} {
|
|
if !seen[user.ID] {
|
|
t.Fatalf("discard fanout missing user %d, seen=%v records=%+v", user.ID, seen, f.sessions.records())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConferenceDiscardAllowsHistoricalParticipantCleanupRPCs(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
aliceCtx := f.userCtx(f.alice, 11)
|
|
bobCtx := f.userCtx(f.bob, 22)
|
|
carolCtx := f.userCtx(f.carol, 33)
|
|
create, err := f.router.onPhoneCreateConferenceCall(aliceCtx, &tg.PhoneCreateConferenceCallRequest{RandomID: 796})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
call := findUpdate[*tg.UpdateGroupCall](t, create).Call.(*tg.GroupCall)
|
|
input := &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash}
|
|
slugCall := &tg.InputGroupCallSlug{Slug: conferenceSlugFromLink(t, call.InviteLink)}
|
|
joinConferenceForTest(t, f, aliceCtx, input, "alice-discard-cleanup-join", 9091)
|
|
joinConferenceForTest(t, f, bobCtx, slugCall, "bob-discard-cleanup-join", 9092)
|
|
joinConferenceForTest(t, f, carolCtx, slugCall, "carol-discard-cleanup-join", 9093)
|
|
|
|
if _, err := f.router.onPhoneDiscardGroupCall(aliceCtx, input); err != nil {
|
|
t.Fatalf("discard conference: %v", err)
|
|
}
|
|
got, err := f.router.onPhoneGetGroupCall(bobCtx, &tg.PhoneGetGroupCallRequest{Call: input, Limit: 10})
|
|
if err != nil {
|
|
t.Fatalf("bob get discarded group call: %v", err)
|
|
}
|
|
if _, ok := got.Call.(*tg.GroupCallDiscarded); !ok {
|
|
t.Fatalf("bob call after discard = %T, want GroupCallDiscarded", got.Call)
|
|
}
|
|
ssrcs, err := f.router.onPhoneCheckGroupCall(bobCtx, &tg.PhoneCheckGroupCallRequest{
|
|
Call: input,
|
|
Sources: []int{9092},
|
|
})
|
|
if err != nil || len(ssrcs) != 0 {
|
|
t.Fatalf("bob check discarded group call = %v err=%v, want empty no error", ssrcs, err)
|
|
}
|
|
chain, err := f.router.onPhoneGetGroupCallChainBlocks(bobCtx, &tg.PhoneGetGroupCallChainBlocksRequest{
|
|
Call: input,
|
|
SubChainID: 0,
|
|
Offset: 0,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("bob get chain blocks after discard: %v", err)
|
|
}
|
|
if blocks := findUpdate[*tg.UpdateGroupCallChainBlocks](t, chain).Blocks; len(blocks) != 3 {
|
|
t.Fatalf("bob cleanup chain blocks len=%d, want 3", len(blocks))
|
|
}
|
|
}
|
|
|
|
func TestConferenceInviteMessageResolvesInputGroupCallInviteMessage(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
aliceCtx := f.userCtx(f.alice, 11)
|
|
create, err := f.router.onPhoneCreateConferenceCall(aliceCtx, &tg.PhoneCreateConferenceCallRequest{RandomID: 88})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
call := findUpdate[*tg.UpdateGroupCall](t, create).Call.(*tg.GroupCall)
|
|
input := &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash}
|
|
invite, err := f.router.onPhoneInviteConferenceCallParticipant(aliceCtx, &tg.PhoneInviteConferenceCallParticipantRequest{
|
|
Call: input,
|
|
UserID: &tg.InputUser{UserID: f.bob.ID, AccessHash: f.bob.AccessHash},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("invite conference: %v", err)
|
|
}
|
|
msg := findUpdate[*tg.UpdateNewMessage](t, invite).Message.(*tg.MessageService)
|
|
if _, ok := msg.Action.(*tg.MessageActionConferenceCall); !ok {
|
|
t.Fatalf("invite action = %T", msg.Action)
|
|
}
|
|
history, err := f.messages.GetHistory(f.ctx, f.bob.ID, domain.MessageFilter{
|
|
HasPeer: true,
|
|
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: f.alice.ID},
|
|
Limit: 10,
|
|
})
|
|
if err != nil || len(history.Messages) != 1 {
|
|
t.Fatalf("bob history len=%d err=%v", len(history.Messages), err)
|
|
}
|
|
bobMsgID := history.Messages[0].ID
|
|
joinConferenceForTest(t, f, aliceCtx, input, "alice-invite-message-broadcast-join", 9091)
|
|
got, err := f.router.onPhoneGetGroupCall(f.userCtx(f.bob, 22), &tg.PhoneGetGroupCallRequest{
|
|
Call: &tg.InputGroupCallInviteMessage{MsgID: bobMsgID},
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("get by invite message: %v", err)
|
|
}
|
|
if got.Call.(*tg.GroupCall).ID != call.ID {
|
|
t.Fatalf("invite message call id = %d, want %d", got.Call.(*tg.GroupCall).ID, call.ID)
|
|
}
|
|
block := conferenceTestBlock(conferenceBroadcastCommitConstructor, "invite-message-chain-block")
|
|
if _, err := f.router.onPhoneSendConferenceCallBroadcast(aliceCtx, &tg.PhoneSendConferenceCallBroadcastRequest{
|
|
Call: input,
|
|
Block: block,
|
|
}); err != nil {
|
|
t.Fatalf("broadcast invite message chain block: %v", err)
|
|
}
|
|
chain, err := f.router.onPhoneGetGroupCallChainBlocks(f.userCtx(f.bob, 22), &tg.PhoneGetGroupCallChainBlocksRequest{
|
|
Call: &tg.InputGroupCallInviteMessage{MsgID: bobMsgID},
|
|
SubChainID: 1,
|
|
Offset: domain.GroupCallChainBlockLatestOffset,
|
|
Limit: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("get latest by invite message: %v", err)
|
|
}
|
|
latest := findUpdate[*tg.UpdateGroupCallChainBlocks](t, chain)
|
|
if latest.SubChainID != 1 || latest.NextOffset != 1 || len(latest.Blocks) != 1 || conferenceTestConstructor(latest.Blocks[0]) != conferenceBroadcastCommitServerConstructor {
|
|
t.Fatalf("invite message latest block = %+v", latest)
|
|
}
|
|
}
|
|
|
|
func TestPhoneDiscardMigrateConferenceCarriesSlug(t *testing.T) {
|
|
f := newConferenceFixture(t)
|
|
aliceCtx := f.userCtx(f.alice, 11)
|
|
bobCtx := f.userCtx(f.bob, 22)
|
|
ga, gaHash, gb := phoneTestKeys()
|
|
requested, err := f.router.onPhoneRequestCall(aliceCtx, &tg.PhoneRequestCallRequest{
|
|
UserID: &tg.InputUser{UserID: f.bob.ID, AccessHash: f.bob.AccessHash},
|
|
RandomID: 9,
|
|
GAHash: gaHash,
|
|
Protocol: phoneTestProtocol(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("request call: %v", err)
|
|
}
|
|
peer := tg.InputPhoneCall{ID: requested.PhoneCall.(*tg.PhoneCallWaiting).ID, AccessHash: requested.PhoneCall.(*tg.PhoneCallWaiting).AccessHash}
|
|
if _, err := f.router.onPhoneAcceptCall(bobCtx, &tg.PhoneAcceptCallRequest{Peer: peer, GB: gb, Protocol: phoneTestProtocol()}); err != nil {
|
|
t.Fatalf("accept call: %v", err)
|
|
}
|
|
if _, err := f.router.onPhoneConfirmCall(aliceCtx, &tg.PhoneConfirmCallRequest{Peer: peer, GA: ga, KeyFingerprint: 123, Protocol: phoneTestProtocol()}); err != nil {
|
|
t.Fatalf("confirm call: %v", err)
|
|
}
|
|
create, err := f.router.onPhoneCreateConferenceCall(aliceCtx, &tg.PhoneCreateConferenceCallRequest{RandomID: 99})
|
|
if err != nil {
|
|
t.Fatalf("create conference: %v", err)
|
|
}
|
|
call := findUpdate[*tg.UpdateGroupCall](t, create).Call.(*tg.GroupCall)
|
|
slug := conferenceSlugFromLink(t, call.InviteLink)
|
|
updates, err := f.router.onPhoneDiscardCall(aliceCtx, &tg.PhoneDiscardCallRequest{
|
|
Peer: peer,
|
|
Duration: 1,
|
|
Reason: &tg.PhoneCallDiscardReasonMigrateConferenceCall{Slug: slug},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("discard migrate: %v", err)
|
|
}
|
|
discarded := findUpdate[*tg.UpdatePhoneCall](t, updates).PhoneCall.(*tg.PhoneCallDiscarded)
|
|
reason, ok := discarded.Reason.(*tg.PhoneCallDiscardReasonMigrateConferenceCall)
|
|
if !ok || reason.Slug != slug {
|
|
t.Fatalf("discard reason = %#v, want slug %q", discarded.Reason, slug)
|
|
}
|
|
}
|
|
|
|
var _ clock.Clock = (*phoneTestClock)(nil)
|
|
|
|
func conferenceSlugFromLink(t *testing.T, link string) string {
|
|
t.Helper()
|
|
u, err := url.Parse(link)
|
|
if err == nil {
|
|
if slug := u.Query().Get("slug"); slug != "" {
|
|
return slug
|
|
}
|
|
}
|
|
idx := strings.LastIndex(link, "slug=")
|
|
if idx < 0 {
|
|
t.Fatalf("link %q does not contain slug", link)
|
|
}
|
|
return link[idx+len("slug="):]
|
|
}
|
|
|
|
func conferenceTestBlock(constructor uint32, suffix string) []byte {
|
|
block := make([]byte, 4+len(suffix))
|
|
binary.LittleEndian.PutUint32(block[:4], constructor)
|
|
copy(block[4:], suffix)
|
|
return block
|
|
}
|
|
|
|
func conferenceTestConstructor(block []byte) uint32 {
|
|
if len(block) < 4 {
|
|
return 0
|
|
}
|
|
return binary.LittleEndian.Uint32(block[:4])
|
|
}
|
|
|
|
func lastPathSegmentFromLink(t *testing.T, link string) string {
|
|
t.Helper()
|
|
u, err := url.Parse(link)
|
|
if err != nil {
|
|
t.Fatalf("parse link %q: %v", link, err)
|
|
}
|
|
segments := strings.Split(strings.Trim(u.EscapedPath(), "/"), "/")
|
|
if len(segments) == 0 || segments[len(segments)-1] == "" {
|
|
t.Fatalf("link %q has no path segment", link)
|
|
}
|
|
segment, err := url.PathUnescape(segments[len(segments)-1])
|
|
if err != nil {
|
|
t.Fatalf("decode path segment in %q: %v", link, err)
|
|
}
|
|
return segment
|
|
}
|