401 lines
18 KiB
Go
401 lines
18 KiB
Go
package rpc
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/gotd/td/clock"
|
||
"github.com/gotd/td/tg"
|
||
"go.uber.org/zap/zaptest"
|
||
appchannels "telesrv/internal/app/channels"
|
||
appusers "telesrv/internal/app/users"
|
||
"telesrv/internal/domain"
|
||
"telesrv/internal/store/memory"
|
||
)
|
||
|
||
// TestChannelMemberRankRPC 覆盖成员 Tag(rank-only 编辑)的权限矩阵:
|
||
// actor ∈ {creator, manage_ranks admin, 普通成员} × target ∈ {自己, 普通成员,
|
||
// 自己提拔的 admin, 他人提拔的 admin, creator} × 群级 edit_rank 开关 {开, 关},
|
||
// 以及 editAdmin 撤管清 rank 的旧语义回归。
|
||
func TestChannelMemberRankRPC(t *testing.T) {
|
||
ctx := context.Background()
|
||
userStore := memory.NewUserStore()
|
||
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 71, Phone: "15550002401", FirstName: "Owner"})
|
||
tagger, _ := userStore.Create(ctx, domain.User{AccessHash: 72, Phone: "15550002402", FirstName: "Tagger"})
|
||
plain, _ := userStore.Create(ctx, domain.User{AccessHash: 73, Phone: "15550002403", FirstName: "Plain"})
|
||
other, _ := userStore.Create(ctx, domain.User{AccessHash: 74, Phone: "15550002404", FirstName: "Other"})
|
||
channelStore := memory.NewChannelStore()
|
||
r := New(Config{}, Deps{
|
||
Users: appusers.NewService(userStore),
|
||
Channels: appchannels.NewService(channelStore),
|
||
}, zaptest.NewLogger(t), clock.System)
|
||
created, err := r.onMessagesCreateChat(WithUserID(ctx, owner.ID), &tg.MessagesCreateChatRequest{
|
||
Users: []tg.InputUserClass{
|
||
&tg.InputUser{UserID: tagger.ID, AccessHash: tagger.AccessHash},
|
||
&tg.InputUser{UserID: plain.ID, AccessHash: plain.AccessHash},
|
||
&tg.InputUser{UserID: other.ID, AccessHash: other.AccessHash},
|
||
},
|
||
Title: "Member Tag Group",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("create chat: %v", err)
|
||
}
|
||
channel := created.Updates.(*tg.Updates).Chats[0].(*tg.Channel)
|
||
inputChannel := &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
|
||
peer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
|
||
editRank := func(actorID int64, participant tg.InputPeerClass, rank string) (tg.UpdatesClass, error) {
|
||
return r.onMessagesEditChatParticipantRank(WithUserID(ctx, actorID), &tg.MessagesEditChatParticipantRankRequest{
|
||
Peer: peer,
|
||
Participant: participant,
|
||
Rank: rank,
|
||
})
|
||
}
|
||
getParticipant := func(viewerID, targetID int64) tg.ChannelParticipantClass {
|
||
t.Helper()
|
||
res, err := r.onChannelsGetParticipant(WithUserID(ctx, viewerID), &tg.ChannelsGetParticipantRequest{
|
||
Channel: inputChannel,
|
||
Participant: &tg.InputPeerUser{UserID: targetID},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("get participant %d: %v", targetID, err)
|
||
}
|
||
return res.Participant
|
||
}
|
||
|
||
// creator 给普通成员设 tag:角色不变、rank 持久、推送 updateChannelParticipant。
|
||
rankUpdates, err := editRank(owner.ID, &tg.InputPeerUser{UserID: plain.ID, AccessHash: plain.AccessHash}, "navigator")
|
||
if err != nil {
|
||
t.Fatalf("creator set member tag: %v", err)
|
||
}
|
||
participantUpdate := tg.UpdateClass(nil)
|
||
for _, update := range rankUpdates.(*tg.Updates).Updates {
|
||
if _, ok := update.(*tg.UpdateChannelParticipant); ok {
|
||
participantUpdate = update
|
||
break
|
||
}
|
||
}
|
||
if participantUpdate == nil {
|
||
t.Fatalf("rank updates = %+v, want updateChannelParticipant", rankUpdates.(*tg.Updates).Updates)
|
||
}
|
||
newParticipant, ok := participantUpdate.(*tg.UpdateChannelParticipant).GetNewParticipant()
|
||
if !ok {
|
||
t.Fatalf("rank update missing new participant: %+v", participantUpdate)
|
||
}
|
||
taggedMember, ok := newParticipant.(*tg.ChannelParticipant)
|
||
if !ok {
|
||
t.Fatalf("tagged member = %T, want plain channelParticipant (role must not change)", newParticipant)
|
||
}
|
||
if rank, _ := taggedMember.GetRank(); rank != "navigator" {
|
||
t.Fatalf("tagged member rank = %q, want navigator", rank)
|
||
}
|
||
if got, ok := getParticipant(owner.ID, plain.ID).(*tg.ChannelParticipant); !ok {
|
||
t.Fatalf("plain participant after tag = %T, want channelParticipant", getParticipant(owner.ID, plain.ID))
|
||
} else if rank, _ := got.GetRank(); rank != "navigator" {
|
||
t.Fatalf("plain participant rank = %q, want navigator", rank)
|
||
}
|
||
|
||
// admins filter 是两端消息徽章的数据源:必须返回管理员 + 带 tag 的普通成员。
|
||
adminsRes, err := r.onChannelsGetParticipants(WithUserID(ctx, other.ID), &tg.ChannelsGetParticipantsRequest{
|
||
Channel: inputChannel,
|
||
Filter: &tg.ChannelParticipantsAdmins{},
|
||
Limit: 100,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("get admins participants: %v", err)
|
||
}
|
||
badgeRanks := map[int64]string{}
|
||
for _, participant := range adminsRes.(*tg.ChannelsChannelParticipants).Participants {
|
||
switch p := participant.(type) {
|
||
case *tg.ChannelParticipantCreator:
|
||
badgeRanks[p.UserID], _ = p.GetRank()
|
||
case *tg.ChannelParticipantAdmin:
|
||
badgeRanks[p.UserID], _ = p.GetRank()
|
||
case *tg.ChannelParticipant:
|
||
rank, _ := p.GetRank()
|
||
badgeRanks[p.UserID] = rank
|
||
}
|
||
}
|
||
if len(badgeRanks) != 2 {
|
||
t.Fatalf("admins filter participants = %+v, want creator + tagged plain member", badgeRanks)
|
||
}
|
||
if badgeRanks[plain.ID] != "navigator" {
|
||
t.Fatalf("admins filter plain rank = %q, want navigator (message badge source)", badgeRanks[plain.ID])
|
||
}
|
||
if _, ok := badgeRanks[other.ID]; ok {
|
||
t.Fatalf("admins filter must not contain untagged plain member: %+v", badgeRanks)
|
||
}
|
||
|
||
// 普通成员改自己(开关默认开,inputPeerSelf 路径)。
|
||
if _, err := editRank(plain.ID, &tg.InputPeerSelf{}, "pilot"); err != nil {
|
||
t.Fatalf("plain member edits own tag: %v", err)
|
||
}
|
||
if self, ok := getParticipant(plain.ID, plain.ID).(*tg.ChannelParticipantSelf); !ok {
|
||
t.Fatalf("self participant = %T, want channelParticipantSelf", getParticipant(plain.ID, plain.ID))
|
||
} else if rank, _ := self.GetRank(); rank != "pilot" {
|
||
t.Fatalf("self rank = %q, want pilot", rank)
|
||
}
|
||
|
||
// 普通成员改别人 → CHAT_ADMIN_REQUIRED。
|
||
if _, err := editRank(plain.ID, &tg.InputPeerUser{UserID: other.ID, AccessHash: other.AccessHash}, "x"); err == nil || !strings.Contains(err.Error(), "CHAT_ADMIN_REQUIRED") {
|
||
t.Fatalf("plain member edits other tag err = %v, want CHAT_ADMIN_REQUIRED", err)
|
||
}
|
||
|
||
// creator 授予 tagger 管理 Tags 的管理员权限;manage_ranks 必须持久化并下发。
|
||
if _, err := r.onChannelsEditAdmin(WithUserID(ctx, owner.ID), &tg.ChannelsEditAdminRequest{
|
||
Channel: inputChannel,
|
||
UserID: &tg.InputUser{UserID: tagger.ID, AccessHash: tagger.AccessHash},
|
||
AdminRights: tg.ChatAdminRights{
|
||
ChangeInfo: true,
|
||
AddAdmins: true,
|
||
ManageRanks: true,
|
||
},
|
||
}); err != nil {
|
||
t.Fatalf("promote tagger with manage_ranks: %v", err)
|
||
}
|
||
taggerAdmin, ok := getParticipant(owner.ID, tagger.ID).(*tg.ChannelParticipantAdmin)
|
||
if !ok {
|
||
t.Fatalf("tagger participant = %T, want channelParticipantAdmin", getParticipant(owner.ID, tagger.ID))
|
||
}
|
||
if !taggerAdmin.AdminRights.ManageRanks {
|
||
t.Fatalf("tagger admin rights = %+v, want manage_ranks=true round-trip", taggerAdmin.AdminRights)
|
||
}
|
||
chats, err := r.onChannelsGetChannels(WithUserID(ctx, tagger.ID), []tg.InputChannelClass{inputChannel})
|
||
if err != nil {
|
||
t.Fatalf("get channels as tagger: %v", err)
|
||
}
|
||
taggerChat := chats.GetChats()[0].(*tg.Channel)
|
||
if rights, ok := taggerChat.GetAdminRights(); !ok || !rights.ManageRanks {
|
||
t.Fatalf("tagger chat admin rights = %+v, want manage_ranks=true", taggerChat)
|
||
}
|
||
|
||
// manage_ranks admin 改普通成员 → OK。
|
||
if _, err := editRank(tagger.ID, &tg.InputPeerUser{UserID: plain.ID, AccessHash: plain.AccessHash}, "lookout"); err != nil {
|
||
t.Fatalf("manage_ranks admin edits plain member: %v", err)
|
||
}
|
||
// manage_ranks admin 改 creator → USER_CREATOR。
|
||
if _, err := editRank(tagger.ID, &tg.InputPeerUser{UserID: owner.ID, AccessHash: owner.AccessHash}, "boss"); err == nil || !strings.Contains(err.Error(), "USER_CREATOR") {
|
||
t.Fatalf("admin edits creator tag err = %v, want USER_CREATOR", err)
|
||
}
|
||
// manage_ranks admin 改自己 → OK。
|
||
if _, err := editRank(tagger.ID, &tg.InputPeerSelf{}, "chief"); err != nil {
|
||
t.Fatalf("admin edits own tag: %v", err)
|
||
}
|
||
|
||
// tagger 提拔 other(promoted_by=tagger)后可改其 tag;creator 重新提拔后不可。
|
||
if _, err := r.onChannelsEditAdmin(WithUserID(ctx, tagger.ID), &tg.ChannelsEditAdminRequest{
|
||
Channel: inputChannel,
|
||
UserID: &tg.InputUser{UserID: other.ID, AccessHash: other.AccessHash},
|
||
AdminRights: tg.ChatAdminRights{ChangeInfo: true},
|
||
}); err != nil {
|
||
t.Fatalf("tagger promotes other: %v", err)
|
||
}
|
||
if _, err := editRank(tagger.ID, &tg.InputPeerUser{UserID: other.ID, AccessHash: other.AccessHash}, "scout"); err != nil {
|
||
t.Fatalf("admin edits tag of admin they promoted: %v", err)
|
||
}
|
||
if _, err := r.onChannelsEditAdmin(WithUserID(ctx, owner.ID), &tg.ChannelsEditAdminRequest{
|
||
Channel: inputChannel,
|
||
UserID: &tg.InputUser{UserID: other.ID, AccessHash: other.AccessHash},
|
||
AdminRights: tg.ChatAdminRights{ChangeInfo: true},
|
||
Rank: "scout",
|
||
}); err != nil {
|
||
t.Fatalf("creator re-promotes other: %v", err)
|
||
}
|
||
if _, err := editRank(tagger.ID, &tg.InputPeerUser{UserID: other.ID, AccessHash: other.AccessHash}, "spy"); err == nil || !strings.Contains(err.Error(), "RIGHT_FORBIDDEN") {
|
||
t.Fatalf("admin edits tag of admin promoted by creator err = %v, want RIGHT_FORBIDDEN", err)
|
||
}
|
||
|
||
// 关闭群级 Member Tags 开关(default_banned_rights.edit_rank=true)。
|
||
if _, err := r.onMessagesEditChatDefaultBannedRights(WithUserID(ctx, owner.ID), &tg.MessagesEditChatDefaultBannedRightsRequest{
|
||
Peer: peer,
|
||
BannedRights: tg.ChatBannedRights{EditRank: true},
|
||
}); err != nil {
|
||
t.Fatalf("disable member tags switch: %v", err)
|
||
}
|
||
chatsAfterSwitch, err := r.onChannelsGetChannels(WithUserID(ctx, plain.ID), []tg.InputChannelClass{inputChannel})
|
||
if err != nil {
|
||
t.Fatalf("get channels after switch: %v", err)
|
||
}
|
||
switchedChat := chatsAfterSwitch.GetChats()[0].(*tg.Channel)
|
||
if rights, ok := switchedChat.GetDefaultBannedRights(); !ok || !rights.EditRank {
|
||
t.Fatalf("default banned rights after switch = %+v, want edit_rank=true round-trip", switchedChat)
|
||
}
|
||
// 开关关闭后:普通成员改自己 → RIGHT_FORBIDDEN;admin/creator 改自己仍可。
|
||
if _, err := editRank(plain.ID, &tg.InputPeerSelf{}, "pilot2"); err == nil || !strings.Contains(err.Error(), "RIGHT_FORBIDDEN") {
|
||
t.Fatalf("plain member edits own tag with switch off err = %v, want RIGHT_FORBIDDEN", err)
|
||
}
|
||
if _, err := editRank(tagger.ID, &tg.InputPeerSelf{}, "chief2"); err != nil {
|
||
t.Fatalf("admin edits own tag with switch off: %v", err)
|
||
}
|
||
if _, err := editRank(owner.ID, &tg.InputPeerUser{UserID: owner.ID, AccessHash: owner.AccessHash}, "captain"); err != nil {
|
||
t.Fatalf("creator edits own tag: %v", err)
|
||
}
|
||
if got, ok := getParticipant(owner.ID, owner.ID).(*tg.ChannelParticipantCreator); !ok {
|
||
t.Fatalf("creator participant = %T, want channelParticipantCreator", getParticipant(owner.ID, owner.ID))
|
||
} else if rank, _ := got.GetRank(); rank != "captain" {
|
||
t.Fatalf("creator rank = %q, want captain", rank)
|
||
}
|
||
|
||
// rank 超长 → 拒绝。
|
||
if _, err := editRank(owner.ID, &tg.InputPeerUser{UserID: plain.ID, AccessHash: plain.AccessHash}, strings.Repeat("r", domain.MaxChannelAdminRankLength+1)); err == nil {
|
||
t.Fatalf("over-length rank accepted, want error")
|
||
}
|
||
|
||
// editAdmin 撤管仍清空 rank(旧语义回归)。
|
||
if _, err := r.onChannelsEditAdmin(WithUserID(ctx, owner.ID), &tg.ChannelsEditAdminRequest{
|
||
Channel: inputChannel,
|
||
UserID: &tg.InputUser{UserID: other.ID, AccessHash: other.AccessHash},
|
||
AdminRights: tg.ChatAdminRights{},
|
||
}); err != nil {
|
||
t.Fatalf("demote other: %v", err)
|
||
}
|
||
demoted, ok := getParticipant(owner.ID, other.ID).(*tg.ChannelParticipant)
|
||
if !ok {
|
||
t.Fatalf("demoted participant = %T, want channelParticipant", getParticipant(owner.ID, other.ID))
|
||
}
|
||
if rank, has := demoted.GetRank(); has && rank != "" {
|
||
t.Fatalf("demoted rank = %q, want cleared", rank)
|
||
}
|
||
|
||
// admin log:rank 编辑必须记 participant_edit_rank,且 edit_rank filter 可检索。
|
||
filter := tg.ChannelAdminLogEventsFilter{}
|
||
filter.SetEditRank(true)
|
||
logReq := &tg.ChannelsGetAdminLogRequest{Channel: inputChannel, Limit: 50}
|
||
logReq.SetEventsFilter(filter)
|
||
adminLog, err := r.onChannelsGetAdminLog(WithUserID(ctx, owner.ID), logReq)
|
||
if err != nil {
|
||
t.Fatalf("get admin log: %v", err)
|
||
}
|
||
if len(adminLog.Events) == 0 {
|
||
t.Fatalf("admin log with edit_rank filter is empty, want participant_edit_rank events")
|
||
}
|
||
|
||
// 重进是全新 participant:admin(带 manage_ranks 与 tag)退群重进后是普通
|
||
// 成员、无 admin rights、无 rank,且不再出现在 admins filter(徽章数据源)。
|
||
if _, err := r.onChannelsLeaveChannel(WithUserID(ctx, tagger.ID), inputChannel); err != nil {
|
||
t.Fatalf("tagger leaves: %v", err)
|
||
}
|
||
if _, err := r.onChannelsJoinChannel(WithUserID(ctx, tagger.ID), inputChannel); err != nil {
|
||
t.Fatalf("tagger rejoins: %v", err)
|
||
}
|
||
rejoined, ok := getParticipant(owner.ID, tagger.ID).(*tg.ChannelParticipant)
|
||
if !ok {
|
||
t.Fatalf("rejoined tagger = %T, want plain channelParticipant", getParticipant(owner.ID, tagger.ID))
|
||
}
|
||
if rank, has := rejoined.GetRank(); has && rank != "" {
|
||
t.Fatalf("rejoined tagger rank = %q, want cleared", rank)
|
||
}
|
||
adminsAfterRejoin, err := r.onChannelsGetParticipants(WithUserID(ctx, owner.ID), &tg.ChannelsGetParticipantsRequest{
|
||
Channel: inputChannel,
|
||
Filter: &tg.ChannelParticipantsAdmins{},
|
||
Limit: 100,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("get admins after rejoin: %v", err)
|
||
}
|
||
for _, p := range adminsAfterRejoin.(*tg.ChannelsChannelParticipants).Participants {
|
||
if p, ok := p.(*tg.ChannelParticipantAdmin); ok && p.UserID == tagger.ID {
|
||
t.Fatalf("rejoined tagger still listed as admin in admins filter")
|
||
}
|
||
}
|
||
|
||
// kick→解禁→重进:tag 不复活。
|
||
if _, err := r.onChannelsEditBanned(WithUserID(ctx, owner.ID), &tg.ChannelsEditBannedRequest{
|
||
Channel: inputChannel,
|
||
Participant: &tg.InputPeerUser{UserID: plain.ID, AccessHash: plain.AccessHash},
|
||
BannedRights: tg.ChatBannedRights{ViewMessages: true},
|
||
}); err != nil {
|
||
t.Fatalf("kick plain: %v", err)
|
||
}
|
||
if _, err := r.onChannelsEditBanned(WithUserID(ctx, owner.ID), &tg.ChannelsEditBannedRequest{
|
||
Channel: inputChannel,
|
||
Participant: &tg.InputPeerUser{UserID: plain.ID, AccessHash: plain.AccessHash},
|
||
BannedRights: tg.ChatBannedRights{},
|
||
}); err != nil {
|
||
t.Fatalf("unban plain: %v", err)
|
||
}
|
||
if _, err := r.onChannelsJoinChannel(WithUserID(ctx, plain.ID), inputChannel); err != nil {
|
||
t.Fatalf("plain rejoins after unban: %v", err)
|
||
}
|
||
if got := getParticipant(owner.ID, plain.ID); true {
|
||
if p, ok := got.(*tg.ChannelParticipant); !ok {
|
||
t.Fatalf("unbanned plain participant = %T, want plain channelParticipant", got)
|
||
} else if rank, has := p.GetRank(); has && rank != "" {
|
||
t.Fatalf("unbanned plain rank = %q, want cleared", rank)
|
||
}
|
||
}
|
||
|
||
// creator 离开会把 owner 交给其他活跃成员;旧 creator 重进是普通成员,
|
||
// 不保留 creator tag。
|
||
if _, err := r.onChannelsLeaveChannel(WithUserID(ctx, owner.ID), inputChannel); err != nil {
|
||
t.Fatalf("creator leaves: %v", err)
|
||
}
|
||
newCreator, ok := getParticipant(tagger.ID, tagger.ID).(*tg.ChannelParticipantCreator)
|
||
if !ok {
|
||
t.Fatalf("future creator = %T, want channelParticipantCreator", getParticipant(tagger.ID, tagger.ID))
|
||
}
|
||
if rank, has := newCreator.GetRank(); has && rank != "" {
|
||
t.Fatalf("future creator rank = %q, want empty", rank)
|
||
}
|
||
if _, err := r.onChannelsJoinChannel(WithUserID(ctx, owner.ID), inputChannel); err != nil {
|
||
t.Fatalf("creator rejoins: %v", err)
|
||
}
|
||
creatorBack, ok := getParticipant(tagger.ID, owner.ID).(*tg.ChannelParticipant)
|
||
if !ok {
|
||
t.Fatalf("rejoined old creator = %T, want plain channelParticipant", getParticipant(tagger.ID, owner.ID))
|
||
}
|
||
if rank, has := creatorBack.GetRank(); has && rank != "" {
|
||
t.Fatalf("rejoined old creator rank = %q, want cleared", rank)
|
||
}
|
||
|
||
// 成员 Tag 是 megagroup 专属:broadcast 频道上任何路径(含 creator/self)
|
||
// 一律 MEGAGROUP_ID_INVALID,broadcast 的 admins filter 才能保持纯管理员列表。
|
||
bcCreated, err := r.onChannelsCreateChannel(WithUserID(ctx, owner.ID), &tg.ChannelsCreateChannelRequest{
|
||
Broadcast: true,
|
||
Title: "Member Tag Broadcast",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("create broadcast: %v", err)
|
||
}
|
||
bcChannel := bcCreated.(*tg.Updates).Chats[0].(*tg.Channel)
|
||
bcInput := &tg.InputChannel{ChannelID: bcChannel.ID, AccessHash: bcChannel.AccessHash}
|
||
bcPeer := &tg.InputPeerChannel{ChannelID: bcChannel.ID, AccessHash: bcChannel.AccessHash}
|
||
if _, err := r.onChannelsInviteToChannel(WithUserID(ctx, owner.ID), &tg.ChannelsInviteToChannelRequest{
|
||
Channel: bcInput,
|
||
Users: []tg.InputUserClass{&tg.InputUser{UserID: plain.ID, AccessHash: plain.AccessHash}},
|
||
}); err != nil {
|
||
t.Fatalf("invite to broadcast: %v", err)
|
||
}
|
||
bcEditRank := func(actorID int64, participant tg.InputPeerClass, rank string) error {
|
||
_, err := r.onMessagesEditChatParticipantRank(WithUserID(ctx, actorID), &tg.MessagesEditChatParticipantRankRequest{
|
||
Peer: bcPeer,
|
||
Participant: participant,
|
||
Rank: rank,
|
||
})
|
||
return err
|
||
}
|
||
for name, attempt := range map[string]func() error{
|
||
"creator tags subscriber": func() error {
|
||
return bcEditRank(owner.ID, &tg.InputPeerUser{UserID: plain.ID, AccessHash: plain.AccessHash}, "vip")
|
||
},
|
||
"subscriber tags self": func() error { return bcEditRank(plain.ID, &tg.InputPeerSelf{}, "fan") },
|
||
"creator tags self": func() error { return bcEditRank(owner.ID, &tg.InputPeerSelf{}, "boss") },
|
||
} {
|
||
if err := attempt(); err == nil || !strings.Contains(err.Error(), "MEGAGROUP_ID_INVALID") {
|
||
t.Fatalf("%s on broadcast err = %v, want MEGAGROUP_ID_INVALID", name, err)
|
||
}
|
||
}
|
||
for _, event := range adminLog.Events {
|
||
action, ok := event.Action.(*tg.ChannelAdminLogEventActionParticipantEditRank)
|
||
if !ok {
|
||
t.Fatalf("admin log action = %T, want channelAdminLogEventActionParticipantEditRank", event.Action)
|
||
}
|
||
if action.UserID == 0 {
|
||
t.Fatalf("admin log edit rank action missing user: %+v", action)
|
||
}
|
||
}
|
||
latest := adminLog.Events[0].Action.(*tg.ChannelAdminLogEventActionParticipantEditRank)
|
||
if latest.UserID != owner.ID || latest.NewRank != "captain" {
|
||
t.Fatalf("latest edit rank action = %+v, want owner captain", latest)
|
||
}
|
||
}
|