115 lines
7.9 KiB
C#
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);
|
|
}
|
|
}
|