using VoiceCat.Server.Data; using Voicecat.V1; using static VoiceCat.Tests.ServerTests; namespace VoiceCat.Tests; public class ChannelManagementTests { internal static async Task AdminAsync(ServerFixture fixture) { using (var store = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db"))) await store.CreateAccountAsync("Admin", "secret", true); Client client = await fixture.ConnectAsync(); client.Send(new() { ClientHello = new() { ProtoVersion = 2 } }); await client.ReadUntilAsync(e => e.ServerHello is not null); client.Send(new() { AuthRequest = new() { Password = new() { Username = "Admin", Password = "secret" } } }); Assert.True((await client.ReadUntilAsync(e => e.AuthResult is not null)).AuthResult.Ok); await client.ReadUntilAsync(e => e.ServerState is not null); return client; } private static Channel Room(string name = "Protected") => new() { Name = name, MaxUsers = 1, Audio = new() { SampleRate = 48000, BitrateBps = 24000, FrameMs = 20, Complexity = 5, Fec = true } }; internal static async Task ResultAsync(Client client, Envelope request) { request.RequestId = 42; client.Send(request); Envelope result = await client.ReadUntilAsync(e => e.GenericResult is not null); Assert.Equal(42UL, result.RequestId); return result.GenericResult.Ok; } [Fact] public async Task ProtectedChannelCrudEnforcesPasswordCapacityAndMovesMembersToLobby() { await using var fixture = new ServerFixture(); await using var guest = await fixture.ConnectAsync(); User user = await guest.LoginAsync("Guest"); await using var admin = await AdminAsync(fixture); Assert.False(await ResultAsync(guest, new() { CreateChannel = new() { Channel = Room() } })); Assert.True(await ResultAsync(admin, new() { CreateChannel = new() { Channel = Room(), Password = "pāssword" } })); Channel room = (await guest.ReadUntilAsync(e => e.ChannelEvent?.Kind == ChannelEvent.Types.Kind.Created)).ChannelEvent.Channel; Assert.True(room.PasswordProtected); foreach (string password in new[] { "", "wrong", "pāssword" }) { guest.Send(new() { RequestId = 7, JoinChannel = new() { ChannelId = room.Id, Password = password } }); Envelope result = await guest.ReadUntilAsync(e => e.JoinChannelResult is not null); Assert.Equal(7UL, result.RequestId); Assert.Equal(password == "pāssword", result.JoinChannelResult.Ok); } admin.Send(new() { JoinChannel = new() { ChannelId = room.Id, Password = "pāssword" } }); Assert.False((await admin.ReadUntilAsync(e => e.JoinChannelResult is not null)).JoinChannelResult.Ok); room.Name = "Renamed"; Assert.False(await ResultAsync(guest, new() { EditChannel = new() { Channel = room } })); Assert.True(await ResultAsync(admin, new() { EditChannel = new() { Channel = room } })); Assert.Equal("Renamed", (await guest.ReadUntilAsync(e => e.ChannelEvent is not null)).ChannelEvent.Channel.Name); using (var store = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db"))) { Assert.Equal("Renamed", store.LoadChannels().Single(c => c.Id == room.Id).Name); Assert.True(store.CheckChannelPassword(room.Id, "pāssword")); Assert.False(store.CheckChannelPassword(room.Id, "wrong")); } Assert.True(await ResultAsync(admin, new() { DeleteChannel = new() { ChannelId = room.Id } })); Assert.Equal(room.Id, (await guest.ReadUntilAsync(e => e.ChannelEvent?.Kind == ChannelEvent.Types.Kind.Deleted)).ChannelEvent.DeletedId); guest.Send(new() { Subscribe = new() }); var snapshot = (await guest.ReadUntilAsync(e => e.ServerState is not null)).ServerState; Assert.Equal(1U, snapshot.Users.Single(u => u.Id == user.Id).ChannelId); Assert.DoesNotContain(snapshot.Channels, c => c.Id == room.Id); using var reopened = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db")); Assert.DoesNotContain(reopened.LoadChannels(), c => c.Id == room.Id); } [Fact] public async Task InvalidChangesCannotCorruptChannelTreeOrLobby() { await using var fixture = new ServerFixture(); await using var admin = await AdminAsync(fixture); Assert.True(await ResultAsync(admin, new() { CreateChannel = new() { Channel = Room("Parent") } })); using var store = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db")); Channel parent = store.LoadChannels().Single(c => c.Name == "Parent"); Channel child = Room("Child"); child.ParentId = parent.Id; Assert.True(await ResultAsync(admin, new() { CreateChannel = new() { Channel = child } })); child = store.LoadChannels().Single(c => c.Name == "Child"); parent.ParentId = child.Id; Assert.False(await ResultAsync(admin, new() { EditChannel = new() { Channel = parent } })); Assert.False(await ResultAsync(admin, new() { DeleteChannel = new() { ChannelId = parent.Id } })); Assert.False(await ResultAsync(admin, new() { DeleteChannel = new() { ChannelId = 1 } })); Channel lobby = store.LoadChannels().Single(c => c.Id == 1); Assert.False(await ResultAsync(admin, new() { EditChannel = new() { Channel = lobby, Password = "lockout" } })); Assert.False(await ResultAsync(admin, new() { CreateChannel = new() { Channel = Room("Child") } })); var invalid = Room("Invalid"); invalid.Audio.SampleRate = 123; Assert.False(await ResultAsync(admin, new() { CreateChannel = new() { Channel = invalid } })); Assert.False(await ResultAsync(admin, new() { CreateChannel = new() })); Assert.Equal(4, store.LoadChannels().Count); Assert.True(await ResultAsync(admin, new() { DeleteChannel = new() { ChannelId = child.Id } })); Assert.True(await ResultAsync(admin, new() { DeleteChannel = new() { ChannelId = parent.Id } })); } }