owpengram-server/internal/app/dialogs/service_test.go
A 8ff3343ae0 business: add privacy-aware user projection
(cherry picked from commit a636192ef22ee69d792b0ca7db1c6be963be9cb2)
2026-06-14 00:19:59 +08:00

363 lines
12 KiB
Go

package dialogs
import (
"context"
"errors"
"testing"
appchannels "telesrv/internal/app/channels"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
)
func TestGetDialogsIncludesChannelReadOutboxAfterOfflineRead(t *testing.T) {
ctx := context.Background()
channelStore := memory.NewChannelStore()
channels := appchannels.NewService(channelStore)
dialogs := NewService(nil, channelStore)
created, err := channels.CreateMegagroupFromCreateChat(ctx, 1001, domain.CreateChannelRequest{
Title: "Offline Read",
MemberUserIDs: []int64{1002},
Date: 10,
})
if err != nil {
t.Fatalf("CreateMegagroupFromCreateChat: %v", err)
}
sent, err := channels.SendMessage(ctx, 1001, domain.SendChannelMessageRequest{
ChannelID: created.Channel.ID,
RandomID: 42,
Message: "restore read outbox",
Date: 11,
})
if err != nil {
t.Fatalf("SendMessage: %v", err)
}
if _, err := channels.ReadHistory(ctx, 1002, domain.ReadChannelHistoryRequest{
ChannelID: created.Channel.ID,
MaxID: sent.Message.ID,
Date: 12,
}); err != nil {
t.Fatalf("ReadHistory: %v", err)
}
list, err := dialogs.GetDialogs(ctx, 1001, domain.DialogFilter{Limit: 10})
if err != nil {
t.Fatalf("GetDialogs: %v", err)
}
dialog := findChannelDialog(t, list, created.Channel.ID)
if dialog.ReadOutboxMaxID != sent.Message.ID {
t.Fatalf("getDialogs read_outbox = %d, want %d", dialog.ReadOutboxMaxID, sent.Message.ID)
}
peerList, err := dialogs.GetPeerDialogs(ctx, 1001, []domain.Peer{
{Type: domain.PeerTypeChannel, ID: created.Channel.ID},
})
if err != nil {
t.Fatalf("GetPeerDialogs: %v", err)
}
peerDialog := findChannelDialog(t, peerList, created.Channel.ID)
if peerDialog.ReadOutboxMaxID != sent.Message.ID {
t.Fatalf("getPeerDialogs read_outbox = %d, want %d", peerDialog.ReadOutboxMaxID, sent.Message.ID)
}
}
func TestGetDialogsProjectsUsersWithCurrentProfilePhoto(t *testing.T) {
ctx := context.Background()
const ownerID int64 = 1001
const peerID int64 = 1002
dialogStore := memory.NewDialogStore()
if err := dialogStore.SaveList(ctx, ownerID, domain.DialogList{
Dialogs: []domain.Dialog{{
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: peerID},
TopMessage: 1,
TopMessageDate: 20,
}},
Users: []domain.User{{
ID: peerID,
AccessHash: 22,
Phone: "15550000002",
FirstName: "Alice A",
}},
}); err != nil {
t.Fatalf("SaveList: %v", err)
}
contacts := memory.NewContactStore()
if _, err := contacts.Upsert(ctx, ownerID, domain.ContactInput{
ContactUserID: peerID,
Phone: "1111",
FirstName: "Alice",
LastName: "Saved",
}); err != nil {
t.Fatalf("upsert contact: %v", err)
}
dialogs := NewService(dialogStore).Configure(
WithContactStore(contacts),
WithPhotoProvider(dialogProfilePhotos{
peerID: {PhotoID: 9201, DCID: 2, Stripped: []byte{7, 8}},
}),
)
list, err := dialogs.GetDialogs(ctx, ownerID, domain.DialogFilter{Limit: 10})
if err != nil {
t.Fatalf("GetDialogs: %v", err)
}
peer := findDialogUser(t, list.Users, peerID)
if peer.PhotoID != 9201 || peer.PhotoDCID != 2 || string(peer.PhotoStripped) != string([]byte{7, 8}) {
t.Fatalf("dialog user photo = id %d dc %d stripped %v, want 9201/2/[7 8]", peer.PhotoID, peer.PhotoDCID, peer.PhotoStripped)
}
if !peer.Contact || peer.FirstName != "Alice" || peer.LastName != "Saved" || peer.Phone != "1111" {
t.Fatalf("dialog user projection = %+v, want contact view", peer)
}
peerList, err := dialogs.GetPeerDialogs(ctx, ownerID, []domain.Peer{{Type: domain.PeerTypeUser, ID: peerID}})
if err != nil {
t.Fatalf("GetPeerDialogs: %v", err)
}
peer = findDialogUser(t, peerList.Users, peerID)
if peer.PhotoID != 9201 || peer.PhotoDCID != 2 {
t.Fatalf("peer dialog user photo = id %d dc %d, want 9201/2", peer.PhotoID, peer.PhotoDCID)
}
}
func TestChannelDialogSettingsPersistThroughUnifiedDialogService(t *testing.T) {
ctx := context.Background()
channelStore := memory.NewChannelStore()
channels := appchannels.NewService(channelStore)
dialogs := NewService(nil, channelStore)
first, err := channels.CreateMegagroupFromCreateChat(ctx, 1001, domain.CreateChannelRequest{
Title: "Pinned One",
Date: 20,
})
if err != nil {
t.Fatalf("CreateMegagroupFromCreateChat first: %v", err)
}
second, err := channels.CreateMegagroupFromCreateChat(ctx, 1001, domain.CreateChannelRequest{
Title: "Pinned Two",
Date: 21,
})
if err != nil {
t.Fatalf("CreateMegagroupFromCreateChat second: %v", err)
}
firstPeer := domain.Peer{Type: domain.PeerTypeChannel, ID: first.Channel.ID}
secondPeer := domain.Peer{Type: domain.PeerTypeChannel, ID: second.Channel.ID}
if changed, err := dialogs.TogglePinned(ctx, 1001, firstPeer, true); err != nil || !changed {
t.Fatalf("TogglePinned first = changed %v err %v, want changed", changed, err)
}
if changed, err := dialogs.TogglePinned(ctx, 1001, secondPeer, true); err != nil || !changed {
t.Fatalf("TogglePinned second = changed %v err %v, want changed", changed, err)
}
if err := dialogs.ReorderPinned(ctx, 1001, []domain.Peer{secondPeer, firstPeer}, true); err != nil {
t.Fatalf("ReorderPinned: %v", err)
}
if changed, err := dialogs.MarkUnread(ctx, 1001, firstPeer, true); err != nil || !changed {
t.Fatalf("MarkUnread = changed %v err %v, want changed", changed, err)
}
if err := dialogs.EditPeerFolders(ctx, 1001, []domain.FolderPeerUpdate{
{Peer: firstPeer, FolderID: domain.DialogArchiveFolderID},
}); err != nil {
t.Fatalf("EditPeerFolders: %v", err)
}
list, err := dialogs.GetDialogs(ctx, 1001, domain.DialogFilter{Limit: 10})
if err != nil {
t.Fatalf("GetDialogs: %v", err)
}
firstDialog := findChannelDialog(t, list, first.Channel.ID)
if !firstDialog.Pinned || firstDialog.PinnedOrder != 1 || !firstDialog.UnreadMark || firstDialog.FolderID != domain.DialogArchiveFolderID {
t.Fatalf("first dialog = %+v, want pinned order 1, unread mark, archived", firstDialog)
}
secondDialog := findChannelDialog(t, list, second.Channel.ID)
if !secondDialog.Pinned || secondDialog.PinnedOrder != 2 {
t.Fatalf("second dialog = %+v, want pinned order 2", secondDialog)
}
marks, err := dialogs.UnreadMarks(ctx, 1001)
if err != nil {
t.Fatalf("UnreadMarks: %v", err)
}
if len(marks) != 1 || marks[0] != firstPeer {
t.Fatalf("unread marks = %+v, want first channel", marks)
}
archived, err := dialogs.GetDialogs(ctx, 1001, domain.DialogFilter{
HasFolderID: true,
FolderID: domain.DialogArchiveFolderID,
Limit: 10,
})
if err != nil {
t.Fatalf("GetDialogs archive: %v", err)
}
if got := findChannelDialog(t, archived, first.Channel.ID); got.FolderID != domain.DialogArchiveFolderID {
t.Fatalf("archived dialog = %+v, want archive folder", got)
}
custom, err := dialogs.GetDialogs(ctx, 1001, domain.DialogFilter{
HasFolderID: true,
FolderID: domain.DialogCustomFolderMinID,
Folder: &domain.DialogFolder{ID: domain.DialogCustomFolderMinID, Groups: true},
Limit: 10,
})
if err != nil {
t.Fatalf("GetDialogs custom groups: %v", err)
}
if got := findChannelDialog(t, custom, first.Channel.ID); got.Peer.ID != first.Channel.ID {
t.Fatalf("custom group dialog = %+v, want first channel", got)
}
}
func TestGetDialogsAppliesChannelDialogOffset(t *testing.T) {
ctx := context.Background()
channelStore := memory.NewChannelStore()
channels := appchannels.NewService(channelStore)
dialogs := NewService(nil, channelStore)
old, err := channels.CreateMegagroupFromCreateChat(ctx, 1001, domain.CreateChannelRequest{
Title: "Older Channel",
Date: 20,
})
if err != nil {
t.Fatalf("CreateMegagroupFromCreateChat old: %v", err)
}
newer, err := channels.CreateMegagroupFromCreateChat(ctx, 1001, domain.CreateChannelRequest{
Title: "Newer Channel",
Date: 30,
})
if err != nil {
t.Fatalf("CreateMegagroupFromCreateChat newer: %v", err)
}
first, err := dialogs.GetDialogs(ctx, 1001, domain.DialogFilter{Limit: 1})
if err != nil {
t.Fatalf("GetDialogs first: %v", err)
}
if len(first.Dialogs) != 1 || first.Dialogs[0].Peer.ID != newer.Channel.ID {
t.Fatalf("first page dialogs = %+v, want newer channel", first.Dialogs)
}
next, err := dialogs.GetDialogs(ctx, 1001, domain.DialogFilter{
OffsetDate: first.Dialogs[0].TopMessageDate,
OffsetID: first.Dialogs[0].TopMessage,
HasOffsetPeer: true,
OffsetPeer: first.Dialogs[0].Peer,
Limit: 10,
})
if err != nil {
t.Fatalf("GetDialogs next: %v", err)
}
if len(next.Dialogs) != 1 || next.Dialogs[0].Peer.ID != old.Channel.ID {
t.Fatalf("next page dialogs = %+v, want only older channel", next.Dialogs)
}
}
func TestGetPeerDialogsRejectsHugeVector(t *testing.T) {
dialogs := NewService(nil, memory.NewChannelStore())
peers := make([]domain.Peer, domain.MaxDialogFolderPeers+1)
for i := range peers {
peers[i] = domain.Peer{Type: domain.PeerTypeChannel, ID: int64(i + 1)}
}
if _, err := dialogs.GetPeerDialogs(context.Background(), 1001, peers); !errors.Is(err, domain.ErrChannelInvalid) {
t.Fatalf("GetPeerDialogs huge vector err = %v, want ErrChannelInvalid", err)
}
}
func TestGetPeerDialogsIncludesPublicChannelPreviewForNonMember(t *testing.T) {
ctx := context.Background()
channelStore := memory.NewChannelStore()
channels := appchannels.NewService(channelStore)
dialogs := NewService(nil, channelStore)
public, err := channels.CreateChannel(ctx, 1001, domain.CreateChannelRequest{
Title: "Public Peer Dialog",
Broadcast: true,
Date: 1700002000,
})
if err != nil {
t.Fatalf("CreateChannel public: %v", err)
}
if _, err := channels.UpdateUsername(ctx, 1001, domain.UpdateChannelUsernameRequest{
UserID: 1001,
ChannelID: public.Channel.ID,
Username: "public_peer_dialog",
}); err != nil {
t.Fatalf("UpdateUsername public: %v", err)
}
sent, err := channels.SendMessage(ctx, 1001, domain.SendChannelMessageRequest{
ChannelID: public.Channel.ID,
RandomID: 99,
Message: "public peer dialog top",
Date: 1700002010,
})
if err != nil {
t.Fatalf("SendMessage public: %v", err)
}
private, err := channels.CreateChannel(ctx, 1001, domain.CreateChannelRequest{
Title: "Private Peer Dialog",
Broadcast: true,
Date: 1700002020,
})
if err != nil {
t.Fatalf("CreateChannel private: %v", err)
}
list, err := dialogs.GetPeerDialogs(ctx, 1002, []domain.Peer{
{Type: domain.PeerTypeChannel, ID: public.Channel.ID},
{Type: domain.PeerTypeChannel, ID: private.Channel.ID},
})
if err != nil {
t.Fatalf("GetPeerDialogs public preview: %v", err)
}
if len(list.Dialogs) != 1 {
t.Fatalf("dialogs = %+v, want only public preview dialog", list.Dialogs)
}
dialog := findChannelDialog(t, list, public.Channel.ID)
if dialog.TopMessage != sent.Message.ID || dialog.TopMessageDate != sent.Message.Date {
t.Fatalf("preview dialog top = id %d date %d, want %d/%d", dialog.TopMessage, dialog.TopMessageDate, sent.Message.ID, sent.Message.Date)
}
if !dialog.ChannelLeft {
t.Fatalf("preview dialog ChannelLeft = false, want read-only left preview")
}
if dialog.UnreadCount != 0 || dialog.ReadInboxMaxID < sent.Message.ID || dialog.ReadOutboxMaxID < sent.Message.ID {
t.Fatalf("preview dialog read/unread = %+v, want read through top and no unread", dialog)
}
if len(list.ChannelMessages) != 1 || list.ChannelMessages[0].Body != "public peer dialog top" {
t.Fatalf("channel messages = %+v, want public top message", list.ChannelMessages)
}
if len(list.Channels) != 1 || list.Channels[0].ID != public.Channel.ID {
t.Fatalf("channels = %+v, want public channel shell", list.Channels)
}
}
func findChannelDialog(t *testing.T, list domain.DialogList, channelID int64) domain.Dialog {
t.Helper()
for _, dialog := range list.Dialogs {
if dialog.Peer.Type == domain.PeerTypeChannel && dialog.Peer.ID == channelID {
return dialog
}
}
t.Fatalf("channel dialog %d not found in %+v", channelID, list.Dialogs)
return domain.Dialog{}
}
func findDialogUser(t *testing.T, users []domain.User, userID int64) domain.User {
t.Helper()
for _, user := range users {
if user.ID == userID {
return user
}
}
t.Fatalf("user %d not found in %+v", userID, users)
return domain.User{}
}
type dialogProfilePhotos map[int64]domain.ProfilePhotoRef
func (p dialogProfilePhotos) CurrentProfilePhotos(_ context.Context, _ domain.PeerType, ids []int64) (map[int64]domain.ProfilePhotoRef, error) {
out := make(map[int64]domain.ProfilePhotoRef, len(ids))
for _, id := range ids {
if ref, ok := p[id]; ok {
out[id] = ref
}
}
return out, nil
}