Add managed codec DSP and initial control server
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using VoiceCat.Crypto;
|
||||
using VoiceCat.Server;
|
||||
using VoiceCat.Server.Transport;
|
||||
using Voicecat.V1;
|
||||
|
||||
namespace VoiceCat.Tests;
|
||||
|
||||
public sealed class ServerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ControlFramesCanSpanMultipleTlsRecordsAndPingEchoesCorrelation()
|
||||
{
|
||||
await using var fixture = new ServerFixture();
|
||||
await using var client = await fixture.ConnectAsync();
|
||||
client.Send(new() { ClientHello = new() { ProtoVersion = 2, ClientName = new string('x', 48000) } });
|
||||
await client.ReadUntilAsync(e => e.ServerHello is not null);
|
||||
client.Send(new() { RequestId = 45, Ping = new() { Nonce = 123456 } });
|
||||
Envelope pong = await client.ReadUntilAsync(e => e.Pong is not null);
|
||||
Assert.Equal(45UL, pong.RequestId);
|
||||
Assert.Equal(123456UL, pong.Pong.Nonce);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GuestsChatJoinChannelsAndDisconnectOverTls()
|
||||
{
|
||||
await using var fixture = new ServerFixture();
|
||||
await using var alice = await fixture.ConnectAsync();
|
||||
User a = await alice.LoginAsync("Alice");
|
||||
await using var bob = await fixture.ConnectAsync();
|
||||
User b = await bob.LoginAsync("Bob");
|
||||
Assert.NotEqual(a.Id, b.Id);
|
||||
Envelope joined = await alice.ReadUntilAsync(e => e.UserEvent?.Kind == UserEvent.Types.Kind.Joined);
|
||||
Assert.Equal(b.Id, joined.UserEvent.User.Id);
|
||||
|
||||
alice.Send(new() { TextMessage = new() { Scope = TextScope.TextChannel, TargetId = 1, SenderId = b.Id, Body = "hello", ClientMsgId = "one" } });
|
||||
TextMessage text = (await bob.ReadUntilAsync(e => e.TextMessage is not null)).TextMessage;
|
||||
Assert.Equal("hello", text.Body);
|
||||
Assert.Equal(a.Id, text.SenderId);
|
||||
Assert.True(text.SentAtUnixMs > 0);
|
||||
Assert.True((await alice.ReadUntilAsync(e => e.TextMessageAck is not null)).TextMessageAck.Ok);
|
||||
|
||||
bob.Send(new() { RequestId = 10, JoinChannel = new() { ChannelId = 2 } });
|
||||
Envelope moved = await bob.ReadUntilAsync(e => e.JoinChannelResult is not null);
|
||||
Assert.Equal(10UL, moved.RequestId);
|
||||
Assert.True(moved.JoinChannelResult.Ok);
|
||||
Assert.Equal(128000U, moved.JoinChannelResult.Audio.BitrateBps);
|
||||
alice.Send(new() { TextMessage = new() { Scope = TextScope.TextChannel, TargetId = 2, Body = "unauthorized", ClientMsgId = "two" } });
|
||||
Assert.False((await alice.ReadUntilAsync(e => e.TextMessageAck is not null)).TextMessageAck.Ok);
|
||||
alice.Send(new() { TextMessage = new() { Scope = TextScope.TextChannel, TargetId = 1, Body = "isolated" } });
|
||||
alice.Send(new() { TextMessage = new() { Scope = TextScope.TextPrivate, TargetId = b.Id, Body = "private" } });
|
||||
Assert.Equal("private", (await bob.ReadUntilAsync(e => e.TextMessage is not null)).TextMessage.Body);
|
||||
|
||||
bob.Send(new() { Disconnect = new() });
|
||||
Envelope left = await alice.ReadUntilAsync(e => e.UserEvent?.Kind == UserEvent.Types.Kind.Left);
|
||||
Assert.Equal(b.Id, left.UserEvent.LeftId);
|
||||
alice.Send(new() { RequestId = 11, Subscribe = new() });
|
||||
ServerStateSnapshot snapshot = (await alice.ReadUntilAsync(e => e.ServerState is not null)).ServerState;
|
||||
Assert.Equal(a.Id, Assert.Single(snapshot.Users).Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PasswordAuthenticationCanRetryAndGuestAccessCanBeDisabled()
|
||||
{
|
||||
await using var fixture = new ServerFixture(false);
|
||||
using (var accounts = new VoiceCat.Server.Data.AccountStore(Path.Combine(fixture.Directory, "voicecat.db")))
|
||||
await accounts.CreateAccountAsync("Admin", "secret", true);
|
||||
await using var client = await fixture.ConnectAsync();
|
||||
client.Send(new() { ClientHello = new() { ProtoVersion = 2 } });
|
||||
ServerHello hello = (await client.ReadUntilAsync(e => e.ServerHello is not null)).ServerHello;
|
||||
Assert.Equal(["password"], hello.AuthMethods);
|
||||
client.Send(new() { AuthRequest = new() { Guest = new() { Nickname = "Guest" } } });
|
||||
Assert.False((await client.ReadUntilAsync(e => e.AuthResult is not null)).AuthResult.Ok);
|
||||
client.Send(new() { AuthRequest = new() { Password = new() { Username = "Admin", Password = "wrong" } } });
|
||||
Assert.False((await client.ReadUntilAsync(e => e.AuthResult is not null)).AuthResult.Ok);
|
||||
client.Send(new() { RequestId = 3, AuthRequest = new() { Password = new() { Username = "Admin", Password = "secret" } } });
|
||||
Envelope authenticated = await client.ReadUntilAsync(e => e.AuthResult is not null);
|
||||
Assert.True(authenticated.AuthResult.Ok);
|
||||
Assert.Equal(3UL, authenticated.RequestId);
|
||||
Assert.True(authenticated.AuthResult.Permissions.IsAdmin);
|
||||
Assert.False(authenticated.AuthResult.Self.IsGuest);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task InvalidVersionAndUnauthenticatedTextAreDisconnected(bool invalidVersion)
|
||||
{
|
||||
await using var fixture = new ServerFixture();
|
||||
await using var client = await fixture.ConnectAsync();
|
||||
client.Send(invalidVersion ? new() { ClientHello = new() { ProtoVersion = 1 } } : new() { TextMessage = new() { Body = "pre-auth" } });
|
||||
Assert.NotEqual(0U, (await client.ReadUntilAsync(e => e.Disconnect is not null)).Disconnect.Code);
|
||||
}
|
||||
|
||||
[CppCliFact]
|
||||
public async Task ExistingCppCliAuthenticatesAndChatsThroughManagedServer()
|
||||
{
|
||||
await using var fixture = new ServerFixture();
|
||||
await using var receiver = await fixture.ConnectAsync();
|
||||
User self = await receiver.LoginAsync("Managed");
|
||||
var start = new ProcessStartInfo(Environment.GetEnvironmentVariable("VOICECAT_VCCLI")!)
|
||||
{
|
||||
WorkingDirectory = fixture.Directory, UseShellExecute = false,
|
||||
RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true
|
||||
};
|
||||
foreach (string argument in new[] { "--host", "127.0.0.1", "--port", fixture.Server.EndPoint.Port.ToString(), "--nick", "Cpp", "--text", "native interoperability", "--wait-ms", "10000" })
|
||||
start.ArgumentList.Add(argument);
|
||||
using var process = Process.Start(start)!;
|
||||
Task<string> output = process.StandardOutput.ReadToEndAsync();
|
||||
Task<string> error = process.StandardError.ReadToEndAsync();
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(receiver.Timeout.Token);
|
||||
string log = await output + await error;
|
||||
Assert.True(process.ExitCode == 0, log);
|
||||
TextMessage text = (await receiver.ReadUntilAsync(e => e.TextMessage is not null)).TextMessage;
|
||||
Assert.Equal("native interoperability", text.Body);
|
||||
Assert.NotEqual(self.Id, text.SenderId);
|
||||
Assert.Contains("native interoperability", log);
|
||||
}
|
||||
finally { if (!process.HasExited) { process.Kill(true); await process.WaitForExitAsync(); } }
|
||||
}
|
||||
|
||||
private sealed class CppCliFactAttribute : FactAttribute
|
||||
{
|
||||
public CppCliFactAttribute()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("VOICECAT_VCCLI"))) Skip = "Set VOICECAT_VCCLI to the existing native CLI.";
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ServerFixture : IAsyncDisposable
|
||||
{
|
||||
public string Directory { get; } = Path.Combine(Path.GetTempPath(), "voicecat-server-" + Guid.NewGuid().ToString("N"));
|
||||
public VoiceServer Server { get; }
|
||||
private readonly string fingerprint;
|
||||
public ServerFixture(bool guests = true)
|
||||
{
|
||||
System.IO.Directory.CreateDirectory(Directory);
|
||||
Server = new(Directory, new(IPAddress.Loopback, 0), guests);
|
||||
using var credentials = ServerCredentials.LoadOrCreate(Directory, "VoiceCat Server");
|
||||
fingerprint = credentials.CertificateFingerprint;
|
||||
}
|
||||
public async Task<Client> ConnectAsync()
|
||||
{
|
||||
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await socket.ConnectAsync(Server.EndPoint);
|
||||
return new(new(socket, TlsSession.CreateClient(value => value == fingerprint), CancellationToken.None));
|
||||
}
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await Server.DisposeAsync();
|
||||
System.IO.Directory.Delete(Directory, true);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Client : IAsyncDisposable
|
||||
{
|
||||
public CancellationTokenSource Timeout { get; } = new(TimeSpan.FromSeconds(30));
|
||||
private readonly TlsControlConnection connection;
|
||||
private readonly IAsyncEnumerator<Envelope> messages;
|
||||
public Client(TlsControlConnection connection)
|
||||
{
|
||||
this.connection = connection;
|
||||
messages = connection.ReadAsync(Timeout.Token).GetAsyncEnumerator();
|
||||
}
|
||||
public void Send(Envelope envelope) => Assert.True(connection.TrySend(envelope));
|
||||
public async Task<Envelope> ReadUntilAsync(Func<Envelope, bool> predicate)
|
||||
{
|
||||
while (await messages.MoveNextAsync()) if (predicate(messages.Current)) return messages.Current;
|
||||
throw new IOException("Connection ended before the expected message.");
|
||||
}
|
||||
public async Task<User> LoginAsync(string nickname)
|
||||
{
|
||||
Send(new() { RequestId = 1, ClientHello = new() { ProtoVersion = 2, ClientName = "Managed test" } });
|
||||
Assert.Equal(1UL, (await ReadUntilAsync(e => e.ServerHello is not null)).RequestId);
|
||||
Send(new() { RequestId = 2, AuthRequest = new() { Guest = new() { Nickname = nickname } } });
|
||||
AuthResult auth = (await ReadUntilAsync(e => e.AuthResult is not null)).AuthResult;
|
||||
Assert.True(auth.Ok, auth.Error);
|
||||
ServerStateSnapshot state = (await ReadUntilAsync(e => e.ServerState is not null)).ServerState;
|
||||
Assert.Equal(2, state.Channels.Count);
|
||||
Assert.Contains(state.Users, user => user.Id == auth.Self.Id);
|
||||
return auth.Self;
|
||||
}
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await messages.DisposeAsync();
|
||||
await connection.DisposeAsync();
|
||||
Timeout.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user