using WebDrop.Core; namespace WebDrop.Core.Tests; public sealed class WorkspaceStructureTests { [Fact] public void NewSpaceHasAnAtomicInitialChannelAndChangesPersist() { using var env = new TestEnv(); var endpoint = TestEnv.Endpoint(); var ch = env.Create(endpoint); var owner = ch.Owner(endpoint); var state = env.Service.GetWorkspace(ch.Id, owner); Assert.Equal("常规", Assert.Single(state.Channels).Name); var group = env.Service.CreateWorkspaceGroup(ch.Id, owner, "项目").Groups.Single(); var changed = env.Service.CreateWorkspaceChannel(ch.Id, owner, "讨论", group.Id); Assert.Equal(group.Id, changed.Channels.Last().GroupId); var reloaded = env.Service.GetWorkspace(ch.Id, owner); Assert.Equal(changed.Revision, reloaded.Revision); Assert.Equal(2, reloaded.Channels.Count); } [Fact] public void InvalidAndCrossSpaceAssociationsRollback() { using var env = new TestEnv(); var endpoint = TestEnv.Endpoint(); var a = env.Create(endpoint); var b = env.Create(endpoint); var group = env.Service.CreateWorkspaceGroup(b.Id, b.Owner(endpoint), "其他空间").Groups.Single(); var previous = env.Service.GetWorkspace(a.Id, a.Owner(endpoint)); Assert.Throws(() => env.Service.CreateWorkspaceChannel(a.Id, a.Owner(endpoint), "讨论", group.Id)); Assert.Equal(previous.Revision, env.Service.GetWorkspace(a.Id, a.Owner(endpoint)).Revision); var first = previous.Channels.Single().Id; var second = env.Service.CreateWorkspaceChannel(a.Id, a.Owner(endpoint), "频道2", null).Channels.Last().Id; var folder = env.Service.CreateWorkspaceFolder(a.Id, a.Owner(endpoint), first, null, "目录").Folders.Single(); Assert.Throws(() => env.Service.CreateWorkspaceFolder(a.Id, a.Owner(endpoint), second, folder.Id, "跨频道目录")); } [Fact] public async Task MembersCannotMoveAnotherAccountsItemsAndTheServerSuppliesTheAuthor() { using var env = new TestEnv(); var ownerEp = TestEnv.Endpoint(); var memberEp = TestEnv.Endpoint(); var ch = env.Create(ownerEp); var member = ch.Member(memberEp) with { AccountName = "member" }; env.Service.Join(ch.Id, member); var item = TestEnv.NewId(); await env.Service.AppendFrameAsync(ch.Id, ch.Owner(ownerEp), TestEnv.Frame(item, 0, TestEnv.SmallEnc, TestEnv.EncPayload), null); var thread = env.Service.GetWorkspace(ch.Id, ch.Owner(ownerEp)).Channels.Single().Id; Assert.Throws(() => env.Service.MoveWorkspaceItems(ch.Id, member, [item.ToBase64Url()], thread, null)); var moved = env.Service.MoveWorkspaceItems(ch.Id, ch.Owner(ownerEp), [item.ToBase64Url()], thread, null); Assert.Equal("owner", moved.Locations.Single().AuthorAccount); Assert.Throws(() => env.Service.MoveWorkspaceItems(ch.Id, ch.Owner(ownerEp), [TestEnv.NewId().ToBase64Url()], thread, null)); } [Fact] public void AnExistingSpaceWithoutBackendStructureIsNotAutomaticallyMigrated() { using var env = new TestEnv(); var endpoint = TestEnv.Endpoint(); var ch = env.Create(endpoint); using var connection = new Microsoft.Data.Sqlite.SqliteConnection($"Data Source={Path.Combine(env.Dir, "test.db")}"); connection.Open(); using var delete = connection.CreateCommand(); delete.CommandText = "DELETE FROM workspace_structures WHERE space_id=$id"; delete.Parameters.AddWithValue("$id", ch.Id.ToArray()); delete.ExecuteNonQuery(); var empty = env.Service.GetWorkspace(ch.Id, ch.Owner(endpoint)); Assert.Empty(empty.Channels); Assert.Equal(0, empty.Revision); var added = env.Service.CreateWorkspaceChannel(ch.Id, ch.Owner(endpoint), "新频道", null); Assert.Equal("新频道", added.Channels.Single().Name); Assert.Equal(1, added.Revision); } [Fact] public async Task MembersCanPlaceTheirUploadsButCannotCreateBusinessStructure() { using var env = new TestEnv(); var ownerEp = TestEnv.Endpoint(); var memberEp = TestEnv.Endpoint(); var ch = env.Create(ownerEp); var guest = ch.Member(memberEp); env.Service.Join(ch.Id, guest); var item = TestEnv.NewId(); await env.Service.AppendFrameAsync(ch.Id, guest, TestEnv.Frame(item, 0, TestEnv.SmallEnc, TestEnv.EncPayload), null); var channelId = env.Service.GetWorkspace(ch.Id, ch.Owner(ownerEp)).Channels.Single().Id; Assert.Single(env.Service.MoveWorkspaceItems(ch.Id, guest, [item.ToBase64Url()], channelId, null).Locations); Assert.Equal(ErrorCode.Forbidden, Assert.Throws(() => 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(() => env.Service.CreateWorkspaceRoleResult(ch.Id, alice, "越权", null, null, null, [])); Assert.Throws(() => env.Service.CreateWorkspaceRoleResult(ch.Id, owner, "坏权限", null, null, null, ["nope"])); Assert.Throws(() => env.Service.CreateWorkspaceRoleResult(ch.Id, owner, "坏颜色", null, null, "pink", [])); Assert.Throws(() => env.Service.SetWorkspaceRoleMembers(ch.Id, owner, created.Role.Id, ["stranger"])); Assert.Throws(() => 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(() => 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(() => env.Service.RevokeInvitation(ch.Id, bob, "alice")); // 没有权限 Assert.Throws(() => env.Service.RevokeInvitation(ch.Id, alice, "owner")); // 不能踢空间主 Assert.Throws(() => env.Service.RevokeInvitation(ch.Id, alice, "carol")); // 不能踢同样有权限的人 Assert.Throws(() => 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(() => 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(() => env.Service.CreateWorkspaceChannel(ch.Id, alice, "新频道", null)); // 新建频道涉及密钥,只有空间主 Assert.Throws(() => env.Service.CreateWorkspaceRoleResult(ch.Id, alice, "自封", null, null, null, [])); } }