owpengram-server/internal/rpc/messages_polls_rpc_test.go

407 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"testing"
"github.com/gotd/td/tg"
"github.com/gotd/td/tgerr"
)
// sendTestPoll 用 sendMedia 发一个 poll返回消息与 poll id。
func sendTestPoll(t *testing.T, r *Router, fromID int64, peer tg.InputPeerClass, randomID int64, mutate func(*tg.InputMediaPoll)) *tg.Message {
t.Helper()
media := &tg.InputMediaPoll{
Poll: tg.Poll{
Question: tg.TextWithEntities{Text: "favorite color?", Entities: []tg.MessageEntityClass{}},
Answers: []tg.PollAnswerClass{
&tg.PollAnswer{Text: tg.TextWithEntities{Text: "red", Entities: []tg.MessageEntityClass{}}, Option: []byte{0}},
&tg.PollAnswer{Text: tg.TextWithEntities{Text: "blue", Entities: []tg.MessageEntityClass{}}, Option: []byte{1}},
&tg.PollAnswer{Text: tg.TextWithEntities{Text: "green", Entities: []tg.MessageEntityClass{}}, Option: []byte{2}},
},
},
}
if mutate != nil {
mutate(media)
}
updates, err := r.onMessagesSendMedia(WithUserID(context.Background(), fromID), &tg.MessagesSendMediaRequest{
Peer: peer,
Media: media,
RandomID: randomID,
})
if err != nil {
t.Fatalf("sendMedia poll: %v", err)
}
return newMessageFromUpdates(t, updates)
}
func pollFromUpdates(t *testing.T, updates tg.UpdatesClass) (*tg.Poll, *tg.PollResults) {
t.Helper()
upd, ok := updates.(*tg.Updates)
if !ok {
t.Fatalf("expected *tg.Updates, got %T", updates)
}
for _, u := range upd.Updates {
if mp, ok := u.(*tg.UpdateMessagePoll); ok {
poll, _ := mp.GetPoll()
return &poll, &mp.Results
}
}
t.Fatalf("no updateMessagePoll in %#v", upd.Updates)
return nil, nil
}
func votersByOption(t *testing.T, results *tg.PollResults) map[byte]tg.PollAnswerVoters {
t.Helper()
out := map[byte]tg.PollAnswerVoters{}
list, _ := results.GetResults()
for _, item := range list {
if len(item.Option) != 1 {
t.Fatalf("unexpected option key %v", item.Option)
}
out[item.Option[0]] = item
}
return out
}
func TestSendMediaPollEcho(t *testing.T) {
ctx := context.Background()
_ = ctx
r, owner, friend := newMediaTestRouter(t)
msg := sendTestPoll(t, r, owner.ID, &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}, 4001, nil)
media, ok := msg.Media.(*tg.MessageMediaPoll)
if !ok {
t.Fatalf("expected MessageMediaPoll, got %T", msg.Media)
}
if media.Poll.ID == 0 {
t.Error("poll id should be allocated server-side")
}
if media.Poll.Question.Text != "favorite color?" || len(media.Poll.Answers) != 3 {
t.Fatalf("poll definition mangled: %+v", media.Poll)
}
if media.Poll.Closed {
t.Error("fresh poll should be open")
}
}
func TestSendVotePrivateChooseChangeRetract(t *testing.T) {
ctx := context.Background()
r, owner, friend := newMediaTestRouter(t)
peerForFriend := &tg.InputPeerUser{UserID: owner.ID, AccessHash: owner.AccessHash}
msg := sendTestPoll(t, r, owner.ID, &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}, 4002, nil)
pollID := msg.Media.(*tg.MessageMediaPoll).Poll.ID
// friend 的 box id 与 owner 的相同(同一会话双方各自 box 计数都从 1 开始且只有这一条)。
friendCtx := WithUserID(ctx, friend.ID)
updates, err := r.onMessagesSendVote(friendCtx, &tg.MessagesSendVoteRequest{
Peer: peerForFriend,
MsgID: msg.ID,
Options: [][]byte{{1}},
})
if err != nil {
t.Fatalf("sendVote: %v", err)
}
poll, results := pollFromUpdates(t, updates)
if poll.ID != pollID {
t.Fatalf("update poll id = %d, want %d", poll.ID, pollID)
}
byOption := votersByOption(t, results)
if total, _ := results.GetTotalVoters(); total != 1 {
t.Fatalf("total voters = %d, want 1", total)
}
if v := byOption[1]; !v.Chosen || mustVoters(t, v) != 1 {
t.Fatalf("option 1 = %+v, want chosen with 1 voter", v)
}
if v := byOption[0]; v.Chosen || mustVoters(t, v) != 0 {
t.Fatalf("option 0 = %+v, want not chosen 0 voters", v)
}
// 改票:单选 poll 重投另一项。
updates, err = r.onMessagesSendVote(friendCtx, &tg.MessagesSendVoteRequest{
Peer: peerForFriend,
MsgID: msg.ID,
Options: [][]byte{{2}},
})
if err != nil {
t.Fatalf("sendVote change: %v", err)
}
_, results = pollFromUpdates(t, updates)
byOption = votersByOption(t, results)
if v := byOption[2]; !v.Chosen || mustVoters(t, v) != 1 {
t.Fatalf("after change option 2 = %+v, want chosen 1", v)
}
if v := byOption[1]; v.Chosen || mustVoters(t, v) != 0 {
t.Fatalf("after change option 1 = %+v, want cleared", v)
}
// 撤票:空 options。
updates, err = r.onMessagesSendVote(friendCtx, &tg.MessagesSendVoteRequest{
Peer: peerForFriend,
MsgID: msg.ID,
})
if err != nil {
t.Fatalf("sendVote retract: %v", err)
}
_, results = pollFromUpdates(t, updates)
if total, _ := results.GetTotalVoters(); total != 0 {
t.Fatalf("total after retract = %d, want 0", total)
}
// 非法选项。
if _, err := r.onMessagesSendVote(friendCtx, &tg.MessagesSendVoteRequest{
Peer: peerForFriend,
MsgID: msg.ID,
Options: [][]byte{{9}},
}); err == nil || !tgerr.Is(err, "OPTION_INVALID") {
t.Fatalf("invalid option err = %v, want OPTION_INVALID", err)
}
// 单选投两项。
if _, err := r.onMessagesSendVote(friendCtx, &tg.MessagesSendVoteRequest{
Peer: peerForFriend,
MsgID: msg.ID,
Options: [][]byte{{0}, {1}},
}); err == nil || !tgerr.Is(err, "OPTION_INVALID") {
t.Fatalf("multi options on single choice err = %v, want OPTION_INVALID", err)
}
}
func mustVoters(t *testing.T, v tg.PollAnswerVoters) int {
t.Helper()
count, _ := v.GetVoters()
return count
}
func TestSendVoteQuizRevealsAndLocks(t *testing.T) {
ctx := context.Background()
r, owner, friend := newMediaTestRouter(t)
peerForFriend := &tg.InputPeerUser{UserID: owner.ID, AccessHash: owner.AccessHash}
msg := sendTestPoll(t, r, owner.ID, &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}, 4003, func(in *tg.InputMediaPoll) {
in.Poll.Quiz = true
in.SetCorrectAnswers([]int{1})
in.SetSolution("blue is correct")
in.SetSolutionEntities([]tg.MessageEntityClass{})
})
friendCtx := WithUserID(ctx, friend.ID)
// 投票前owner 自己查结果)不得泄漏 correct/solution。
ownerCtx := WithUserID(ctx, owner.ID)
preUpdates, err := r.onMessagesGetPollResults(ownerCtx, &tg.MessagesGetPollResultsRequest{
Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash},
MsgID: msg.ID,
})
if err != nil {
t.Fatalf("getPollResults pre-vote: %v", err)
}
_, preResults := pollFromUpdates(t, preUpdates)
if _, has := preResults.GetSolution(); has {
t.Fatal("solution must not leak before voting/closing")
}
for _, v := range votersByOption(t, preResults) {
if v.Correct {
t.Fatal("correct flag must not leak before voting/closing")
}
}
// friend 答错correct 标在正确选项上solution 揭示。
updates, err := r.onMessagesSendVote(friendCtx, &tg.MessagesSendVoteRequest{
Peer: peerForFriend,
MsgID: msg.ID,
Options: [][]byte{{0}},
})
if err != nil {
t.Fatalf("quiz vote: %v", err)
}
_, results := pollFromUpdates(t, updates)
byOption := votersByOption(t, results)
if !byOption[0].Chosen || byOption[0].Correct {
t.Fatalf("wrong answer entry = %+v, want chosen non-correct", byOption[0])
}
if !byOption[1].Correct {
t.Fatalf("correct answer entry = %+v, want correct flag", byOption[1])
}
if solution, has := results.GetSolution(); !has || solution != "blue is correct" {
t.Fatalf("solution = %q (%v), want revealed after voting", solution, has)
}
// quiz 不可改票/撤票。
if _, err := r.onMessagesSendVote(friendCtx, &tg.MessagesSendVoteRequest{
Peer: peerForFriend,
MsgID: msg.ID,
Options: [][]byte{{1}},
}); err == nil || !tgerr.Is(err, "REVOTE_NOT_ALLOWED") {
t.Fatalf("quiz revote err = %v, want REVOTE_NOT_ALLOWED", err)
}
if _, err := r.onMessagesSendVote(friendCtx, &tg.MessagesSendVoteRequest{
Peer: peerForFriend,
MsgID: msg.ID,
}); err == nil || !tgerr.Is(err, "REVOTE_NOT_ALLOWED") {
t.Fatalf("quiz retract err = %v, want REVOTE_NOT_ALLOWED", err)
}
}
func TestEditMessageClosePoll(t *testing.T) {
ctx := context.Background()
r, owner, friend := newMediaTestRouter(t)
ownerPeer := &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}
friendPeer := &tg.InputPeerUser{UserID: owner.ID, AccessHash: owner.AccessHash}
msg := sendTestPoll(t, r, owner.ID, ownerPeer, 4004, nil)
pollID := msg.Media.(*tg.MessageMediaPoll).Poll.ID
ownerCtx := WithUserID(ctx, owner.ID)
friendCtx := WithUserID(ctx, friend.ID)
closeReq := func() *tg.MessagesEditMessageRequest {
req := &tg.MessagesEditMessageRequest{Peer: ownerPeer, ID: msg.ID}
closed := tg.Poll{ID: pollID, Closed: true}
req.SetMedia(&tg.InputMediaPoll{Poll: closed})
return req
}
// 非创建者不能关闭。
friendClose := &tg.MessagesEditMessageRequest{Peer: friendPeer, ID: msg.ID}
friendClose.SetMedia(&tg.InputMediaPoll{Poll: tg.Poll{ID: pollID, Closed: true}})
if _, err := r.onMessagesEditMessage(friendCtx, friendClose); err == nil || !tgerr.Is(err, "MESSAGE_AUTHOR_REQUIRED") {
t.Fatalf("non-creator close err = %v, want MESSAGE_AUTHOR_REQUIRED", err)
}
updates, err := r.onMessagesEditMessage(ownerCtx, closeReq())
if err != nil {
t.Fatalf("close poll: %v", err)
}
poll, _ := pollFromUpdates(t, updates)
if !poll.Closed {
t.Fatal("poll should be closed after edit")
}
// 关闭后投票必须被拒。
if _, err := r.onMessagesSendVote(friendCtx, &tg.MessagesSendVoteRequest{
Peer: friendPeer,
MsgID: msg.ID,
Options: [][]byte{{0}},
}); err == nil || !tgerr.Is(err, "MESSAGE_POLL_CLOSED") {
t.Fatalf("vote on closed poll err = %v, want MESSAGE_POLL_CLOSED", err)
}
// 对端视角读取也必须看到 closed权威覆盖快照
res, err := r.onMessagesGetPollResults(friendCtx, &tg.MessagesGetPollResultsRequest{Peer: friendPeer, MsgID: msg.ID})
if err != nil {
t.Fatalf("getPollResults after close: %v", err)
}
peerPoll, _ := pollFromUpdates(t, res)
if !peerPoll.Closed {
t.Fatal("peer view should see closed poll (authority overrides snapshot)")
}
}
func TestGetPollVotesPublicOnly(t *testing.T) {
ctx := context.Background()
r, owner, friend := newMediaTestRouter(t)
ownerPeer := &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}
friendPeer := &tg.InputPeerUser{UserID: owner.ID, AccessHash: owner.AccessHash}
ownerCtx := WithUserID(ctx, owner.ID)
friendCtx := WithUserID(ctx, friend.ID)
// 非公开 pollgetPollVotes 拒绝。
anonymous := sendTestPoll(t, r, owner.ID, ownerPeer, 4005, nil)
if _, err := r.onMessagesGetPollVotes(ownerCtx, &tg.MessagesGetPollVotesRequest{
Peer: ownerPeer, ID: anonymous.ID, Limit: 10,
}); err == nil || !tgerr.Is(err, "POLL_VOTE_REQUIRED") {
t.Fatalf("anonymous poll votes err = %v, want POLL_VOTE_REQUIRED", err)
}
public := sendTestPoll(t, r, owner.ID, ownerPeer, 4006, func(in *tg.InputMediaPoll) {
in.Poll.PublicVoters = true
})
if _, err := r.onMessagesSendVote(friendCtx, &tg.MessagesSendVoteRequest{
Peer: friendPeer, MsgID: public.ID, Options: [][]byte{{1}},
}); err != nil {
t.Fatalf("public vote: %v", err)
}
votes, err := r.onMessagesGetPollVotes(ownerCtx, &tg.MessagesGetPollVotesRequest{
Peer: ownerPeer, ID: public.ID, Limit: 10,
})
if err != nil {
t.Fatalf("getPollVotes: %v", err)
}
if votes.Count != 1 || len(votes.Votes) != 1 {
t.Fatalf("votes = %+v, want exactly friend's vote", votes)
}
single, ok := votes.Votes[0].(*tg.MessagePeerVote)
if !ok {
t.Fatalf("vote entry type = %T, want MessagePeerVote", votes.Votes[0])
}
if peerUser, ok := single.Peer.(*tg.PeerUser); !ok || peerUser.UserID != friend.ID {
t.Fatalf("vote peer = %#v, want friend", single.Peer)
}
if len(single.Option) != 1 || single.Option[0] != 1 {
t.Fatalf("vote option = %v, want [1]", single.Option)
}
foundFriend := false
for _, u := range votes.Users {
if got, ok := u.(*tg.User); ok && got.ID == friend.ID {
foundFriend = true
}
}
if !foundFriend {
t.Fatal("votes.users should contain the voter entity")
}
}
func TestSendMediaPollValidation(t *testing.T) {
ctx := context.Background()
r, owner, friend := newMediaTestRouter(t)
peer := &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}
ownerCtx := WithUserID(ctx, owner.ID)
send := func(mutate func(*tg.InputMediaPoll)) error {
media := &tg.InputMediaPoll{
Poll: tg.Poll{
Question: tg.TextWithEntities{Text: "q", Entities: []tg.MessageEntityClass{}},
Answers: []tg.PollAnswerClass{
&tg.PollAnswer{Text: tg.TextWithEntities{Text: "a", Entities: []tg.MessageEntityClass{}}, Option: []byte{0}},
&tg.PollAnswer{Text: tg.TextWithEntities{Text: "b", Entities: []tg.MessageEntityClass{}}, Option: []byte{1}},
},
},
}
if mutate != nil {
mutate(media)
}
_, err := r.onMessagesSendMedia(ownerCtx, &tg.MessagesSendMediaRequest{Peer: peer, Media: media, RandomID: int64(5000 + len(media.Poll.Answers))})
return err
}
if err := send(func(in *tg.InputMediaPoll) {
in.Poll.Answers = in.Poll.Answers[:1]
}); err == nil || !tgerr.Is(err, "OPTION_INVALID") {
t.Fatalf("single answer err = %v, want OPTION_INVALID", err)
}
if err := send(func(in *tg.InputMediaPoll) {
in.Poll.Question.Text = " "
}); err == nil || !tgerr.Is(err, "MEDIA_EMPTY") {
t.Fatalf("empty question err = %v, want MEDIA_EMPTY", err)
}
if err := send(func(in *tg.InputMediaPoll) {
in.Poll.Answers[1].(*tg.PollAnswer).Option = []byte{0}
}); err == nil || !tgerr.Is(err, "POLL_OPTION_INVALID") {
t.Fatalf("duplicate option err = %v, want POLL_OPTION_INVALID", err)
}
if err := send(func(in *tg.InputMediaPoll) {
in.Poll.Quiz = true
}); err == nil || !tgerr.Is(err, "QUIZ_CORRECT_ANSWERS_INVALID") {
t.Fatalf("quiz without correct err = %v, want QUIZ_CORRECT_ANSWERS_INVALID", err)
}
if err := send(func(in *tg.InputMediaPoll) {
in.Poll.Quiz = true
in.SetCorrectAnswers([]int{5})
}); err == nil || !tgerr.Is(err, "QUIZ_CORRECT_ANSWERS_INVALID") {
t.Fatalf("quiz bad index err = %v, want QUIZ_CORRECT_ANSWERS_INVALID", err)
}
if err := send(func(in *tg.InputMediaPoll) {
in.SetCorrectAnswers([]int{0})
}); err == nil || !tgerr.Is(err, "MEDIA_INVALID") {
t.Fatalf("non-quiz with correct err = %v, want MEDIA_INVALID", err)
}
}