332 lines
13 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|
|
}
|