using VoiceCat.Server.Data; using Voicecat.V1; using static VoiceCat.Tests.ServerTests; using static VoiceCat.Tests.ChannelManagementTests; using static VoiceCat.Tests.MediaRelayTests; namespace VoiceCat.Tests; public class AdministrationTests { [Fact] public async Task AccountAdministrationIsPermissionGatedAndPersistsPasswordChanges() { 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() { ListAccounts = new() })); Assert.False(await ResultAsync(guest, new() { CreateAccount = new() { Username = "new", Password = "secret" } })); Assert.False(await ResultAsync(guest, new() { SetPermission = new() { UserId = user.Id, Permissions = new() { IsAdmin = true } } })); Assert.True(await ResultAsync(admin, new() { SetPermission = new() { UserId = user.Id, Permissions = new() { CanAdminAccounts = true, CanCreateTempChannel = true } } })); Assert.True(await ResultAsync(guest, new() { CreateAccount = new() { Username = "new", Password = "first" } })); Assert.False(await ResultAsync(guest, new() { CreateAccount = new() { Username = "new", Password = "first" } })); Assert.False(await ResultAsync(guest, new() { SetPermission = new() { UserId = user.Id, Permissions = new() { IsAdmin = true } } })); Assert.False(await ResultAsync(guest, new() { CreateChannel = new() { Channel = new() { Name = "Permanent" } } })); Assert.True(await ResultAsync(guest, new() { CreateChannel = new() { Channel = new() { Name = "Temporary", Type = ChannelType.ChannelTemporary, Audio = new() { SampleRate = 48000, BitrateBps = 24000, FrameMs = 20 } } } })); Assert.True(await ResultAsync(guest, new() { ResetPassword = new() { Username = "new", NewPassword = "second" } })); Assert.False(await ResultAsync(guest, new() { ResetPassword = new() { Username = "missing", NewPassword = "second" } })); using (var store = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db"))) { Assert.Null(await store.AuthenticateAsync("new", "first")); Assert.NotNull(await store.AuthenticateAsync("new", "second")); } guest.Send(new() { RequestId = 50, ListAccounts = new() }); Envelope list = await guest.ReadUntilAsync(e => e.ListAccountsResult is not null); Assert.Equal(50UL, list.RequestId); Assert.Equal(2, list.ListAccountsResult.Accounts.Count); var entry = list.ListAccountsResult.Accounts.Single(a => a.Username == "new"); Assert.False(entry.IsAdmin); Assert.True(entry.CreatedAtUnixMs > 1_000_000_000_000); Assert.True(entry.LastLoginUnixMs > 1_000_000_000_000); Assert.True(await ResultAsync(guest, new() { DeleteAccount = new() { Username = "new" } })); Assert.False(await ResultAsync(guest, new() { DeleteAccount = new() { Username = "new" } })); using var reopened = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db")); Assert.Null(await reopened.AuthenticateAsync("new", "second")); } [Fact] public async Task AccountBanPersistsByUsernameAndBlocksNewAuthentication() { await using var fixture = new ServerFixture(); await using var admin = await AdminAsync(fixture); using var store = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db")); await store.CreateAccountAsync("Member", "password"); await using var member = await fixture.ConnectAsync(); member.Send(new() { ClientHello = new() { ProtoVersion = 2 } }); await member.ReadUntilAsync(e => e.ServerHello is not null); member.Send(new() { AuthRequest = new() { Password = new() { Username = "Member", Password = "password" } } }); AuthResult auth = (await member.ReadUntilAsync(e => e.AuthResult is not null)).AuthResult; Assert.True(auth.Ok); Assert.True(await ResultAsync(admin, new() { Ban = new() { UserId = auth.Self.Id, Reason = "account banned" } })); Assert.NotNull((await member.ReadUntilAsync(e => e.Disconnect is not null)).Disconnect); Assert.True(store.IsBanned("username", "Member")); Assert.False(store.IsBanned("ip", "127.0.0.1")); await using var retry = await fixture.ConnectAsync(); retry.Send(new() { ClientHello = new() { ProtoVersion = 2 } }); await retry.ReadUntilAsync(e => e.ServerHello is not null); retry.Send(new() { AuthRequest = new() { Password = new() { Username = "Member", Password = "password" } } }); Assert.False((await retry.ReadUntilAsync(e => e.AuthResult is not null)).AuthResult.Ok); Assert.False(await ResultAsync(admin, new() { Kick = new() { UserId = uint.MaxValue } })); } [Fact] public async Task ServerMuteDeafenAndMoveImmediatelyChangeEncryptedMediaRouting() { await using var fixture = new ServerFixture(); await using var alice = await VoicePeer.ConnectAsync(fixture, "Alice"); await using var bob = await VoicePeer.ConnectAsync(fixture, "Bob"); await using var admin = await AdminAsync(fixture); uint a = alice.Client.Authentication!.Self.Id, b = bob.Client.Authentication!.Self.Id; var stream = await alice.AnnounceAsync(StreamKind.StreamMic); Assert.False(await ResultAsync(bob.Client, new() { ServerMute = new() { UserId = a, Muted = true } })); Assert.True(await ResultAsync(admin, new() { ServerMute = new() { UserId = a, Muted = true } })); await alice.SendAsync(alice.Seal(stream.Ssrc, [1])); await bob.AssertNoVoiceAsync(); Assert.True(await ResultAsync(admin, new() { ServerMute = new() { UserId = a } })); Assert.True(await ResultAsync(admin, new() { ServerMute = new() { UserId = b, Deafened = true } })); await alice.SendAsync(alice.Seal(stream.Ssrc, [2])); await bob.AssertNoVoiceAsync(); Assert.True(await ResultAsync(admin, new() { ServerMute = new() { UserId = b } })); await alice.SendAsync(alice.Seal(stream.Ssrc, [3])); Assert.Equal(new byte[] { 3 }, (await bob.ReceiveVoiceAsync()).Payload); Assert.True(await ResultAsync(admin, new() { MoveUser = new() { UserId = a, ChannelId = 2 } })); await alice.SendAsync(alice.Seal(stream.Ssrc, [4])); await bob.AssertNoVoiceAsync(); Assert.True(await ResultAsync(admin, new() { MoveUser = new() { UserId = a, ChannelId = 1 } })); await alice.SendAsync(alice.Seal(stream.Ssrc, [5])); await bob.AssertNoVoiceAsync(); var replacement = await alice.AnnounceAsync(StreamKind.StreamMic); await alice.SendAsync(alice.Seal(replacement.Ssrc, [6])); Assert.Equal(new byte[] { 6 }, (await bob.ReceiveVoiceAsync()).Payload); } [Theory] [InlineData(false)] [InlineData(true)] public async Task KickAndGuestBanDisconnectWithOneDepartureAndRetireMedia(bool ban) { await using var fixture = new ServerFixture(); await using var alice = await VoicePeer.ConnectAsync(fixture, "Alice"); await using var observer = await VoicePeer.ConnectAsync(fixture, "Observer"); await using var admin = await AdminAsync(fixture); uint id = alice.Client.Authentication!.Self.Id; var stream = await alice.AnnounceAsync(StreamKind.StreamMic); Envelope request = ban ? new() { Ban = new() { UserId = id, Reason = "removed", ExpiresUnixMs = (ulong)DateTimeOffset.UtcNow.AddMinutes(1).ToUnixTimeMilliseconds() } } : new() { Kick = new() { UserId = id, Reason = "removed" } }; Assert.True(await ResultAsync(admin, request)); Assert.Equal("removed", (await alice.Client.ReadUntilAsync(e => e.Disconnect is not null)).Disconnect.Reason); var left = (await observer.Client.ReadUntilAsync(e => e.UserEvent?.LeftId == id)).UserEvent; Assert.Equal("removed", left.Reason); observer.Client.Send(new() { Ping = new() { Nonce = 99 } }); while (true) { Envelope message = await observer.Client.ReadUntilAsync(_ => true); Assert.False(message.UserEvent?.LeftId == id); if (message.Pong?.Nonce == 99) break; } await alice.SendAsync(alice.Seal(stream.Ssrc, [1])); await observer.AssertNoVoiceAsync(); await using var reconnect = await fixture.ConnectAsync(); if (ban) { reconnect.Send(new() { ClientHello = new() { ProtoVersion = 2 } }); Assert.NotNull((await reconnect.ReadUntilAsync(e => e.Disconnect is not null)).Disconnect); using var store = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db")); Assert.True(store.IsBanned("ip", "127.0.0.1")); store.Ban("username", "expired", "", 1); Assert.False(store.IsBanned("username", "expired")); } else await reconnect.LoginAsync("Alice"); } }