feat(api): add space roles with delegated member removal and structure management

This commit is contained in:
justme committed 2026-10-16 19:42:31 +08:00
1 parent 059a29a8d5
commit 1dd54243ae
12 files changed
+551 -19

No files matched your search

@@ -68,4 +68,76 @@ public sealed class WorkspaceStructureTests
Assert.Equal(ErrorCode.Forbidden, Assert.Throws<WebDropException>(() => env.Service.CreateWorkspaceGroup(ch.Id, guest, "成员分组")).Code);
}
// ───── 身份组 ─────
private static (TestEnv Env, TestChannel Ch, Credentials Owner, Credentials Alice, Credentials Bob, Credentials Carol) RoleSpace()
{
var env = new TestEnv(); var ownerEp = TestEnv.Endpoint(); var ch = env.Create(ownerEp, settings: new ChannelSettings { MaxMembers = 10 });
Credentials Join(string name) { var creds = ch.Member(TestEnv.Endpoint()) with { AccountName = name }; env.Service.Join(ch.Id, creds); return creds; }
return (env, ch, ch.Owner(ownerEp), Join("alice"), Join("bob"), Join("carol"));
}
[Fact]
public void OnlyTheOwnerManagesRolesAndMembersMustHaveJoined()
{
var (env, ch, owner, alice, _, _) = RoleSpace(); using var _env = env;
var created = env.Service.CreateWorkspaceRoleResult(ch.Id, owner, "管理组", null, "管理员", "blue", [WorkspacePermission.RemoveMembers]);
Assert.Throws<WebDropException>(() => env.Service.CreateWorkspaceRoleResult(ch.Id, alice, "越权", null, null, null, []));
Assert.Throws<WebDropException>(() => env.Service.CreateWorkspaceRoleResult(ch.Id, owner, "坏权限", null, null, null, ["nope"]));
Assert.Throws<WebDropException>(() => env.Service.CreateWorkspaceRoleResult(ch.Id, owner, "坏颜色", null, null, "pink", []));
Assert.Throws<WebDropException>(() => env.Service.SetWorkspaceRoleMembers(ch.Id, owner, created.Role.Id, ["stranger"]));
Assert.Throws<WebDropException>(() => env.Service.SetWorkspaceRoleMembers(ch.Id, alice, created.Role.Id, ["alice"]));
var state = env.Service.SetWorkspaceRoleMembers(ch.Id, owner, created.Role.Id, ["Alice", "alice"]);
var role = state.Roles.Single(r => r.Id == created.Role.Id);
Assert.Equal("管理员", role.Suffix); Assert.Equal(["alice"], role.Members);
// 默认的「管理组」一直在;删掉自己建的不影响它,删掉默认组后也不会再自动出现
var remaining = env.Service.DeleteWorkspaceRole(ch.Id, owner, created.Role.Id);
Assert.Equal(WorkspaceRole.DefaultId, Assert.Single(remaining.Roles).Id);
Assert.Empty(env.Service.DeleteWorkspaceRole(ch.Id, owner, WorkspaceRole.DefaultId).Roles);
Assert.Empty(env.Service.CreateWorkspaceChannel(ch.Id, owner, "再写一次", null).Roles);
}
[Fact]
public void StructureStoredBeforeRolesExistedStillLoadsWithAnEmptyRoleList()
{
var old = System.Text.Json.JsonSerializer.Deserialize("{\"revision\":3,\"channels\":[],\"groups\":[],\"folders\":[],\"locations\":[]}", WorkspaceJson.Default.WorkspaceState)!;
Assert.Empty(old.Roles); Assert.False(old.RolesSeeded);
}
[Fact]
public void EverySpaceStartsWithADefaultAdminGroupThatCanHoldMembers()
{
var (env, ch, owner, alice, bob, _) = RoleSpace(); using var _env = env;
var admin = Assert.Single(env.Service.GetWorkspace(ch.Id, owner).Roles);
Assert.Equal(WorkspaceRole.DefaultId, admin.Id); Assert.Equal("管理组", admin.Name); Assert.Empty(admin.Members);
Assert.Contains(WorkspacePermission.RemoveMembers, admin.Permissions); Assert.Contains(WorkspacePermission.ManageStructure, admin.Permissions);
env.Service.SetWorkspaceRoleMembers(ch.Id, owner, admin.Id, ["alice"]);
env.Service.RevokeInvitation(ch.Id, alice, "bob"); // 加入默认管理组即可移除成员
Assert.Throws<WebDropException>(() => env.Service.RevokeInvitation(ch.Id, bob, "alice"));
}
[Fact]
public void ARoleWithRemovePermissionCanKickOrdinaryMembersOnly()
{
var (env, ch, owner, alice, bob, carol) = RoleSpace(); using var _env = env;
var peer = env.Service.CreateWorkspaceRoleResult(ch.Id, owner, "管理组", null, "管理员", "blue", [WorkspacePermission.RemoveMembers]).Role;
env.Service.SetWorkspaceRoleMembers(ch.Id, owner, peer.Id, ["alice", "carol"]);
Assert.Throws<WebDropException>(() => env.Service.RevokeInvitation(ch.Id, bob, "alice")); // 没有权限
Assert.Throws<WebDropException>(() => env.Service.RevokeInvitation(ch.Id, alice, "owner")); // 不能踢空间主
Assert.Throws<WebDropException>(() => env.Service.RevokeInvitation(ch.Id, alice, "carol")); // 不能踢同样有权限的人
Assert.Throws<WebDropException>(() => env.Service.RevokeInvitation(ch.Id, alice, "alice")); // 不能踢自己
env.Service.RevokeInvitation(ch.Id, alice, "bob");
Assert.DoesNotContain(env.Service.Members(ch.Id, owner), member => member.Username == "bob");
_ = carol;
}
[Fact]
public void ARoleWithStructurePermissionManagesGroupsButCannotCreateChannels()
{
var (env, ch, owner, alice, bob, _) = RoleSpace(); using var _env = env;
var role = env.Service.CreateWorkspaceRoleResult(ch.Id, owner, "整理组", "【", "整理】", "green", [WorkspacePermission.ManageStructure]).Role;
env.Service.SetWorkspaceRoleMembers(ch.Id, owner, role.Id, ["alice"]);
var channel = env.Service.GetWorkspace(ch.Id, owner).Channels.Single();
Assert.Throws<WebDropException>(() => env.Service.CreateWorkspaceGroup(ch.Id, bob, "越权"));
var group = env.Service.CreateWorkspaceGroup(ch.Id, alice, "项目").Groups.Single();
env.Service.RenameWorkspaceChannel(ch.Id, alice, channel.Id, "改名");
env.Service.GroupWorkspaceChannel(ch.Id, alice, channel.Id, group.Id);
Assert.Empty(env.Service.DeleteWorkspaceGroup(ch.Id, alice, group.Id).Groups);
Assert.Throws<WebDropException>(() => env.Service.CreateWorkspaceChannel(ch.Id, alice, "新频道", null)); // 新建频道涉及密钥,只有空间主
Assert.Throws<WebDropException>(() => env.Service.CreateWorkspaceRoleResult(ch.Id, alice, "自封", null, null, null, []));
}
}