289 lines
11 KiB
Go
289 lines
11 KiB
Go
package rpc
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"github.com/iamxvbaba/td/clock"
|
||
"github.com/iamxvbaba/td/tg"
|
||
"go.uber.org/zap/zaptest"
|
||
|
||
appaccount "telesrv/internal/app/account"
|
||
"telesrv/internal/domain"
|
||
"telesrv/internal/store/memory"
|
||
)
|
||
|
||
func notifyRouter(t *testing.T) (*Router, *captureSessions) {
|
||
t.Helper()
|
||
passwordStore := memory.NewPasswordStore()
|
||
sessions := &captureSessions{}
|
||
r := New(Config{}, Deps{
|
||
Account: appaccount.NewService(passwordStore, appaccount.WithNotifySettings(passwordStore)),
|
||
Sessions: sessions,
|
||
}, zaptest.NewLogger(t), clock.System)
|
||
return r, sessions
|
||
}
|
||
|
||
// TestNotifySettingsRoundTripAndDialogProjection 回归:get/update/reset NotifySettings
|
||
// 此前是回显 stub(不持久化),mute 重启即丢、dialog 列表不反映静音。本测试验证
|
||
// per-peer 持久化往返 + dialog 列表投影出 mute + updateNotifySettings 推送 + reset。
|
||
func TestNotifySettingsRoundTripAndDialogProjection(t *testing.T) {
|
||
r, sessions := notifyRouter(t)
|
||
const viewer = int64(1000000001)
|
||
const peerID = int64(555)
|
||
ctx := WithUserID(context.Background(), viewer)
|
||
peerInput := &tg.InputNotifyPeer{Peer: &tg.InputPeerUser{UserID: peerID}}
|
||
|
||
// 默认:未配置 → mute_until=0(不静音)。
|
||
def, err := r.onAccountGetNotifySettings(ctx, peerInput)
|
||
if err != nil {
|
||
t.Fatalf("get default: %v", err)
|
||
}
|
||
if mu, _ := def.GetMuteUntil(); mu != 0 {
|
||
t.Fatalf("default mute_until = %d, want 0", mu)
|
||
}
|
||
|
||
// mute 该 peer。
|
||
in := tg.InputPeerNotifySettings{}
|
||
in.SetMuteUntil(2000000000)
|
||
in.SetSilent(true)
|
||
if ok, err := r.onAccountUpdateNotifySettings(ctx, &tg.AccountUpdateNotifySettingsRequest{Peer: peerInput, Settings: in}); err != nil || !ok {
|
||
t.Fatalf("update notify = ok %v err %v", ok, err)
|
||
}
|
||
|
||
// 推送 updateNotifySettings。
|
||
snap := sessions.snapshot()
|
||
updates, ok := snap.message.(*tg.Updates)
|
||
if !ok || len(updates.Updates) == 0 {
|
||
t.Fatalf("pushed message = %#v, want *tg.Updates with updates", snap.message)
|
||
}
|
||
upd, ok := updates.Updates[0].(*tg.UpdateNotifySettings)
|
||
if !ok {
|
||
t.Fatalf("pushed update = %T, want *tg.UpdateNotifySettings", updates.Updates[0])
|
||
}
|
||
if np, ok := upd.Peer.(*tg.NotifyPeer); !ok {
|
||
t.Fatalf("pushed notify peer = %T, want *tg.NotifyPeer", upd.Peer)
|
||
} else if pu, ok := np.Peer.(*tg.PeerUser); !ok || pu.UserID != peerID {
|
||
t.Fatalf("pushed notify peer = %#v, want user %d", np.Peer, peerID)
|
||
}
|
||
|
||
// get 读回 mute。
|
||
got, err := r.onAccountGetNotifySettings(ctx, peerInput)
|
||
if err != nil {
|
||
t.Fatalf("get after mute: %v", err)
|
||
}
|
||
if mu, _ := got.GetMuteUntil(); mu != 2000000000 {
|
||
t.Fatalf("mute_until after mute = %d, want 2000000000", mu)
|
||
}
|
||
if silent, ok := got.GetSilent(); !ok || !silent {
|
||
t.Fatalf("silent after mute = %v ok %v, want true", silent, ok)
|
||
}
|
||
|
||
// dialog 列表投影出 mute(跨重启恢复的关键)。
|
||
list := domain.DialogList{Dialogs: []domain.Dialog{
|
||
{Peer: domain.Peer{Type: domain.PeerTypeUser, ID: peerID}, TopMessage: 1},
|
||
{Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 999}, TopMessage: 1}, // 未静音对照
|
||
}}
|
||
out, ok := r.tgMessagesDialogs(ctx, viewer, list).(*tg.MessagesDialogs)
|
||
if !ok {
|
||
t.Fatalf("dialogs projection type = %T", r.tgMessagesDialogs(ctx, viewer, list))
|
||
}
|
||
muted := dialogByPeerUser(t, out.Dialogs, peerID)
|
||
if mu, _ := muted.NotifySettings.GetMuteUntil(); mu != 2000000000 {
|
||
t.Fatalf("dialog mute_until = %d, want 2000000000(列表未反映静音)", mu)
|
||
}
|
||
unmuted := dialogByPeerUser(t, out.Dialogs, 999)
|
||
if mu, _ := unmuted.NotifySettings.GetMuteUntil(); mu != 0 {
|
||
t.Fatalf("unmuted dialog mute_until = %d, want 0", mu)
|
||
}
|
||
|
||
// reset → 回默认。
|
||
if ok, err := r.onAccountResetNotifySettings(ctx); err != nil || !ok {
|
||
t.Fatalf("reset = ok %v err %v", ok, err)
|
||
}
|
||
after, err := r.onAccountGetNotifySettings(ctx, peerInput)
|
||
if err != nil {
|
||
t.Fatalf("get after reset: %v", err)
|
||
}
|
||
if mu, _ := after.GetMuteUntil(); mu != 0 {
|
||
t.Fatalf("mute_until after reset = %d, want 0", mu)
|
||
}
|
||
}
|
||
|
||
// TestNotifySettingsCategoryDefaultScope 验证 inputNotifyUsers 等类别默认作用域独立持久化。
|
||
func TestNotifySettingsCategoryDefaultScope(t *testing.T) {
|
||
r, _ := notifyRouter(t)
|
||
ctx := WithUserID(context.Background(), 1000000001)
|
||
|
||
in := tg.InputPeerNotifySettings{}
|
||
in.SetMuteUntil(123456)
|
||
if ok, err := r.onAccountUpdateNotifySettings(ctx, &tg.AccountUpdateNotifySettingsRequest{Peer: &tg.InputNotifyUsers{}, Settings: in}); err != nil || !ok {
|
||
t.Fatalf("update users-default = ok %v err %v", ok, err)
|
||
}
|
||
// users 默认有值,chats 默认仍为默认(作用域隔离)。
|
||
usersGot, err := r.onAccountGetNotifySettings(ctx, &tg.InputNotifyUsers{})
|
||
if err != nil {
|
||
t.Fatalf("get users-default: %v", err)
|
||
}
|
||
if mu, _ := usersGot.GetMuteUntil(); mu != 123456 {
|
||
t.Fatalf("users-default mute_until = %d, want 123456", mu)
|
||
}
|
||
chatsGot, err := r.onAccountGetNotifySettings(ctx, &tg.InputNotifyChats{})
|
||
if err != nil {
|
||
t.Fatalf("get chats-default: %v", err)
|
||
}
|
||
if mu, _ := chatsGot.GetMuteUntil(); mu != 0 {
|
||
t.Fatalf("chats-default mute_until = %d, want 0(作用域应隔离)", mu)
|
||
}
|
||
}
|
||
|
||
// TestGetNotifyExceptions 验证 getNotifyExceptions 列出 per-peer 非默认设置:
|
||
// mute 多个 peer → 出现在异常列表;unmute → 退出;compare_stories 过滤 story-only。
|
||
func TestGetNotifyExceptions(t *testing.T) {
|
||
r, _ := notifyRouter(t)
|
||
ctx := WithUserID(context.Background(), 1000000001)
|
||
mute := func(peer tg.InputPeerClass, set func(*tg.InputPeerNotifySettings)) {
|
||
in := tg.InputPeerNotifySettings{}
|
||
set(&in)
|
||
if ok, err := r.onAccountUpdateNotifySettings(ctx, &tg.AccountUpdateNotifySettingsRequest{Peer: &tg.InputNotifyPeer{Peer: peer}, Settings: in}); err != nil || !ok {
|
||
t.Fatalf("update notify = ok %v err %v", ok, err)
|
||
}
|
||
}
|
||
|
||
// mute 一个 user + 一个 channel。
|
||
mute(&tg.InputPeerUser{UserID: 555}, func(in *tg.InputPeerNotifySettings) { in.SetMuteUntil(2000000000) })
|
||
mute(&tg.InputPeerChannel{ChannelID: 777}, func(in *tg.InputPeerNotifySettings) { in.SetSilent(true) })
|
||
|
||
ex := notifyExceptions(t, r, ctx, &tg.AccountGetNotifyExceptionsRequest{})
|
||
if len(ex) != 2 {
|
||
t.Fatalf("exceptions = %d, want 2", len(ex))
|
||
}
|
||
if !exceptionsHaveUser(ex, 555) || !exceptionsHaveChannel(ex, 777) {
|
||
t.Fatalf("exceptions missing expected peers: %#v", ex)
|
||
}
|
||
|
||
// unmute user 555(发空设置→清空)→ 退出异常列表。
|
||
mute(&tg.InputPeerUser{UserID: 555}, func(in *tg.InputPeerNotifySettings) {})
|
||
ex = notifyExceptions(t, r, ctx, &tg.AccountGetNotifyExceptionsRequest{})
|
||
if len(ex) != 1 || !exceptionsHaveChannel(ex, 777) {
|
||
t.Fatalf("after unmute exceptions = %#v, want only channel 777", ex)
|
||
}
|
||
|
||
// story-only 异常:默认不计入,compare_stories 计入。
|
||
mute(&tg.InputPeerUser{UserID: 888}, func(in *tg.InputPeerNotifySettings) { in.SetStoriesMuted(true) })
|
||
if got := notifyExceptions(t, r, ctx, &tg.AccountGetNotifyExceptionsRequest{}); len(got) != 1 {
|
||
t.Fatalf("default exceptions = %d, want 1 (story-only excluded)", len(got))
|
||
}
|
||
withStories := &tg.AccountGetNotifyExceptionsRequest{}
|
||
withStories.CompareStories = true
|
||
if got := notifyExceptions(t, r, ctx, withStories); len(got) != 2 || !exceptionsHaveUser(got, 888) {
|
||
t.Fatalf("compare_stories exceptions = %#v, want story-only 888 included", got)
|
||
}
|
||
}
|
||
|
||
func notifyExceptions(t *testing.T, r *Router, ctx context.Context, req *tg.AccountGetNotifyExceptionsRequest) []*tg.UpdateNotifySettings {
|
||
t.Helper()
|
||
out, err := r.onAccountGetNotifyExceptions(ctx, req)
|
||
if err != nil {
|
||
t.Fatalf("getNotifyExceptions: %v", err)
|
||
}
|
||
upd, ok := out.(*tg.Updates)
|
||
if !ok {
|
||
t.Fatalf("response = %T, want *tg.Updates", out)
|
||
}
|
||
res := make([]*tg.UpdateNotifySettings, 0, len(upd.Updates))
|
||
for _, u := range upd.Updates {
|
||
if uns, ok := u.(*tg.UpdateNotifySettings); ok {
|
||
res = append(res, uns)
|
||
}
|
||
}
|
||
return res
|
||
}
|
||
|
||
func exceptionsHaveUser(ex []*tg.UpdateNotifySettings, userID int64) bool {
|
||
for _, e := range ex {
|
||
if np, ok := e.Peer.(*tg.NotifyPeer); ok {
|
||
if pu, ok := np.Peer.(*tg.PeerUser); ok && pu.UserID == userID {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func exceptionsHaveChannel(ex []*tg.UpdateNotifySettings, channelID int64) bool {
|
||
for _, e := range ex {
|
||
if np, ok := e.Peer.(*tg.NotifyPeer); ok {
|
||
if pc, ok := np.Peer.(*tg.PeerChannel); ok && pc.ChannelID == channelID {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// countingNotifyService 包 *appaccount.Service 计数 AllPeerNotifySettings 调用,
|
||
// 验证 per-user notify 缓存短路了热路径查询。
|
||
type countingNotifyService struct {
|
||
*appaccount.Service
|
||
allCalls int
|
||
}
|
||
|
||
func (s *countingNotifyService) AllPeerNotifySettings(ctx context.Context, userID int64) (map[domain.Peer]domain.PeerNotifySettings, error) {
|
||
s.allCalls++
|
||
return s.Service.AllPeerNotifySettings(ctx, userID)
|
||
}
|
||
|
||
// TestNotifySettingsDialogProjectionCached 回归 P2-1:dialog 投影从 per-user notify
|
||
// 缓存读取——重复 getDialogs 只加载一次(命中 0 PG),update/reset 失效后重载。
|
||
func TestNotifySettingsDialogProjectionCached(t *testing.T) {
|
||
passwordStore := memory.NewPasswordStore()
|
||
svc := &countingNotifyService{Service: appaccount.NewService(passwordStore, appaccount.WithNotifySettings(passwordStore))}
|
||
r := New(Config{}, Deps{Account: svc, Sessions: &captureSessions{}}, zaptest.NewLogger(t), clock.System)
|
||
const viewer = int64(1000000001)
|
||
ctx := WithUserID(context.Background(), viewer)
|
||
|
||
in := tg.InputPeerNotifySettings{}
|
||
in.SetMuteUntil(2000000000)
|
||
if ok, err := r.onAccountUpdateNotifySettings(ctx, &tg.AccountUpdateNotifySettingsRequest{Peer: &tg.InputNotifyPeer{Peer: &tg.InputPeerUser{UserID: 555}}, Settings: in}); err != nil || !ok {
|
||
t.Fatalf("update notify: ok %v err %v", ok, err)
|
||
}
|
||
|
||
list := domain.DialogList{Dialogs: []domain.Dialog{{Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 555}, TopMessage: 1}}}
|
||
for i := 0; i < 3; i++ {
|
||
out := r.tgMessagesDialogs(ctx, viewer, list).(*tg.MessagesDialogs)
|
||
dlg := dialogByPeerUser(t, out.Dialogs, 555)
|
||
if mu, _ := dlg.NotifySettings.GetMuteUntil(); mu != 2000000000 {
|
||
t.Fatalf("iter %d dialog mute = %d, want 2000000000", i, mu)
|
||
}
|
||
}
|
||
if svc.allCalls != 1 {
|
||
t.Fatalf("AllPeerNotifySettings calls = %d, want 1 (后两次 getDialogs 应命中缓存)", svc.allCalls)
|
||
}
|
||
|
||
// update 失效缓存 → 下次 getDialogs 重载。
|
||
in2 := tg.InputPeerNotifySettings{}
|
||
in2.SetMuteUntil(2100000000)
|
||
if _, err := r.onAccountUpdateNotifySettings(ctx, &tg.AccountUpdateNotifySettingsRequest{Peer: &tg.InputNotifyPeer{Peer: &tg.InputPeerUser{UserID: 555}}, Settings: in2}); err != nil {
|
||
t.Fatalf("re-update notify: %v", err)
|
||
}
|
||
r.tgMessagesDialogs(ctx, viewer, list)
|
||
if svc.allCalls != 2 {
|
||
t.Fatalf("after invalidation calls = %d, want 2 (缓存失效应重载)", svc.allCalls)
|
||
}
|
||
}
|
||
|
||
func dialogByPeerUser(t *testing.T, dialogs []tg.DialogClass, userID int64) *tg.Dialog {
|
||
t.Helper()
|
||
for _, d := range dialogs {
|
||
dlg, ok := d.(*tg.Dialog)
|
||
if !ok {
|
||
continue
|
||
}
|
||
if pu, ok := dlg.Peer.(*tg.PeerUser); ok && pu.UserID == userID {
|
||
return dlg
|
||
}
|
||
}
|
||
t.Fatalf("dialog for user %d not found", userID)
|
||
return nil
|
||
}
|