2026-09-16 16:48:06 +02:00
using VoiceCat.Transport ;
2026-09-15 23:24:11 +02:00
using System.Net ;
using System.Net.Sockets ;
using System.Text.Json ;
using VoiceCat.Crypto ;
using VoiceCat.Server ;
using VoiceCat.Server.Data ;
using VoiceCat.Server.Transport ;
using static VoiceCat . Tests . ServerTests ;
namespace VoiceCat.Tests ;
public class ProductionServerTests
{
2026-09-16 17:08:17 +02:00
[Fact]
public async Task HealthCheckPerformsTlsHandshakeAndCanPinCertificate ()
{
await using var fixture = new ServerFixture ();
using var credentials = ServerCredentials . LoadOrCreate ( fixture . Directory , "VoiceCat Server" );
var output = new StringWriter (); var error = new StringWriter ();
string endpoint = "127.0.0.1:" + fixture . Server . EndPoint . Port ;
Assert . Equal ( 0 , await ServerCommand . RunAsync ([ "--health-check" , endpoint ], output , error ));
Assert . Contains ( "\"status\":\"healthy\"" , output . ToString ());
output . GetStringBuilder (). Clear ();
Assert . Equal ( 0 , await ServerCommand . RunAsync ([ "--health-check" , endpoint , "--expect-fingerprint" , credentials . CertificateFingerprint ], output , error ));
Assert . Equal ( 1 , await ServerCommand . RunAsync ([ "--health-check" , endpoint , "--expect-fingerprint" , new string ( '0' , 64 )], output , error ));
Assert . Equal ( 1 , await ServerCommand . RunAsync ([ "--health-check" , "127.0.0.1:1" ], output , error ));
}
2026-09-15 23:24:11 +02:00
[PublishedServerFact]
public async Task PublishedExecutableProvisionsAdminAndReportsFingerprintsWithoutPasswordOutput ()
{
string directory = Path . Combine ( Path . GetTempPath (), "voicecat-published-" + Guid . NewGuid (). ToString ( "N" ));
async Task < string > Run ( params string [] arguments )
{
var start = new System . Diagnostics . ProcessStartInfo ( Environment . GetEnvironmentVariable ( "VOICECAT_PUBLISHED_SERVER" )!)
{ UseShellExecute = false , CreateNoWindow = true , RedirectStandardOutput = true , RedirectStandardError = true };
start . Environment [ "VOICECAT_ADMIN_PASSWORD" ] = "published test password" ;
foreach ( string argument in arguments . Concat ( new [] { "--data-dir" , directory })) start . ArgumentList . Add ( argument );
using var process = System . Diagnostics . Process . Start ( start )!;
Task < string > stdout = process . StandardOutput . ReadToEndAsync (), stderr = process . StandardError . ReadToEndAsync ();
using var timeout = new CancellationTokenSource ( TimeSpan . FromSeconds ( 20 ));
try
{
await process . WaitForExitAsync ( timeout . Token );
string log = await stdout + await stderr ;
Assert . True ( process . ExitCode == 0 , log ); Assert . DoesNotContain ( "published test password" , log );
return log ;
}
finally { if (! process . HasExited ) { process . Kill ( true ); await process . WaitForExitAsync (); } }
}
try
{
await Run ( "account" , "add" , "Operator" , "--admin" );
Assert . Contains ( "Operator" , await Run ( "account" , "list" ));
using ( var store = new AccountStore ( Path . Combine ( directory , "voicecat.db" )))
Assert . True (( await store . AuthenticateAsync ( "Operator" , "published test password" ))!. IsAdmin );
string fingerprint = ( await Run ( "--print-fingerprint" )). Trim ();
Assert . Equal ( 64 , fingerprint . Length );
Assert . Equal ( fingerprint , ( await Run ( "--print-fingerprint" )). Trim ());
await Run ( "account" , "reset" , "Operator" ); await Run ( "account" , "delete" , "Operator" );
Assert . DoesNotContain ( "Operator" , await Run ( "account" , "list" ));
}
finally { if ( Directory . Exists ( directory )) Directory . Delete ( directory , true ); }
}
private sealed class PublishedServerFactAttribute : FactAttribute
{
public PublishedServerFactAttribute ()
{
if ( string . IsNullOrEmpty ( Environment . GetEnvironmentVariable ( "VOICECAT_PUBLISHED_SERVER" ))) Skip = "Set VOICECAT_PUBLISHED_SERVER to the self-contained executable." ;
}
}
[Fact]
public void ConfigurationHonorsEnvironmentAndCommandPrecedenceWithoutCreatingFiles ()
{
var env = new Dictionary < string , string > { [ "VOICECAT_BIND_PORT" ] = "9000" , [ "VOICECAT_ALLOW_GUESTS" ] = "false" , [ "VOICECAT_SERVER_NAME" ] = "Environment" };
var config = ServerCommand . Parse ([ "--port" , "0" , "--name" , "Command" , "--bind" , "::1" , "--idle-seconds" , "0" ], key => env . GetValueOrDefault ( key ));
Assert . Equal ( 0 , config . Port ); Assert . Equal ( "Command" , config . Options . Name );
Assert . False ( config . Options . AllowGuests ); Assert . Equal ( "::1" , config . BindAddress );
Assert . Equal ( TimeSpan . Zero , config . Options . IdleTimeout );
Assert . Equal ( 8384 , ServerCommand . Parse ([], _ => null ). Port );
Assert . Equal ( "0.0.0.0" , ServerCommand . Parse ([], _ => null ). BindAddress );
Assert . Throws < ArgumentException >(() => ServerCommand . Parse ([ "--port" , "65536" ], _ => null ));
Assert . Throws < ArgumentException >(() => ServerCommand . Parse ([ "--bind" , "example.com" ], _ => null ));
Assert . Throws < ArgumentException >(() => ServerCommand . Parse ([ "--port" ], _ => null ));
Assert . Throws < ArgumentException >(() => ServerCommand . Parse ([ "--unencrypted" , "true" ], _ => null ));
Assert . Throws < ArgumentOutOfRangeException >(() => ServerCommand . Parse ([ "--auth-burst" , "0" ], _ => null ));
Assert . EndsWith ( "admin-data" , ServerCommand . Parse ([ "account" , "list" , "--data-dir" , "admin-data" ], _ => null ). Directory );
}
[Fact]
public void AuthenticationLimitsAreSharedAcrossConnectionsAndAddressesWithBackoff ()
{
var clock = new TestClock ();
var limiter = new AuthenticationLimiter ( new () { AuthenticationBurst = 5 }, clock );
for ( int i = 0 ; i < 3 ; i ++) { Assert . True ( limiter . TryAcquire ( "a" , "user" )); limiter . Record ( "a" , "user" , false ); }
Assert . False ( limiter . TryAcquire ( "a" , "other" ));
Assert . False ( limiter . TryAcquire ( "b" , "user" ));
clock . Advance ( 1 );
Assert . True ( limiter . TryAcquire ( "b" , "user" )); limiter . Record ( "b" , "user" , false );
clock . Advance ( 1 ); Assert . False ( limiter . TryAcquire ( "c" , "user" ));
clock . Advance ( 1 ); Assert . True ( limiter . TryAcquire ( "c" , "user" )); limiter . Record ( "c" , "user" , true );
Assert . False ( limiter . TryAcquire ( "d" , "user" )); // Success does not restore spent tokens.
clock . Advance ( 10 ); Assert . True ( limiter . TryAcquire ( "d" , "user" ));
}
[Fact]
public async Task PasswordThrottleSurvivesReconnectAndRefillsBeforeSuccessfulLogin ()
{
var clock = new TestClock ();
await using var fixture = new ServerFixture ( options : new () { AuthenticationBurst = 1 }, timeProvider : clock );
using ( var store = new AccountStore ( Path . Combine ( fixture . Directory , "voicecat.db" ))) await store . CreateAccountAsync ( "Member" , "secret" );
async Task < bool > Login ( string password )
{
await using var client = await fixture . ConnectAsync ();
client . Send ( new () { ClientHello = new () { ProtoVersion = 2 } }); await client . ReadUntilAsync ( e => e . ServerHello is not null );
client . Send ( new () { AuthRequest = new () { Password = new () { Username = "Member" , Password = password } } });
return ( await client . ReadUntilAsync ( e => e . AuthResult is not null )). AuthResult . Ok ;
}
Assert . False ( await Login ( "wrong" )); Assert . False ( await Login ( "secret" ));
clock . Advance ( 10 ); Assert . True ( await Login ( "secret" ));
}
[Fact]
public async Task HostPublishesReadinessPreventsDuplicateInstancesAndShutsDownActiveTls ()
{
string directory = Path . Combine ( Path . GetTempPath (), "voicecat-host-" + Guid . NewGuid (). ToString ( "N" ));
using var stop = new CancellationTokenSource ( TimeSpan . FromSeconds ( 20 ));
var output = new ReadyWriter (); var error = new StringWriter ();
Task < int > running = ServerCommand . RunAsync ([ "--data-dir" , directory , "--bind" , "127.0.0.1" , "--port" , "0" ], output , error , stop . Token );
try
{
using JsonDocument ready = JsonDocument . Parse ( await output . Ready . Task . WaitAsync ( stop . Token ));
int port = ready . RootElement . GetProperty ( "port" ). GetInt32 ();
Assert . Equal ( port , ready . RootElement . GetProperty ( "udp_port" ). GetInt32 ());
Assert . Equal ( 64 , ready . RootElement . GetProperty ( "certificate_fingerprint" ). GetString ()!. Length );
Assert . Equal ( 1 , await ServerCommand . RunAsync ([ "--data-dir" , directory , "--port" , "0" ], new StringWriter (), new StringWriter ()));
var socket = new Socket ( AddressFamily . InterNetwork , SocketType . Stream , ProtocolType . Tcp );
await socket . ConnectAsync ( new IPEndPoint ( IPAddress . Loopback , port ), stop . Token );
string fingerprint = ready . RootElement . GetProperty ( "certificate_fingerprint" ). GetString ()!;
await using var client = new Client ( new TlsControlConnection ( socket , TlsSession . CreateClient ( value => value == fingerprint ), CancellationToken . None ));
await client . LoginAsync ( "Host test" );
stop . Cancel (); Assert . Equal ( 0 , await running . WaitAsync ( TimeSpan . FromSeconds ( 10 )));
Assert . Equal ( "" , error . ToString ());
}
finally { stop . Cancel (); await running ; Directory . Delete ( directory , true ); }
}
private sealed class ReadyWriter : StringWriter
{
internal TaskCompletionSource < string > Ready = new ( TaskCreationOptions . RunContinuationsAsynchronously );
public override Task WriteLineAsync ( string? value ) { if ( value ?. Contains ( "\"ready\"" , StringComparison . Ordinal ) == true ) Ready . TrySetResult ( value ); return base . WriteLineAsync ( value ); }
}
private sealed class TestClock : TimeProvider
{
private long timestamp ;
public override long TimestampFrequency => 1000 ;
public override long GetTimestamp () => timestamp ;
internal void Advance ( int seconds ) => timestamp += seconds * 1000 ;
}
}