owpengram-server/internal/store/postgres/channel_member_rank_integration_test.go

256 lines
9.5 KiB
Go
Raw Permalink 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 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 logparticipant_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)
}
// 重进是全新 participantadminmanage_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)
}
}