2026-09-15 22:51:33 +02:00
using Microsoft.Data.Sqlite ;
using System.Diagnostics ;
using VoiceCat.Server.Data ;
namespace VoiceCat.Tests ;
public sealed class AccountStoreTests
{
[Fact]
public void UnsupportedSchemaIsRejectedWithoutCreatingAccountTables ()
{
string path = Path . Combine ( Path . GetTempPath (), "voicecat-future-" + Guid . NewGuid (). ToString ( "N" ) + ".db" );
try
{
SQLitePCL . Batteries_V2 . Init ();
using var connection = new SqliteConnection ( new SqliteConnectionStringBuilder { DataSource = path , Pooling = false }. ToString ());
connection . Open ();
using var command = connection . CreateCommand ();
command . CommandText = "CREATE TABLE server_meta (key TEXT PRIMARY KEY,value TEXT NOT NULL); INSERT INTO server_meta VALUES ('schema_version','99');" ;
command . ExecuteNonQuery ();
Assert . Throws < InvalidDataException >(() => new AccountStore ( path ));
command . CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE name='accounts'" ;
Assert . Equal ( 0L , command . ExecuteScalar ());
command . CommandText = "SELECT value FROM server_meta WHERE key='schema_version'" ;
Assert . Equal ( "99" , command . ExecuteScalar ());
}
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 ( 7 U , channel . MaxUsers );
Assert . Equal ( 32000 U , 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 (); } }
}
2026-09-15 23:11:09 +02:00
[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 ); }
}
2026-09-15 22:51:33 +02:00
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 ()
{
string directory = Path . Combine ( Path . GetTempPath (), "voicecat-db-" + Guid . NewGuid (). ToString ( "N" ));
Directory . CreateDirectory ( directory );
string path = Path . Combine ( directory , "voicecat.db" );
try
{
Account account ;
using ( var store = new AccountStore ( path )) account = await store . CreateAccountAsync ( "admin'" , "secret" , true );
using ( var store = new AccountStore ( path ))
{
Assert . Null ( await store . AuthenticateAsync ( "admin'" , "wrong" ));
Assert . Null ( await store . AuthenticateAsync ( "missing" , "secret" ));
using var connection = new SqliteConnection ( new SqliteConnectionStringBuilder { DataSource = path , Pooling = false }. ToString ());
connection . Open ();
using var command = connection . CreateCommand ();
command . CommandText = "SELECT last_login FROM accounts WHERE id=$id" ;
command . Parameters . AddWithValue ( "$id" , account . Id );
Assert . Equal ( 0L , command . ExecuteScalar ());
Account authenticated = Assert . IsType < Account >( await store . AuthenticateAsync ( "admin'" , "secret" ));
Assert . Equal ( account . Id , authenticated . Id );
Assert . True ( authenticated . IsAdmin );
Assert . True ( authenticated . LastLogin > 0 );
}
}
finally { Directory . Delete ( directory , true ); }
}
}