1287 lines
50 KiB
Go
1287 lines
50 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"github.com/gotd/td/bin"
|
|
"github.com/gotd/td/clock"
|
|
"github.com/gotd/td/tg"
|
|
"go.uber.org/zap/zaptest"
|
|
"reflect"
|
|
"strings"
|
|
appchannels "telesrv/internal/app/channels"
|
|
appcontacts "telesrv/internal/app/contacts"
|
|
appstories "telesrv/internal/app/stories"
|
|
appupdates "telesrv/internal/app/updates"
|
|
appusers "telesrv/internal/app/users"
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/store/memory"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestContactsSearchFindsUsers(t *testing.T) {
|
|
ctx := context.Background()
|
|
users := memory.NewUserStore()
|
|
owner, err := users.Create(ctx, domain.User{AccessHash: 1, Phone: "15550000001", FirstName: "Owner"})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
friend, err := users.Create(ctx, domain.User{AccessHash: 2, Phone: "15550000002", FirstName: "Searchable", LastName: "Friend", Username: "search_friend"})
|
|
if err != nil {
|
|
t.Fatalf("create friend: %v", err)
|
|
}
|
|
r := New(Config{}, Deps{
|
|
Contacts: appcontacts.NewService(memory.NewContactStore(), users),
|
|
}, zaptest.NewLogger(t), clock.System)
|
|
|
|
var in bin.Buffer
|
|
if err := (&tg.ContactsSearchRequest{Q: "@search", Limit: 20}).Encode(&in); err != nil {
|
|
t.Fatalf("encode request: %v", err)
|
|
}
|
|
enc, err := r.Dispatch(WithUserID(ctx, owner.ID), [8]byte{}, 0, &in)
|
|
if err != nil {
|
|
t.Fatalf("dispatch: %v", err)
|
|
}
|
|
box, ok := enc.(*tg.ContactsFound)
|
|
if !ok {
|
|
t.Fatalf("result type = %T, want *tg.ContactsFound", enc)
|
|
}
|
|
if len(box.Results) != 1 || len(box.Users) != 1 {
|
|
t.Fatalf("search result sizes = results %d users %d, want 1/1", len(box.Results), len(box.Users))
|
|
}
|
|
peer, ok := box.Results[0].(*tg.PeerUser)
|
|
if !ok || peer.UserID != friend.ID {
|
|
t.Fatalf("peer = %T %+v, want friend", box.Results[0], box.Results[0])
|
|
}
|
|
}
|
|
|
|
func TestContactsEditCloseFriendsProjectsUserFlag(t *testing.T) {
|
|
ctx := context.Background()
|
|
users := memory.NewUserStore()
|
|
contactsStore := memory.NewContactStore()
|
|
owner, err := users.Create(ctx, domain.User{AccessHash: 11, Phone: "15550001001", FirstName: "Owner"})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
friend, err := users.Create(ctx, domain.User{AccessHash: 12, Phone: "15550001002", FirstName: "Friend"})
|
|
if err != nil {
|
|
t.Fatalf("create friend: %v", err)
|
|
}
|
|
if _, err := contactsStore.Upsert(ctx, owner.ID, domain.ContactInput{ContactUserID: friend.ID, FirstName: "Saved"}); err != nil {
|
|
t.Fatalf("upsert contact: %v", err)
|
|
}
|
|
r := New(Config{}, Deps{
|
|
Contacts: appcontacts.NewService(contactsStore, users),
|
|
}, zaptest.NewLogger(t), clock.System)
|
|
|
|
var edit bin.Buffer
|
|
if err := (&tg.ContactsEditCloseFriendsRequest{ID: []int64{friend.ID, friend.ID, owner.ID, 0}}).Encode(&edit); err != nil {
|
|
t.Fatalf("encode edit close friends: %v", err)
|
|
}
|
|
enc, err := r.Dispatch(WithUserID(ctx, owner.ID), [8]byte{}, 0, &edit)
|
|
if err != nil {
|
|
t.Fatalf("dispatch edit close friends: %v", err)
|
|
}
|
|
box, ok := enc.(*tg.BoolBox)
|
|
if !ok {
|
|
t.Fatalf("edit close friends result = %T, want BoolBox", enc)
|
|
}
|
|
if _, ok := box.Bool.(*tg.BoolTrue); !ok {
|
|
t.Fatalf("edit close friends bool = %T, want BoolTrue", box.Bool)
|
|
}
|
|
|
|
var get bin.Buffer
|
|
if err := (&tg.ContactsGetContactsRequest{}).Encode(&get); err != nil {
|
|
t.Fatalf("encode get contacts: %v", err)
|
|
}
|
|
got, err := r.Dispatch(WithUserID(ctx, owner.ID), [8]byte{}, 0, &get)
|
|
if err != nil {
|
|
t.Fatalf("dispatch get contacts: %v", err)
|
|
}
|
|
contactsBox, ok := got.(*tg.ContactsContactsBox)
|
|
if !ok {
|
|
t.Fatalf("contacts result = %T %+v, want ContactsContactsBox", got, got)
|
|
}
|
|
list, ok := contactsBox.Contacts.(*tg.ContactsContacts)
|
|
if !ok || len(list.Users) != 1 {
|
|
t.Fatalf("contacts result = %T %+v, want one contact user", got, got)
|
|
}
|
|
user, ok := list.Users[0].(*tg.User)
|
|
if !ok || !user.CloseFriend {
|
|
t.Fatalf("contact user = %T %+v, want close_friend", list.Users[0], list.Users[0])
|
|
}
|
|
}
|
|
|
|
func TestContactsEditCloseFriendsFanoutsCloseFriendStories(t *testing.T) {
|
|
ctx := context.Background()
|
|
users := memory.NewUserStore()
|
|
contactsStore := memory.NewContactStore()
|
|
storyStore := memory.NewStoryStore()
|
|
stateStore := memory.NewUpdateStateStore()
|
|
updateStore := memory.NewUpdateEventStore()
|
|
owner, err := users.Create(ctx, domain.User{AccessHash: 21, Phone: "15550002001", FirstName: "Owner"})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
friend, err := users.Create(ctx, domain.User{AccessHash: 22, Phone: "15550002002", FirstName: "Friend"})
|
|
if err != nil {
|
|
t.Fatalf("create friend: %v", err)
|
|
}
|
|
if _, err := contactsStore.Upsert(ctx, owner.ID, domain.ContactInput{ContactUserID: friend.ID, FirstName: "Friend"}); err != nil {
|
|
t.Fatalf("upsert contact: %v", err)
|
|
}
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
for _, story := range []domain.Story{
|
|
{
|
|
Owner: ownerPeer,
|
|
ID: 1,
|
|
Date: 1700000000,
|
|
ExpireDate: 1700003600,
|
|
CloseFriends: true,
|
|
Out: true,
|
|
Views: domain.StoryViews{ViewsCount: 1, HasViewers: true, RecentViewers: []int64{friend.ID}},
|
|
},
|
|
{
|
|
Owner: ownerPeer,
|
|
ID: 2,
|
|
Date: 1700000001,
|
|
ExpireDate: 1700003600,
|
|
CloseFriends: true,
|
|
AllowUserIDs: []int64{friend.ID},
|
|
},
|
|
{
|
|
Owner: ownerPeer,
|
|
ID: 3,
|
|
Date: 1700000002,
|
|
ExpireDate: 1700000005,
|
|
CloseFriends: true,
|
|
},
|
|
} {
|
|
if _, err := storyStore.UpsertStory(ctx, domain.UpsertStoryRequest{Story: story}); err != nil {
|
|
t.Fatalf("upsert story %d: %v", story.ID, err)
|
|
}
|
|
}
|
|
r := New(Config{}, Deps{
|
|
Contacts: appcontacts.NewService(contactsStore, users),
|
|
Stories: appstories.NewService(storyStore),
|
|
Updates: appupdates.NewService(stateStore, updateStore),
|
|
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(1700000100, 0)})
|
|
|
|
ownerAuth := [8]byte{1, 2, 3}
|
|
var add bin.Buffer
|
|
if err := (&tg.ContactsEditCloseFriendsRequest{ID: []int64{friend.ID}}).Encode(&add); err != nil {
|
|
t.Fatalf("encode add close friend: %v", err)
|
|
}
|
|
ownerCtx := WithSessionID(WithAuthKeyID(WithUserID(ctx, owner.ID), ownerAuth), 44)
|
|
if _, err := r.Dispatch(ownerCtx, ownerAuth, 44, &add); err != nil {
|
|
t.Fatalf("dispatch add close friend: %v", err)
|
|
}
|
|
if _, found, err := stateStore.Get(ctx, ownerAuth, friend.ID); err != nil || found {
|
|
t.Fatalf("owner auth state for friend found=%v err=%v, want absent", found, err)
|
|
}
|
|
|
|
var clear bin.Buffer
|
|
if err := (&tg.ContactsEditCloseFriendsRequest{ID: []int64{}}).Encode(&clear); err != nil {
|
|
t.Fatalf("encode clear close friends: %v", err)
|
|
}
|
|
if _, err := r.Dispatch(ownerCtx, ownerAuth, 44, &clear); err != nil {
|
|
t.Fatalf("dispatch clear close friends: %v", err)
|
|
}
|
|
|
|
diff, err := r.onUpdatesGetDifference(WithAuthKeyID(WithUserID(ctx, friend.ID), [8]byte{9}), &tg.UpdatesGetDifferenceRequest{Pts: 0})
|
|
if err != nil {
|
|
t.Fatalf("get friend difference: %v", err)
|
|
}
|
|
got, ok := diff.(*tg.UpdatesDifference)
|
|
if !ok {
|
|
t.Fatalf("difference = %T, want *tg.UpdatesDifference", diff)
|
|
}
|
|
if got.State.Pts != 3 || len(got.OtherUpdates) != 3 {
|
|
t.Fatalf("difference pts/updates = %d/%d, want 3/3", got.State.Pts, len(got.OtherUpdates))
|
|
}
|
|
for i, update := range got.OtherUpdates[:2] {
|
|
storyUpdate, ok := update.(*tg.UpdateStory)
|
|
if !ok {
|
|
t.Fatalf("add update %d = %T, want *tg.UpdateStory", i, update)
|
|
}
|
|
item, ok := storyUpdate.Story.(*tg.StoryItem)
|
|
if !ok {
|
|
t.Fatalf("add story %d = %T, want *tg.StoryItem", i, storyUpdate.Story)
|
|
}
|
|
if _, ok := item.GetViews(); item.Out || ok {
|
|
t.Fatalf("add story %d = %+v, want viewer fanout without out/views", i, item)
|
|
}
|
|
}
|
|
last, ok := got.OtherUpdates[2].(*tg.UpdateStory)
|
|
if !ok {
|
|
t.Fatalf("remove update = %T, want *tg.UpdateStory", got.OtherUpdates[2])
|
|
}
|
|
deleted, ok := last.Story.(*tg.StoryItemDeleted)
|
|
if !ok {
|
|
t.Fatalf("remove story = %T, want *tg.StoryItemDeleted", last.Story)
|
|
}
|
|
if deleted.ID != 1 {
|
|
t.Fatalf("deleted story id = %d, want only story 1 removed", deleted.ID)
|
|
}
|
|
}
|
|
|
|
func TestContactsBlockUnblockFanoutsStoryVisibilityChanges(t *testing.T) {
|
|
ctx := context.Background()
|
|
users := memory.NewUserStore()
|
|
contactsStore := memory.NewContactStore()
|
|
storyStore := memory.NewStoryStore()
|
|
updateStore := memory.NewUpdateEventStore()
|
|
owner, err := users.Create(ctx, domain.User{AccessHash: 31, Phone: "15550003001", FirstName: "Owner"})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
viewer, err := users.Create(ctx, domain.User{AccessHash: 32, Phone: "15550003002", FirstName: "Viewer"})
|
|
if err != nil {
|
|
t.Fatalf("create viewer: %v", err)
|
|
}
|
|
if _, err := contactsStore.Upsert(ctx, owner.ID, domain.ContactInput{ContactUserID: viewer.ID, FirstName: "Viewer"}); err != nil {
|
|
t.Fatalf("upsert contact: %v", err)
|
|
}
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
for _, story := range []domain.Story{
|
|
{
|
|
Owner: ownerPeer,
|
|
ID: 1,
|
|
Date: 1700000000,
|
|
ExpireDate: 1700003600,
|
|
Public: true,
|
|
Out: true,
|
|
Views: domain.StoryViews{ViewsCount: 3, HasViewers: true, RecentViewers: []int64{viewer.ID}},
|
|
},
|
|
{
|
|
Owner: ownerPeer,
|
|
ID: 2,
|
|
Date: 1700000001,
|
|
ExpireDate: 1700003600,
|
|
SelectedContacts: true,
|
|
AllowUserIDs: []int64{viewer.ID},
|
|
},
|
|
{
|
|
Owner: ownerPeer,
|
|
ID: 3,
|
|
Date: 1700000002,
|
|
ExpireDate: 1700003600,
|
|
Contacts: true,
|
|
},
|
|
{
|
|
Owner: ownerPeer,
|
|
ID: 4,
|
|
Date: 1700000003,
|
|
ExpireDate: 1700003600,
|
|
CloseFriends: true,
|
|
},
|
|
{
|
|
Owner: ownerPeer,
|
|
ID: 5,
|
|
Date: 1700000004,
|
|
ExpireDate: 1700003600,
|
|
Public: true,
|
|
DisallowUserIDs: []int64{viewer.ID},
|
|
},
|
|
{
|
|
Owner: ownerPeer,
|
|
ID: 6,
|
|
Date: 1700000005,
|
|
ExpireDate: 1700000006,
|
|
Public: true,
|
|
},
|
|
} {
|
|
if _, err := storyStore.UpsertStory(ctx, domain.UpsertStoryRequest{Story: story}); err != nil {
|
|
t.Fatalf("upsert story %d: %v", story.ID, err)
|
|
}
|
|
}
|
|
r := New(Config{}, Deps{
|
|
Contacts: appcontacts.NewService(contactsStore, users),
|
|
Stories: appstories.NewService(storyStore),
|
|
Updates: appupdates.NewService(memory.NewUpdateStateStore(), updateStore),
|
|
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(1700000100, 0)})
|
|
|
|
ownerAuth := [8]byte{7, 7, 1}
|
|
ownerCtx := WithSessionID(WithAuthKeyID(WithUserID(ctx, owner.ID), ownerAuth), 51)
|
|
if ok, err := r.onContactsBlock(ownerCtx, &tg.ContactsBlockRequest{
|
|
MyStoriesFrom: true,
|
|
ID: &tg.InputPeerUser{UserID: viewer.ID, AccessHash: viewer.AccessHash},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("contacts.block = %v, %v", ok, err)
|
|
}
|
|
ownerBlockDiff, err := r.onUpdatesGetDifference(WithAuthKeyID(WithUserID(ctx, owner.ID), [8]byte{7, 7, 2}), &tg.UpdatesGetDifferenceRequest{Pts: 0})
|
|
if err != nil {
|
|
t.Fatalf("get owner block difference: %v", err)
|
|
}
|
|
ownerBlockUpdates, ok := ownerBlockDiff.(*tg.UpdatesDifference)
|
|
if !ok {
|
|
t.Fatalf("owner block difference = %T, want *tg.UpdatesDifference", ownerBlockDiff)
|
|
}
|
|
if ownerBlockUpdates.State.Pts != 2 || len(ownerBlockUpdates.OtherUpdates) != 2 {
|
|
t.Fatalf("owner block difference pts/updates = %d/%d, want 2/2", ownerBlockUpdates.State.Pts, len(ownerBlockUpdates.OtherUpdates))
|
|
}
|
|
ownerBlocked, ok := ownerBlockUpdates.OtherUpdates[0].(*tg.UpdatePeerBlocked)
|
|
if !ok || !ownerBlocked.Blocked || !ownerBlocked.BlockedMyStoriesFrom {
|
|
t.Fatalf("owner block update[0] = %+v (%T), want story block", ownerBlockUpdates.OtherUpdates[0], ownerBlockUpdates.OtherUpdates[0])
|
|
}
|
|
if peer, ok := ownerBlocked.PeerID.(*tg.PeerUser); !ok || peer.UserID != viewer.ID {
|
|
t.Fatalf("owner block peer = %#v, want viewer", ownerBlocked.PeerID)
|
|
}
|
|
if _, ok := ownerBlockUpdates.OtherUpdates[1].(*tg.UpdatePeerSettings); !ok {
|
|
t.Fatalf("owner block update[1] = %T, want peer settings", ownerBlockUpdates.OtherUpdates[1])
|
|
}
|
|
blockDiff, err := r.onUpdatesGetDifference(WithAuthKeyID(WithUserID(ctx, viewer.ID), [8]byte{3, 1}), &tg.UpdatesGetDifferenceRequest{Pts: 0})
|
|
if err != nil {
|
|
t.Fatalf("get viewer block difference: %v", err)
|
|
}
|
|
blockUpdates, ok := blockDiff.(*tg.UpdatesDifference)
|
|
if !ok {
|
|
t.Fatalf("block difference = %T, want *tg.UpdatesDifference", blockDiff)
|
|
}
|
|
if blockUpdates.State.Pts != 3 || len(blockUpdates.OtherUpdates) != 3 {
|
|
t.Fatalf("block difference pts/updates = %d/%d, want 3/3", blockUpdates.State.Pts, len(blockUpdates.OtherUpdates))
|
|
}
|
|
for i, update := range blockUpdates.OtherUpdates {
|
|
storyUpdate, ok := update.(*tg.UpdateStory)
|
|
if !ok {
|
|
t.Fatalf("block update %d = %T, want *tg.UpdateStory", i, update)
|
|
}
|
|
if peer, ok := storyUpdate.Peer.(*tg.PeerUser); !ok || peer.UserID != owner.ID {
|
|
t.Fatalf("block update %d peer = %#v, want owner peer", i, storyUpdate.Peer)
|
|
}
|
|
deleted, ok := storyUpdate.Story.(*tg.StoryItemDeleted)
|
|
if !ok {
|
|
t.Fatalf("block story %d = %T, want *tg.StoryItemDeleted", i, storyUpdate.Story)
|
|
}
|
|
if deleted.ID != i+1 {
|
|
t.Fatalf("block deleted story id = %d, want %d", deleted.ID, i+1)
|
|
}
|
|
}
|
|
|
|
if ok, err := r.onContactsUnblock(ownerCtx, &tg.ContactsUnblockRequest{
|
|
MyStoriesFrom: true,
|
|
ID: &tg.InputPeerUser{UserID: viewer.ID, AccessHash: viewer.AccessHash},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("contacts.unblock = %v, %v", ok, err)
|
|
}
|
|
ownerUnblockDiff, err := r.onUpdatesGetDifference(WithAuthKeyID(WithUserID(ctx, owner.ID), [8]byte{7, 7, 3}), &tg.UpdatesGetDifferenceRequest{Pts: ownerBlockUpdates.State.Pts})
|
|
if err != nil {
|
|
t.Fatalf("get owner unblock difference: %v", err)
|
|
}
|
|
ownerUnblockUpdates, ok := ownerUnblockDiff.(*tg.UpdatesDifference)
|
|
if !ok {
|
|
t.Fatalf("owner unblock difference = %T, want *tg.UpdatesDifference", ownerUnblockDiff)
|
|
}
|
|
if ownerUnblockUpdates.State.Pts != 4 || len(ownerUnblockUpdates.OtherUpdates) != 2 {
|
|
t.Fatalf("owner unblock difference pts/updates = %d/%d, want 4/2", ownerUnblockUpdates.State.Pts, len(ownerUnblockUpdates.OtherUpdates))
|
|
}
|
|
ownerUnblocked, ok := ownerUnblockUpdates.OtherUpdates[0].(*tg.UpdatePeerBlocked)
|
|
if !ok || ownerUnblocked.Blocked || !ownerUnblocked.BlockedMyStoriesFrom {
|
|
t.Fatalf("owner unblock update[0] = %+v (%T), want story unblock", ownerUnblockUpdates.OtherUpdates[0], ownerUnblockUpdates.OtherUpdates[0])
|
|
}
|
|
if peer, ok := ownerUnblocked.PeerID.(*tg.PeerUser); !ok || peer.UserID != viewer.ID {
|
|
t.Fatalf("owner unblock peer = %#v, want viewer", ownerUnblocked.PeerID)
|
|
}
|
|
unblockDiff, err := r.onUpdatesGetDifference(WithAuthKeyID(WithUserID(ctx, viewer.ID), [8]byte{3, 2}), &tg.UpdatesGetDifferenceRequest{Pts: blockUpdates.State.Pts})
|
|
if err != nil {
|
|
t.Fatalf("get viewer unblock difference: %v", err)
|
|
}
|
|
unblockUpdates, ok := unblockDiff.(*tg.UpdatesDifference)
|
|
if !ok {
|
|
t.Fatalf("unblock difference = %T, want *tg.UpdatesDifference", unblockDiff)
|
|
}
|
|
if unblockUpdates.State.Pts != 6 || len(unblockUpdates.OtherUpdates) != 3 {
|
|
t.Fatalf("unblock difference pts/updates = %d/%d, want 6/3", unblockUpdates.State.Pts, len(unblockUpdates.OtherUpdates))
|
|
}
|
|
for i, update := range unblockUpdates.OtherUpdates {
|
|
storyUpdate, ok := update.(*tg.UpdateStory)
|
|
if !ok {
|
|
t.Fatalf("unblock update %d = %T, want *tg.UpdateStory", i, update)
|
|
}
|
|
item, ok := storyUpdate.Story.(*tg.StoryItem)
|
|
if !ok {
|
|
t.Fatalf("unblock story %d = %T, want *tg.StoryItem", i, storyUpdate.Story)
|
|
}
|
|
if item.ID != i+1 {
|
|
t.Fatalf("unblock story id = %d, want %d", item.ID, i+1)
|
|
}
|
|
if _, ok := item.GetViews(); item.Out || ok {
|
|
t.Fatalf("unblock story %d = %+v, want viewer fanout without out/views", i, item)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestContactsSetBlockedReplacesStoryBlocklistFanouts(t *testing.T) {
|
|
ctx := context.Background()
|
|
users := memory.NewUserStore()
|
|
contactsStore := memory.NewContactStore()
|
|
storyStore := memory.NewStoryStore()
|
|
updateStore := memory.NewUpdateEventStore()
|
|
owner, err := users.Create(ctx, domain.User{AccessHash: 41, Phone: "15550004001", FirstName: "Owner"})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
oldViewer, err := users.Create(ctx, domain.User{AccessHash: 42, Phone: "15550004002", FirstName: "Old"})
|
|
if err != nil {
|
|
t.Fatalf("create old viewer: %v", err)
|
|
}
|
|
newViewer, err := users.Create(ctx, domain.User{AccessHash: 43, Phone: "15550004003", FirstName: "New"})
|
|
if err != nil {
|
|
t.Fatalf("create new viewer: %v", err)
|
|
}
|
|
for _, viewer := range []domain.User{oldViewer, newViewer} {
|
|
if _, err := contactsStore.Upsert(ctx, owner.ID, domain.ContactInput{ContactUserID: viewer.ID, FirstName: viewer.FirstName}); err != nil {
|
|
t.Fatalf("upsert contact %d: %v", viewer.ID, err)
|
|
}
|
|
}
|
|
if _, err := contactsStore.Block(ctx, owner.ID, oldViewer.ID, 1700000000); err != nil {
|
|
t.Fatalf("pre-block old viewer: %v", err)
|
|
}
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
if _, err := storyStore.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
|
Owner: ownerPeer,
|
|
ID: 1,
|
|
Date: 1700000000,
|
|
ExpireDate: 1700003600,
|
|
Public: true,
|
|
Out: true,
|
|
Views: domain.StoryViews{ViewsCount: 2, HasViewers: true, RecentViewers: []int64{oldViewer.ID, newViewer.ID}},
|
|
}}); err != nil {
|
|
t.Fatalf("upsert story: %v", err)
|
|
}
|
|
r := New(Config{}, Deps{
|
|
Contacts: appcontacts.NewService(contactsStore, users),
|
|
Stories: appstories.NewService(storyStore),
|
|
Updates: appupdates.NewService(memory.NewUpdateStateStore(), updateStore),
|
|
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(1700000100, 0)})
|
|
|
|
ownerAuth := [8]byte{8, 8, 1}
|
|
ownerCtx := WithSessionID(WithAuthKeyID(WithUserID(ctx, owner.ID), ownerAuth), 61)
|
|
ok, err := r.onContactsSetBlocked(ownerCtx, &tg.ContactsSetBlockedRequest{
|
|
MyStoriesFrom: true,
|
|
ID: []tg.InputPeerClass{
|
|
&tg.InputPeerUser{UserID: newViewer.ID, AccessHash: newViewer.AccessHash},
|
|
&tg.InputPeerUser{UserID: newViewer.ID, AccessHash: newViewer.AccessHash},
|
|
},
|
|
Limit: 2,
|
|
})
|
|
if err != nil || !ok {
|
|
t.Fatalf("contacts.setBlocked = %v, %v", ok, err)
|
|
}
|
|
|
|
blockedList, err := r.onContactsGetBlocked(WithUserID(ctx, owner.ID), &tg.ContactsGetBlockedRequest{MyStoriesFrom: true, Limit: 10})
|
|
if err != nil {
|
|
t.Fatalf("contacts.getBlocked: %v", err)
|
|
}
|
|
blocked, ok := blockedList.(*tg.ContactsBlocked)
|
|
if !ok || len(blocked.Blocked) != 1 {
|
|
t.Fatalf("blocked list = %T %+v, want one blocked peer", blockedList, blockedList)
|
|
}
|
|
if peer, ok := blocked.Blocked[0].PeerID.(*tg.PeerUser); !ok || peer.UserID != newViewer.ID {
|
|
t.Fatalf("blocked peer = %#v, want new viewer", blocked.Blocked[0].PeerID)
|
|
}
|
|
|
|
ownerDiff, err := r.onUpdatesGetDifference(WithAuthKeyID(WithUserID(ctx, owner.ID), [8]byte{8, 8, 2}), &tg.UpdatesGetDifferenceRequest{Pts: 0})
|
|
if err != nil {
|
|
t.Fatalf("owner difference: %v", err)
|
|
}
|
|
ownerUpdates, ok := ownerDiff.(*tg.UpdatesDifference)
|
|
if !ok {
|
|
t.Fatalf("owner difference = %T, want *tg.UpdatesDifference", ownerDiff)
|
|
}
|
|
if ownerUpdates.State.Pts != 4 || len(ownerUpdates.OtherUpdates) != 4 {
|
|
t.Fatalf("owner difference pts/updates = %d/%d, want 4/4", ownerUpdates.State.Pts, len(ownerUpdates.OtherUpdates))
|
|
}
|
|
blockedUpdate, ok := ownerUpdates.OtherUpdates[0].(*tg.UpdatePeerBlocked)
|
|
if !ok || !blockedUpdate.Blocked || !blockedUpdate.BlockedMyStoriesFrom {
|
|
t.Fatalf("owner update[0] = %+v (%T), want new viewer story block", ownerUpdates.OtherUpdates[0], ownerUpdates.OtherUpdates[0])
|
|
}
|
|
if peer, ok := blockedUpdate.PeerID.(*tg.PeerUser); !ok || peer.UserID != newViewer.ID {
|
|
t.Fatalf("owner block peer = %#v, want new viewer", blockedUpdate.PeerID)
|
|
}
|
|
unblockedUpdate, ok := ownerUpdates.OtherUpdates[2].(*tg.UpdatePeerBlocked)
|
|
if !ok || unblockedUpdate.Blocked || !unblockedUpdate.BlockedMyStoriesFrom {
|
|
t.Fatalf("owner update[2] = %+v (%T), want old viewer story unblock", ownerUpdates.OtherUpdates[2], ownerUpdates.OtherUpdates[2])
|
|
}
|
|
if peer, ok := unblockedUpdate.PeerID.(*tg.PeerUser); !ok || peer.UserID != oldViewer.ID {
|
|
t.Fatalf("owner unblock peer = %#v, want old viewer", unblockedUpdate.PeerID)
|
|
}
|
|
|
|
oldDiff, err := r.onUpdatesGetDifference(WithAuthKeyID(WithUserID(ctx, oldViewer.ID), [8]byte{8, 8, 3}), &tg.UpdatesGetDifferenceRequest{Pts: 0})
|
|
if err != nil {
|
|
t.Fatalf("old viewer difference: %v", err)
|
|
}
|
|
oldUpdates, ok := oldDiff.(*tg.UpdatesDifference)
|
|
if !ok || oldUpdates.State.Pts != 1 || len(oldUpdates.OtherUpdates) != 1 {
|
|
t.Fatalf("old viewer difference = %T %+v, want one restored story", oldDiff, oldDiff)
|
|
}
|
|
oldStory, ok := oldUpdates.OtherUpdates[0].(*tg.UpdateStory)
|
|
if !ok {
|
|
t.Fatalf("old viewer update = %T, want updateStory", oldUpdates.OtherUpdates[0])
|
|
}
|
|
if item, ok := oldStory.Story.(*tg.StoryItem); !ok || item.ID != 1 || item.Out {
|
|
t.Fatalf("old viewer story = %+v (%T), want visible non-owner story", oldStory.Story, oldStory.Story)
|
|
}
|
|
|
|
newDiff, err := r.onUpdatesGetDifference(WithAuthKeyID(WithUserID(ctx, newViewer.ID), [8]byte{8, 8, 4}), &tg.UpdatesGetDifferenceRequest{Pts: 0})
|
|
if err != nil {
|
|
t.Fatalf("new viewer difference: %v", err)
|
|
}
|
|
newUpdates, ok := newDiff.(*tg.UpdatesDifference)
|
|
if !ok || newUpdates.State.Pts != 1 || len(newUpdates.OtherUpdates) != 1 {
|
|
t.Fatalf("new viewer difference = %T %+v, want one deleted story", newDiff, newDiff)
|
|
}
|
|
newStory, ok := newUpdates.OtherUpdates[0].(*tg.UpdateStory)
|
|
if !ok {
|
|
t.Fatalf("new viewer update = %T, want updateStory", newUpdates.OtherUpdates[0])
|
|
}
|
|
if deleted, ok := newStory.Story.(*tg.StoryItemDeleted); !ok || deleted.ID != 1 {
|
|
t.Fatalf("new viewer story = %+v (%T), want deleted story 1", newStory.Story, newStory.Story)
|
|
}
|
|
}
|
|
|
|
func TestContactsSearchFindsPublicChannels(t *testing.T) {
|
|
ctx := context.Background()
|
|
userStore := memory.NewUserStore()
|
|
owner, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550000011", FirstName: "Owner"})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
viewer, err := userStore.Create(ctx, domain.User{AccessHash: 12, Phone: "15550000012", FirstName: "Viewer"})
|
|
if err != nil {
|
|
t.Fatalf("create viewer: %v", err)
|
|
}
|
|
stranger, err := userStore.Create(ctx, domain.User{AccessHash: 13, Phone: "15550000013", FirstName: "Stranger"})
|
|
if err != nil {
|
|
t.Fatalf("create stranger: %v", err)
|
|
}
|
|
channelStore := memory.NewChannelStore()
|
|
channelSvc := appchannels.NewService(channelStore)
|
|
created, err := channelSvc.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
|
|
Title: "CU Public RPC",
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{viewer.ID},
|
|
Date: 1700000012,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
public, err := channelSvc.UpdateUsername(ctx, owner.ID, domain.UpdateChannelUsernameRequest{
|
|
ChannelID: created.Channel.ID,
|
|
Username: "cu_public_rpc",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("set channel username: %v", err)
|
|
}
|
|
r := New(Config{}, Deps{
|
|
Users: appusers.NewService(userStore),
|
|
Contacts: appcontacts.NewService(memory.NewContactStore(), userStore),
|
|
Channels: channelSvc,
|
|
}, zaptest.NewLogger(t), clock.System)
|
|
|
|
var in bin.Buffer
|
|
if err := (&tg.ContactsSearchRequest{Q: "CU Public", Limit: 20}).Encode(&in); err != nil {
|
|
t.Fatalf("encode request: %v", err)
|
|
}
|
|
enc, err := r.Dispatch(WithUserID(ctx, viewer.ID), [8]byte{}, 0, &in)
|
|
if err != nil {
|
|
t.Fatalf("dispatch: %v", err)
|
|
}
|
|
box, ok := enc.(*tg.ContactsFound)
|
|
if !ok {
|
|
t.Fatalf("result type = %T, want *tg.ContactsFound", enc)
|
|
}
|
|
if len(box.MyResults) != 0 || len(box.Results) != 0 || len(box.Chats) != 0 {
|
|
t.Fatalf("member public search = my %d results %d chats %d, want no discovery channel for active member", len(box.MyResults), len(box.Results), len(box.Chats))
|
|
}
|
|
|
|
var strangerIn bin.Buffer
|
|
if err := (&tg.ContactsSearchRequest{Q: "CU Public", Limit: 20}).Encode(&strangerIn); err != nil {
|
|
t.Fatalf("encode stranger request: %v", err)
|
|
}
|
|
strangerEnc, err := r.Dispatch(WithUserID(ctx, stranger.ID), [8]byte{}, 0, &strangerIn)
|
|
if err != nil {
|
|
t.Fatalf("dispatch stranger: %v", err)
|
|
}
|
|
strangerBox, ok := strangerEnc.(*tg.ContactsFound)
|
|
if !ok {
|
|
t.Fatalf("stranger result type = %T, want *tg.ContactsFound", strangerEnc)
|
|
}
|
|
if len(strangerBox.Results) != 1 || len(strangerBox.Chats) != 1 {
|
|
t.Fatalf("stranger search sizes = results %d chats %d, want 1/1", len(strangerBox.Results), len(strangerBox.Chats))
|
|
}
|
|
strangerPeer, ok := strangerBox.Results[0].(*tg.PeerChannel)
|
|
if !ok || strangerPeer.ChannelID != public.ID {
|
|
t.Fatalf("stranger peer = %T %+v, want public channel", strangerBox.Results[0], strangerBox.Results[0])
|
|
}
|
|
strangerChat, ok := strangerBox.Chats[0].(*tg.Channel)
|
|
if !ok || !strangerChat.Left || strangerChat.ID != public.ID {
|
|
t.Fatalf("stranger chat = %T %+v, want left public channel", strangerBox.Chats[0], strangerBox.Chats[0])
|
|
}
|
|
|
|
resolved, err := r.onContactsResolveUsername(WithUserID(ctx, viewer.ID), &tg.ContactsResolveUsernameRequest{Username: "@CU_PUBLIC_RPC"})
|
|
if err != nil {
|
|
t.Fatalf("resolve username: %v", err)
|
|
}
|
|
resolvedPeer, ok := resolved.Peer.(*tg.PeerChannel)
|
|
if !ok || resolvedPeer.ChannelID != public.ID || len(resolved.Chats) != 1 {
|
|
t.Fatalf("resolved channel = %+v, want peer + chat", resolved)
|
|
}
|
|
resolvedChat, ok := resolved.Chats[0].(*tg.Channel)
|
|
if !ok || resolvedChat.ID != public.ID || resolvedChat.Username != "cu_public_rpc" || resolvedChat.AccessHash != public.AccessHash {
|
|
t.Fatalf("resolved chat = %T %+v, want full public channel projection", resolved.Chats[0], resolved.Chats[0])
|
|
}
|
|
if resolvedChat.Left {
|
|
t.Fatalf("member resolved chat left = true, want active member channel")
|
|
}
|
|
|
|
strangerResolved, err := r.onContactsResolveUsername(WithUserID(ctx, stranger.ID), &tg.ContactsResolveUsernameRequest{Username: "@CU_PUBLIC_RPC"})
|
|
if err != nil {
|
|
t.Fatalf("resolve username stranger: %v", err)
|
|
}
|
|
strangerResolvedPeer, ok := strangerResolved.Peer.(*tg.PeerChannel)
|
|
if !ok || strangerResolvedPeer.ChannelID != public.ID || len(strangerResolved.Chats) != 1 {
|
|
t.Fatalf("stranger resolved channel = %+v, want peer + chat", strangerResolved)
|
|
}
|
|
strangerResolvedChat, ok := strangerResolved.Chats[0].(*tg.Channel)
|
|
if !ok || strangerResolvedChat.ID != public.ID || strangerResolvedChat.Username != "cu_public_rpc" || strangerResolvedChat.AccessHash != public.AccessHash {
|
|
t.Fatalf("stranger resolved chat = %T %+v, want full public channel projection", strangerResolved.Chats[0], strangerResolved.Chats[0])
|
|
}
|
|
if !strangerResolvedChat.Left {
|
|
t.Fatalf("stranger resolved chat left = false, want public preview channel")
|
|
}
|
|
}
|
|
|
|
func TestUsernameRPCLifecycle(t *testing.T) {
|
|
ctx := context.Background()
|
|
userStore := memory.NewUserStore()
|
|
owner, err := userStore.Create(ctx, domain.User{AccessHash: 1, Phone: "15550000001", FirstName: "Owner"})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
other, err := userStore.Create(ctx, domain.User{AccessHash: 2, Phone: "15550000002", FirstName: "Other", Username: "taken_name"})
|
|
if err != nil {
|
|
t.Fatalf("create other: %v", err)
|
|
}
|
|
r := New(Config{}, Deps{Users: appusers.NewService(userStore)}, zaptest.NewLogger(t), clock.System)
|
|
reqCtx := WithUserID(ctx, owner.ID)
|
|
|
|
ok, err := r.onAccountCheckUsername(reqCtx, "taken_name")
|
|
if err != nil || ok {
|
|
t.Fatalf("check occupied = ok %v err %v, want false/nil", ok, err)
|
|
}
|
|
user, err := r.onAccountUpdateUsername(reqCtx, "owner_name")
|
|
if err != nil {
|
|
t.Fatalf("update username: %v", err)
|
|
}
|
|
self, ok := user.(*tg.User)
|
|
if !ok || self.Username != "owner_name" || len(self.Usernames) != 1 || !self.Usernames[0].Active {
|
|
t.Fatalf("updated user = %T %+v, want self with active username", user, user)
|
|
}
|
|
|
|
resolved, err := r.onContactsResolveUsername(reqCtx, &tg.ContactsResolveUsernameRequest{Username: "@OWNER_NAME"})
|
|
if err != nil {
|
|
t.Fatalf("resolve username: %v", err)
|
|
}
|
|
peer, ok := resolved.Peer.(*tg.PeerUser)
|
|
if !ok || peer.UserID != owner.ID || len(resolved.Users) != 1 {
|
|
t.Fatalf("resolved username = %+v, want owner peer", resolved)
|
|
}
|
|
|
|
resolvedPhone, err := r.onContactsResolvePhone(reqCtx, "+15550000002")
|
|
if err != nil {
|
|
t.Fatalf("resolve phone: %v", err)
|
|
}
|
|
phonePeer, ok := resolvedPhone.Peer.(*tg.PeerUser)
|
|
if !ok || phonePeer.UserID != other.ID {
|
|
t.Fatalf("resolved phone peer = %+v, want other", resolvedPhone.Peer)
|
|
}
|
|
}
|
|
|
|
func TestAccountUpdateProfileRPC(t *testing.T) {
|
|
ctx := context.Background()
|
|
userStore := memory.NewUserStore()
|
|
owner, err := userStore.Create(ctx, domain.User{AccessHash: 1, Phone: "15550000001", FirstName: "Owner"})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
r := New(Config{}, Deps{Users: appusers.NewService(userStore)}, zaptest.NewLogger(t), clock.System)
|
|
req := &tg.AccountUpdateProfileRequest{}
|
|
req.SetFirstName("Updated")
|
|
req.SetLastName("User")
|
|
req.SetAbout("profile bio")
|
|
|
|
user, err := r.onAccountUpdateProfile(WithUserID(ctx, owner.ID), req)
|
|
if err != nil {
|
|
t.Fatalf("update profile: %v", err)
|
|
}
|
|
self, ok := user.(*tg.User)
|
|
if !ok || self.FirstName != "Updated" || self.LastName != "User" {
|
|
t.Fatalf("updated user = %T %+v, want updated self user", user, user)
|
|
}
|
|
full, err := r.onUsersGetFullUser(WithUserID(ctx, owner.ID), &tg.InputUserSelf{})
|
|
if err != nil {
|
|
t.Fatalf("get full user: %v", err)
|
|
}
|
|
if full.FullUser.About != "profile bio" {
|
|
t.Fatalf("full about = %q, want profile bio", full.FullUser.About)
|
|
}
|
|
}
|
|
|
|
func TestUsersSavedMusicStubsValidateInput(t *testing.T) {
|
|
ctx := context.Background()
|
|
userStore := memory.NewUserStore()
|
|
owner, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550000001", FirstName: "Owner"})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
friend, err := userStore.Create(ctx, domain.User{AccessHash: 22, Phone: "15550000002", FirstName: "Friend"})
|
|
if err != nil {
|
|
t.Fatalf("create friend: %v", err)
|
|
}
|
|
r := New(Config{}, Deps{Users: appusers.NewService(userStore)}, zaptest.NewLogger(t), clock.System)
|
|
reqCtx := WithUserID(ctx, owner.ID)
|
|
|
|
got, err := r.onUsersGetSavedMusic(reqCtx, &tg.UsersGetSavedMusicRequest{
|
|
ID: &tg.InputUser{UserID: friend.ID, AccessHash: friend.AccessHash},
|
|
Offset: 0,
|
|
Limit: 20,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("users.getSavedMusic: %v", err)
|
|
}
|
|
music, ok := got.(*tg.UsersSavedMusic)
|
|
if !ok || music.Count != 0 || len(music.Documents) != 0 {
|
|
t.Fatalf("saved music = %T %+v, want empty users.savedMusic", got, got)
|
|
}
|
|
if _, err := r.onUsersGetSavedMusic(reqCtx, &tg.UsersGetSavedMusicRequest{
|
|
ID: &tg.InputUserSelf{},
|
|
Limit: maxSavedMusicLimit + 1,
|
|
}); err == nil || !strings.Contains(err.Error(), "LIMIT_INVALID") {
|
|
t.Fatalf("large saved music limit err = %v, want LIMIT_INVALID", err)
|
|
}
|
|
if _, err := r.onUsersGetSavedMusic(reqCtx, &tg.UsersGetSavedMusicRequest{
|
|
ID: &tg.InputUser{UserID: friend.ID, AccessHash: friend.AccessHash + 1},
|
|
Limit: 20,
|
|
}); err == nil || !strings.Contains(err.Error(), "USER_ID_INVALID") {
|
|
t.Fatalf("bad saved music user err = %v, want USER_ID_INVALID", err)
|
|
}
|
|
}
|
|
|
|
func TestUsersGetRequirementsToContactReturnsEmptyRequirements(t *testing.T) {
|
|
ctx := context.Background()
|
|
userStore := memory.NewUserStore()
|
|
owner, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550000001", FirstName: "Owner"})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
friend, err := userStore.Create(ctx, domain.User{AccessHash: 22, Phone: "15550000002", FirstName: "Friend"})
|
|
if err != nil {
|
|
t.Fatalf("create friend: %v", err)
|
|
}
|
|
r := New(Config{}, Deps{Users: appusers.NewService(userStore)}, zaptest.NewLogger(t), clock.System)
|
|
reqCtx := WithUserID(ctx, owner.ID)
|
|
|
|
got, err := r.onUsersGetRequirementsToContact(reqCtx, []tg.InputUserClass{
|
|
&tg.InputUser{UserID: friend.ID, AccessHash: friend.AccessHash},
|
|
&tg.InputUserSelf{},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("users.getRequirementsToContact: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("requirements len = %d, want 2", len(got))
|
|
}
|
|
for i, req := range got {
|
|
if _, ok := req.(*tg.RequirementToContactEmpty); !ok {
|
|
t.Fatalf("requirement %d = %T, want requirementToContactEmpty", i, req)
|
|
}
|
|
}
|
|
got, err = r.onUsersGetRequirementsToContact(reqCtx, []tg.InputUserClass{
|
|
&tg.InputUser{UserID: friend.ID, AccessHash: friend.AccessHash + 1},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("bad requirements user should degrade to empty requirement: %v", err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Fatalf("bad requirements len = %d, want 1", len(got))
|
|
}
|
|
if _, ok := got[0].(*tg.RequirementToContactEmpty); !ok {
|
|
t.Fatalf("bad requirements result = %T, want requirementToContactEmpty", got[0])
|
|
}
|
|
}
|
|
|
|
func TestTGUserIncludesRecentlyStatusForTypingEligibility(t *testing.T) {
|
|
got := tgUser(domain.User{ID: 1000000002, FirstName: "Bob"})
|
|
if _, ok := got.Status.(*tg.UserStatusRecently); !ok {
|
|
t.Fatalf("status = %T, want *tg.UserStatusRecently", got.Status)
|
|
}
|
|
|
|
online := tgUser(domain.User{
|
|
ID: 1000000002,
|
|
FirstName: "Bob",
|
|
Status: domain.UserStatus{Kind: domain.UserStatusOnline, Expires: 1700000300},
|
|
})
|
|
if status, ok := online.Status.(*tg.UserStatusOnline); !ok || status.Expires != 1700000300 {
|
|
t.Fatalf("online status = %#v, want userStatusOnline expires=1700000300", online.Status)
|
|
}
|
|
|
|
offline := tgUser(domain.User{
|
|
ID: 1000000002,
|
|
FirstName: "Bob",
|
|
Status: domain.UserStatus{Kind: domain.UserStatusOffline, WasOnline: 1700000000},
|
|
})
|
|
if status, ok := offline.Status.(*tg.UserStatusOffline); !ok || status.WasOnline != 1700000000 {
|
|
t.Fatalf("offline status = %#v, want userStatusOffline was_online=1700000000", offline.Status)
|
|
}
|
|
}
|
|
|
|
func TestAccountUpdateStatusPushesPresenceToOnlineContacts(t *testing.T) {
|
|
ctx := context.Background()
|
|
alice := domain.User{ID: 1000000001, AccessHash: 11, FirstName: "Alice"}
|
|
bob := domain.User{ID: 1000000002, AccessHash: 22, FirstName: "Bob", Contact: true}
|
|
contacts := memory.NewContactStore()
|
|
if err := contacts.SaveList(ctx, alice.ID, domain.ContactList{
|
|
Contacts: []domain.Contact{{User: bob}},
|
|
}); err != nil {
|
|
t.Fatalf("save contacts: %v", err)
|
|
}
|
|
sessions := &captureSessions{onlineUserIDs: []int64{bob.ID}}
|
|
r := New(Config{}, Deps{
|
|
Contacts: appcontacts.NewService(contacts),
|
|
Sessions: sessions,
|
|
}, zaptest.NewLogger(t), clock.System)
|
|
callCtx := WithSessionID(WithUserID(ctx, alice.ID), 77)
|
|
|
|
ok, err := r.onAccountUpdateStatus(callCtx, false)
|
|
if err != nil || !ok {
|
|
t.Fatalf("account.updateStatus online = %v, %v", ok, err)
|
|
}
|
|
gotPushes := sessions.pushedUserIDs()
|
|
if !reflect.DeepEqual(gotPushes, []int64{alice.ID, bob.ID}) {
|
|
t.Fatalf("pushed users = %+v, want self and online contact", gotPushes)
|
|
}
|
|
update := pushedUserStatus(t, sessions.snapshot().message)
|
|
if update.UserID != alice.ID {
|
|
t.Fatalf("status user = %d, want alice", update.UserID)
|
|
}
|
|
if status, ok := update.Status.(*tg.UserStatusOnline); !ok || status.Expires <= int(time.Now().Unix()) {
|
|
t.Fatalf("status = %#v, want online with future expires", update.Status)
|
|
}
|
|
|
|
ok, err = r.onAccountUpdateStatus(callCtx, true)
|
|
if err != nil || !ok {
|
|
t.Fatalf("account.updateStatus offline = %v, %v", ok, err)
|
|
}
|
|
update = pushedUserStatus(t, sessions.snapshot().message)
|
|
if update.UserID != alice.ID {
|
|
t.Fatalf("offline status user = %d, want alice", update.UserID)
|
|
}
|
|
if status, ok := update.Status.(*tg.UserStatusOffline); !ok || status.WasOnline == 0 {
|
|
t.Fatalf("status = %#v, want offline with was_online", update.Status)
|
|
}
|
|
}
|
|
|
|
func TestAccountUpdateStatusPersistsLastSeen(t *testing.T) {
|
|
ctx := context.Background()
|
|
userStore := memory.NewUserStore()
|
|
alice, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "1001", FirstName: "Alice"})
|
|
if err != nil {
|
|
t.Fatalf("create alice: %v", err)
|
|
}
|
|
const now = 1700000200
|
|
r := New(Config{}, Deps{
|
|
Users: appusers.NewService(userStore),
|
|
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(now, 0)})
|
|
|
|
ok, err := r.onAccountUpdateStatus(WithSessionID(WithUserID(ctx, alice.ID), 77), false)
|
|
if err != nil || !ok {
|
|
t.Fatalf("account.updateStatus online = %v, %v", ok, err)
|
|
}
|
|
stored, found, err := userStore.ByID(ctx, alice.ID)
|
|
if err != nil || !found {
|
|
t.Fatalf("load alice found=%v err=%v", found, err)
|
|
}
|
|
if stored.LastSeenAt != now {
|
|
t.Fatalf("last_seen_at after online = %d, want %d", stored.LastSeenAt, now)
|
|
}
|
|
}
|
|
|
|
func TestContactsStatusesUsesPersistedLastSeen(t *testing.T) {
|
|
ctx := context.Background()
|
|
userStore := memory.NewUserStore()
|
|
alice, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "1001", FirstName: "Alice"})
|
|
if err != nil {
|
|
t.Fatalf("create alice: %v", err)
|
|
}
|
|
bob, err := userStore.Create(ctx, domain.User{AccessHash: 22, Phone: "1002", FirstName: "Bob"})
|
|
if err != nil {
|
|
t.Fatalf("create bob: %v", err)
|
|
}
|
|
const lastSeen = 1700000000
|
|
if err := userStore.UpdateLastSeen(ctx, bob.ID, lastSeen); err != nil {
|
|
t.Fatalf("update last seen: %v", err)
|
|
}
|
|
contacts := memory.NewContactStore()
|
|
if err := contacts.SaveList(ctx, alice.ID, domain.ContactList{
|
|
Contacts: []domain.Contact{{User: domain.User{ID: bob.ID, FirstName: "Old Bob", Contact: true}}},
|
|
}); err != nil {
|
|
t.Fatalf("save contacts: %v", err)
|
|
}
|
|
r := New(Config{}, Deps{
|
|
Contacts: appcontacts.NewService(contacts, userStore),
|
|
Users: appusers.NewService(userStore),
|
|
}, zaptest.NewLogger(t), clock.System)
|
|
|
|
statuses, err := r.onContactsGetStatuses(WithUserID(ctx, alice.ID))
|
|
if err != nil {
|
|
t.Fatalf("contacts.getStatuses: %v", err)
|
|
}
|
|
if len(statuses) != 1 || statuses[0].UserID != bob.ID {
|
|
t.Fatalf("statuses = %+v, want bob only", statuses)
|
|
}
|
|
if status, ok := statuses[0].Status.(*tg.UserStatusOffline); !ok || status.WasOnline != lastSeen {
|
|
t.Fatalf("status = %#v, want userStatusOffline was_online=%d", statuses[0].Status, lastSeen)
|
|
}
|
|
}
|
|
|
|
func TestContactsStatusesUsePresenceAndContactsKeepStableProjection(t *testing.T) {
|
|
ctx := context.Background()
|
|
alice := domain.User{ID: 1000000001, AccessHash: 11, FirstName: "Alice"}
|
|
bob := domain.User{ID: 1000000002, AccessHash: 22, FirstName: "Bob", Contact: true}
|
|
contacts := memory.NewContactStore()
|
|
if err := contacts.SaveList(ctx, alice.ID, domain.ContactList{
|
|
Contacts: []domain.Contact{{User: bob}},
|
|
}); err != nil {
|
|
t.Fatalf("save contacts: %v", err)
|
|
}
|
|
r := New(Config{}, Deps{
|
|
Contacts: appcontacts.NewService(contacts),
|
|
}, zaptest.NewLogger(t), clock.System)
|
|
|
|
if ok, err := r.onAccountUpdateStatus(WithSessionID(WithUserID(ctx, bob.ID), 22), false); err != nil || !ok {
|
|
t.Fatalf("bob account.updateStatus = %v, %v", ok, err)
|
|
}
|
|
statuses, err := r.onContactsGetStatuses(WithUserID(ctx, alice.ID))
|
|
if err != nil {
|
|
t.Fatalf("contacts.getStatuses: %v", err)
|
|
}
|
|
if len(statuses) != 1 || statuses[0].UserID != bob.ID {
|
|
t.Fatalf("statuses = %+v, want bob only", statuses)
|
|
}
|
|
if _, ok := statuses[0].Status.(*tg.UserStatusOnline); !ok {
|
|
t.Fatalf("contacts.getStatuses status = %T, want online", statuses[0].Status)
|
|
}
|
|
|
|
contactsRes, err := r.onContactsGetContacts(WithUserID(ctx, alice.ID), 0)
|
|
if err != nil {
|
|
t.Fatalf("contacts.getContacts: %v", err)
|
|
}
|
|
full, ok := contactsRes.(*tg.ContactsContacts)
|
|
if !ok || len(full.Users) != 1 {
|
|
t.Fatalf("contacts result = %T %+v, want one full contacts response", contactsRes, contactsRes)
|
|
}
|
|
user, ok := full.Users[0].(*tg.User)
|
|
if !ok || user.ID != bob.ID {
|
|
t.Fatalf("contacts user = %T %+v, want bob", full.Users[0], full.Users[0])
|
|
}
|
|
if _, ok := user.Status.(*tg.UserStatusRecently); !ok {
|
|
t.Fatalf("contacts user status = %T, want stable non-presence projection", user.Status)
|
|
}
|
|
}
|
|
|
|
func TestContactsAcceptContactReturnsSettingsAndReset(t *testing.T) {
|
|
ctx := context.Background()
|
|
userStore := memory.NewUserStore()
|
|
contactsStore := memory.NewContactStore()
|
|
alice, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "1001", FirstName: "Alice", LastName: "A"})
|
|
if err != nil {
|
|
t.Fatalf("create alice: %v", err)
|
|
}
|
|
bob, err := userStore.Create(ctx, domain.User{AccessHash: 22, Phone: "1002", FirstName: "Bob", LastName: "B"})
|
|
if err != nil {
|
|
t.Fatalf("create bob: %v", err)
|
|
}
|
|
contactsSvc := appcontacts.NewService(contactsStore, userStore)
|
|
if _, err := contactsSvc.AddContact(ctx, alice.ID, domain.ContactInput{
|
|
ContactUserID: bob.ID,
|
|
Phone: bob.Phone,
|
|
FirstName: "Bobby",
|
|
LastName: "Remark",
|
|
}); err != nil {
|
|
t.Fatalf("alice add bob: %v", err)
|
|
}
|
|
updatesSvc := &captureUpdates{state: domain.UpdateState{Pts: 10, Date: 1700000400}}
|
|
r := New(Config{}, Deps{
|
|
Contacts: contactsSvc,
|
|
Users: appusers.NewService(userStore, appusers.WithContactStore(contactsStore)),
|
|
Updates: updatesSvc,
|
|
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(1700000400, 0)})
|
|
|
|
out, err := r.onContactsAcceptContact(WithUserID(ctx, alice.ID), &tg.InputUser{UserID: bob.ID, AccessHash: bob.AccessHash})
|
|
if err != nil {
|
|
t.Fatalf("contacts.acceptContact: %v", err)
|
|
}
|
|
got, ok := out.(*tg.Updates)
|
|
if !ok {
|
|
t.Fatalf("updates = %T, want *tg.Updates", out)
|
|
}
|
|
if len(got.Updates) != 2 {
|
|
t.Fatalf("updates = %+v, want peer settings + contacts reset", got.Updates)
|
|
}
|
|
settings, ok := got.Updates[0].(*tg.UpdatePeerSettings)
|
|
if !ok {
|
|
t.Fatalf("update[0] = %T, want UpdatePeerSettings", got.Updates[0])
|
|
}
|
|
if settings.Settings.ShareContact || settings.Settings.AddContact {
|
|
t.Fatalf("peer settings = %+v, want share/add false", settings.Settings)
|
|
}
|
|
if _, ok := got.Updates[1].(*tg.UpdateContactsReset); !ok {
|
|
t.Fatalf("update[1] = %T, want UpdateContactsReset", got.Updates[1])
|
|
}
|
|
if len(updatesSvc.events) != 4 {
|
|
t.Fatalf("recorded events = %+v, want current peer/reset and target peer/reset", updatesSvc.events)
|
|
}
|
|
if updatesSvc.events[0].UserID != alice.ID || updatesSvc.events[0].Settings.ShareContact {
|
|
t.Fatalf("current peer settings event = %+v, want alice share=false", updatesSvc.events[0])
|
|
}
|
|
if updatesSvc.events[2].UserID != bob.ID || updatesSvc.events[2].Settings.ShareContact {
|
|
t.Fatalf("target peer settings event = %+v, want bob share=false", updatesSvc.events[2])
|
|
}
|
|
reverse, found, err := contactsStore.Get(ctx, bob.ID, alice.ID)
|
|
if err != nil || !found {
|
|
t.Fatalf("bob contact alice found=%v err=%v", found, err)
|
|
}
|
|
if reverse.Phone != alice.Phone || !reverse.Mutual {
|
|
t.Fatalf("bob contact alice = %+v, want shared phone and mutual", reverse)
|
|
}
|
|
}
|
|
|
|
func TestContactsStatusesUsesOnlineSessionFallback(t *testing.T) {
|
|
ctx := context.Background()
|
|
alice := domain.User{ID: 1000000001, AccessHash: 11, FirstName: "Alice"}
|
|
bob := domain.User{ID: 1000000002, AccessHash: 22, FirstName: "Bob", Contact: true}
|
|
contacts := memory.NewContactStore()
|
|
if err := contacts.SaveList(ctx, alice.ID, domain.ContactList{
|
|
Contacts: []domain.Contact{{User: bob}},
|
|
}); err != nil {
|
|
t.Fatalf("save contacts: %v", err)
|
|
}
|
|
r := New(Config{}, Deps{
|
|
Contacts: appcontacts.NewService(contacts),
|
|
Sessions: &captureSessions{onlineUserIDs: []int64{bob.ID}},
|
|
}, zaptest.NewLogger(t), clock.System)
|
|
|
|
statuses, err := r.onContactsGetStatuses(WithUserID(ctx, alice.ID))
|
|
if err != nil {
|
|
t.Fatalf("contacts.getStatuses: %v", err)
|
|
}
|
|
if len(statuses) != 1 || statuses[0].UserID != bob.ID {
|
|
t.Fatalf("statuses = %+v, want bob only", statuses)
|
|
}
|
|
if status, ok := statuses[0].Status.(*tg.UserStatusOnline); !ok || status.Expires <= int(time.Now().Unix()) {
|
|
t.Fatalf("fallback status = %#v, want online from active session", statuses[0].Status)
|
|
}
|
|
}
|
|
|
|
func TestContactsStatusesHonorsExplicitOffline(t *testing.T) {
|
|
ctx := context.Background()
|
|
alice := domain.User{ID: 1000000001, AccessHash: 11, FirstName: "Alice"}
|
|
bob := domain.User{ID: 1000000002, AccessHash: 22, FirstName: "Bob", Contact: true}
|
|
contacts := memory.NewContactStore()
|
|
if err := contacts.SaveList(ctx, alice.ID, domain.ContactList{
|
|
Contacts: []domain.Contact{{User: bob}},
|
|
}); err != nil {
|
|
t.Fatalf("save contacts: %v", err)
|
|
}
|
|
r := New(Config{}, Deps{
|
|
Contacts: appcontacts.NewService(contacts),
|
|
Sessions: &captureSessions{onlineUserIDs: []int64{bob.ID}},
|
|
}, zaptest.NewLogger(t), clock.System)
|
|
if ok, err := r.onAccountUpdateStatus(WithSessionID(WithUserID(ctx, bob.ID), 22), true); err != nil || !ok {
|
|
t.Fatalf("bob account.updateStatus offline = %v, %v", ok, err)
|
|
}
|
|
|
|
statuses, err := r.onContactsGetStatuses(WithUserID(ctx, alice.ID))
|
|
if err != nil {
|
|
t.Fatalf("contacts.getStatuses: %v", err)
|
|
}
|
|
if len(statuses) != 1 || statuses[0].UserID != bob.ID {
|
|
t.Fatalf("statuses = %+v, want bob only", statuses)
|
|
}
|
|
if status, ok := statuses[0].Status.(*tg.UserStatusOffline); !ok || status.WasOnline == 0 {
|
|
t.Fatalf("status = %#v, want explicit offline", statuses[0].Status)
|
|
}
|
|
}
|
|
|
|
func TestSessionOfflinePersistsLastSeenAndPushes(t *testing.T) {
|
|
ctx := context.Background()
|
|
userStore := memory.NewUserStore()
|
|
alice, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "1001", FirstName: "Alice"})
|
|
if err != nil {
|
|
t.Fatalf("create alice: %v", err)
|
|
}
|
|
bob, err := userStore.Create(ctx, domain.User{AccessHash: 22, Phone: "1002", FirstName: "Bob"})
|
|
if err != nil {
|
|
t.Fatalf("create bob: %v", err)
|
|
}
|
|
contacts := memory.NewContactStore()
|
|
if err := contacts.SaveList(ctx, bob.ID, domain.ContactList{Contacts: []domain.Contact{{User: alice}}}); err != nil {
|
|
t.Fatalf("save bob contacts: %v", err)
|
|
}
|
|
const now = 1700000300
|
|
sessions := &captureSessions{onlineUserIDs: []int64{alice.ID}}
|
|
r := New(Config{}, Deps{
|
|
Contacts: appcontacts.NewService(contacts, userStore),
|
|
Users: appusers.NewService(userStore),
|
|
Sessions: sessions,
|
|
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(now, 0)})
|
|
|
|
// offline 广播经去抖宽限后异步执行;缩短宽限并轮询等待。
|
|
grace := offlineAnnounceGrace
|
|
offlineAnnounceGrace = 10 * time.Millisecond
|
|
defer func() { offlineAnnounceGrace = grace }()
|
|
|
|
r.SessionOffline([8]byte{1, 2, 3}, 22, bob.ID, true)
|
|
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for len(sessions.pushedUserIDs()) < 2 {
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("offline announce never fired; pushed users = %+v", sessions.pushedUserIDs())
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
|
|
stored, found, err := userStore.ByID(ctx, bob.ID)
|
|
if err != nil || !found {
|
|
t.Fatalf("load bob found=%v err=%v", found, err)
|
|
}
|
|
if stored.LastSeenAt != now {
|
|
t.Fatalf("last_seen_at = %d, want %d", stored.LastSeenAt, now)
|
|
}
|
|
if got := sessions.pushedUserIDs(); !reflect.DeepEqual(got, []int64{bob.ID, alice.ID}) {
|
|
t.Fatalf("pushed users = %+v, want bob self and online contact alice", got)
|
|
}
|
|
update := pushedUserStatus(t, sessions.snapshot().message)
|
|
if update.UserID != bob.ID {
|
|
t.Fatalf("status user = %d, want bob", update.UserID)
|
|
}
|
|
if status, ok := update.Status.(*tg.UserStatusOffline); !ok || status.WasOnline != now {
|
|
t.Fatalf("status = %#v, want offline was_online=%d", update.Status, now)
|
|
}
|
|
}
|
|
|
|
// TestSessionOfflineSkipsAnnounceWhenUserReconnects 验证去抖宽限内用户重新上线时
|
|
// 跳过 offline 广播——移动端重连/换 session 是「先断旧再建新」,没有去抖会对
|
|
// 全部好友广播一轮 offline→online 抖动。
|
|
func TestSessionOfflineSkipsAnnounceWhenUserReconnects(t *testing.T) {
|
|
ctx := context.Background()
|
|
userStore := memory.NewUserStore()
|
|
bob, err := userStore.Create(ctx, domain.User{AccessHash: 22, Phone: "1002", FirstName: "Bob"})
|
|
if err != nil {
|
|
t.Fatalf("create bob: %v", err)
|
|
}
|
|
const now = 1700000300
|
|
// bob 在 IsUserOnline 名单里:模拟宽限期内已重新上线。
|
|
sessions := &captureSessions{onlineUserIDs: []int64{bob.ID}}
|
|
r := New(Config{}, Deps{
|
|
Users: appusers.NewService(userStore),
|
|
Sessions: sessions,
|
|
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(now, 0)})
|
|
|
|
grace := offlineAnnounceGrace
|
|
offlineAnnounceGrace = 10 * time.Millisecond
|
|
defer func() { offlineAnnounceGrace = grace }()
|
|
|
|
r.SessionOffline([8]byte{1, 2, 3}, 22, bob.ID, true)
|
|
time.Sleep(20 * offlineAnnounceGrace)
|
|
|
|
if got := sessions.pushedUserIDs(); len(got) != 0 {
|
|
t.Fatalf("pushed users = %+v, want none (user reconnected within grace)", got)
|
|
}
|
|
stored, found, err := userStore.ByID(ctx, bob.ID)
|
|
if err != nil || !found {
|
|
t.Fatalf("load bob found=%v err=%v", found, err)
|
|
}
|
|
if stored.LastSeenAt != 0 {
|
|
t.Fatalf("last_seen_at = %d, want untouched 0", stored.LastSeenAt)
|
|
}
|
|
}
|
|
|
|
func TestAccountUpdateStatusKeepsUserOnlineUntilAllSessionsOffline(t *testing.T) {
|
|
const userID = int64(1000000001)
|
|
r := New(Config{}, Deps{}, zaptest.NewLogger(t), clock.System)
|
|
session1 := WithSessionID(WithUserID(context.Background(), userID), 1)
|
|
session2 := WithSessionID(WithUserID(context.Background(), userID), 2)
|
|
|
|
if ok, err := r.onAccountUpdateStatus(session1, false); err != nil || !ok {
|
|
t.Fatalf("session1 online = %v, %v", ok, err)
|
|
}
|
|
if ok, err := r.onAccountUpdateStatus(session2, false); err != nil || !ok {
|
|
t.Fatalf("session2 online = %v, %v", ok, err)
|
|
}
|
|
if ok, err := r.onAccountUpdateStatus(session1, true); err != nil || !ok {
|
|
t.Fatalf("session1 offline = %v, %v", ok, err)
|
|
}
|
|
if status := r.userPresenceStatus(userID); status.Kind != domain.UserStatusOnline {
|
|
t.Fatalf("aggregate after one offline = %+v, want online", status)
|
|
}
|
|
if ok, err := r.onAccountUpdateStatus(session2, true); err != nil || !ok {
|
|
t.Fatalf("session2 offline = %v, %v", ok, err)
|
|
}
|
|
if status := r.userPresenceStatus(userID); status.Kind != domain.UserStatusOffline || status.WasOnline == 0 {
|
|
t.Fatalf("aggregate after all offline = %+v, want offline", status)
|
|
}
|
|
}
|
|
|
|
func TestContactsBlockGetBlockedAndUnblockRPC(t *testing.T) {
|
|
ctx := context.Background()
|
|
userStore := memory.NewUserStore()
|
|
alice, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550009001", FirstName: "Alice"})
|
|
if err != nil {
|
|
t.Fatalf("create alice: %v", err)
|
|
}
|
|
bob, err := userStore.Create(ctx, domain.User{AccessHash: 22, Phone: "15550009002", FirstName: "Bob"})
|
|
if err != nil {
|
|
t.Fatalf("create bob: %v", err)
|
|
}
|
|
r := New(Config{}, Deps{
|
|
Users: appusers.NewService(userStore),
|
|
Contacts: appcontacts.NewService(memory.NewContactStore(), userStore),
|
|
}, zaptest.NewLogger(t), clock.System)
|
|
|
|
ok, err := r.onContactsBlock(WithUserID(ctx, bob.ID), &tg.ContactsBlockRequest{
|
|
ID: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash},
|
|
})
|
|
if err != nil || !ok {
|
|
t.Fatalf("contacts.block = %v, %v", ok, err)
|
|
}
|
|
blocked, err := r.onContactsGetBlocked(WithUserID(ctx, bob.ID), &tg.ContactsGetBlockedRequest{Limit: 10})
|
|
if err != nil {
|
|
t.Fatalf("contacts.getBlocked: %v", err)
|
|
}
|
|
full, ok := blocked.(*tg.ContactsBlocked)
|
|
if !ok || len(full.Blocked) != 1 || len(full.Users) != 1 {
|
|
t.Fatalf("blocked = %T %+v, want one blocked user", blocked, blocked)
|
|
}
|
|
if peer, ok := full.Blocked[0].PeerID.(*tg.PeerUser); !ok || peer.UserID != alice.ID {
|
|
t.Fatalf("blocked peer = %#v, want alice", full.Blocked[0].PeerID)
|
|
}
|
|
if user, ok := full.Users[0].(*tg.User); !ok || user.ID != alice.ID {
|
|
t.Fatalf("blocked user = %#v, want alice", full.Users[0])
|
|
}
|
|
|
|
ok, err = r.onContactsUnblock(WithUserID(ctx, bob.ID), &tg.ContactsUnblockRequest{
|
|
ID: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash},
|
|
})
|
|
if err != nil || !ok {
|
|
t.Fatalf("contacts.unblock = %v, %v", ok, err)
|
|
}
|
|
blocked, err = r.onContactsGetBlocked(WithUserID(ctx, bob.ID), &tg.ContactsGetBlockedRequest{Limit: 10})
|
|
if err != nil {
|
|
t.Fatalf("contacts.getBlocked after unblock: %v", err)
|
|
}
|
|
if full, ok := blocked.(*tg.ContactsBlocked); !ok || len(full.Blocked) != 0 {
|
|
t.Fatalf("blocked after unblock = %T %+v, want empty contacts.blocked", blocked, blocked)
|
|
}
|
|
}
|