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(() => 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(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(); } } } 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(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); } } }