owpengram-server/internal/rpc/welcome_messages_rpc_test.go
2026-09-01 12:06:31 +03:00

332 lines
13 KiB
Go

package rpc
import (
"context"
"testing"
"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"
"telesrv/internal/domain"
)
type welcomeRPCService struct {
messages []domain.WelcomeMessage
hash int64
hasAny bool
authorizeErr error
}
func (s *welcomeRPCService) Authorize(context.Context, int64, domain.Peer) error {
return s.authorizeErr
}
func (s *welcomeRPCService) Create(_ context.Context, userID int64, peer domain.Peer, randomID int64, content domain.WelcomeMessageContent) (domain.WelcomeMessage, bool, error) {
s.hash++
message := domain.WelcomeMessage{
ID: len(s.messages) + 1, Peer: peer, CreatorUserID: userID, Date: 1700000100,
RandomID: randomID, Content: content, CreateFingerprint: [32]byte{1}, Version: 1,
}
s.messages = append(s.messages, message)
s.hasAny = true
return message, true, nil
}
func (s *welcomeRPCService) Edit(_ context.Context, _ int64, _ domain.Peer, id int, fields domain.WelcomeMessageEditFields) (domain.WelcomeMessage, error) {
for index := range s.messages {
if s.messages[index].ID == id {
content, err := fields.Apply(s.messages[index].Content)
if err != nil {
return domain.WelcomeMessage{}, err
}
s.messages[index].Content = content
s.messages[index].EditDate = 1700000101
s.messages[index].Version++
s.hash++
return s.messages[index], nil
}
}
return domain.WelcomeMessage{}, domain.ErrWelcomeMessageNotFound
}
func (s *welcomeRPCService) List(_ context.Context, _ int64, _ domain.Peer, hash int64) (domain.WelcomeMessageList, error) {
if hash == s.hash {
return domain.WelcomeMessageList{Hash: s.hash, NotModified: true}, nil
}
return domain.WelcomeMessageList{Hash: s.hash, Messages: append([]domain.WelcomeMessage(nil), s.messages...)}, nil
}
func (s *welcomeRPCService) Delete(_ context.Context, _ int64, _ domain.Peer, id int) (bool, error) {
for index := range s.messages {
if s.messages[index].ID == id {
s.messages = append(s.messages[:index], s.messages[index+1:]...)
s.hash++
s.hasAny = len(s.messages) != 0
return true, nil
}
}
return true, nil
}
func (s *welcomeRPCService) DeleteAll(context.Context, int64, domain.Peer) (bool, error) {
if len(s.messages) != 0 {
s.messages = nil
s.hash++
}
s.hasAny = false
return true, nil
}
func (s *welcomeRPCService) HasAny(context.Context, domain.Peer) (bool, error) {
return s.hasAny, nil
}
type welcomeRPCChannels struct {
ChannelsService
view domain.ChannelView
}
func (s *welcomeRPCChannels) ResolveChannel(context.Context, int64, int64) (domain.ChannelView, error) {
return s.view, nil
}
func (s *welcomeRPCChannels) GetChannel(context.Context, int64, int64) (domain.ChannelView, error) {
return s.view, nil
}
type welcomeRPCUsers struct {
user domain.User
}
func (s *welcomeRPCUsers) Self(context.Context, int64) (domain.User, error) {
return s.user, nil
}
func (s *welcomeRPCUsers) ByID(context.Context, int64, int64) (domain.User, bool, error) {
return s.user, true, nil
}
func (s *welcomeRPCUsers) ByIDs(context.Context, int64, []int64) ([]domain.User, error) {
return []domain.User{s.user}, nil
}
func newWelcomeRPCRouter(t *testing.T) (*Router, *welcomeRPCService, context.Context, *tg.InputPeerChannel) {
t.Helper()
const userID, channelID, accessHash = int64(9), int64(77), int64(88)
service := &welcomeRPCService{hash: domain.InitialWelcomeRevision}
channels := &welcomeRPCChannels{view: domain.ChannelView{
Channel: domain.Channel{ID: channelID, AccessHash: accessHash, Title: "Welcome", Megagroup: true, Pts: 1},
Self: domain.ChannelMember{
ChannelID: channelID, UserID: userID, Role: domain.ChannelRoleCreator, Status: domain.ChannelMemberActive,
},
}}
users := &welcomeRPCUsers{user: domain.User{ID: userID, AccessHash: 99, FirstName: "Owner"}}
router := New(Config{DC: 2}, Deps{WelcomeMessages: service, Channels: channels, Users: users}, zaptest.NewLogger(t), clock.System)
return router, service, WithUserID(context.Background(), userID), &tg.InputPeerChannel{ChannelID: channelID, AccessHash: accessHash}
}
func TestWelcomeMessageRPCTextMediaRichAndCRUD(t *testing.T) {
router, service, ctx, peer := newWelcomeRPCRouter(t)
tests := []struct {
name string
request *tg.EphemeralSendMessageRequest
assert func(*testing.T, tg.EphemeralMessage)
}{
{
name: "text",
request: &tg.EphemeralSendMessageRequest{
Welcome: true, Peer: peer, ReceiverID: &tg.InputUserEmpty{}, Message: "Hello", RandomID: 1001,
},
assert: func(t *testing.T, message tg.EphemeralMessage) {
if message.Message != "Hello" || message.Media != nil || !message.RichMessage.Zero() {
t.Fatalf("text welcome = %+v", message)
}
},
},
{
name: "media",
request: &tg.EphemeralSendMessageRequest{
Welcome: true, Peer: peer, ReceiverID: &tg.InputUserEmpty{}, Message: "Contact", RandomID: 1002,
Media: &tg.InputMediaContact{PhoneNumber: "+10000000000", FirstName: "Guest"},
},
assert: func(t *testing.T, message tg.EphemeralMessage) {
if message.Media == nil {
t.Fatalf("media welcome = %+v", message)
}
},
},
{
name: "rich",
request: &tg.EphemeralSendMessageRequest{
Welcome: true, Peer: peer, ReceiverID: &tg.InputUserEmpty{}, RandomID: 1003,
RichMessage: &tg.InputRichMessage{Blocks: []tg.PageBlockClass{
&tg.PageBlockParagraph{Text: &tg.TextPlain{Text: "Rich welcome"}},
}},
},
assert: func(t *testing.T, message tg.EphemeralMessage) {
if message.RichMessage.Zero() || len(message.RichMessage.Blocks) != 1 {
t.Fatalf("rich welcome = %+v", message)
}
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
updates, err := router.onEphemeralSendMessage(ctx, test.request)
if err != nil {
t.Fatal(err)
}
result, ok := updates.(*tg.Updates)
if !ok || len(result.Updates) != 1 || result.Seq != 0 {
t.Fatalf("send updates = %#v", updates)
}
created, ok := result.Updates[0].(*tg.UpdateNewEphemeralMessage)
if !ok || !created.Message.WelcomeTemplate || created.Message.PeerID == nil ||
created.Message.ReceiverID != 0 || !created.Message.Out {
t.Fatalf("new welcome update = %#v", result.Updates[0])
}
test.assert(t, created.Message)
})
}
edit := &tg.EphemeralEditMessageRequest{
Welcome: true, Peer: peer, ReceiverID: &tg.InputUserEmpty{}, ID: 1,
}
edit.SetMessage("Edited")
editedUpdates, err := router.onEphemeralEditWelcomeMessage(ctx, edit)
if err != nil {
t.Fatal(err)
}
edited := editedUpdates.(*tg.Updates).Updates[0].(*tg.UpdateEditEphemeralMessage).Message
if edited.Message != "Edited" || !edited.WelcomeTemplate {
t.Fatalf("edited welcome = %+v", edited)
}
listed, err := router.onEphemeralGetWelcomeMessages(ctx, &tg.EphemeralGetWelcomeMessagesRequest{Peer: peer, Hash: 0})
if err != nil {
t.Fatal(err)
}
modified, ok := listed.(*tg.EphemeralWelcomeMessages)
if !ok || len(modified.Messages) != 3 || modified.Hash != service.hash {
t.Fatalf("listed welcomes = %#v", listed)
}
if same, err := router.onEphemeralGetWelcomeMessages(ctx, &tg.EphemeralGetWelcomeMessagesRequest{Peer: peer, Hash: service.hash}); err != nil {
t.Fatal(err)
} else if _, ok := same.(*tg.EphemeralWelcomeMessagesNotModified); !ok {
t.Fatalf("same hash = %#v", same)
}
if ok, err := router.onEphemeralDeleteWelcomeMessage(ctx, &tg.EphemeralDeleteWelcomeMessageRequest{Peer: peer, ID: 1}); err != nil || !ok {
t.Fatalf("delete = %v,%v", ok, err)
}
if ok, err := router.onEphemeralDeleteAllWelcomeMessages(ctx, &tg.EphemeralDeleteAllWelcomeMessagesRequest{Peer: peer}); err != nil || !ok {
t.Fatalf("delete all = %v,%v", ok, err)
}
}
func TestWelcomeMessageFullChatProjectionAndAdminRight(t *testing.T) {
router, service, ctx, _ := newWelcomeRPCRouter(t)
channelFull := &tg.ChannelFull{}
chatFull := &tg.ChatFull{}
service.hasAny = true
if err := router.applyWelcomeMessagesToFullChat(ctx, 77, channelFull); err != nil {
t.Fatal(err)
}
if err := router.applyWelcomeMessagesToFullChat(ctx, 77, chatFull); err != nil {
t.Fatal(err)
}
if !channelFull.HasWelcomeMessages || !chatFull.HasWelcomeMessages {
t.Fatalf("full projections channel=%v chat=%v", channelFull.HasWelcomeMessages, chatFull.HasWelcomeMessages)
}
service.hasAny = false
if err := router.applyWelcomeMessagesToFullChat(ctx, 77, channelFull); err != nil {
t.Fatal(err)
}
if channelFull.HasWelcomeMessages {
t.Fatal("deleted templates left stale channelFull.has_welcome_messages")
}
rights := tgChatAdminRights(domain.ChannelAdminRights{ManageWelcomeMessages: true})
if !rights.ManageWelcomeMessages || !domainChannelAdminRights(rights).ManageWelcomeMessages {
t.Fatalf("manage_welcome_messages conversion = %+v", rights)
}
}
func TestWelcomeMessageAuthorizationPrecedesRichMaterialization(t *testing.T) {
router, service, ctx, peer := newWelcomeRPCRouter(t)
service.authorizeErr = domain.ErrWelcomeMessageForbidden
request := &tg.EphemeralSendMessageRequest{
Welcome: true, Peer: peer, ReceiverID: &tg.InputUserEmpty{}, RandomID: 1001,
// This rich payload is deliberately invalid. CHAT_ADMIN_REQUIRED proves
// the permission gate ran before rich parsing or media resolution.
RichMessage: &tg.InputRichMessage{},
}
if _, err := router.onEphemeralSendMessage(ctx, request); err == nil || !tgerr.Is(err, "CHAT_ADMIN_REQUIRED") {
t.Fatalf("unauthorized invalid rich send err=%v, want CHAT_ADMIN_REQUIRED", err)
}
}
func TestWelcomeMessagesExactLayer229Only(t *testing.T) {
router := New(Config{DC: 2}, Deps{}, zaptest.NewLogger(t), clock.System)
peer := &tg.InputPeerChannel{ChannelID: 77, AccessHash: 88}
methods := []struct {
name string
request bin.Object
semantic tlprofile.SemanticID
}{
{"send", &tg.EphemeralSendMessageRequest{Welcome: true, Peer: peer, ReceiverID: &tg.InputUserEmpty{}, Message: "Hello", RandomID: 1001}, tlprofile.SemanticMethodEphemeralSendMessage},
{"edit", &tg.EphemeralEditMessageRequest{Welcome: true, Peer: peer, ReceiverID: &tg.InputUserEmpty{}, ID: 1}, tlprofile.SemanticMethodEphemeralEditMessage},
{"delete", &tg.EphemeralDeleteWelcomeMessageRequest{Peer: peer, ID: 1}, tlprofile.SemanticMethodEphemeralDeleteWelcomeMessage},
{"delete-all", &tg.EphemeralDeleteAllWelcomeMessagesRequest{Peer: peer}, tlprofile.SemanticMethodEphemeralDeleteAllWelcomeMessages},
{"get", &tg.EphemeralGetWelcomeMessagesRequest{Peer: peer, Hash: 0}, tlprofile.SemanticMethodEphemeralGetWelcomeMessages},
}
var getAdmission tlprofile.Admission
hasGetAdmission := false
for _, method := range methods {
t.Run(method.name, func(t *testing.T) {
body := encodeExactLayerRPC(t, tlprofile.Profile229, method.request)
raw := body.Copy()
admission, err := router.AdmitLayer(tlprofile.Profile229, &body, tlprofile.Limits{})
if err != nil || admission.Call().Method() != method.semantic || body.Len() != 0 {
t.Fatalf("Layer 229 admission method=%#x want=%#x remaining=%d err=%v", admission.Call().Method(), method.semantic, body.Len(), err)
}
if method.semantic == tlprofile.SemanticMethodEphemeralGetWelcomeMessages {
getAdmission = admission
hasGetAdmission = true
}
for _, profile := range []tlprofile.Profile{tlprofile.Profile225, tlprofile.Profile226, tlprofile.Profile227, tlprofile.Profile228} {
older := bin.Buffer{Buf: append([]byte(nil), raw...)}
if _, err := router.AdmitLayer(profile, &older, tlprofile.Limits{}); err == nil {
t.Fatalf("exact Layer %d admitted Layer 229 %s RPC", profile, method.name)
}
}
})
}
if !hasGetAdmission {
t.Fatal("missing Layer 229 getWelcomeMessages admission")
}
message := domain.WelcomeMessage{
ID: 1, Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 77}, CreatorUserID: 9,
Date: 1700000100, RandomID: 1001, Content: domain.WelcomeMessageContent{Message: "Hello"},
CreateFingerprint: [32]byte{1}, Version: 1,
}
wire, err := tgWelcomeMessage(message)
if err != nil {
t.Fatal(err)
}
result := &tg.EphemeralWelcomeMessages{Hash: 2, Messages: []tg.EphemeralMessage{wire}}
var encoded229 bin.Buffer
if err := getAdmission.Call().EncodeResult(result, &encoded229); err != nil {
t.Fatalf("encode Layer 229 welcome result: %v", err)
}
for _, profile := range []tlprofile.Profile{tlprofile.Profile225, tlprofile.Profile226, tlprofile.Profile227, tlprofile.Profile228} {
var older bin.Buffer
if err := tlprofile.EncodeObject(profile, result, &older); err == nil {
t.Fatalf("exact Layer %d encoded Layer 229 welcome result", profile)
}
for _, update := range []tg.UpdateClass{
&tg.UpdateNewEphemeralMessage{Message: wire},
&tg.UpdateEditEphemeralMessage{Message: wire},
} {
var olderUpdate bin.Buffer
if err := tlprofile.EncodeObject(profile, update, &olderUpdate); err == nil {
t.Fatalf("exact Layer %d encoded Layer 229 welcome update %T", profile, update)
}
}
}
}