Files
voice-cat/tests/VoiceCat.Tests/ChannelManagementTests.cs
T
Talon 7b0c003ad4
Build and test / test (macos-latest) (push) Canceled after 0s
Build and test / test (ubuntu-24.04) (push) Canceled after 0s
Build and test / test (windows-latest) (push) Canceled after 0s
Build and test / apple-client (push) Canceled after 0s
Add adaptive packet loss handling
2026-09-22 16:54:49 +02:00

165 lines
9.2 KiB
C#

using VoiceCat.Server.Data;
using VoiceCat.Protocol;
using Voicecat.V1;
using static VoiceCat.Tests.ServerTests;
namespace VoiceCat.Tests;
public class ChannelManagementTests
{
internal static async Task<Client> 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();
var hello = new ClientHello { ProtoVersion = 2 }; hello.Features.Add(ProtocolFeatures.AdaptivePacketLoss);
client.Send(new() { ClientHello = hello });
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<bool> 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 } }));
}
[Fact]
public async Task DredAudioSettingsRoundTripThroughEditAndDatabaseRestart()
{
await using var fixture = new ServerFixture();
await using var admin = await AdminAsync(fixture);
using (var store = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db")))
{
Channel music = store.LoadChannels().Single(c => c.Name == "Music Room");
music.Audio.SampleRate = 48_000;
music.Audio.BitrateBps = 128_000;
music.Audio.ExpectedPacketLoss = 15;
music.Audio.Dtx = true;
music.Audio.Fec = true;
music.Audio.Dred = true;
music.Audio.PacketLossMode = PacketLossMode.PacketLossAutoStable;
music.Audio.Complexity = 10;
Assert.True(await ResultAsync(admin, new() { EditChannel = new() { Channel = music } }));
}
using var reopened = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db"));
AudioConfig audio = reopened.LoadChannels().Single(c => c.Name == "Music Room").Audio;
Assert.Equal(48_000U, audio.SampleRate);
Assert.Equal(128_000U, audio.BitrateBps);
Assert.Equal(15U, audio.ExpectedPacketLoss);
Assert.True(audio.Dtx);
Assert.True(audio.Fec);
Assert.True(audio.Dred);
Assert.Equal(PacketLossMode.PacketLossAutoStable, audio.PacketLossMode);
Assert.Equal(10U, audio.Complexity);
}
[Fact]
public async Task LegacyAdministratorEditsPreserveAutomaticPacketLossMode()
{
await using var fixture = new ServerFixture();
await using var current = await AdminAsync(fixture);
Channel music;
using (var store = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db")))
{
music = store.LoadChannels().Single(channel => channel.Name == "Music Room");
music.Audio.PacketLossMode = PacketLossMode.PacketLossAutoBalanced;
}
Assert.True(await ResultAsync(current, new() { EditChannel = new() { Channel = music } }));
await using Client legacy = await fixture.ConnectAsync();
legacy.Send(new() { ClientHello = new() { ProtoVersion = 2 } });
await legacy.ReadUntilAsync(envelope => envelope.ServerHello is not null);
legacy.Send(new() { AuthRequest = new() { Password = new() { Username = "Admin", Password = "secret" } } });
Assert.True((await legacy.ReadUntilAsync(envelope => envelope.AuthResult is not null)).AuthResult.Ok);
await legacy.ReadUntilAsync(envelope => envelope.ServerState is not null);
music.Audio.PacketLossMode = PacketLossMode.PacketLossManual;
music.Audio.ExpectedPacketLoss = 23;
Assert.True(await ResultAsync(legacy, new() { EditChannel = new() { Channel = music } }));
using var verify = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db"));
AudioConfig saved = verify.LoadChannels().Single(channel => channel.Id == music.Id).Audio;
Assert.Equal(PacketLossMode.PacketLossAutoBalanced, saved.PacketLossMode);
Assert.Equal(23U, saved.ExpectedPacketLoss);
}
}