fix(usernames): sync index active collectible aliases

This commit is contained in:
iamxvbaba 2026-07-28 16:22:34 +08:00
parent 2f995f607a
commit 5156b17c1b
29 changed files with 657 additions and 112 deletions

View file

@ -234,12 +234,12 @@ 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},
{Username: "nft", Active: true, SortOrder: 0, CollectibleID: 7},
{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: "gem", Active: false, SortOrder: 0, CollectibleID: 8},
{Username: "gem4", Active: false, SortOrder: 1, CollectibleID: 8},
}
ctx := WithUserID(context.Background(), f.owner.ID)
@ -254,26 +254,29 @@ func TestUsersGetUsersProjectsCollectibleUsernamesInOneBatch(t *testing.T) {
t.Fatalf("users = %d, want 2", len(out))
}
self := out[0].(*tg.User)
if _, ok := self.GetUsername(); ok {
t.Fatalf("self scalar username is set together with collectible vector")
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] != "owner_slot" || got[1] != "nft" {
t.Fatalf("self usernames = %v, want [owner_slot nft]", got)
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("editable slot flags = %+v, want editable+active", vector[0])
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("collectible flags = %+v, want non-editable+active", vector[1])
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] != "gem" {
t.Fatalf("friend usernames = %v, want [friend_slot gem]", got)
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])
@ -663,7 +666,7 @@ func TestChannelsGetChannelsProjectsCollectibleUsernames(t *testing.T) {
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},
{Username: "chan_slot", Editable: true, Active: true, SortOrder: 1},
{Username: "chan_nft", Active: true, SortOrder: 0, CollectibleID: 44},
}
@ -681,11 +684,11 @@ func TestChannelsGetChannelsProjectsCollectibleUsernames(t *testing.T) {
if !ok {
t.Fatalf("channel usernames unset, want registry vector")
}
if got := usernameStrings(vector); len(got) != 2 || got[0] != "chan_slot" || got[1] != "chan_nft" {
t.Fatalf("channel usernames = %v, want [chan_slot chan_nft]", got)
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 != "" {
t.Fatalf("scalar channel username = %q (set %v), want absent with collectible vector", scalar, ok)
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)
}
}

View file

