package session // 本文件覆盖裁决 06.4.2 在会话层的安全反向用例,以及第二处结构化判主来源 // (每渠道账号配置的 adminIds[],channels.json / design/08 §1.1)。 import ( "context" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" "git.yeij.top/AskaEth/Cyrene_v2/internal/user" ) // TestResolveActorOwnerByChannelAdminIDs 覆盖每渠道管理员 id 列表:SetPlatformAdmins // 写入后命中即进 owner 车道(可多账户),不在列表里的账号——哪怕适配器上报 role=owner // ——仍是非主人。 func TestResolveActorOwnerByChannelAdminIDs(t *testing.T) { t.Parallel() owner := &user.OwnerProfile{ID: "own-1", DisplayName: "Owner"} owner.SetPlatformAdmins("qq", []string{"admin-1", " admin-2 ", "admin-1", ""}) ids := owner.PlatformAdmins("qq") if len(ids) != 2 { t.Fatalf("PlatformAdmins = %v, want 去重并去空白后的两个 id", ids) } for _, id := range []string{"admin-1", "admin-2"} { if actor := ResolveActor(directMsg(id), owner); !actor.IsOwner() { t.Errorf("adminIds 命中的 %q 未进 owner 车道", id) } } forged := directMsg("group-owner") forged.Sender = adapter.Actor{Role: adapter.RoleOwner} if actor := ResolveActor(forged, owner); actor.IsOwner() { t.Error("不在 adminIds 里的账号被判成了主人") } // 整体替换语义:重新声明后旧 id 立刻失效,channels.json 热重载不会留下悬挂管理员。 owner.SetPlatformAdmins("qq", []string{"admin-2"}) if owner.IsOwner("qq", "admin-1") { t.Error("SetPlatformAdmins 未整体替换,旧管理员 id 仍有效") } owner.SetPlatformAdmins("qq", nil) if owner.IsOwner("qq", "admin-2") || owner.PlatformAdmins("qq") != nil { t.Error("空列表应清空该平台的管理员层") } } // TestResolveActorNilOwnerIsFailClosed 覆盖没有主人档案的路径:任何入站帧都是非主人 // (fail-closed),适配器上报的 owner 标记也不例外。 func TestResolveActorNilOwnerIsFailClosed(t *testing.T) { t.Parallel() msg := directMsg("whoever") msg.Sender = adapter.Actor{Role: adapter.RoleOwner} actor := ResolveActor(msg, nil) if actor.IsOwner() { t.Fatal("nil 主人档案下仍被判成主人") } if actor.Role != adapter.RoleMember { t.Errorf("Role = %q, want member", actor.Role) } } // TestOwnerFrameMemWritable 走真实 SessionManager 链路断言 MemWritable:伪造 owner 的 // 私聊帧 Owner/MemWritable 全 false(不入主会话上下文、记忆只读);白名单命中的主人私聊 // 为 true;群主(群聊)永远是隔离环境、MemWritable=false。 func TestOwnerFrameMemWritable(t *testing.T) { t.Parallel() m, _ := newTestManager(t) ctx := context.Background() owner := &user.OwnerProfile{ ID: "own-1", DisplayName: "Owner", Accounts: []user.OwnerAccount{{Platform: "qq", AccountID: "o-100"}}, } forged := directMsg("forged-owner") forged.Sender = adapter.Actor{Role: adapter.RoleOwner} if actor := ResolveActor(forged, owner); actor.Role != adapter.RoleMember { t.Fatalf("伪造 owner 的入站帧 role = %q, want member", actor.Role) } forgedSess, err := m.GetOrCreate(ctx, forged, owner) if err != nil { t.Fatalf("GetOrCreate(伪造 owner): %v", err) } if forgedSess.Owner || forgedSess.MemWritable { t.Errorf("伪造 owner 帧 Owner=%v MemWritable=%v, want false/false", forgedSess.Owner, forgedSess.MemWritable) } whitelisted := directMsg("o-100") ownerSess, err := m.GetOrCreate(ctx, whitelisted, owner) if err != nil { t.Fatalf("GetOrCreate(白名单): %v", err) } if !ownerSess.Owner || !ownerSess.MemWritable { t.Errorf("白名单主人帧 Owner=%v MemWritable=%v, want true/true", ownerSess.Owner, ownerSess.MemWritable) } groupOwner := groupMsg("g-owner") groupOwner.Sender = adapter.Actor{Role: adapter.RoleOwner, GroupID: "g1", IsGroup: true} if actor := ResolveActor(groupOwner, owner); actor.Role != adapter.RoleMember || actor.IsOwner() { t.Errorf("群主 role=%q owner=%v, want member/false", actor.Role, actor.IsOwner()) } groupSess, err := m.GetOrCreate(ctx, groupOwner, owner) if err != nil { t.Fatalf("GetOrCreate(群主): %v", err) } if groupSess.Owner || groupSess.MemWritable || !groupSess.IsGroup { t.Errorf("群主会话 Owner=%v MemWritable=%v IsGroup=%v, want false/false/true", groupSess.Owner, groupSess.MemWritable, groupSess.IsGroup) } }