Retire legacy sources and verify managed iOS deployment
.NET port / test (macos-latest) (push) Canceled after 0s
.NET port / test (ubuntu-24.04) (push) Canceled after 0s
.NET port / test (windows-latest) (push) Canceled after 0s
.NET port / apple-client (push) Canceled after 0s

This commit is contained in:
2026-09-19 22:40:48 +02:00
parent 42e3bbe14c
commit c9ed832459
109 changed files with 877 additions and 4981 deletions
@@ -1,5 +1,4 @@
using Microsoft.Data.Sqlite;
using System.Diagnostics;
using VoiceCat.Server.Data;
namespace VoiceCat.Tests;
@@ -27,71 +26,6 @@ public sealed class AccountStoreTests
finally { File.Delete(path); File.Delete(path + "-wal"); File.Delete(path + "-shm"); }
}
[NativeDatabaseFact]
public async Task ExistingCppDatabaseAndManagedAccountsWorkInBothImplementations()
{
string directory = Path.Combine(Path.GetTempPath(), "voicecat-import-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory);
string path = Path.Combine(directory, "voicecat.db");
try
{
await RunOracleAsync("create", path);
using (var store = new AccountStore(path))
{
Account account = Assert.IsType<Account>(await store.AuthenticateAsync("legacy", "legacy password"));
Assert.True(account.IsAdmin);
var channel = Assert.Single(store.LoadChannels());
Assert.Equal("Preserved native topic", channel.Topic);
Assert.Equal(7U, channel.MaxUsers);
Assert.Equal(32000U, channel.Audio.BitrateBps);
await store.CreateAccountAsync("managed", "managed password", true);
}
await RunOracleAsync("verify", path);
}
finally { Directory.Delete(directory, true); }
}
private static async Task RunOracleAsync(string mode, string path)
{
var start = new ProcessStartInfo(Environment.GetEnvironmentVariable("VOICECAT_DATABASE_ORACLE")!) { UseShellExecute = false, CreateNoWindow = true };
start.ArgumentList.Add(mode);
start.ArgumentList.Add(path);
using var process = Process.Start(start)!;
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try { await process.WaitForExitAsync(timeout.Token); Assert.Equal(0, process.ExitCode); }
finally { if (!process.HasExited) { process.Kill(true); await process.WaitForExitAsync(); } }
}
[NativeDatabaseFact]
public async Task ChannelPasswordHashesWorkInBothImplementations()
{
string directory = Path.Combine(Path.GetTempPath(), "voicecat-channels-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory);
string path = Path.Combine(directory, "voicecat.db");
try
{
await RunOracleAsync("create-protected", path);
using (var store = new AccountStore(path))
{
var channel = Assert.Single(store.LoadChannels());
Assert.True(store.CheckChannelPassword(channel.Id, "channel password"));
Assert.False(store.CheckChannelPassword(channel.Id, "wrong"));
channel.Name = "Managed protected";
store.SaveChannel(channel, "channel password", true);
}
await RunOracleAsync("verify-protected", path);
}
finally { Directory.Delete(directory, true); }
}
private sealed class NativeDatabaseFactAttribute : FactAttribute
{
public NativeDatabaseFactAttribute()
{
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("VOICECAT_DATABASE_ORACLE"))) Skip = "Set VOICECAT_DATABASE_ORACLE to the native database oracle.";
}
}
[Fact]
public async Task AccountsSurviveRestartAndFailedAuthDoesNotChangeLastLogin()
{
@@ -8,41 +8,6 @@ namespace VoiceCat.Tests;
public class AdministrationTests
{
[NativeCliFact]
public async Task ExistingCppCliCreatesProtectedChannelsAndAdministersAccounts()
{
await using var fixture = new ServerFixture();
await using var admin = await AdminAsync(fixture);
var start = new System.Diagnostics.ProcessStartInfo(Environment.GetEnvironmentVariable("VOICECAT_VCCLI")!)
{
WorkingDirectory = fixture.Directory, UseShellExecute = false, CreateNoWindow = true,
RedirectStandardOutput = true, RedirectStandardError = true
};
foreach (string argument in new[] { "--host", "127.0.0.1", "--port", fixture.Server.EndPoint.Port.ToString(), "--username", "Admin", "--password", "secret",
"--create-channel", "--new-channel-name", "Native room", "--new-channel-password", "protected", "--create-account", "native", "secret", "--list-accounts", "--wait-ms", "10000" })
start.ArgumentList.Add(argument);
using var process = System.Diagnostics.Process.Start(start)!;
Task<string> stdout = process.StandardOutput.ReadToEndAsync(), stderr = process.StandardError.ReadToEndAsync();
try
{
await process.WaitForExitAsync(admin.Timeout.Token);
Assert.True(process.ExitCode == 0, await stdout + await stderr);
using var store = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db"));
var room = store.LoadChannels().Single(c => c.Name == "Native room");
Assert.True(store.CheckChannelPassword(room.Id, "protected"));
Assert.NotNull(await store.AuthenticateAsync("native", "secret"));
}
finally { if (!process.HasExited) { process.Kill(true); await process.WaitForExitAsync(); } }
}
private sealed class NativeCliFactAttribute : FactAttribute
{
public NativeCliFactAttribute()
{
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("VOICECAT_VCCLI"))) Skip = "Set VOICECAT_VCCLI to the existing native CLI.";
}
}
[Fact]
public async Task AccountAdministrationIsPermissionGatedAndPersistsPasswordChanges()
{
+1 -1
View File
@@ -20,7 +20,7 @@ public sealed class DspTests
if (frame >= 60) foreach (short value in pcm) outputEnergy += (double)value * value;
}
Assert.True(Math.Sqrt(outputEnergy / inputEnergy) < 0.2);
using var fixture = JsonDocument.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "cpp-noise.json")));
using var fixture = JsonDocument.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "rnnoise.json")));
short[] expected = fixture.RootElement.GetProperty("samples").EnumerateArray().Select(value => value.GetInt16()).ToArray();
Assert.Equal(pcm.Length, expected.Length);
for (int i = 0; i < pcm.Length; i++) Assert.InRange(Math.Abs(pcm[i] - expected[i]), 0, 1);
+3 -3
View File
@@ -9,7 +9,7 @@ namespace VoiceCat.Tests;
public class GoldenTests
{
[Fact]
public void EnvelopeMatchesCppFixture()
public void EnvelopeMatchesCanonicalWireVector()
{
using var fixture = Load();
var expected = Convert.FromHexString(fixture.RootElement.GetProperty("envelope").GetString()!);
@@ -23,7 +23,7 @@ public class GoldenTests
[Theory]
[InlineData(false)]
[InlineData(true)]
public void MediaPacketsMatchCppFixtures(bool managed)
public void MediaPacketsMatchCanonicalWireVectors(bool managed)
{
using var fixture = Load();
foreach (var vector in fixture.RootElement.GetProperty("media").EnumerateArray())
@@ -46,5 +46,5 @@ public class GoldenTests
}
}
private static JsonDocument Load() => JsonDocument.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "cpp-wire.json")));
private static JsonDocument Load() => JsonDocument.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "wire.json")));
}
+1 -1
View File
@@ -41,7 +41,7 @@ public class IdentityTests
}
[Fact]
public void TofuRequiresExplicitPinAndPreservesCppFileFormat()
public void TofuRequiresExplicitPinAndPreservesFileFormat()
{
string directory = Path.Combine(Path.GetTempPath(), "voicecat-pins-" + Guid.NewGuid());
Directory.CreateDirectory(directory);
@@ -5,30 +5,6 @@ namespace VoiceCat.Tests;
public class ManagedCliTests
{
[Fact]
public async Task ManagedCliInteroperatesWithExistingCppCli()
{
string? nativeCli = Environment.GetEnvironmentVariable("VOICECAT_VCCLI");
if (string.IsNullOrEmpty(nativeCli)) return; // Full conformance runs set this explicitly.
await using var fixture = new ServerFixture();
string cli = Path.Combine(FindRoot(), "dotnet", "src", "VoiceCat.Cli", "bin", "Release", "net10.0", "VoiceCat.Cli.dll");
using Process managed = Start(cli, fixture, "Managed", "Managed checkpoint", "Native checkpoint", 2);
await Task.Delay(750); // Native vccli sends its one-shot text immediately after auth.
var nativeStart = new ProcessStartInfo(nativeCli) { WorkingDirectory = fixture.Directory, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true };
foreach (string argument in new[] { "--host", "127.0.0.1", "--port", fixture.Server.EndPoint.Port.ToString(), "--nick", "Native",
"--channel", "2", "--text", "Native checkpoint", "--test-tone-ms", "5000" }) nativeStart.ArgumentList.Add(argument);
using Process native = Process.Start(nativeStart)!;
Task<string> managedOut = managed.StandardOutput.ReadToEndAsync(), managedError = managed.StandardError.ReadToEndAsync();
Task<string> nativeOut = native.StandardOutput.ReadToEndAsync(), nativeError = native.StandardError.ReadToEndAsync();
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(35));
await Task.WhenAll(managed.WaitForExitAsync(timeout.Token), native.WaitForExitAsync(timeout.Token));
string mout = await managedOut, nout = await nativeOut;
Assert.True(managed.ExitCode == 0, await managedError + Environment.NewLine + mout);
Assert.True(native.ExitCode == 0, await nativeError + Environment.NewLine + nout);
Assert.Contains("Native checkpoint", mout); Assert.Contains("Managed checkpoint", nout);
Assert.Contains("[test-tone] received=", nout); Assert.DoesNotContain("\"voiceEnergy\":0", mout);
}
[Theory]
[InlineData(1u)]
[InlineData(2u)]
@@ -1,6 +1,5 @@
using VoiceCat.Transport;
using System.Net;
using System.Diagnostics;
using System.Net.Sockets;
using VoiceCat.Protocol;
using VoiceCat.Server.Transport;
@@ -11,89 +10,6 @@ namespace VoiceCat.Tests;
public sealed class MediaRelayTests
{
[CppCliVoiceTheory]
[InlineData(1)]
[InlineData(2)]
public async Task TwoCppCliProcessesJoinChatAndExchangeVoice(int channel)
{
await using var fixture = new ServerFixture();
await using var observer = await fixture.ConnectAsync();
await observer.LoginAsync("Observer");
observer.Send(new() { JoinChannel = new() { ChannelId = checked((uint)channel) } });
Assert.True((await observer.ReadUntilAsync(e => e.JoinChannelResult is not null)).JoinChannelResult.Ok);
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await Task.WhenAll(RunAsync("Cli Alice"), RunAsync("Cli Bob"));
var first = (await observer.ReadUntilAsync(e => e.TextMessage is not null)).TextMessage;
var second = (await observer.ReadUntilAsync(e => e.TextMessage is not null)).TextMessage;
Assert.Equal("CLI voice checkpoint", first.Body);
Assert.Equal(first.Body, second.Body);
Assert.NotEqual(first.SenderId, second.SenderId);
async Task RunAsync(string nickname)
{
var start = new ProcessStartInfo(Environment.GetEnvironmentVariable("VOICECAT_VCCLI")!)
{
WorkingDirectory = fixture.Directory, UseShellExecute = false, CreateNoWindow = true,
RedirectStandardOutput = true, RedirectStandardError = true
};
foreach (string argument in new[] { "--host", "127.0.0.1", "--port", fixture.Server.EndPoint.Port.ToString(System.Globalization.CultureInfo.InvariantCulture),
"--nick", nickname, "--channel", channel.ToString(System.Globalization.CultureInfo.InvariantCulture), "--text", "CLI voice checkpoint", "--test-tone-ms", "4000" })
start.ArgumentList.Add(argument);
using var process = Process.Start(start)!;
Task<string> stdout = process.StandardOutput.ReadToEndAsync();
Task<string> stderr = process.StandardError.ReadToEndAsync();
try
{
await process.WaitForExitAsync(timeout.Token);
string log = await stdout + await stderr;
Assert.True(process.ExitCode == 0, log);
Assert.Contains("[test-tone] received=", log);
}
finally { if (!process.HasExited) { process.Kill(true); await process.WaitForExitAsync(); } }
}
}
private sealed class CppCliVoiceTheoryAttribute : TheoryAttribute
{
public CppCliVoiceTheoryAttribute()
{
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("VOICECAT_VCCLI"))) Skip = "Set VOICECAT_VCCLI to the existing native CLI.";
}
}
[VoiceOracleTheory]
[InlineData(1)]
[InlineData(2)]
public async Task ExistingCppClientsExchangeBidirectionalVoiceThroughManagedServer(int channel)
{
await using var fixture = new ServerFixture();
var start = new ProcessStartInfo(Environment.GetEnvironmentVariable("VOICECAT_VOICE_ORACLE")!)
{
WorkingDirectory = fixture.Directory, UseShellExecute = false, CreateNoWindow = true,
RedirectStandardOutput = true, RedirectStandardError = true
};
start.ArgumentList.Add(fixture.Server.EndPoint.Port.ToString(System.Globalization.CultureInfo.InvariantCulture));
start.ArgumentList.Add(channel.ToString(System.Globalization.CultureInfo.InvariantCulture));
using var process = Process.Start(start)!;
Task<string> output = process.StandardOutput.ReadToEndAsync();
Task<string> error = process.StandardError.ReadToEndAsync();
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(45));
try
{
await process.WaitForExitAsync(timeout.Token);
Assert.True(process.ExitCode == 0, await output + await error);
}
finally { if (!process.HasExited) { process.Kill(true); await process.WaitForExitAsync(); } }
}
private sealed class VoiceOracleTheoryAttribute : TheoryAttribute
{
public VoiceOracleTheoryAttribute()
{
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("VOICECAT_VOICE_ORACLE"))) Skip = "Set VOICECAT_VOICE_ORACLE to the native voice conformance executable.";
}
}
[Fact]
public async Task DisconnectInvalidatesBothBindingAndActiveStreams()
{
+1 -1
View File
@@ -10,7 +10,7 @@ public sealed class PasswordTests
public void VerifiesLibsodiumHashesWithoutPasswordNormalization()
{
var hasher = new PasswordHasher();
using var fixture = JsonDocument.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "cpp-passwords.json")));
using var fixture = JsonDocument.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "argon2id.json")));
foreach (var item in fixture.RootElement.GetProperty("hashes").EnumerateArray())
{
string encodedPassword = item.GetProperty("passwordBase64").GetString()!;
@@ -95,43 +95,6 @@ public sealed class ServerTests
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.";
}
}
internal sealed class ServerFixture : IAsyncDisposable
{
public string Directory { get; } = Path.Combine(Path.GetTempPath(), "voicecat-server-" + Guid.NewGuid().ToString("N"));
@@ -1,97 +0,0 @@
using System.Diagnostics;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using VoiceCat.Crypto;
using VoiceCat.Protocol;
namespace VoiceCat.Tests;
public class TlsInteropTests
{
[TlsOracleFact]
public async Task ManagedClientAndCppServerAgreeOnExporterKeysAndCertificate()
{
string? oracle = Environment.GetEnvironmentVariable("VOICECAT_TLS_ORACLE");
string directory = Path.Combine(Path.GetTempPath(), "voicecat-tls-" + Guid.NewGuid());
Directory.CreateDirectory(directory);
var start = new ProcessStartInfo(oracle!) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardError = true, RedirectStandardOutput = true };
start.ArgumentList.Add(directory);
using var process = Process.Start(start)!;
var error = process.StandardError.ReadToEndAsync();
var stdout = process.StandardOutput.ReadToEndAsync();
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
int port = 0;
while (!int.TryParse(File.Exists(Path.Combine(directory, "port.txt")) ? await File.ReadAllTextAsync(Path.Combine(directory, "port.txt"), timeout.Token) : "", out port))
{
Assert.False(process.HasExited, "C++ TLS oracle exited before listening.");
await Task.Delay(20, timeout.Token);
}
using var certificate = X509Certificate2.CreateFromPem(await File.ReadAllTextAsync(Path.Combine(directory, "server.crt"), timeout.Token));
string fingerprint = Convert.ToHexString(SHA256.HashData(certificate.RawData));
using var credentials = ServerCredentials.LoadOrCreate(directory, "existing C++ identity");
Assert.Equal(fingerprint, credentials.CertificateFingerprint);
Assert.Equal(32, credentials.Identity.PublicKey.Length);
using var client = TlsSession.CreateClient(value => value == fingerprint);
using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync("127.0.0.1", port, timeout.Token);
byte[] buffer = new byte[16384];
async Task Flush()
{
while (client.PendingCiphertextBytes > 0)
{
int count = client.DrainCiphertext(buffer);
int sent = 0;
while (sent < count) sent += await socket.SendAsync(buffer.AsMemory(sent, count - sent), SocketFlags.None, timeout.Token);
}
}
async Task Receive()
{
int count = await socket.ReceiveAsync(buffer, SocketFlags.None, timeout.Token);
Assert.True(count > 0, "TLS oracle closed unexpectedly.");
client.ReceiveCiphertext(buffer.AsSpan(0, count));
}
while (!client.IsReady) { await Flush(); await Receive(); }
await Flush();
byte[] packet = new byte[41];
int received = 0;
while (received < packet.Length)
{
int count = client.ReadPlaintext(packet.AsSpan(received));
received += count;
if (count == 0) { await Flush(); await Receive(); }
}
using var decryptor = client.CreateMediaDecryptor();
byte[] plaintext = new byte[5];
Assert.True(decryptor.TryDecrypt(packet, plaintext, out var header, out _));
Assert.Equal("hello"u8.ToArray(), plaintext);
Assert.Equal(fingerprint, client.PeerCertificateFingerprint);
using var encryptor = client.CreateMediaEncryptor();
encryptor.Encrypt(header, plaintext, packet);
client.WritePlaintext(packet);
await Flush();
byte[] ack = new byte[1];
while (client.ReadPlaintext(ack) == 0) { await Flush(); await Receive(); }
Assert.Equal(1, ack[0]);
await process.WaitForExitAsync(timeout.Token);
Assert.True(process.ExitCode == 0, await error);
await stdout;
}
finally
{
if (!process.HasExited) { process.Kill(entireProcessTree: true); await process.WaitForExitAsync(); }
Directory.Delete(directory, recursive: true);
}
}
}
public sealed class TlsOracleFactAttribute : FactAttribute
{
public TlsOracleFactAttribute()
{
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("VOICECAT_TLS_ORACLE")))
Skip = "Build the native TLS oracle and set VOICECAT_TLS_ORACLE to its executable path.";
}
}