Files
voice-cat/tests/VoiceCat.Tests/ManagedClientTests.cs
T
Talon 08e6c5930a
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
Retire legacy implementations and flatten managed layout
2026-09-21 00:11:32 +02:00

115 lines
7.9 KiB
C#

using VoiceCat.Core;
using VoiceCat.Crypto;
using VoiceCat.Server.Data;
using Voicecat.V1;
using static VoiceCat.Tests.ServerTests;
using static VoiceCat.Tests.MediaRelayTests;
namespace VoiceCat.Tests;
public class ManagedClientTests
{
private static VoiceCatClient NewClient(ServerFixture fixture, string name) => new(name, "test", Path.Combine(fixture.Directory, name + ".pins"));
private static Task Connect(VoiceCatClient client, ServerFixture fixture) => client.ConnectAsync("127.0.0.1", (ushort)fixture.Server.EndPoint.Port, (_, _) => ValueTask.FromResult(true));
private static async Task<Envelope> Event(VoiceCatClient client, Func<Envelope, bool> predicate)
{
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
await foreach (Envelope message in client.ReadEventsAsync(timeout.Token)) if (predicate(message)) return message;
throw new IOException("Expected client event was not received.");
}
[Fact]
public async Task ManagedClientsAuthenticateChatAndCorrelateConcurrentRequests()
{
await using var fixture = new ServerFixture();
await using var alice = NewClient(fixture, "Alice"); await using var bob = NewClient(fixture, "Bob");
await Connect(alice, fixture); await Connect(bob, fixture);
Assert.True((await alice.AuthenticateGuestAsync("Alice")).Ok); Assert.True((await bob.AuthenticateGuestAsync("Bob")).Ok);
await Event(bob, e => e.ServerState is not null); await Event(alice, e => e.UserEvent?.User?.Nickname == "Bob");
Assert.Equal(2, alice.Users.Count);
var copy = alice.Users[0]; copy.Nickname = "Mutated"; Assert.DoesNotContain(alice.Users, u => u.Nickname == "Mutated");
alice.Send(new() { TextMessage = new() { Scope = TextScope.TextChannel, TargetId = 1, Body = "Managed conversation", ClientMsgId = "a1" } });
Assert.Equal("Managed conversation", (await Event(bob, e => e.TextMessage is not null)).TextMessage.Body);
var requests = Enumerable.Range(1, 20).Select(async i =>
{
Envelope response = await alice.RequestAsync(new() { Ping = new() { Nonce = (ulong)i } });
Assert.Equal((ulong)i, response.Pong.Nonce); return response.RequestId;
});
Assert.Equal(20, (await Task.WhenAll(requests)).Distinct().Count());
await Assert.ThrowsAsync<InvalidOperationException>(() => Connect(alice, fixture));
Assert.Equal(ClientConnectionState.Connected, alice.State);
await alice.DisconnectAsync();
await Event(bob, e => e.UserEvent?.Kind == UserEvent.Types.Kind.Left);
await Connect(alice, fixture); Assert.True((await alice.AuthenticateGuestAsync("Returned")).Ok);
}
[Fact]
public async Task TofuRequiresApprovalPinsAcceptedCertificateAndRejectsChanges()
{
await using var first = new ServerFixture(); await using var second = new ServerFixture();
await using var client = NewClient(first, "Tofu");
await Assert.ThrowsAsync<System.Security.Authentication.AuthenticationException>(() => client.ConnectAsync("127.0.0.1", (ushort)first.Server.EndPoint.Port));
await client.ConnectAsync("127.0.0.1", (ushort)first.Server.EndPoint.Port, (challenge, _) =>
{ Assert.Equal(TofuStatus.FirstConnect, challenge.Status); return ValueTask.FromResult(true); });
await client.DisconnectAsync();
await client.ConnectAsync("127.0.0.1", (ushort)first.Server.EndPoint.Port); await client.DisconnectAsync();
// Pin the other server's certificate to this endpoint, simulating a changed server certificate.
using var credentials = ServerCredentials.LoadOrCreate(second.Directory, "VoiceCat Server");
new TofuStore(Path.Combine(first.Directory, "Other.pins")).Pin("127.0.0.1", (ushort)first.Server.EndPoint.Port, credentials.CertificateFingerprint);
await using var changed = new VoiceCatClient(tofuStorePath: Path.Combine(first.Directory, "Other.pins"));
await Assert.ThrowsAsync<System.Security.Authentication.AuthenticationException>(() => changed.ConnectAsync("127.0.0.1", (ushort)first.Server.EndPoint.Port,
(challenge, _) => { Assert.Equal(TofuStatus.Mismatch, challenge.Status); return ValueTask.FromResult(false); }));
}
[Fact]
public async Task ManagedClientSendsAndReceivesAuthenticatedEncodedVoice()
{
await using var fixture = new ServerFixture();
await using var managed = NewClient(fixture, "Managed"); await Connect(managed, fixture); await managed.AuthenticateGuestAsync("Managed");
Assert.True((await managed.SubscribeVoiceAsync()).Ok);
await using var peer = await VoicePeer.ConnectAsync(fixture, "Peer");
var remote = await peer.AnnounceAsync(StreamKind.StreamMic);
var local = (await managed.RequestAsync(new() { StreamAnnounce = new() { Kind = StreamKind.StreamMic } })).StreamAnnounceResult;
Assert.True(local.Ok);
var received = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
managed.VoiceReceived += (header, payload) => { Assert.Equal(remote.Ssrc, header.Ssrc); received.TrySetResult(payload.ToArray()); };
await peer.SendAsync(peer.Seal(remote.Ssrc, [1, 2, 3]));
Assert.Equal(new byte[] { 1, 2, 3 }, await received.Task.WaitAsync(TimeSpan.FromSeconds(5)));
Assert.True(managed.TrySendEncodedVoice(local.Ssrc, 960, [4, 5, 6]));
Assert.Equal(new byte[] { 4, 5, 6 }, (await peer.ReceiveVoiceAsync()).Payload);
}
[Fact]
public async Task ManagedAdministrationHelpersRoundTripTypedResults()
{
await using var fixture = new ServerFixture();
using (var store = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db")))
await store.CreateAccountAsync("Admin", "secret", true);
await using var admin = NewClient(fixture, "ManagedAdmin"); await Connect(admin, fixture);
Assert.True((await admin.AuthenticateUserAsync("Admin", "secret")).Ok);
await Event(admin, envelope => envelope.ServerState is not null);
Assert.True(admin.Permissions.IsAdmin);
await using var member = NewClient(fixture, "ManagedMember"); await Connect(member, fixture);
AuthResult memberAuth = await member.AuthenticateGuestAsync("ManagedMember"); Assert.True(memberAuth.Ok);
await Event(member, envelope => envelope.ServerState is not null);
await Event(admin, envelope => envelope.UserEvent?.User?.Id == memberAuth.Self.Id);
Assert.True((await admin.CreateAccountAsync("managed-ui", "first")).Ok);
Assert.Contains(await admin.ListAccountsAsync(), account => account.Username == "managed-ui");
Assert.True((await admin.ResetPasswordAsync("managed-ui", "second")).Ok);
var room = new Voicecat.V1.Channel { Name = "Managed UI room", Audio = new()
{ SampleRate = 48000, BitrateBps = 64000, FrameMs = 20, Complexity = 10, Fec = true } };
Assert.True((await admin.CreateChannelAsync(room, "protected")).Ok);
Envelope created = await Event(admin, envelope => envelope.ChannelEvent?.Channel?.Name == room.Name);
Channel edited = created.ChannelEvent.Channel.Clone(); edited.Topic = "Edited from managed UI";
Assert.True((await admin.EditChannelAsync(edited)).Ok);
Assert.True((await admin.SetPermissionsAsync(memberAuth.Self.Id, new() { CanCreateTempChannel = true })).Ok);
Assert.True((await admin.SetServerMuteAsync(memberAuth.Self.Id, true, true)).Ok);
Assert.True((await admin.SetServerMuteAsync(memberAuth.Self.Id, false, false)).Ok);
Assert.True((await admin.MoveUserAsync(memberAuth.Self.Id, created.ChannelEvent.Channel.Id)).Ok);
Assert.True((await admin.KickUserAsync(memberAuth.Self.Id, "managed helper test")).Ok);
Assert.True((await admin.DeleteChannelAsync(created.ChannelEvent.Channel.Id)).Ok);
Assert.True((await admin.DeleteAccountAsync("managed-ui")).Ok);
}
}