256 lines
9.5 KiB
Go
256 lines
9.5 KiB
Go
package postgres
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"telesrv/internal/domain"
|
||
"testing"
|
||
)
|
||
|
||
// TestChannelStoreEditMemberRank 验证 rank-only 编辑在 PG 上的持久化、权限矩阵、
|
||
// 以及 participant_edit_rank 通过 channel_admin_log_events_type_check 约束(0086)。
|
||
func TestChannelStoreEditMemberRank(t *testing.T) {
|
||
pool := testPool(t)
|
||
ctx := context.Background()
|
||
suffix := randomSuffix(t)
|
||
|
||
users := NewUserStore(pool)
|
||
owner, err := users.Create(ctx, domain.User{AccessHash: 141, Phone: "+1889" + suffix + "01", FirstName: "RankOwner"})
|
||
if err != nil {
|
||
t.Fatalf("create owner: %v", err)
|
||
}
|
||
member, err := users.Create(ctx, domain.User{AccessHash: 142, Phone: "+1889" + suffix + "02", FirstName: "RankMember"})
|
||
if err != nil {
|
||
t.Fatalf("create member: %v", err)
|
||
}
|
||
var channelID int64
|
||
t.Cleanup(func() {
|
||
if channelID != 0 {
|
||
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
||
}
|
||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, member.ID})
|
||
})
|
||
|
||
channels := NewChannelStore(pool)
|
||
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
||
CreatorUserID: owner.ID,
|
||
Title: "Rank Integration",
|
||
Megagroup: true,
|
||
MemberUserIDs: []int64{member.ID},
|
||
Date: 1700009000,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("create channel: %v", err)
|
||
}
|
||
channelID = created.Channel.ID
|
||
|
||
// creator 给普通成员设 tag:角色不变、rank 持久化。
|
||
res, err := channels.EditChannelMemberRank(ctx, domain.EditChannelMemberRankRequest{
|
||
UserID: owner.ID,
|
||
ChannelID: channelID,
|
||
MemberID: member.ID,
|
||
Rank: "navigator",
|
||
Date: 1700009001,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("creator edits member rank: %v", err)
|
||
}
|
||
if res.Participant.Role != domain.ChannelRoleMember || res.Participant.Rank != "navigator" {
|
||
t.Fatalf("participant after rank edit = %+v, want member role with navigator rank", res.Participant)
|
||
}
|
||
stored, err := channels.GetParticipant(ctx, owner.ID, channelID, member.ID)
|
||
if err != nil {
|
||
t.Fatalf("get participant: %v", err)
|
||
}
|
||
if stored.Role != domain.ChannelRoleMember || stored.Rank != "navigator" {
|
||
t.Fatalf("stored participant = %+v, want member role with navigator rank", stored)
|
||
}
|
||
|
||
// 普通成员改自己:开关开 → 成功;default_banned_rights.edit_rank=true → 拒绝。
|
||
if _, err := channels.EditChannelMemberRank(ctx, domain.EditChannelMemberRankRequest{
|
||
UserID: member.ID,
|
||
ChannelID: channelID,
|
||
MemberID: member.ID,
|
||
Rank: "pilot",
|
||
Date: 1700009002,
|
||
}); err != nil {
|
||
t.Fatalf("member edits own rank: %v", err)
|
||
}
|
||
if _, err := channels.EditChannelDefaultBannedRights(ctx, domain.EditChannelDefaultBannedRightsRequest{
|
||
UserID: owner.ID,
|
||
ChannelID: channelID,
|
||
BannedRights: domain.ChannelBannedRights{EditRank: true},
|
||
Date: 1700009003,
|
||
}); err != nil {
|
||
t.Fatalf("disable member tags: %v", err)
|
||
}
|
||
view, err := channels.GetChannel(ctx, member.ID, channelID)
|
||
if err != nil {
|
||
t.Fatalf("get channel: %v", err)
|
||
}
|
||
if !view.Channel.DefaultBannedRights.EditRank {
|
||
t.Fatalf("default banned rights = %+v, want edit_rank=true round-trip", view.Channel.DefaultBannedRights)
|
||
}
|
||
if _, err := channels.EditChannelMemberRank(ctx, domain.EditChannelMemberRankRequest{
|
||
UserID: member.ID,
|
||
ChannelID: channelID,
|
||
MemberID: member.ID,
|
||
Rank: "pilot2",
|
||
Date: 1700009004,
|
||
}); !errors.Is(err, domain.ErrChannelRightForbidden) {
|
||
t.Fatalf("member self edit with switch off err = %v, want ErrChannelRightForbidden", err)
|
||
}
|
||
// 普通成员改别人 → 需要管理权限。
|
||
if _, err := channels.EditChannelMemberRank(ctx, domain.EditChannelMemberRankRequest{
|
||
UserID: member.ID,
|
||
ChannelID: channelID,
|
||
MemberID: owner.ID,
|
||
Rank: "x",
|
||
Date: 1700009005,
|
||
}); !errors.Is(err, domain.ErrChannelUserCreator) && !errors.Is(err, domain.ErrChannelAdminRequired) {
|
||
t.Fatalf("member edits creator err = %v, want creator/admin-required rejection", err)
|
||
}
|
||
|
||
// manage_ranks 管理员权限位经 PG round-trip。
|
||
if _, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
|
||
UserID: owner.ID,
|
||
ChannelID: channelID,
|
||
MemberID: member.ID,
|
||
AdminRights: domain.ChannelAdminRights{ChangeInfo: true, ManageRanks: true},
|
||
Date: 1700009006,
|
||
}); err != nil {
|
||
t.Fatalf("promote with manage_ranks: %v", err)
|
||
}
|
||
promoted, err := channels.GetParticipant(ctx, owner.ID, channelID, member.ID)
|
||
if err != nil {
|
||
t.Fatalf("get promoted participant: %v", err)
|
||
}
|
||
if !promoted.AdminRights.ManageRanks {
|
||
t.Fatalf("promoted admin rights = %+v, want manage_ranks=true round-trip", promoted.AdminRights)
|
||
}
|
||
|
||
// admin log:participant_edit_rank 行已通过 0086 CHECK 落库,filter 可检索。
|
||
log, err := channels.ListAdminLog(ctx, domain.ChannelAdminLogRequest{
|
||
UserID: owner.ID,
|
||
ChannelID: channelID,
|
||
Limit: 20,
|
||
Filter: domain.ChannelAdminLogFilter{EditRank: true},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("list admin log: %v", err)
|
||
}
|
||
if len(log.Events) != 2 {
|
||
t.Fatalf("edit rank admin log events = %d (%+v), want 2", len(log.Events), log.Events)
|
||
}
|
||
for _, event := range log.Events {
|
||
if event.Type != domain.ChannelAdminLogParticipantEditRank {
|
||
t.Fatalf("admin log event type = %s, want participant_edit_rank", event.Type)
|
||
}
|
||
if event.Participant == nil || event.Participant.UserID == 0 {
|
||
t.Fatalf("admin log event participant missing: %+v", event)
|
||
}
|
||
}
|
||
if log.Events[0].NewString != "pilot" || log.Events[1].NewString != "navigator" {
|
||
t.Fatalf("admin log ranks = %q,%q, want pilot,navigator (desc order)", log.Events[0].NewString, log.Events[1].NewString)
|
||
}
|
||
|
||
// 重进是全新 participant:admin(manage_ranks + rank)退群重进后为普通成员、
|
||
// 无 admin rights、无 rank;部分限制名单独立于在群与否、重进后仍生效。
|
||
if _, err := channels.EditChannelBanned(ctx, domain.EditChannelBannedRequest{
|
||
UserID: owner.ID,
|
||
ChannelID: channelID,
|
||
Participant: domain.Peer{Type: domain.PeerTypeUser, ID: member.ID},
|
||
BannedRights: domain.ChannelBannedRights{SendMessages: true},
|
||
Date: 1700009007,
|
||
}); err != nil {
|
||
t.Fatalf("restrict member: %v", err)
|
||
}
|
||
if _, err := channels.LeaveChannel(ctx, channelID, member.ID, 1700009008); err != nil {
|
||
t.Fatalf("member leaves: %v", err)
|
||
}
|
||
if _, err := channels.JoinChannel(ctx, channelID, member.ID, 1700009009); err != nil {
|
||
t.Fatalf("member rejoins: %v", err)
|
||
}
|
||
rejoined, err := channels.GetParticipant(ctx, owner.ID, channelID, member.ID)
|
||
if err != nil {
|
||
t.Fatalf("get rejoined member: %v", err)
|
||
}
|
||
if rejoined.Role != domain.ChannelRoleMember || rejoined.Rank != "" || rejoined.AdminRights != (domain.ChannelAdminRights{}) {
|
||
t.Fatalf("rejoined member = %+v, want plain member without rank/admin rights", rejoined)
|
||
}
|
||
if !rejoined.BannedRights.SendMessages {
|
||
t.Fatalf("rejoined member banned rights = %+v, want partial restriction kept", rejoined.BannedRights)
|
||
}
|
||
if rejoined.JoinedAt != 1700009009 || rejoined.InviterUserID != 0 {
|
||
t.Fatalf("rejoined member joined_at/inviter = %d/%d, want fresh join date and no inviter", rejoined.JoinedAt, rejoined.InviterUserID)
|
||
}
|
||
|
||
// creator 离开会把 owner 交给其他活跃成员;旧 creator 重进是普通成员,
|
||
// 不保留 creator tag。
|
||
if _, err := channels.EditChannelMemberRank(ctx, domain.EditChannelMemberRankRequest{
|
||
UserID: owner.ID,
|
||
ChannelID: channelID,
|
||
MemberID: owner.ID,
|
||
Rank: "captain",
|
||
Date: 1700009010,
|
||
}); err != nil {
|
||
t.Fatalf("creator sets own rank: %v", err)
|
||
}
|
||
if _, err := channels.LeaveChannel(ctx, channelID, owner.ID, 1700009011); err != nil {
|
||
t.Fatalf("creator leaves: %v", err)
|
||
}
|
||
newCreator, err := channels.GetParticipant(ctx, member.ID, channelID, member.ID)
|
||
if err != nil {
|
||
t.Fatalf("get future creator: %v", err)
|
||
}
|
||
if newCreator.Role != domain.ChannelRoleCreator {
|
||
t.Fatalf("future creator = %+v, want creator role", newCreator)
|
||
}
|
||
if _, err := channels.JoinChannel(ctx, channelID, owner.ID, 1700009012); err != nil {
|
||
t.Fatalf("creator rejoins: %v", err)
|
||
}
|
||
creatorBack, err := channels.GetParticipant(ctx, member.ID, channelID, owner.ID)
|
||
if err != nil {
|
||
t.Fatalf("get rejoined old creator: %v", err)
|
||
}
|
||
if creatorBack.Role != domain.ChannelRoleMember || creatorBack.Rank != "" {
|
||
t.Fatalf("rejoined old creator = %+v, want plain member without rank", creatorBack)
|
||
}
|
||
|
||
// 成员 Tag 是 megagroup 专属:broadcast 上 creator/self 一律 MEGAGROUP_ID_INVALID。
|
||
var broadcastID int64
|
||
t.Cleanup(func() {
|
||
if broadcastID != 0 {
|
||
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", broadcastID)
|
||
}
|
||
})
|
||
broadcast, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
||
CreatorUserID: owner.ID,
|
||
Title: "Rank Broadcast",
|
||
Broadcast: true,
|
||
MemberUserIDs: []int64{member.ID},
|
||
Date: 1700009100,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("create broadcast: %v", err)
|
||
}
|
||
broadcastID = broadcast.Channel.ID
|
||
if _, err := channels.EditChannelMemberRank(ctx, domain.EditChannelMemberRankRequest{
|
||
UserID: owner.ID,
|
||
ChannelID: broadcastID,
|
||
MemberID: member.ID,
|
||
Rank: "vip",
|
||
Date: 1700009101,
|
||
}); !errors.Is(err, domain.ErrMegagroupIDInvalid) {
|
||
t.Fatalf("creator tags broadcast subscriber err = %v, want ErrMegagroupIDInvalid", err)
|
||
}
|
||
if _, err := channels.EditChannelMemberRank(ctx, domain.EditChannelMemberRankRequest{
|
||
UserID: member.ID,
|
||
ChannelID: broadcastID,
|
||
MemberID: member.ID,
|
||
Rank: "fan",
|
||
Date: 1700009102,
|
||
}); !errors.Is(err, domain.ErrMegagroupIDInvalid) {
|
||
t.Fatalf("broadcast subscriber tags self err = %v, want ErrMegagroupIDInvalid", err)
|
||
}
|
||
}
|