owpengram-server/internal/rpc/collectible_usernames_rpc_test.go

995 lines
41 KiB
Go

package rpc
import (
"context"
"errors"
"reflect"
"strings"
"testing"
"time"
"github.com/iamxvbaba/td/bin"
"github.com/iamxvbaba/td/clock"
"github.com/iamxvbaba/td/tg"
"github.com/iamxvbaba/td/tgerr"
"github.com/iamxvbaba/td/tlprofile"
"go.uber.org/zap/zaptest"
appchannels "telesrv/internal/app/channels"
appusers "telesrv/internal/app/users"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
)
// fakeUsernameRegistry is an in-memory UsernameRegistryService. It enforces the
// same domain rules the real registry does, so the RPC tests exercise the actual
// error mapping rather than hand-rolled sentinels.
type fakeUsernameRegistry struct {
byPeer map[domain.Peer][]domain.Username
collectibles map[string]domain.CollectibleUsername
// err, when set, fails every read. Used for the degradation tests.
err error
// batchCalls / peerCalls count read fan-out so the tests can assert no N+1.
batchCalls int
peerCalls int
}
func newFakeUsernameRegistry() *fakeUsernameRegistry {
return &fakeUsernameRegistry{
byPeer: make(map[domain.Peer][]domain.Username),
collectibles: make(map[string]domain.CollectibleUsername),
}
}
func (f *fakeUsernameRegistry) PeerUsernames(_ context.Context, peer domain.Peer) ([]domain.Username, error) {
f.peerCalls++
if f.err != nil {
return nil, f.err
}
return f.byPeer[peer], nil
}
func (f *fakeUsernameRegistry) UsernamesBatch(_ context.Context, peers []domain.Peer) (map[domain.Peer][]domain.Username, error) {
f.batchCalls++
if f.err != nil {
return nil, f.err
}
out := make(map[domain.Peer][]domain.Username, len(peers))
for _, peer := range peers {
if list, ok := f.byPeer[peer]; ok {
out[peer] = list
}
}
return out, nil
}
func (f *fakeUsernameRegistry) ToggleUsername(_ context.Context, peer domain.Peer, username string, active bool) (bool, error) {
if f.err != nil {
return false, f.err
}
current := f.byPeer[peer]
if err := domain.ValidateUsernameToggle(current, username, active); err != nil {
return false, err
}
changed := false
next := append([]domain.Username(nil), current...)
for i := range next {
if next[i].Username != domain.NormalizeUsername(username) {
continue
}
if next[i].Active != active {
next[i].Active = active
changed = true
}
}
f.byPeer[peer] = next
return changed, nil
}
func (f *fakeUsernameRegistry) ReorderUsernames(_ context.Context, peer domain.Peer, order []string) (bool, error) {
if f.err != nil {
return false, f.err
}
current := f.byPeer[peer]
next, err := domain.ApplyUsernameReorder(current, order)
if err != nil {
return false, err
}
// Same contract as both real backends: the renumbering is always persisted,
// and "changed" is only about what a client can see.
changed := !domain.SameUsernameOrder(current, next)
f.byPeer[peer] = next
return changed, nil
}
func (f *fakeUsernameRegistry) DeactivateAllUsernames(_ context.Context, peer domain.Peer) (bool, error) {
if f.err != nil {
return false, f.err
}
next := append([]domain.Username(nil), f.byPeer[peer]...)
changed := false
for i := range next {
if next[i].Collectible() && next[i].Active {
next[i].Active = false
changed = true
}
}
f.byPeer[peer] = next
return changed, nil
}
func (f *fakeUsernameRegistry) Collectible(_ context.Context, username string) (domain.CollectibleUsername, error) {
if f.err != nil {
return domain.CollectibleUsername{}, f.err
}
// Usernames are case-insensitive in the registry, as they are in the store.
asset, ok := f.collectibles[strings.ToLower(domain.NormalizeUsername(username))]
if !ok {
return domain.CollectibleUsername{}, domain.ErrCollectibleUsernameNotFound
}
return asset, nil
}
var _ UsernameRegistryService = (*fakeUsernameRegistry)(nil)
func TestTgUsernamesFromRegistryFollowsStoredOrder(t *testing.T) {
// Legacy numbering, which is what every peer that never reordered carries:
// the editable slot and the first collectible share sort_order 0 and the
// editable slot wins the tie, so it still projects first.
assertUsernameVector(t,
[]domain.Username{
{Username: "second", Active: false, SortOrder: 1, CollectibleID: 22},
{Username: "editable_slot", Active: true, Editable: true, SortOrder: 0},
{Username: "first", Active: true, SortOrder: 0, CollectibleID: 11},
},
[]tg.Username{
{Editable: true, Active: true, Username: "editable_slot"},
{Editable: false, Active: true, Username: "first"},
{Editable: false, Active: false, Username: "second"},
})
// After a reorder made a collectible primary, stored order wins: clients read
// usernames[0] as the peer's primary username
// (core.telegram.org/api/fragment), so the editable slot must be able to
// leave that position.
assertUsernameVector(t,
[]domain.Username{
{Username: "second", Active: false, SortOrder: 2, CollectibleID: 22},
{Username: "editable_slot", Active: true, Editable: true, SortOrder: 1},
{Username: "first", Active: true, SortOrder: 0, CollectibleID: 11},
},
[]tg.Username{
{Editable: false, Active: true, Username: "first"},
{Editable: true, Active: true, Username: "editable_slot"},
{Editable: false, Active: false, Username: "second"},
})
}
func assertUsernameVector(t *testing.T, list []domain.Username, want []tg.Username) {
t.Helper()
got := tgUsernamesFromRegistry(list, "editable_slot")
if len(got) != len(want) {
t.Fatalf("usernames = %+v, want %+v", got, want)
}
for i := range want {
if got[i].Username != want[i].Username || got[i].Editable != want[i].Editable || got[i].Active != want[i].Active {
t.Fatalf("usernames[%d] = %+v, want %+v", i, got[i], want[i])
}
}
}
func TestTgUsernamesFromRegistryDegradesToScalar(t *testing.T) {
// Empty registry contribution must be byte-identical to the legacy vector.
if got, want := tgUsernamesFromRegistry(nil, "legacy"), tgUsernames("legacy"); len(got) != 1 || got[0] != want[0] {
t.Fatalf("empty registry = %+v, want %+v", got, want)
}
if got := tgUsernamesFromRegistry(nil, ""); got != nil {
t.Fatalf("empty registry with empty scalar = %+v, want nil", got)
}
// A registry list that normalizes away entirely also degrades rather than
// emitting an empty vector.
if got, want := tgUsernamesFromRegistry([]domain.Username{{Username: " "}}, "legacy"), tgUsernames("legacy"); len(got) != 1 || got[0] != want[0] {
t.Fatalf("blank registry rows = %+v, want %+v", got, want)
}
}
type usernameProjectionFixture struct {
router *Router
registry *fakeUsernameRegistry
owner domain.User
friend domain.User
}
func newUsernameProjectionFixture(t *testing.T, registry UsernameRegistryService) usernameProjectionFixture {
t.Helper()
ctx := context.Background()
userStore := memory.NewUserStore()
owner, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550002001", FirstName: "Owner", Username: "owner_slot"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
friend, err := userStore.Create(ctx, domain.User{AccessHash: 22, Phone: "15550002002", FirstName: "Friend", Username: "friend_slot"})
if err != nil {
t.Fatalf("create friend: %v", err)
}
fake, _ := registry.(*fakeUsernameRegistry)
return usernameProjectionFixture{
router: New(Config{}, Deps{
Users: appusers.NewService(userStore),
Usernames: registry,
}, zaptest.NewLogger(t), clock.System),
registry: fake,
owner: owner,
friend: friend,
}
}
func usernameStrings(list []tg.Username) []string {
out := make([]string, 0, len(list))
for _, item := range list {
out = append(out, item.Username)
}
return out
}
func TestUsersGetUsersProjectsCollectibleUsernamesInOneBatch(t *testing.T) {
registry := newFakeUsernameRegistry()
f := newUsernameProjectionFixture(t, registry)
registry.byPeer[domain.Peer{Type: domain.PeerTypeUser, ID: f.owner.ID}] = []domain.Username{
{Username: "owner_slot", Editable: true, Active: true, SortOrder: 1},
{Username: "nft4", Active: true, SortOrder: 0, CollectibleID: 7},
}
registry.byPeer[domain.Peer{Type: domain.PeerTypeUser, ID: f.friend.ID}] = []domain.Username{
{Username: "friend_slot", Editable: true, Active: true},
{Username: "gem4", Active: false, SortOrder: 1, CollectibleID: 8},
}
ctx := WithUserID(context.Background(), f.owner.ID)
out, err := f.router.onUsersGetUsers(ctx, []tg.InputUserClass{
&tg.InputUserSelf{},
&tg.InputUser{UserID: f.friend.ID, AccessHash: f.friend.AccessHash},
})
if err != nil {
t.Fatalf("get users: %v", err)
}
if len(out) != 2 {
t.Fatalf("users = %d, want 2", len(out))
}
self := out[0].(*tg.User)
if scalar, ok := self.GetUsername(); !ok || scalar != "nft4" {
t.Fatalf("self scalar username = %q (set %v), want primary collectible nft4", scalar, ok)
}
vector, ok := self.GetUsernames()
if !ok {
t.Fatalf("self usernames unset, want registry vector")
}
if got := usernameStrings(vector); len(got) != 2 || got[0] != "nft4" || got[1] != "owner_slot" {
t.Fatalf("self usernames = %v, want [nft4 owner_slot]", got)
}
if vector[0].Editable || !vector[0].Active {
t.Fatalf("primary collectible flags = %+v, want non-editable+active", vector[0])
}
if !vector[1].Editable || !vector[1].Active {
t.Fatalf("editable slot flags = %+v, want editable+active", vector[1])
}
friend := out[1].(*tg.User)
if scalar, ok := friend.GetUsername(); !ok || scalar != "friend_slot" {
t.Fatalf("friend scalar username = %q (set %v), want friend_slot", scalar, ok)
}
friendVector, _ := friend.GetUsernames()
if got := usernameStrings(friendVector); len(got) != 2 || got[1] != "gem4" {
t.Fatalf("friend usernames = %v, want [friend_slot gem4]", got)
}
if friendVector[1].Active {
t.Fatalf("inactive collectible projected active: %+v", friendVector[1])
}
// Two users, one batch read: no N+1.
if registry.batchCalls != 1 || registry.peerCalls != 0 {
t.Fatalf("registry reads = batch %d / peer %d, want batch 1 / peer 0", registry.batchCalls, registry.peerCalls)
}
}
func TestResolveUsernamePreservesCompleteUsernamesThroughLayer228(t *testing.T) {
registry := newFakeUsernameRegistry()
f := newUsernameProjectionFixture(t, registry)
registry.byPeer[domain.Peer{Type: domain.PeerTypeUser, ID: f.owner.ID}] = []domain.Username{
{Username: "owner_slot", Editable: true, Active: true, SortOrder: 0},
{Username: "owner_collectible_b", Active: true, SortOrder: 1, CollectibleID: 22},
{Username: "owner_collectible_a", Active: true, SortOrder: 2, CollectibleID: 21},
}
resolved, err := f.router.onContactsResolveUsername(
WithUserID(context.Background(), f.friend.ID),
&tg.ContactsResolveUsernameRequest{Username: "@owner_slot"},
)
if err != nil {
t.Fatalf("resolve username: %v", err)
}
assertResolvedUsernames := func(stage string, value *tg.ContactsResolvedPeer) {
t.Helper()
if value == nil || len(value.Users) != 1 {
t.Fatalf("%s resolved users = %+v, want one user", stage, value)
}
user, ok := value.Users[0].(*tg.User)
if !ok {
t.Fatalf("%s resolved user = %T, want *tg.User", stage, value.Users[0])
}
if scalar, set := user.GetUsername(); !set || scalar != "owner_slot" {
t.Fatalf("%s scalar username = %q (set %v), want owner_slot", stage, scalar, set)
}
vector, set := user.GetUsernames()
want := []string{"owner_slot", "owner_collectible_b", "owner_collectible_a"}
if !set || !reflect.DeepEqual(usernameStrings(vector), want) {
t.Fatalf("%s usernames = %v (set %v), want %v", stage, usernameStrings(vector), set, want)
}
}
assertResolvedUsernames("canonical", resolved)
var wire bin.Buffer
if err := tlprofile.EncodeObject(tlprofile.Profile228, resolved, &wire); err != nil {
t.Fatalf("encode Layer 228 resolved peer: %v", err)
}
decoded, err := tlprofile.DecodeObject(
tlprofile.Profile228,
&bin.Buffer{Buf: wire.Copy()},
tlprofile.Limits{},
)
if err != nil {
t.Fatalf("decode Layer 228 resolved peer: %v", err)
}
decodedResolved, ok := decoded.(*tg.ContactsResolvedPeer)
if !ok {
t.Fatalf("decoded Layer 228 object = %T, want *tg.ContactsResolvedPeer", decoded)
}
assertResolvedUsernames("Layer 228", decodedResolved)
requestBody := encodeExactLayerRPC(t, tlprofile.Profile228, &tg.ContactsResolveUsernameRequest{
Username: "@owner_slot",
})
admitted, err := f.router.AdmitLayer(tlprofile.Profile228, &requestBody, tlprofile.Limits{})
if err != nil {
t.Fatalf("admit Layer 228 contacts.resolveUsername: %v", err)
}
result, method, err := f.router.DispatchAdmitted(
WithUserID(context.Background(), f.friend.ID),
[8]byte{1},
1,
1,
1,
admitted,
)
if err != nil || method != "contacts.resolveUsername" {
t.Fatalf("dispatch Layer 228 method=%q err=%v", method, err)
}
var resultWire bin.Buffer
if err := result.Encode(&resultWire); err != nil {
t.Fatalf("encode Layer 228 method result: %v", err)
}
decodedResult, err := tlprofile.DecodeObject(
tlprofile.Profile228,
&bin.Buffer{Buf: resultWire.Copy()},
tlprofile.Limits{},
)
if err != nil {
t.Fatalf("decode Layer 228 method result: %v", err)
}
methodResolved, ok := decodedResult.(*tg.ContactsResolvedPeer)
if !ok {
t.Fatalf("decoded Layer 228 method result = %T, want *tg.ContactsResolvedPeer", decodedResult)
}
assertResolvedUsernames("Layer 228 method result", methodResolved)
}
func TestAuthLoginTokenSuccessProjectsCompleteSelfUsernames(t *testing.T) {
registry := newFakeUsernameRegistry()
f := newUsernameProjectionFixture(t, registry)
registry.byPeer[domain.Peer{Type: domain.PeerTypeUser, ID: f.owner.ID}] = []domain.Username{
{Username: "owner_slot", Editable: true, Active: true, SortOrder: 0},
{Username: "owner_collectible_b", Active: true, SortOrder: 1, CollectibleID: 22},
{Username: "owner_collectible_a", Active: true, SortOrder: 2, CollectibleID: 21},
}
result, err := f.router.authLoginTokenSuccess(context.Background(), domain.Authorization{UserID: f.owner.ID})
if err != nil {
t.Fatalf("auth login token success: %v", err)
}
success, ok := result.(*tg.AuthLoginTokenSuccess)
if !ok {
t.Fatalf("auth login token result = %T, want *tg.AuthLoginTokenSuccess", result)
}
authorization, ok := success.Authorization.(*tg.AuthAuthorization)
if !ok {
t.Fatalf("authorization = %T, want *tg.AuthAuthorization", success.Authorization)
}
self, ok := authorization.User.(*tg.User)
if !ok {
t.Fatalf("authorization user = %T, want *tg.User", authorization.User)
}
want := []string{"owner_slot", "owner_collectible_b", "owner_collectible_a"}
vector, set := self.GetUsernames()
if !set || !reflect.DeepEqual(usernameStrings(vector), want) {
t.Fatalf("authorization usernames = %v (set %v), want %v", usernameStrings(vector), set, want)
}
var wire bin.Buffer
if err := tlprofile.EncodeObject(tlprofile.Profile228, success, &wire); err != nil {
t.Fatalf("encode Layer 228 login token success: %v", err)
}
decoded, err := tlprofile.DecodeObject(tlprofile.Profile228, &bin.Buffer{Buf: wire.Copy()}, tlprofile.Limits{})
if err != nil {
t.Fatalf("decode Layer 228 login token success: %v", err)
}
decodedSuccess, ok := decoded.(*tg.AuthLoginTokenSuccess)
if !ok {
t.Fatalf("decoded login token success = %T", decoded)
}
decodedAuthorization := decodedSuccess.Authorization.(*tg.AuthAuthorization)
decodedSelf := decodedAuthorization.User.(*tg.User)
decodedVector, decodedSet := decodedSelf.GetUsernames()
if !decodedSet || !reflect.DeepEqual(usernameStrings(decodedVector), want) {
t.Fatalf("decoded authorization usernames = %v (set %v), want %v",
usernameStrings(decodedVector), decodedSet, want)
}
if registry.peerCalls != 1 || registry.batchCalls != 0 {
t.Fatalf("authorization username reads = peer:%d batch:%d, want 1/0",
registry.peerCalls, registry.batchCalls)
}
}
func TestMessageEchoProjectsCompleteUsernamesInOneBatch(t *testing.T) {
registry := newFakeUsernameRegistry()
f := newUsernameProjectionFixture(t, registry)
registry.byPeer[domain.Peer{Type: domain.PeerTypeUser, ID: f.owner.ID}] = []domain.Username{
{Username: "owner_slot", Editable: true, Active: true, SortOrder: 0},
{Username: "owner_collectible", Active: true, SortOrder: 1, CollectibleID: 21},
}
registry.byPeer[domain.Peer{Type: domain.PeerTypeUser, ID: f.friend.ID}] = []domain.Username{
{Username: "friend_slot", Editable: true, Active: true, SortOrder: 0},
{Username: "friend_collectible", Active: true, SortOrder: 1, CollectibleID: 22},
}
users := f.router.usersForMessageUpdate(context.Background(), f.owner.ID, domain.Message{
OwnerUserID: f.owner.ID,
From: domain.Peer{Type: domain.PeerTypeUser, ID: f.owner.ID},
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: f.friend.ID},
})
if len(users) != 2 {
t.Fatalf("message echo users = %d, want owner and friend", len(users))
}
want := map[int64][]string{
f.owner.ID: {"owner_slot", "owner_collectible"},
f.friend.ID: {"friend_slot", "friend_collectible"},
}
for _, item := range users {
user, ok := item.(*tg.User)
if !ok {
t.Fatalf("message echo user = %T, want *tg.User", item)
}
vector, set := user.GetUsernames()
if !set || !reflect.DeepEqual(usernameStrings(vector), want[user.ID]) {
t.Fatalf("user %d usernames = %v (set %v), want %v", user.ID, usernameStrings(vector), set, want[user.ID])
}
}
if registry.batchCalls != 1 || registry.peerCalls != 0 {
t.Fatalf("registry reads = batch %d / peer %d, want one batch read for the response", registry.batchCalls, registry.peerCalls)
}
}
func TestChannelMessageUpdatesProjectCompleteUsernames(t *testing.T) {
const (
viewerUserID = int64(1001)
senderUserID = int64(1002)
channelID = int64(2001)
)
registry := newFakeUsernameRegistry()
registry.byPeer[domain.Peer{Type: domain.PeerTypeUser, ID: senderUserID}] = []domain.Username{
{Username: "channel_sender", Editable: true, Active: true, SortOrder: 0},
{Username: "channel_collectible", Active: true, SortOrder: 1, CollectibleID: 31},
}
router := New(Config{}, Deps{
Users: mapUsersService{users: map[int64]domain.User{
senderUserID: {ID: senderUserID, FirstName: "Sender", Username: "channel_sender"},
}},
Usernames: registry,
}, zaptest.NewLogger(t), clock.System)
message := domain.ChannelMessage{
ID: 41,
ChannelID: channelID,
SenderUserID: senderUserID,
From: domain.Peer{Type: domain.PeerTypeUser, ID: senderUserID},
Date: 1700000500,
Pts: 9,
}
updates := router.channelMessageUpdatesWithPeerCache(context.Background(), viewerUserID, domain.SendChannelMessageResult{
Channel: domain.Channel{ID: channelID, AccessHash: 22, Title: "group", Megagroup: true, Date: 1700000000},
Message: message,
Event: domain.ChannelUpdateEvent{
ChannelID: channelID,
Type: domain.ChannelUpdateNewMessage,
Pts: 9,
PtsCount: 1,
Date: message.Date,
Message: message,
},
}, 0, newViewerPeerCache(router))
if updates == nil || len(updates.Users) != 1 {
t.Fatalf("channel updates users = %+v, want one sender", updates)
}
user := updates.Users[0].(*tg.User)
vector, set := user.GetUsernames()
if !set || !reflect.DeepEqual(usernameStrings(vector), []string{"channel_sender", "channel_collectible"}) {
t.Fatalf("channel sender usernames = %v (set %v), want complete vector", usernameStrings(vector), set)
}
if registry.peerCalls != 0 || registry.batchCalls != 1 {
t.Fatalf("registry reads = peer %d / batch %d, want one batched user+channel read", registry.peerCalls, registry.batchCalls)
}
}
func TestChannelDifferenceProjectsCompleteUsernames(t *testing.T) {
const (
viewerUserID = int64(1001)
senderUserID = int64(1002)
channelID = int64(2001)
)
registry := newFakeUsernameRegistry()
registry.byPeer[domain.Peer{Type: domain.PeerTypeUser, ID: senderUserID}] = []domain.Username{
{Username: "difference_sender", Editable: true, Active: true, SortOrder: 0},
{Username: "difference_collectible", Active: true, SortOrder: 1, CollectibleID: 41},
}
router := New(Config{}, Deps{Usernames: registry}, zaptest.NewLogger(t), clock.System)
out := router.tgChannelDifference(context.Background(), viewerUserID, domain.ChannelDifference{
Final: true,
Pts: 9,
Channel: domain.Channel{ID: channelID, AccessHash: 22, Title: "group", Megagroup: true, Date: 1700000000},
Self: domain.ChannelMember{ChannelID: channelID, UserID: viewerUserID, Status: domain.ChannelMemberActive},
Users: []domain.User{{
ID: senderUserID,
FirstName: "Sender",
Username: "difference_sender",
}},
NewMessages: []domain.ChannelMessage{{
ID: 51,
ChannelID: channelID,
SenderUserID: senderUserID,
From: domain.Peer{Type: domain.PeerTypeUser, ID: senderUserID},
Date: 1700000600,
Pts: 9,
}},
})
diff, ok := out.(*tg.UpdatesChannelDifference)
if !ok || len(diff.Users) != 1 {
t.Fatalf("channel difference = %T %+v, want one user", out, out)
}
user := diff.Users[0].(*tg.User)
vector, set := user.GetUsernames()
if !set || !reflect.DeepEqual(usernameStrings(vector), []string{"difference_sender", "difference_collectible"}) {
t.Fatalf("channel difference usernames = %v (set %v), want complete vector", usernameStrings(vector), set)
}
if registry.batchCalls != 1 || registry.peerCalls != 0 {
t.Fatalf("registry reads = batch %d / peer %d, want one batched user+channel read", registry.batchCalls, registry.peerCalls)
}
}
func TestUsersGetUsersDegradesWithoutRegistry(t *testing.T) {
f := newUsernameProjectionFixture(t, nil)
ctx := WithUserID(context.Background(), f.owner.ID)
out, err := f.router.onUsersGetUsers(ctx, []tg.InputUserClass{&tg.InputUserSelf{}})
if err != nil {
t.Fatalf("get users: %v", err)
}
self := out[0].(*tg.User)
// Without a collectible registry the official legacy shape is scalar-only.
self.SetFlags()
vector, ok := self.GetUsernames()
if ok || len(vector) != 0 {
t.Fatalf("usernames = %+v (set %v), want vector absent", vector, ok)
}
if scalar, ok := self.GetUsername(); !ok || scalar != "owner_slot" {
t.Fatalf("scalar username = %q (set %v), want owner_slot", scalar, ok)
}
}
func TestUsersGetUsersDegradesWhenRegistryFails(t *testing.T) {
registry := newFakeUsernameRegistry()
registry.err = errors.New("registry unavailable")
f := newUsernameProjectionFixture(t, registry)
ctx := WithUserID(context.Background(), f.owner.ID)
out, err := f.router.onUsersGetUsers(ctx, []tg.InputUserClass{
&tg.InputUserSelf{},
&tg.InputUser{UserID: f.friend.ID, AccessHash: f.friend.AccessHash},
})
if err != nil {
t.Fatalf("get users must not fail when the registry does: %v", err)
}
if len(out) != 2 {
t.Fatalf("users = %d, want 2", len(out))
}
for i, want := range []string{"owner_slot", "friend_slot"} {
user := out[i].(*tg.User)
user.SetFlags()
vector, ok := user.GetUsernames()
if ok || len(vector) != 0 {
t.Fatalf("users[%d] usernames = %+v, want vector absent", i, vector)
}
if scalar, ok := user.GetUsername(); !ok || scalar != want {
t.Fatalf("users[%d] scalar username = %q (set %v), want %q", i, scalar, ok, want)
}
}
}
func TestFragmentGetCollectibleInfoRPC(t *testing.T) {
registry := newFakeUsernameRegistry()
f := newUsernameProjectionFixture(t, registry)
peer := domain.Peer{Type: domain.PeerTypeUser, ID: f.owner.ID}
registry.collectibles["nfts"] = domain.CollectibleUsername{
ID: 7,
Username: "nfts",
Status: domain.CollectibleUsernameStatusOwned,
Owner: peer,
PurchaseDate: time.Unix(1700000000, 0),
Currency: domain.CollectibleCurrencyUSD,
Amount: 550000,
CryptoCurrency: domain.CollectibleCryptoCurrencyTON,
CryptoAmount: 1200000000,
URL: "https://fragment.example/username/nfts",
}
registry.byPeer[peer] = []domain.Username{{Username: "nfts", Active: false, CollectibleID: 7}}
ctx := WithUserID(context.Background(), f.owner.ID)
info, err := f.router.onFragmentGetCollectibleInfo(ctx, &tg.FragmentGetCollectibleInfoRequest{
Collectible: &tg.InputCollectibleUsername{Username: "@NFTS"},
})
if err != nil {
t.Fatalf("get collectible info: %v", err)
}
if info.PurchaseDate != 1700000000 || info.Currency != domain.CollectibleCurrencyUSD || info.Amount != 550000 {
t.Fatalf("collectible info = %+v, want the stored purchase record", info)
}
if info.CryptoCurrency != domain.CollectibleCryptoCurrencyTON || info.CryptoAmount != 1200000000 {
t.Fatalf("collectible crypto = %s/%d, want TON/1200000000", info.CryptoCurrency, info.CryptoAmount)
}
if info.URL != "https://fragment.example/username/nfts" {
t.Fatalf("collectible url = %q", info.URL)
}
// Inactive collectibles are private to their current owner.
friendCtx := WithUserID(context.Background(), f.friend.ID)
if _, err := f.router.onFragmentGetCollectibleInfo(friendCtx, &tg.FragmentGetCollectibleInfoRequest{
Collectible: &tg.InputCollectibleUsername{Username: "nfts"},
}); !tgerr.Is(err, "COLLECTIBLE_NOT_FOUND") {
t.Fatalf("inactive collectible seen by another user: %v, want COLLECTIBLE_NOT_FOUND", err)
}
registry.byPeer[peer][0].Active = true
if _, err := f.router.onFragmentGetCollectibleInfo(friendCtx, &tg.FragmentGetCollectibleInfoRequest{
Collectible: &tg.InputCollectibleUsername{Username: "nfts"},
}); err != nil {
t.Fatalf("active collectible hidden from another user: %v", err)
}
// A name with no collectible asset behind it is not occupied as a collectible.
if _, err := f.router.onFragmentGetCollectibleInfo(ctx, &tg.FragmentGetCollectibleInfoRequest{
Collectible: &tg.InputCollectibleUsername{Username: "owner_slot"},
}); !tgerr.Is(err, "COLLECTIBLE_NOT_FOUND") {
t.Fatalf("non-collectible username err = %v, want COLLECTIBLE_NOT_FOUND", err)
}
// Syntactically impossible name is rejected before the registry is consulted.
if _, err := f.router.onFragmentGetCollectibleInfo(ctx, &tg.FragmentGetCollectibleInfoRequest{
Collectible: &tg.InputCollectibleUsername{Username: "a"},
}); !tgerr.Is(err, "COLLECTIBLE_INVALID") {
t.Fatalf("malformed username err = %v, want COLLECTIBLE_INVALID", err)
}
// Collectible phones do not exist in this server.
if _, err := f.router.onFragmentGetCollectibleInfo(ctx, &tg.FragmentGetCollectibleInfoRequest{
Collectible: &tg.InputCollectiblePhone{Phone: "15550002001"},
}); !tgerr.Is(err, "COLLECTIBLE_NOT_FOUND") {
t.Fatalf("collectible phone err = %v, want COLLECTIBLE_NOT_FOUND", err)
}
}
func TestFragmentGetCollectibleInfoWithoutRegistry(t *testing.T) {
f := newUsernameProjectionFixture(t, nil)
ctx := WithUserID(context.Background(), f.owner.ID)
if _, err := f.router.onFragmentGetCollectibleInfo(ctx, &tg.FragmentGetCollectibleInfoRequest{
Collectible: &tg.InputCollectibleUsername{Username: "owner_slot"},
}); !tgerr.Is(err, "COLLECTIBLE_NOT_FOUND") {
t.Fatalf("no registry err = %v, want COLLECTIBLE_NOT_FOUND", err)
}
}
func TestAccountToggleAndReorderUsernamesRPC(t *testing.T) {
registry := newFakeUsernameRegistry()
f := newUsernameProjectionFixture(t, registry)
peer := domain.Peer{Type: domain.PeerTypeUser, ID: f.owner.ID}
registry.byPeer[peer] = []domain.Username{
{Username: "owner_slot", Editable: true, Active: true},
{Username: "alpha", Active: true, SortOrder: 0, CollectibleID: 1},
{Username: "bravo", Active: true, SortOrder: 1, CollectibleID: 2},
}
ctx := WithUserID(context.Background(), f.owner.ID)
if ok, err := f.router.onAccountToggleUsername(ctx, &tg.AccountToggleUsernameRequest{Username: "alpha", Active: false}); !ok || err != nil {
t.Fatalf("toggle collectible = %v,%v, want true,nil", ok, err)
}
if list := registry.byPeer[peer]; list[1].Active {
t.Fatalf("alpha still active after toggle: %+v", list)
}
// Toggling the same value again changes nothing.
if ok, err := f.router.onAccountToggleUsername(ctx, &tg.AccountToggleUsernameRequest{Username: "alpha", Active: false}); ok || !tgerr.Is(err, "USERNAME_NOT_MODIFIED") {
t.Fatalf("idempotent toggle = %v,%v, want false,USERNAME_NOT_MODIFIED", ok, err)
}
// The editable slot is not a collectible: account.updateUsername owns it.
if ok, err := f.router.onAccountToggleUsername(ctx, &tg.AccountToggleUsernameRequest{Username: "owner_slot", Active: false}); ok || !tgerr.Is(err, "USERNAME_INVALID") {
t.Fatalf("toggle editable slot = %v,%v, want false,USERNAME_INVALID", ok, err)
}
// An unknown name is not occupied.
if ok, err := f.router.onAccountToggleUsername(ctx, &tg.AccountToggleUsernameRequest{Username: "ghost", Active: true}); ok || !tgerr.Is(err, "USERNAME_NOT_OCCUPIED") {
t.Fatalf("toggle unknown = %v,%v, want false,USERNAME_NOT_OCCUPIED", ok, err)
}
// alpha is inactive at this point, so the order does not have to mention it.
if ok, err := f.router.onAccountReorderUsernames(ctx, &tg.AccountReorderUsernamesRequest{Order: []string{"owner_slot", "bravo"}}); !ok || err != nil {
t.Fatalf("reorder = %v,%v, want true,nil", ok, err)
}
list := domain.SortUsernames(registry.byPeer[peer])
if len(list) != 3 || !list[0].Editable || list[1].Username != "bravo" || list[2].Username != "alpha" {
t.Fatalf("order after reorder = %+v, want editable, bravo, alpha", list)
}
// The editable slot may lead the order or follow a collectible.
if ok, err := f.router.onAccountReorderUsernames(ctx, &tg.AccountReorderUsernamesRequest{Order: []string{"bravo", "owner_slot"}}); !ok || err != nil {
t.Fatalf("reorder with a collectible first = %v,%v, want true,nil", ok, err)
}
if list := domain.SortUsernames(registry.byPeer[peer]); list[0].Username != "bravo" || !list[1].Editable {
t.Fatalf("order after promoting a collectible = %+v, want bravo, editable, alpha", list)
}
// An order that omits an active username is still rejected.
if ok, err := f.router.onAccountReorderUsernames(ctx, &tg.AccountReorderUsernamesRequest{Order: []string{"bravo"}}); ok || !tgerr.Is(err, "ORDER_INVALID") {
t.Fatalf("partial reorder = %v,%v, want false,ORDER_INVALID", ok, err)
}
// So is one naming something the peer does not own.
if ok, err := f.router.onAccountReorderUsernames(ctx, &tg.AccountReorderUsernamesRequest{Order: []string{"owner_slot", "bravo", "ghost"}}); ok || !tgerr.Is(err, "ORDER_INVALID") {
t.Fatalf("reorder with an unknown name = %v,%v, want false,ORDER_INVALID", ok, err)
}
if ok, err := f.router.onAccountReorderUsernames(ctx, &tg.AccountReorderUsernamesRequest{
Order: make([]string, domain.MaxPeerCollectibleUsernames+2),
}); ok || !tgerr.Is(err, "LIMIT_INVALID") {
t.Fatalf("oversized reorder = %v,%v, want false,LIMIT_INVALID", ok, err)
}
}
func TestAccountUsernameManagementWithoutRegistry(t *testing.T) {
f := newUsernameProjectionFixture(t, nil)
ctx := WithUserID(context.Background(), f.owner.ID)
if ok, err := f.router.onAccountToggleUsername(ctx, &tg.AccountToggleUsernameRequest{Username: "owner_slot", Active: true}); ok || !tgerr.Is(err, "USERNAME_NOT_MODIFIED") {
t.Fatalf("toggle without registry = %v,%v, want false,USERNAME_NOT_MODIFIED", ok, err)
}
if ok, err := f.router.onAccountReorderUsernames(ctx, &tg.AccountReorderUsernamesRequest{Order: []string{"owner_slot"}}); ok || !tgerr.Is(err, "USERNAME_NOT_MODIFIED") {
t.Fatalf("reorder without registry = %v,%v, want false,USERNAME_NOT_MODIFIED", ok, err)
}
}
// TestCollectibleUsernameRPCsAreRegistered proves the three previously missing
// methods reach a handler through the real dispatcher rather than the unregistered
// fallback: an ordinary client calls them by wire constructor, not by Go method.
func TestCollectibleUsernameRPCsAreRegistered(t *testing.T) {
f := newUsernameProjectionFixture(t, nil)
ctx := WithUserID(context.Background(), f.owner.ID)
cases := []struct {
name string
req bin.Encoder
want string
}{
{name: "account.reorderUsernames", req: &tg.AccountReorderUsernamesRequest{Order: []string{"owner_slot"}}, want: "USERNAME_NOT_MODIFIED"},
{name: "account.toggleUsername", req: &tg.AccountToggleUsernameRequest{Username: "owner_slot", Active: true}, want: "USERNAME_NOT_MODIFIED"},
{name: "fragment.getCollectibleInfo", req: &tg.FragmentGetCollectibleInfoRequest{Collectible: &tg.InputCollectibleUsername{Username: "owner_slot"}}, want: "COLLECTIBLE_NOT_FOUND"},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
var in bin.Buffer
if err := tt.req.Encode(&in); err != nil {
t.Fatalf("encode request: %v", err)
}
if _, err := f.router.Dispatch(ctx, [8]byte{}, 0, &in); !tgerr.Is(err, tt.want) {
t.Fatalf("dispatch err = %v, want %s", err, tt.want)
}
})
}
}
func newCollectibleChannelFixture(t *testing.T, registry UsernameRegistryService) (*Router, domain.User, *tg.Channel) {
t.Helper()
ctx := context.Background()
userStore := memory.NewUserStore()
owner, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550003001", FirstName: "Owner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
channelStore := memory.NewChannelStore()
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: appchannels.NewService(channelStore),
Usernames: registry,
}, zaptest.NewLogger(t), clock.System)
ownerCtx := WithUserID(ctx, owner.ID)
created, err := r.onChannelsCreateChannel(ownerCtx, &tg.ChannelsCreateChannelRequest{
Broadcast: true,
Title: "Collectible Channel",
About: "about",
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
channel := created.(*tg.Updates).Chats[0].(*tg.Channel)
if _, err := r.onChannelsUpdateUsername(ownerCtx, &tg.ChannelsUpdateUsernameRequest{
Channel: &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
Username: "chan_slot",
}); err != nil {
t.Fatalf("update channel username: %v", err)
}
return r, owner, channel
}
func TestChannelsUsernameManagementUsesRegistry(t *testing.T) {
registry := newFakeUsernameRegistry()
r, owner, channel := newCollectibleChannelFixture(t, registry)
ctx := WithUserID(context.Background(), owner.ID)
input := &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID}
registry.byPeer[peer] = []domain.Username{
{Username: "chan_slot", Editable: true, Active: true},
{Username: "alpha", Active: true, SortOrder: 0, CollectibleID: 31},
{Username: "bravo", Active: true, SortOrder: 1, CollectibleID: 32},
}
if ok, err := r.onChannelsToggleUsername(ctx, &tg.ChannelsToggleUsernameRequest{Channel: input, Username: "alpha", Active: false}); !ok || err != nil {
t.Fatalf("toggle channel collectible = %v,%v, want true,nil", ok, err)
}
if ok, err := r.onChannelsToggleUsername(ctx, &tg.ChannelsToggleUsernameRequest{Channel: input, Username: "alpha", Active: false}); ok || !tgerr.Is(err, "USERNAME_NOT_MODIFIED") {
t.Fatalf("idempotent channel toggle = %v,%v, want false,USERNAME_NOT_MODIFIED", ok, err)
}
if ok, err := r.onChannelsToggleUsername(ctx, &tg.ChannelsToggleUsernameRequest{Channel: input, Username: "chan_slot", Active: false}); ok || !tgerr.Is(err, "USERNAME_INVALID") {
t.Fatalf("toggle channel editable slot = %v,%v, want false,USERNAME_INVALID", ok, err)
}
// alpha was just deactivated, so the order carries only the active names.
if ok, err := r.onChannelsReorderUsernames(ctx, &tg.ChannelsReorderUsernamesRequest{Channel: input, Order: []string{"chan_slot", "bravo"}}); !ok || err != nil {
t.Fatalf("reorder channel usernames = %v,%v, want true,nil", ok, err)
}
if list := domain.SortUsernames(registry.byPeer[peer]); list[1].Username != "bravo" {
t.Fatalf("channel order = %+v, want bravo first collectible", list)
}
if ok, err := r.onChannelsDeactivateAllUsernames(ctx, input); !ok || err != nil {
t.Fatalf("deactivate all = %v,%v, want true,nil", ok, err)
}
for _, item := range registry.byPeer[peer] {
if item.Collectible() && item.Active {
t.Fatalf("collectible still active after deactivateAll: %+v", item)
}
}
// Deactivate-all is idempotent, while reorder follows the documented
// USERNAME_NOT_MODIFIED result for an unchanged order.
if ok, err := r.onChannelsDeactivateAllUsernames(ctx, input); !ok || err != nil {
t.Fatalf("repeat deactivate all = %v,%v, want true,nil", ok, err)
}
if ok, err := r.onChannelsReorderUsernames(ctx, &tg.ChannelsReorderUsernamesRequest{Channel: input, Order: []string{"chan_slot"}}); ok || !tgerr.Is(err, "USERNAME_NOT_MODIFIED") {
t.Fatalf("repeat reorder after deactivateAll = %v,%v, want false,USERNAME_NOT_MODIFIED", ok, err)
}
// A non-admin must not reach the registry at all.
other := WithUserID(context.Background(), owner.ID+9999)
if _, err := r.onChannelsToggleUsername(other, &tg.ChannelsToggleUsernameRequest{Channel: input, Username: "alpha", Active: true}); err == nil {
t.Fatalf("toggle as stranger err = nil, want permission failure")
}
}
func TestChannelsUsernameManagementWithoutRegistryKeepsLegacyAnswers(t *testing.T) {
r, owner, channel := newCollectibleChannelFixture(t, nil)
ctx := WithUserID(context.Background(), owner.ID)
input := &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
if ok, err := r.onChannelsToggleUsername(ctx, &tg.ChannelsToggleUsernameRequest{Channel: input, Username: "chan_slot", Active: true}); !ok || err != nil {
t.Fatalf("toggle without registry = %v,%v, want true,nil", ok, err)
}
if ok, err := r.onChannelsReorderUsernames(ctx, &tg.ChannelsReorderUsernamesRequest{Channel: input, Order: []string{"chan_slot"}}); !ok || err != nil {
t.Fatalf("reorder without registry = %v,%v, want true,nil", ok, err)
}
if ok, err := r.onChannelsDeactivateAllUsernames(ctx, input); !ok || err != nil {
t.Fatalf("deactivate without registry = %v,%v, want true,nil", ok, err)
}
}
// TestChannelsDeactivateAllUsernamesIsIdempotent covers the report "I created a
// channel without a username and later could not set one": Telegram Desktop
// drives its channel-username editor by calling channels.deactivateAllUsernames
// first and only continuing when it answers true. A channel that owns no
// collectible username at all -- every freshly created channel -- has nothing to
// deactivate, and answering USERNAME_NOT_MODIFIED there aborted the whole flow
// client-side. Deactivating an empty set is success.
func TestChannelsDeactivateAllUsernamesIsIdempotent(t *testing.T) {
registry := newFakeUsernameRegistry()
r, owner, channel := newCollectibleChannelFixture(t, registry)
ctx := WithUserID(context.Background(), owner.ID)
input := &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID}
// A channel with only its editable slot: no collectible username exists.
registry.byPeer[peer] = []domain.Username{{Username: "chan_slot", Editable: true, Active: true}}
for attempt := 0; attempt < 2; attempt++ {
if ok, err := r.onChannelsDeactivateAllUsernames(ctx, input); !ok || err != nil {
t.Fatalf("deactivate all with nothing collectible (attempt %d) = %v,%v, want true,nil", attempt, ok, err)
}
}
// The whole visible list is just the editable slot, which is exactly what
// Telegram Desktop sends here.
if ok, err := r.onChannelsReorderUsernames(ctx, &tg.ChannelsReorderUsernamesRequest{Channel: input, Order: []string{"chan_slot"}}); ok || !tgerr.Is(err, "USERNAME_NOT_MODIFIED") {
t.Fatalf("reorder with nothing collectible = %v,%v, want false,USERNAME_NOT_MODIFIED", ok, err)
}
// The editable slot is untouched by either call: only collectibles are hidden.
if len(registry.byPeer[peer]) != 1 || !registry.byPeer[peer][0].Active {
t.Fatalf("editable slot after bulk calls = %+v, want it left active", registry.byPeer[peer])
}
// A channel with no rows at all behaves the same way.
delete(registry.byPeer, peer)
if ok, err := r.onChannelsDeactivateAllUsernames(ctx, input); !ok || err != nil {
t.Fatalf("deactivate all on an empty registry = %v,%v, want true,nil", ok, err)
}
// Permission is still checked before the registry is consulted.
stranger := WithUserID(context.Background(), owner.ID+4242)
if ok, err := r.onChannelsDeactivateAllUsernames(stranger, input); ok || err == nil {
t.Fatalf("deactivate all as stranger = %v,%v, want false and a permission failure", ok, err)
}
}
func TestChannelsGetChannelsProjectsCollectibleUsernames(t *testing.T) {
registry := newFakeUsernameRegistry()
r, owner, channel := newCollectibleChannelFixture(t, registry)
ctx := WithUserID(context.Background(), owner.ID)
registry.byPeer[domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID}] = []domain.Username{
{Username: "chan_slot", Editable: true, Active: true, SortOrder: 1},
{Username: "chan_nft", Active: true, SortOrder: 0, CollectibleID: 44},
}
chats, err := r.onChannelsGetChannels(ctx, []tg.InputChannelClass{
&tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
})
if err != nil {
t.Fatalf("get channels: %v", err)
}
out := chats.(*tg.MessagesChats).Chats
if len(out) != 1 {
t.Fatalf("chats = %d, want 1", len(out))
}
vector, ok := out[0].(*tg.Channel).GetUsernames()
if !ok {
t.Fatalf("channel usernames unset, want registry vector")
}
if got := usernameStrings(vector); len(got) != 2 || got[0] != "chan_nft" || got[1] != "chan_slot" {
t.Fatalf("channel usernames = %v, want [chan_nft chan_slot]", got)
}
if scalar, ok := out[0].(*tg.Channel).GetUsername(); !ok || scalar != "chan_nft" {
t.Fatalf("scalar channel username = %q (set %v), want primary collectible chan_nft", scalar, ok)
}
}
func TestChannelsGetChannelsDegradesWithoutRegistry(t *testing.T) {
r, owner, channel := newCollectibleChannelFixture(t, nil)
ctx := WithUserID(context.Background(), owner.ID)
chats, err := r.onChannelsGetChannels(ctx, []tg.InputChannelClass{
&tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
})
if err != nil {
t.Fatalf("get channels: %v", err)
}
vector, ok := chats.(*tg.MessagesChats).Chats[0].(*tg.Channel).GetUsernames()
if ok || len(vector) != 0 {
t.Fatalf("legacy channel usernames = %+v (set %v), want vector absent", vector, ok)
}
if scalar, ok := chats.(*tg.MessagesChats).Chats[0].(*tg.Channel).GetUsername(); !ok || scalar != "chan_slot" {
t.Fatalf("legacy scalar username = %q (set %v), want chan_slot", scalar, ok)
}
}