owpengram-server/internal/rpc/bot_verification_flags_test.go
Egor Egorov fff8de783a
feat: add NFT usernames and bot verification (#22)
Implements collectible usernames, official verification workflows, and third-party bot verification after maintainer protocol and migration review.

The composite activity/moderation rating remains an admin-only read model; Telegram Stars Rating wire fields stay unset pending a dedicated official-semantics implementation.

Reviewed-Head: 2796345775ea0f908fb7734601e5e1dee4b653b9
Original-Head: fa082b892fd5180c9c9bc53c81c21cf5d250a75b

Co-authored-by: Egor Egorov <business.egor.sg@gmail.com>
2026-07-28 01:18:00 +08:00

616 lines
26 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))
}
}
// 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)
}
}