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 Event(VoiceCatClient client, Func 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(() => 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(() => 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(() => 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(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); 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); Assert.True((await admin.DeleteChannelAsync(created.ChannelEvent.Channel.Id)).Ok); Assert.True((await admin.DeleteAccountAsync("managed-ui")).Ok); } }