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) } }