chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,139 @@
package memory
import (
"context"
"reflect"
"testing"
"telesrv/internal/domain"
)
// TestChannelPollFanoutViewsByteEquivalentToGetMessages 锁定 Phase 4 poll 模板化的安全网:批量
// ChannelPollFanoutViews 对每个 viewer 产出的 enrich poll,必须与逐 viewer GetChannelMessages(旧
// fan-out 路径)字节等价;且可见性判定一致(不可见 viewer 在批量里为 nil、在旧路径里取不到消息)。
// 覆盖:作者(创建者)、已投票非作者、未投票非作者、hide_results 期非创建者、late-joiner(AvailableMinID)、
// 被封成员、ViewMessages 受限成员、非成员。
func TestChannelPollFanoutViewsByteEquivalentToGetMessages(t *testing.T) {
ctx := context.Background()
const (
channelID = int64(8000000001)
msgID = 50
pollID = int64(9000000001)
creator = int64(7001)
votedA = int64(7002)
votedB = int64(7003)
unvoted = int64(7004)
late = int64(7005) // AvailableMinID >= msgID(pre-history 隐藏)
banned = int64(7006)
restricted = int64(7007) // active 但 BannedRights.ViewMessages
nonMember = int64(7008)
)
opt1, opt2, opt3 := []byte{1}, []byte{2}, []byte{3}
for _, hideResults := range []bool{false, true} {
t.Run(map[bool]string{false: "visible_counts", true: "hide_results"}[hideResults], func(t *testing.T) {
channels := NewChannelStore()
polls := NewPollStore()
channels.AttachPollStore(polls)
channels.channels[channelID] = domain.Channel{ID: channelID, CreatorUserID: creator, Megagroup: true, PreHistoryHidden: true}
channels.members[channelID] = map[int64]domain.ChannelMember{
creator: {UserID: creator, Status: domain.ChannelMemberActive, Role: domain.ChannelRoleCreator},
votedA: {UserID: votedA, Status: domain.ChannelMemberActive},
votedB: {UserID: votedB, Status: domain.ChannelMemberActive},
unvoted: {UserID: unvoted, Status: domain.ChannelMemberActive},
late: {UserID: late, Status: domain.ChannelMemberActive, AvailableMinID: msgID + 10},
banned: {UserID: banned, Status: domain.ChannelMemberBanned},
restricted: {UserID: restricted, Status: domain.ChannelMemberActive, BannedRights: domain.ChannelBannedRights{ViewMessages: true}},
}
def := domain.PollDefinition{
ID: pollID,
CreatorUserID: creator,
Options: [][]byte{opt1, opt2, opt3},
PublicVoters: true,
MultipleChoice: false,
HideResultsUntilClose: hideResults,
}
if err := polls.CreatePoll(ctx, def); err != nil {
t.Fatalf("create poll: %v", err)
}
basePoll := &domain.MessagePoll{
ID: pollID,
Question: "Q?",
Answers: []domain.MessagePollAnswer{{Text: "A", Option: opt1}, {Text: "B", Option: opt2}, {Text: "C", Option: opt3}},
PublicVoters: true,
HideResultsUntilClose: hideResults,
}
channels.messages[channelID] = []domain.ChannelMessage{{
ChannelID: channelID,
ID: msgID,
SenderUserID: creator,
Media: &domain.MessageMedia{Kind: domain.MessageMediaKindPoll, Poll: basePoll},
}}
if err := polls.Vote(pollID, votedA, [][]byte{opt1}, 100); err != nil {
t.Fatalf("vote A: %v", err)
}
if err := polls.Vote(pollID, votedB, [][]byte{opt2}, 101); err != nil {
t.Fatalf("vote B: %v", err)
}
viewers := []int64{creator, votedA, votedB, unvoted, late, banned, restricted, nonMember}
views, err := channels.ChannelPollFanoutViews(ctx, channelID, msgID, viewers, 200)
if err != nil {
t.Fatalf("ChannelPollFanoutViews: %v", err)
}
if !views.Found {
t.Fatal("poll fan-out views Found=false, want true")
}
for _, viewer := range viewers {
got, evaluated := views.Polls[viewer]
if !evaluated {
t.Fatalf("viewer %d not evaluated in batch (all passed viewers must be evaluated)", viewer)
}
// 旧路径参考:GetChannelMessages 按 viewer 取消息(含可见性 + per-viewer poll enrich)。
ref, err := channels.GetChannelMessages(ctx, viewer, channelID, []int{msgID})
var refPoll *domain.MessagePoll
if err == nil {
for _, m := range ref.Messages {
if m.ID == msgID && m.Media != nil {
refPoll = m.Media.Poll
}
}
}
if (got != nil) != (refPoll != nil) {
t.Fatalf("viewer %d visibility mismatch: batch nil=%v, GetMessages nil=%v (err=%v)", viewer, got == nil, refPoll == nil, err)
}
if got != nil && !reflect.DeepEqual(got.Results, refPoll.Results) {
t.Fatalf("viewer %d poll Results mismatch:\n batch=%+v\n old=%+v", viewer, got.Results, refPoll.Results)
}
}
// 显式断言可见性分类,防 GetMessages 与批量同时漏判。
if views.Polls[creator] == nil || views.Polls[votedA] == nil || views.Polls[votedB] == nil || views.Polls[unvoted] == nil {
t.Fatalf("active members should be visible: creator/votedA/votedB/unvoted")
}
if views.Polls[late] != nil || views.Polls[banned] != nil || views.Polls[restricted] != nil || views.Polls[nonMember] != nil {
t.Fatalf("late/banned/restricted/nonMember must be invisible (nil)")
}
// hide_results 期:非创建者已投票者计数应被隐藏(Voters=0),创建者可见真实计数。
if hideResults {
for _, v := range views.Polls[votedA].Results.Voters {
if v.Voters != 0 {
t.Fatalf("hide_results: non-creator votedA should see Voters=0, got %d", v.Voters)
}
}
sawCount := false
for _, v := range views.Polls[creator].Results.Voters {
if v.Voters > 0 {
sawCount = true
}
}
if !sawCount {
t.Fatal("hide_results: creator should see real counts")
}
}
})
}
}