@ -13,6 +13,7 @@ import (
appprivacy "telesrv/internal/app/privacy"
appstories "telesrv/internal/app/stories"
appupdates "telesrv/internal/app/updates"
usernamesapp "telesrv/internal/app/usernames"
"telesrv/internal/app/userprojection"
appusers "telesrv/internal/app/users"
"telesrv/internal/domain"
@ -24,6 +25,8 @@ import (
func TestContactsSearchFindsUsers(t *testing.T) {
ctx := context.Background()
users := memory.NewUserStore()
registry := memory.NewCollectibleUsernameStore()
users.AttachUsernameRegistry(registry)
owner, err := users.Create(ctx, domain.User{AccessHash: 1, Phone: "15550000001", FirstName: "Owner"})
if err != nil {
t.Fatalf("create owner: %v", err)
@ -32,12 +35,29 @@ func TestContactsSearchFindsUsers(t *testing.T) {
if err != nil {
t.Fatalf("create friend: %v", err)
}
friendPeer := domain.Peer{Type: domain.PeerTypeUser, ID: friend.ID}
if _, err := registry.SetEditableUsername(ctx, friendPeer, friend.Username); err != nil {
t.Fatalf("seed editable username: %v", err)
}
if _, created, err := registry.MintCollectibleUsername(ctx, domain.MintCollectibleUsernameRequest{
Username: "nft4",
Owner: friendPeer,
Currency: domain.CollectibleCurrencyStars,
Amount: 1,
Actor: "test",
}); err != nil || !created {
t.Fatalf("mint collectible: created=%v err=%v", created, err)
}
r := New(Config{}, Deps{
Contacts: appcontacts.NewService(memory.NewContactStore(), users),
Usernames: usernamesapp.NewService(
usernamesapp.WithRegistryStore(registry),
usernamesapp.WithCollectibleStore(registry),
),
}, zaptest.NewLogger(t), clock.System)
var in bin.Buffer
if err := (&tg.ContactsSearchRequest{Q: "@search", Limit: 20}).Encode(&in); err != nil {
if err := (&tg.ContactsSearchRequest{Q: "@NFT4", Limit: 20}).Encode(&in); err != nil {
t.Fatalf("encode request: %v", err)
}
enc, err := r.Dispatch(WithUserID(ctx, owner.ID), [8]byte{}, 0, &in)
@ -55,6 +75,14 @@ func TestContactsSearchFindsUsers(t *testing.T) {
if !ok || peer.UserID != friend.ID {
t.Fatalf("peer = %T %+v, want friend", box.Results[0], box.Results[0])
}
user := box.Users[0].(*tg.User)
if scalar, ok := user.GetUsername(); !ok || scalar != "search_friend" {
t.Fatalf("search result scalar username = %q (set %v), want search_friend", scalar, ok)
}
vector, ok := user.GetUsernames()
if !ok || len(vector) != 2 || vector[1].Username != "nft4" || !vector[1].Active {
t.Fatalf("search result username vector = %+v (set %v), want active nft4 alias", vector, ok)
}
}
func TestContactsEditCloseFriendsProjectsUserFlag(t *testing.T) {

View file

@ -302,8 +302,16 @@ func applyUsernamesFromRegistry(users []tg.UserClass, chats []tg.ChatClass, byPe
continue
}
if vector := tgUsernamesFromRegistry(list, u.Username); len(vector) > 0 {
u.Flags.Unset(3)
u.Username = ""
// Layer 228 defines username as the main active username, not as a
// legacy alternative to usernames. TDesktop seeds its local search
// index from this scalar before consuming the complete vector, so
// both fields must be projected together.
if primary := domain.ActiveUsername(list); primary != "" {
u.SetUsername(primary)
} else {
u.Flags.Unset(3)
u.Username = ""
}
u.SetUsernames(vector)
}
}
@ -320,8 +328,12 @@ func applyUsernamesFromRegistry(users []tg.UserClass, chats []tg.ChatClass, byPe
// when unset, which is exactly the fallback tgUsernamesFromRegistry wants.
scalar, _ := ch.GetUsername()
if vector := tgUsernamesFromRegistry(list, scalar); len(vector) > 0 {
ch.Flags.Unset(6)
ch.Username = ""
if primary := domain.ActiveUsername(list); primary != "" {
ch.SetUsername(primary)
} else {
ch.Flags.Unset(6)
ch.Username = ""
}
ch.SetUsernames(vector)
}
}

View file

@ -26,8 +26,8 @@ func TestNotifyPeerUsernamesChangedUserPushesPreloadedVector(t *testing.T) {
}
registry := newFakeUsernameRegistry()
registry.byPeer[domain.Peer{Type: domain.PeerTypeUser, ID: targetID}] = []domain.Username{
{Username: "owner_slot", Editable: true, Active: true},
{Username: "nft", Active: true, CollectibleID: 7},
{Username: "owner_slot", Editable: true, Active: true, SortOrder: 1},
{Username: "nft4", Active: true, SortOrder: 0, CollectibleID: 7},
}
sessions := &captureSessions{onlineUserIDs: []int64{targetID, viewerID}}
r := New(Config{}, Deps{Users: users, Usernames: registry, Sessions: sessions}, zap.NewNop(), clock.System)
@ -42,11 +42,11 @@ func TestNotifyPeerUsernamesChangedUserPushesPreloadedVector(t *testing.T) {
}
updates := sessions.lastUserPush().(*tg.Updates)
user := updates.Users[0].(*tg.User)
if scalar, ok := user.GetUsername(); ok || scalar != "" {
t.Fatalf("pushed scalar username = %q (set %v), want absent", scalar, ok)
if scalar, ok := user.GetUsername(); !ok || scalar != "nft4" {
t.Fatalf("pushed scalar username = %q (set %v), want primary collectible nft4", scalar, ok)
}
vector, ok := user.GetUsernames()
if !ok || len(vector) != 2 || vector[1].Username != "nft" {
if !ok || len(vector) != 2 || vector[0].Username != "nft4" {
t.Fatalf("pushed username vector = %+v (set %v)", vector, ok)
}
}
@ -63,8 +63,8 @@ func TestNotifyPeerUsernamesChangedChannelPushesPreloadedVector(t *testing.T) {
}}
registry := newFakeUsernameRegistry()
registry.byPeer[domain.Peer{Type: domain.PeerTypeChannel, ID: channelID}] = []domain.Username{
{Username: "channel_slot", Editable: true, Active: true},
{Username: "collectible", Active: true, CollectibleID: 9},
{Username: "channel_slot", Editable: true, Active: true, SortOrder: 1},
{Username: "collectible", Active: true, SortOrder: 0, CollectibleID: 9},
}
sessions := &captureSessions{
onlineUserIDs: []int64{ownerID, memberID},
@ -84,11 +84,11 @@ func TestNotifyPeerUsernamesChangedChannelPushesPreloadedVector(t *testing.T) {
}
updates := sessions.lastUserPush().(*tg.Updates)
channel := updates.Chats[0].(*tg.Channel)
if scalar, ok := channel.GetUsername(); ok || scalar != "" {
t.Fatalf("pushed scalar username = %q (set %v), want absent", scalar, ok)
if scalar, ok := channel.GetUsername(); !ok || scalar != "collectible" {
t.Fatalf("pushed scalar username = %q (set %v), want primary collectible", scalar, ok)
}
vector, ok := channel.GetUsernames()
if !ok || len(vector) != 2 || vector[1].Username != "collectible" {
if !ok || len(vector) != 2 || vector[0].Username != "collectible" {
t.Fatalf("pushed username vector = %+v (set %v)", vector, ok)
}
}