681 lines
29 KiB
Go
681 lines
29 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/iamxvbaba/td/bin"
|
|
"github.com/iamxvbaba/td/clock"
|
|
"github.com/iamxvbaba/td/tg"
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zaptest"
|
|
|
|
"telesrv/internal/domain"
|
|
)
|
|
|
|
// This file is the load-bearing test of the third-party verification feature.
|
|
//
|
|
// An official client renders the badge off one specific bit of one specific flags
|
|
// word. A projection that sets the neighbouring bit encodes a different, valid
|
|
// field -- the response still decodes, no error is raised anywhere, and the badge
|
|
// simply never appears. So the assertions below are not "the Go field is populated"
|
|
// but "the encoded flags word differs from the unmarked encoding in exactly the bit
|
|
// layer 228 assigns":
|
|
//
|
|
// user#b1b8cc83 bot_verification_icon:flags2.14?long
|
|
// channel#d49f34c6 bot_verification_icon:flags2.13?long
|
|
// userFull#6cbe645 bot_verification:flags2.12?BotVerification
|
|
// channelFull#a04e8d3a bot_verification:flags2.17?BotVerification
|
|
// chatInvite#5c9d3702 bot_verification:flags.13?BotVerification
|
|
// botInfo#4d8a0299 verifier_settings:flags.9?BotVerifierSettings
|
|
//
|
|
// Each case encodes the same object twice through the real handler -- once before
|
|
// the mark exists and once after -- and XORs the two flags words. That catches an
|
|
// off-by-one bit, and also catches a projection that quietly disturbs an unrelated
|
|
// flag while adding the badge.
|
|
|
|
// tlRoundTrip serialises through the wire and decodes back, so every assertion
|
|
// reads the encoded form rather than the in-memory struct.
|
|
func tlRoundTrip(t *testing.T, in bin.Encoder, out bin.Decoder) {
|
|
t.Helper()
|
|
buf := &bin.Buffer{}
|
|
if err := in.Encode(buf); err != nil {
|
|
t.Fatalf("encode %T: %v", in, err)
|
|
}
|
|
if err := out.Decode(buf); err != nil {
|
|
t.Fatalf("decode %T: %v", out, err)
|
|
}
|
|
if buf.Len() != 0 {
|
|
t.Fatalf("decode %T left %d trailing bytes", out, buf.Len())
|
|
}
|
|
}
|
|
|
|
// assertFlagBitDelta pins that adding the mark flipped exactly one bit of the flags
|
|
// word, at the index layer 228 assigns.
|
|
func assertFlagBitDelta(t *testing.T, label string, before, after bin.Fields, wantBit int) {
|
|
t.Helper()
|
|
want := uint32(1) << uint(wantBit)
|
|
delta := uint32(before) ^ uint32(after)
|
|
if delta != want {
|
|
t.Fatalf("%s flags delta = %032b, want exactly bit %d (%032b); before %032b after %032b",
|
|
label, delta, wantBit, want, uint32(before), uint32(after))
|
|
}
|
|
if uint32(after)&want == 0 {
|
|
t.Fatalf("%s did not set bit %d: flags = %032b", label, wantBit, uint32(after))
|
|
}
|
|
}
|
|
|
|
// TestBotVerificationConstructorIDs pins the two constructor ids the feature
|
|
// serialises. A drift here silently reshapes every payload below.
|
|
func TestBotVerificationConstructorIDs(t *testing.T) {
|
|
if tg.BotVerificationTypeID != 0xf93cd45c {
|
|
t.Fatalf("botVerification constructor id = %#x, want 0xf93cd45c", tg.BotVerificationTypeID)
|
|
}
|
|
if tg.BotVerifierSettingsTypeID != 0xb0cd6617 {
|
|
t.Fatalf("botVerifierSettings constructor id = %#x, want 0xb0cd6617", tg.BotVerifierSettingsTypeID)
|
|
}
|
|
// The carriers, so a layer bump that reshuffles them is caught here too.
|
|
if tg.UserTypeID != 0xb1b8cc83 || tg.ChannelTypeID != 0xd49f34c6 {
|
|
t.Fatalf("peer constructor ids = user %#x / channel %#x", tg.UserTypeID, tg.ChannelTypeID)
|
|
}
|
|
if tg.UserFullTypeID != 0x6cbe645 || tg.ChannelFullTypeID != 0xa04e8d3a {
|
|
t.Fatalf("full constructor ids = userFull %#x / channelFull %#x", tg.UserFullTypeID, tg.ChannelFullTypeID)
|
|
}
|
|
if tg.ChatInviteTypeID != 0x5c9d3702 || tg.BotInfoTypeID != 0x4d8a0299 {
|
|
t.Fatalf("constructor ids = chatInvite %#x / botInfo %#x", tg.ChatInviteTypeID, tg.BotInfoTypeID)
|
|
}
|
|
if tg.BotsSetCustomVerificationRequestTypeID != 0x8b89dfbd {
|
|
t.Fatalf("bots.setCustomVerification id = %#x, want 0x8b89dfbd", tg.BotsSetCustomVerificationRequestTypeID)
|
|
}
|
|
}
|
|
|
|
// TestBotVerificationTLProjectionShapes pins the two payload builders: the icon is a
|
|
// custom emoji document id, so a mark without one is omitted rather than encoded as
|
|
// a badge the client draws as nothing.
|
|
func TestBotVerificationTLProjectionShapes(t *testing.T) {
|
|
value, ok := tgBotVerification(domain.BotVerification{BotID: 42, Icon: 777, Description: "Verified by Acme"})
|
|
if !ok {
|
|
t.Fatal("complete mark was rejected")
|
|
}
|
|
decoded := &tg.BotVerification{}
|
|
tlRoundTrip(t, &value, decoded)
|
|
if decoded.BotID != 42 || decoded.Icon != 777 || decoded.Description != "Verified by Acme" {
|
|
t.Fatalf("botVerification = %+v", decoded)
|
|
}
|
|
for _, bad := range []domain.BotVerification{
|
|
{BotID: 0, Icon: 777},
|
|
{BotID: 42, Icon: 0},
|
|
} {
|
|
if _, ok := tgBotVerification(bad); ok {
|
|
t.Fatalf("unrenderable mark %+v was projected", bad)
|
|
}
|
|
}
|
|
|
|
settings, ok := tgBotVerifierSettings(domain.BotVerifierSettings{
|
|
BotID: 42, IconDocumentID: 777, CompanyName: "Acme Trust",
|
|
DefaultDescription: "Verified by Acme Trust", CanModifyCustomDescription: true, Enabled: true,
|
|
})
|
|
if !ok {
|
|
t.Fatal("valid verifier settings were rejected")
|
|
}
|
|
decodedSettings := &tg.BotVerifierSettings{}
|
|
tlRoundTrip(t, &settings, decodedSettings)
|
|
if decodedSettings.Icon != 777 || decodedSettings.Company != "Acme Trust" {
|
|
t.Fatalf("botVerifierSettings = %+v", decodedSettings)
|
|
}
|
|
if !decodedSettings.GetCanModifyCustomDescription() || !decodedSettings.Flags.Has(1) {
|
|
t.Fatalf("can_modify_custom_description not on flags.1: %032b", uint32(decodedSettings.Flags))
|
|
}
|
|
if desc, ok := decodedSettings.GetCustomDescription(); !ok || desc != "Verified by Acme Trust" ||
|
|
!decodedSettings.Flags.Has(0) {
|
|
t.Fatalf("custom_description not on flags.0: %q ok=%v flags %032b", desc, ok, uint32(decodedSettings.Flags))
|
|
}
|
|
// A verifier with no default description leaves flags.0 clear.
|
|
bare, ok := tgBotVerifierSettings(domain.BotVerifierSettings{
|
|
BotID: 42, IconDocumentID: 777, CompanyName: "Acme Trust", Enabled: true,
|
|
})
|
|
if !ok {
|
|
t.Fatal("verifier settings without a default description were rejected")
|
|
}
|
|
decodedBare := &tg.BotVerifierSettings{}
|
|
tlRoundTrip(t, &bare, decodedBare)
|
|
if decodedBare.Flags.Has(0) || decodedBare.Flags.Has(1) {
|
|
t.Fatalf("bare verifier settings set optional flags: %032b", uint32(decodedBare.Flags))
|
|
}
|
|
// A configuration that does not validate is omitted entirely.
|
|
if _, ok := tgBotVerifierSettings(domain.BotVerifierSettings{BotID: 42, IconDocumentID: 777, Enabled: true}); ok {
|
|
t.Fatal("verifier settings without a company were projected")
|
|
}
|
|
}
|
|
|
|
// getUsersUser runs users.getUsers and returns the wire form of one projected user.
|
|
func getUsersUser(t *testing.T, r *Router, viewerUserID int64, target domain.User) *tg.User {
|
|
t.Helper()
|
|
out, err := r.onUsersGetUsers(WithUserID(context.Background(), viewerUserID),
|
|
[]tg.InputUserClass{&tg.InputUser{UserID: target.ID, AccessHash: target.AccessHash}})
|
|
if err != nil {
|
|
t.Fatalf("get users: %v", err)
|
|
}
|
|
if len(out) != 1 {
|
|
t.Fatalf("get users returned %d users, want 1", len(out))
|
|
}
|
|
decoded := &tg.User{}
|
|
tlRoundTrip(t, out[0].(*tg.User), decoded)
|
|
return decoded
|
|
}
|
|
|
|
// getFullUserProjection runs users.getFullUser and returns the wire forms of the
|
|
// userFull and (when present) its bot_info.
|
|
func getFullUserProjection(t *testing.T, r *Router, viewerUserID int64, target domain.User) (*tg.UserFull, *tg.BotInfo) {
|
|
t.Helper()
|
|
res, err := r.onUsersGetFullUser(WithUserID(context.Background(), viewerUserID),
|
|
&tg.InputUser{UserID: target.ID, AccessHash: target.AccessHash})
|
|
if err != nil {
|
|
t.Fatalf("get full user: %v", err)
|
|
}
|
|
full := res.FullUser
|
|
decoded := &tg.UserFull{}
|
|
tlRoundTrip(t, &full, decoded)
|
|
info, ok := decoded.GetBotInfo()
|
|
if !ok {
|
|
return decoded, nil
|
|
}
|
|
return decoded, &info
|
|
}
|
|
|
|
// TestUserAndUserFullCarryBotVerificationOnLayer228Bits is the user half: the icon
|
|
// on user#b1b8cc83 flags2.14 and the full payload on userFull#6cbe645 flags2.12.
|
|
func TestUserAndUserFullCarryBotVerificationOnLayer228Bits(t *testing.T) {
|
|
f := newBotVerificationFixture(t, newFakeBotVerifications())
|
|
f.enableVerifier(f.bot.ID, 8800001, true)
|
|
ownerCtx := WithUserID(context.Background(), f.owner.ID)
|
|
|
|
plainUser := getUsersUser(t, f.router, f.owner.ID, f.target)
|
|
if _, ok := plainUser.GetBotVerificationIcon(); ok {
|
|
t.Fatalf("unmarked user carries an icon: %+v", plainUser)
|
|
}
|
|
plainFull, _ := getFullUserProjection(t, f.router, f.owner.ID, f.target)
|
|
if _, ok := plainFull.GetBotVerification(); ok {
|
|
t.Fatalf("unmarked userFull carries a mark: %+v", plainFull)
|
|
}
|
|
|
|
if ok, err := f.router.onBotsSetCustomVerification(ownerCtx,
|
|
setCustomVerificationRequest(inputPeerUser(f.target), inputUser(f.bot), true, "Official reseller")); err != nil || !ok {
|
|
t.Fatalf("grant = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
markedUser := getUsersUser(t, f.router, f.owner.ID, f.target)
|
|
icon, ok := markedUser.GetBotVerificationIcon()
|
|
if !ok || icon != 8800001 {
|
|
t.Fatalf("user bot_verification_icon = %d, ok=%v, want 8800001", icon, ok)
|
|
}
|
|
assertFlagBitDelta(t, "user", plainUser.Flags2, markedUser.Flags2, 14)
|
|
if uint32(plainUser.Flags) != uint32(markedUser.Flags) {
|
|
t.Fatalf("user flags word changed: before %032b after %032b", uint32(plainUser.Flags), uint32(markedUser.Flags))
|
|
}
|
|
// The operator-granted checkmark is a different mechanism and must stay clear.
|
|
if markedUser.Verified || markedUser.Flags.Has(17) {
|
|
t.Fatalf("third-party mark leaked into official verified:flags.17: %+v", markedUser)
|
|
}
|
|
|
|
markedFull, _ := getFullUserProjection(t, f.router, f.owner.ID, f.target)
|
|
mark, ok := markedFull.GetBotVerification()
|
|
if !ok {
|
|
t.Fatalf("userFull bot_verification unset: %+v", markedFull)
|
|
}
|
|
if mark.BotID != f.bot.ID || mark.Icon != 8800001 || mark.Description != "Official reseller" {
|
|
t.Fatalf("userFull bot_verification = %+v, want verifier %d icon 8800001", mark, f.bot.ID)
|
|
}
|
|
assertFlagBitDelta(t, "userFull", plainFull.Flags2, markedFull.Flags2, 12)
|
|
if uint32(plainFull.Flags) != uint32(markedFull.Flags) {
|
|
t.Fatalf("userFull flags word changed: before %032b after %032b", uint32(plainFull.Flags), uint32(markedFull.Flags))
|
|
}
|
|
|
|
// Revoking clears the bit again on both surfaces: the payload is an overlay, so
|
|
// it must not survive inside the userFull projection cache.
|
|
if ok, err := f.router.onBotsSetCustomVerification(ownerCtx,
|
|
setCustomVerificationRequest(inputPeerUser(f.target), inputUser(f.bot), false, "")); err != nil || !ok {
|
|
t.Fatalf("revoke = %v,%v, want true,nil", ok, err)
|
|
}
|
|
if revokedUser := getUsersUser(t, f.router, f.owner.ID, f.target); revokedUser.Flags2.Has(14) {
|
|
t.Fatalf("revoked user still carries flags2.14: %032b", uint32(revokedUser.Flags2))
|
|
}
|
|
revokedFull, _ := getFullUserProjection(t, f.router, f.owner.ID, f.target)
|
|
if revokedFull.Flags2.Has(12) {
|
|
t.Fatalf("revoked userFull still carries flags2.12: %032b", uint32(revokedFull.Flags2))
|
|
}
|
|
}
|
|
|
|
// TestPeerSettingsUserPreservesBotVerificationOnLayer228Bit covers the chat-open
|
|
// race behind a disappearing badge. Official clients merge messages.peerSettings
|
|
// users into the same peer cache as users.getFullUser; therefore the auxiliary
|
|
// User must carry the same flags2.14 value regardless of which RPC arrives last.
|
|
func TestPeerSettingsUserPreservesBotVerificationOnLayer228Bit(t *testing.T) {
|
|
f := newBotVerificationFixture(t, newFakeBotVerifications())
|
|
f.enableVerifier(f.bot.ID, 8800011, true)
|
|
ownerCtx := WithUserID(context.Background(), f.owner.ID)
|
|
peer := &tg.InputPeerUser{UserID: f.target.ID, AccessHash: f.target.AccessHash}
|
|
|
|
peerSettingsUser := func() *tg.User {
|
|
t.Helper()
|
|
out, err := f.router.onMessagesGetPeerSettings(ownerCtx, peer)
|
|
if err != nil {
|
|
t.Fatalf("get peer settings: %v", err)
|
|
}
|
|
if len(out.Users) != 1 {
|
|
t.Fatalf("get peer settings returned %d users, want 1", len(out.Users))
|
|
}
|
|
wire := &tg.User{}
|
|
tlRoundTrip(t, out.Users[0].(*tg.User), wire)
|
|
return wire
|
|
}
|
|
|
|
plain := peerSettingsUser()
|
|
if _, ok := plain.GetBotVerificationIcon(); ok {
|
|
t.Fatalf("unmarked peer settings user carries an icon: %+v", plain)
|
|
}
|
|
|
|
if ok, err := f.router.onBotsSetCustomVerification(ownerCtx,
|
|
setCustomVerificationRequest(inputPeerUser(f.target), inputUser(f.bot), true, "Stable in chat cache")); err != nil || !ok {
|
|
t.Fatalf("grant = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
// Exercise both possible response orders. Neither fullUser -> peerSettings nor
|
|
// peerSettings -> fullUser may turn the marked peer back into an unmarked one.
|
|
markedFull, _ := getFullUserProjection(t, f.router, f.owner.ID, f.target)
|
|
fullMark, ok := markedFull.GetBotVerification()
|
|
if !ok || fullMark.Icon != 8800011 {
|
|
t.Fatalf("userFull bot_verification = %+v, ok=%v, want icon 8800011", fullMark, ok)
|
|
}
|
|
marked := peerSettingsUser()
|
|
icon, ok := marked.GetBotVerificationIcon()
|
|
if !ok || icon != 8800011 {
|
|
t.Fatalf("peer settings user bot_verification_icon = %d, ok=%v, want 8800011", icon, ok)
|
|
}
|
|
assertFlagBitDelta(t, "peerSettings.user", plain.Flags2, marked.Flags2, 14)
|
|
|
|
marked = peerSettingsUser()
|
|
markedFull, _ = getFullUserProjection(t, f.router, f.owner.ID, f.target)
|
|
icon, ok = marked.GetBotVerificationIcon()
|
|
fullMark, fullOK := markedFull.GetBotVerification()
|
|
if !ok || icon != 8800011 || !fullOK || fullMark.Icon != icon {
|
|
t.Fatalf("reverse order drift: peer icon=%d ok=%v full=%+v ok=%v", icon, ok, fullMark, fullOK)
|
|
}
|
|
|
|
if ok, err := f.router.onBotsSetCustomVerification(ownerCtx,
|
|
setCustomVerificationRequest(inputPeerUser(f.target), inputUser(f.bot), false, "")); err != nil || !ok {
|
|
t.Fatalf("revoke = %v,%v, want true,nil", ok, err)
|
|
}
|
|
if revoked := peerSettingsUser(); revoked.Flags2.Has(14) {
|
|
t.Fatalf("revoked peer settings user still carries flags2.14: %032b", uint32(revoked.Flags2))
|
|
}
|
|
}
|
|
|
|
// TestBotInfoCarriesVerifierSettingsOnFlags9 pins botInfo#4d8a0299
|
|
// verifier_settings:flags.9 inside the verifier bot's own userFull, and the operator
|
|
// kill switch: a disabled verifier stops advertising itself.
|
|
func TestBotInfoCarriesVerifierSettingsOnFlags9(t *testing.T) {
|
|
f := newBotVerificationFixture(t, newFakeBotVerifications())
|
|
|
|
_, plainInfo := getFullUserProjection(t, f.router, f.owner.ID, f.bot)
|
|
if plainInfo == nil {
|
|
t.Fatal("bot userFull carries no bot_info at all")
|
|
}
|
|
if _, ok := plainInfo.GetVerifierSettings(); ok {
|
|
t.Fatalf("ordinary bot advertises verifier settings: %+v", plainInfo)
|
|
}
|
|
|
|
f.enableVerifier(f.bot.ID, 8800002, true)
|
|
f.router.invalidateRPCProjectionForUser(f.bot.ID)
|
|
_, markedInfo := getFullUserProjection(t, f.router, f.owner.ID, f.bot)
|
|
if markedInfo == nil {
|
|
t.Fatal("verifier bot userFull carries no bot_info")
|
|
}
|
|
settings, ok := markedInfo.GetVerifierSettings()
|
|
if !ok {
|
|
t.Fatalf("verifier bot bot_info has no verifier_settings: %+v", markedInfo)
|
|
}
|
|
if settings.Icon != 8800002 || settings.Company != "Acme Trust" || !settings.GetCanModifyCustomDescription() {
|
|
t.Fatalf("verifier_settings = %+v", settings)
|
|
}
|
|
assertFlagBitDelta(t, "botInfo", plainInfo.Flags, markedInfo.Flags, 9)
|
|
|
|
// Operator kill switch: the row stays, the advertisement stops.
|
|
disabled := f.verify.settings[f.bot.ID]
|
|
disabled.Enabled = false
|
|
f.verify.settings[f.bot.ID] = disabled
|
|
f.router.invalidateRPCProjectionForUser(f.bot.ID)
|
|
_, offInfo := getFullUserProjection(t, f.router, f.owner.ID, f.bot)
|
|
if offInfo == nil || offInfo.Flags.Has(9) {
|
|
t.Fatalf("disabled verifier still advertises verifier_settings: %+v", offInfo)
|
|
}
|
|
}
|
|
|
|
// botVerificationGroup creates a megagroup owned by the fixture owner and returns its
|
|
// projected channel object.
|
|
func (f botVerificationFixture) botVerificationGroup(t *testing.T, title string) *tg.Channel {
|
|
t.Helper()
|
|
created, err := f.router.onMessagesCreateChat(WithUserID(context.Background(), f.owner.ID),
|
|
&tg.MessagesCreateChatRequest{
|
|
Users: []tg.InputUserClass{inputUser(f.stranger)},
|
|
Title: title,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create chat: %v", err)
|
|
}
|
|
return created.Updates.(*tg.Updates).Chats[0].(*tg.Channel)
|
|
}
|
|
|
|
func getChannelsChannel(t *testing.T, r *Router, viewerUserID int64, channel *tg.Channel) *tg.Channel {
|
|
t.Helper()
|
|
res, err := r.onChannelsGetChannels(WithUserID(context.Background(), viewerUserID),
|
|
[]tg.InputChannelClass{&tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}})
|
|
if err != nil {
|
|
t.Fatalf("get channels: %v", err)
|
|
}
|
|
chats := res.(*tg.MessagesChats).Chats
|
|
if len(chats) != 1 {
|
|
t.Fatalf("get channels returned %d chats, want 1", len(chats))
|
|
}
|
|
decoded := &tg.Channel{}
|
|
tlRoundTrip(t, chats[0].(*tg.Channel), decoded)
|
|
return decoded
|
|
}
|
|
|
|
func getFullChannelProjection(t *testing.T, r *Router, viewerUserID int64, channel *tg.Channel) *tg.ChannelFull {
|
|
t.Helper()
|
|
res, err := r.onChannelsGetFullChannel(WithUserID(context.Background(), viewerUserID),
|
|
&tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash})
|
|
if err != nil {
|
|
t.Fatalf("get full channel: %v", err)
|
|
}
|
|
decoded := &tg.ChannelFull{}
|
|
tlRoundTrip(t, res.FullChat.(*tg.ChannelFull), decoded)
|
|
return decoded
|
|
}
|
|
|
|
// TestChannelAndChannelFullCarryBotVerificationOnLayer228Bits is the channel half:
|
|
// channel#d49f34c6 flags2.13 and channelFull#a04e8d3a flags2.17. Note the two bits
|
|
// differ from the user ones, which is exactly the mistake this test exists to catch.
|
|
func TestChannelAndChannelFullCarryBotVerificationOnLayer228Bits(t *testing.T) {
|
|
f := newBotVerificationFixture(t, newFakeBotVerifications())
|
|
f.enableVerifier(f.bot.ID, 8800003, true)
|
|
ownerCtx := WithUserID(context.Background(), f.owner.ID)
|
|
group := f.botVerificationGroup(t, "Verified Group")
|
|
|
|
plainChannel := getChannelsChannel(t, f.router, f.owner.ID, group)
|
|
if _, ok := plainChannel.GetBotVerificationIcon(); ok {
|
|
t.Fatalf("unmarked channel carries an icon: %+v", plainChannel)
|
|
}
|
|
plainFull := getFullChannelProjection(t, f.router, f.owner.ID, group)
|
|
if _, ok := plainFull.GetBotVerification(); ok {
|
|
t.Fatalf("unmarked channelFull carries a mark: %+v", plainFull)
|
|
}
|
|
|
|
if ok, err := f.router.onBotsSetCustomVerification(ownerCtx, setCustomVerificationRequest(
|
|
&tg.InputPeerChannel{ChannelID: group.ID, AccessHash: group.AccessHash},
|
|
inputUser(f.bot), true, "Community partner")); err != nil || !ok {
|
|
t.Fatalf("grant on channel = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
markedChannel := getChannelsChannel(t, f.router, f.owner.ID, group)
|
|
icon, ok := markedChannel.GetBotVerificationIcon()
|
|
if !ok || icon != 8800003 {
|
|
t.Fatalf("channel bot_verification_icon = %d, ok=%v, want 8800003", icon, ok)
|
|
}
|
|
assertFlagBitDelta(t, "channel", plainChannel.Flags2, markedChannel.Flags2, 13)
|
|
if uint32(plainChannel.Flags) != uint32(markedChannel.Flags) {
|
|
t.Fatalf("channel flags word changed: before %032b after %032b",
|
|
uint32(plainChannel.Flags), uint32(markedChannel.Flags))
|
|
}
|
|
if markedChannel.Verified || markedChannel.Flags.Has(7) {
|
|
t.Fatalf("third-party mark leaked into official verified:flags.7: %+v", markedChannel)
|
|
}
|
|
|
|
markedFull := getFullChannelProjection(t, f.router, f.owner.ID, group)
|
|
mark, ok := markedFull.GetBotVerification()
|
|
if !ok {
|
|
t.Fatalf("channelFull bot_verification unset: %+v", markedFull)
|
|
}
|
|
if mark.BotID != f.bot.ID || mark.Icon != 8800003 || mark.Description != "Community partner" {
|
|
t.Fatalf("channelFull bot_verification = %+v, want verifier %d icon 8800003", mark, f.bot.ID)
|
|
}
|
|
assertFlagBitDelta(t, "channelFull", plainFull.Flags2, markedFull.Flags2, 17)
|
|
if uint32(plainFull.Flags) != uint32(markedFull.Flags) {
|
|
t.Fatalf("channelFull flags word changed: before %032b after %032b",
|
|
uint32(plainFull.Flags), uint32(markedFull.Flags))
|
|
}
|
|
|
|
if ok, err := f.router.onBotsSetCustomVerification(ownerCtx, setCustomVerificationRequest(
|
|
&tg.InputPeerChannel{ChannelID: group.ID, AccessHash: group.AccessHash},
|
|
inputUser(f.bot), false, "")); err != nil || !ok {
|
|
t.Fatalf("revoke on channel = %v,%v, want true,nil", ok, err)
|
|
}
|
|
if revoked := getChannelsChannel(t, f.router, f.owner.ID, group); revoked.Flags2.Has(13) {
|
|
t.Fatalf("revoked channel still carries flags2.13: %032b", uint32(revoked.Flags2))
|
|
}
|
|
if revokedFull := getFullChannelProjection(t, f.router, f.owner.ID, group); revokedFull.Flags2.Has(17) {
|
|
t.Fatalf("revoked channelFull still carries flags2.17: %032b", uint32(revokedFull.Flags2))
|
|
}
|
|
}
|
|
|
|
// TestChannelFullBotInfoCarriesVerifierSettingsInOneBatch covers the batched botInfo
|
|
// path (channelFull.bot_info): the verifier settings for the whole bot list must cost
|
|
// one query, not one per bot.
|
|
func TestChannelFullBotInfoCarriesVerifierSettingsInOneBatch(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newBotVerificationFixture(t, newFakeBotVerifications())
|
|
f.enableVerifier(f.bot.ID, 8800004, true)
|
|
group := f.botVerificationGroup(t, "Bot Group")
|
|
if _, err := f.bots.SetJoinGroups(ctx, f.bot.ID, true); err != nil {
|
|
t.Fatalf("enable join groups: %v", err)
|
|
}
|
|
if _, err := f.router.onChannelsInviteToChannel(WithUserID(ctx, f.owner.ID), &tg.ChannelsInviteToChannelRequest{
|
|
Channel: &tg.InputChannel{ChannelID: group.ID, AccessHash: group.AccessHash},
|
|
Users: []tg.InputUserClass{inputUser(f.bot)},
|
|
}); err != nil {
|
|
t.Fatalf("invite verifier bot: %v", err)
|
|
}
|
|
|
|
f.verify.settingsBatchCalls = 0
|
|
f.verify.settingsCalls = 0
|
|
full := getFullChannelProjection(t, f.router, f.owner.ID, group)
|
|
if len(full.BotInfo) != 1 || full.BotInfo[0].UserID != f.bot.ID {
|
|
t.Fatalf("channelFull bot_info = %+v, want the verifier bot", full.BotInfo)
|
|
}
|
|
settings, ok := full.BotInfo[0].GetVerifierSettings()
|
|
if !ok || !full.BotInfo[0].Flags.Has(9) {
|
|
t.Fatalf("channelFull bot_info verifier_settings unset: %+v", full.BotInfo[0])
|
|
}
|
|
if settings.Icon != 8800004 || settings.Company != "Acme Trust" {
|
|
t.Fatalf("channelFull verifier_settings = %+v", settings)
|
|
}
|
|
if f.verify.settingsBatchCalls != 1 || f.verify.settingsCalls != 0 {
|
|
t.Fatalf("verifier settings reads = batch %d / single %d, want batch 1 / single 0",
|
|
f.verify.settingsBatchCalls, f.verify.settingsCalls)
|
|
}
|
|
}
|
|
|
|
// TestChatInviteCarriesBotVerificationOnFlags13 pins chatInvite#5c9d3702
|
|
// bot_verification:flags.13 on the preview a non-member sees.
|
|
func TestChatInviteCarriesBotVerificationOnFlags13(t *testing.T) {
|
|
const channelID = int64(4242)
|
|
channel := domain.Channel{
|
|
ID: channelID, AccessHash: 42, Title: "Partner", Username: "partner",
|
|
Broadcast: true, ParticipantsCount: 11,
|
|
}
|
|
verify := newFakeBotVerifications()
|
|
r := New(Config{}, Deps{
|
|
Channels: &inviteBadgeChannels{result: domain.CheckChannelInviteResult{
|
|
Channel: channel,
|
|
Invite: domain.ChannelInvite{Hash: "hash", ChannelID: channelID},
|
|
}},
|
|
BotVerifications: verify,
|
|
}, zap.NewNop(), clock.System)
|
|
ctx := WithUserID(context.Background(), 1001)
|
|
|
|
preview := func() *tg.ChatInvite {
|
|
t.Helper()
|
|
res, err := r.onMessagesCheckChatInvite(ctx, "hash")
|
|
if err != nil {
|
|
t.Fatalf("check chat invite: %v", err)
|
|
}
|
|
decoded := &tg.ChatInvite{}
|
|
tlRoundTrip(t, res.(*tg.ChatInvite), decoded)
|
|
return decoded
|
|
}
|
|
|
|
plain := preview()
|
|
if _, ok := plain.GetBotVerification(); ok {
|
|
t.Fatalf("unmarked invite carries a mark: %+v", plain)
|
|
}
|
|
|
|
verify.marks[domain.Peer{Type: domain.PeerTypeChannel, ID: channelID}] = domain.CustomVerification{
|
|
VerifierBotID: 777000123, Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: channelID},
|
|
IconDocumentID: 8800005, Description: "Verified by Acme Trust",
|
|
}
|
|
marked := preview()
|
|
mark, ok := marked.GetBotVerification()
|
|
if !ok {
|
|
t.Fatalf("invite bot_verification unset: %+v", marked)
|
|
}
|
|
if mark.BotID != 777000123 || mark.Icon != 8800005 || mark.Description != "Verified by Acme Trust" {
|
|
t.Fatalf("invite bot_verification = %+v", mark)
|
|
}
|
|
assertFlagBitDelta(t, "chatInvite", plain.Flags, marked.Flags, 13)
|
|
// The official badge and the moderation warnings are untouched.
|
|
if marked.GetVerified() || marked.GetScam() || marked.GetFake() {
|
|
t.Fatalf("third-party mark leaked into the moderation flags: %+v", marked)
|
|
}
|
|
if !marked.Channel || !marked.Broadcast || !marked.Public ||
|
|
marked.Title != "Partner" || marked.ParticipantsCount != 11 {
|
|
t.Fatalf("invite lost unrelated fields: %+v", marked)
|
|
}
|
|
}
|
|
|
|
// TestBotVerificationDegradesWithoutService is the whole-feature degradation test:
|
|
// with no verification service wired, not one of the six flags may be set, and every
|
|
// response must stay exactly what it was before the feature existed.
|
|
func TestBotVerificationDegradesWithoutService(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newBotVerificationFixture(t, nil)
|
|
group := f.botVerificationGroup(t, "Plain Group")
|
|
if _, err := f.bots.SetJoinGroups(ctx, f.bot.ID, true); err != nil {
|
|
t.Fatalf("enable join groups: %v", err)
|
|
}
|
|
if _, err := f.router.onChannelsInviteToChannel(WithUserID(ctx, f.owner.ID), &tg.ChannelsInviteToChannelRequest{
|
|
Channel: &tg.InputChannel{ChannelID: group.ID, AccessHash: group.AccessHash},
|
|
Users: []tg.InputUserClass{inputUser(f.bot)},
|
|
}); err != nil {
|
|
t.Fatalf("invite bot: %v", err)
|
|
}
|
|
|
|
if user := getUsersUser(t, f.router, f.owner.ID, f.target); user.Flags2.Has(14) {
|
|
t.Fatalf("user set flags2.14 without a service: %032b", uint32(user.Flags2))
|
|
}
|
|
userFull, botInfo := getFullUserProjection(t, f.router, f.owner.ID, f.bot)
|
|
if userFull.Flags2.Has(12) {
|
|
t.Fatalf("userFull set flags2.12 without a service: %032b", uint32(userFull.Flags2))
|
|
}
|
|
if botInfo == nil {
|
|
t.Fatal("bot userFull carries no bot_info")
|
|
}
|
|
if botInfo.Flags.Has(9) {
|
|
t.Fatalf("botInfo set flags.9 without a service: %032b", uint32(botInfo.Flags))
|
|
}
|
|
if channel := getChannelsChannel(t, f.router, f.owner.ID, group); channel.Flags2.Has(13) {
|
|
t.Fatalf("channel set flags2.13 without a service: %032b", uint32(channel.Flags2))
|
|
}
|
|
full := getFullChannelProjection(t, f.router, f.owner.ID, group)
|
|
if full.Flags2.Has(17) {
|
|
t.Fatalf("channelFull set flags2.17 without a service: %032b", uint32(full.Flags2))
|
|
}
|
|
if len(full.BotInfo) != 1 || full.BotInfo[0].Flags.Has(9) {
|
|
t.Fatalf("channelFull bot_info set flags.9 without a service: %+v", full.BotInfo)
|
|
}
|
|
|
|
invite := New(Config{}, Deps{Channels: &inviteBadgeChannels{result: domain.CheckChannelInviteResult{
|
|
Channel: domain.Channel{ID: 4343, AccessHash: 43, Title: "Plain", Broadcast: true},
|
|
Invite: domain.ChannelInvite{Hash: "hash", ChannelID: 4343},
|
|
}}}, zaptest.NewLogger(t), clock.System)
|
|
res, err := invite.onMessagesCheckChatInvite(WithUserID(ctx, 1001), "hash")
|
|
if err != nil {
|
|
t.Fatalf("check chat invite: %v", err)
|
|
}
|
|
decodedInvite := &tg.ChatInvite{}
|
|
tlRoundTrip(t, res.(*tg.ChatInvite), decodedInvite)
|
|
if decodedInvite.Flags.Has(13) {
|
|
t.Fatalf("chatInvite set flags.13 without a service: %032b", uint32(decodedInvite.Flags))
|
|
}
|
|
}
|
|
|
|
// TestBotVerificationDegradesWhenServiceFails is the other half of the degradation
|
|
// contract: a failing read model must be indistinguishable from an unmarked peer, so
|
|
// a storage blip cannot turn a peer response into an error.
|
|
func TestBotVerificationDegradesWhenServiceFails(t *testing.T) {
|
|
verify := newFakeBotVerifications()
|
|
verify.err = context.DeadlineExceeded
|
|
f := newBotVerificationFixture(t, verify)
|
|
|
|
if user := getUsersUser(t, f.router, f.owner.ID, f.target); user.Flags2.Has(14) {
|
|
t.Fatalf("failing service still set flags2.14: %032b", uint32(user.Flags2))
|
|
}
|
|
full, botInfo := getFullUserProjection(t, f.router, f.owner.ID, f.bot)
|
|
if full.Flags2.Has(12) {
|
|
t.Fatalf("failing service still set flags2.12: %032b", uint32(full.Flags2))
|
|
}
|
|
if botInfo == nil || botInfo.Flags.Has(9) {
|
|
t.Fatalf("failing service still set botInfo flags.9: %+v", botInfo)
|
|
}
|
|
}
|
|
|
|
// TestUsersGetUsersResolvesBotVerificationInOneBatch pins the absence of an N+1: the
|
|
// icon overlay runs once per response over the whole user set, never once per user.
|
|
func TestUsersGetUsersResolvesBotVerificationInOneBatch(t *testing.T) {
|
|
verify := newFakeBotVerifications()
|
|
f := newBotVerificationFixture(t, verify)
|
|
verify.marks[domain.Peer{Type: domain.PeerTypeUser, ID: f.target.ID}] = domain.CustomVerification{
|
|
VerifierBotID: f.bot.ID, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: f.target.ID},
|
|
IconDocumentID: 8800006, Description: "Verified by Acme Trust",
|
|
}
|
|
verify.marks[domain.Peer{Type: domain.PeerTypeUser, ID: f.stranger.ID}] = domain.CustomVerification{
|
|
VerifierBotID: f.bot.ID, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: f.stranger.ID},
|
|
IconDocumentID: 8800007, Description: "Verified by Acme Trust",
|
|
}
|
|
verify.peerCalls = 0
|
|
verify.batchCalls = 0
|
|
|
|
out, err := f.router.onUsersGetUsers(WithUserID(context.Background(), f.owner.ID), []tg.InputUserClass{
|
|
&tg.InputUserSelf{},
|
|
&tg.InputUser{UserID: f.target.ID, AccessHash: f.target.AccessHash},
|
|
&tg.InputUser{UserID: f.stranger.ID, AccessHash: f.stranger.AccessHash},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("get users: %v", err)
|
|
}
|
|
if len(out) != 3 {
|
|
t.Fatalf("users = %d, want 3", len(out))
|
|
}
|
|
icons := map[int64]int64{}
|
|
for _, item := range out {
|
|
decoded := &tg.User{}
|
|
tlRoundTrip(t, item.(*tg.User), decoded)
|
|
if icon, ok := decoded.GetBotVerificationIcon(); ok {
|
|
if !decoded.Flags2.Has(14) {
|
|
t.Fatalf("icon set without flags2.14 on user %d: %032b", decoded.ID, uint32(decoded.Flags2))
|
|
}
|
|
icons[decoded.ID] = icon
|
|
}
|
|
}
|
|
if icons[f.target.ID] != 8800006 || icons[f.stranger.ID] != 8800007 {
|
|
t.Fatalf("projected icons = %v", icons)
|
|
}
|
|
if _, marked := icons[f.owner.ID]; marked {
|
|
t.Fatalf("unmarked self got an icon: %v", icons)
|
|
}
|
|
// Three users, one batch read: no N+1.
|
|
if verify.batchCalls != 1 || verify.peerCalls != 0 {
|
|
t.Fatalf("verification reads = batch %d / peer %d, want batch 1 / peer 0",
|
|
verify.batchCalls, verify.peerCalls)
|
|
}
|
|
}
|