using System.Diagnostics; using System.Text.Json; using Cloud; using Cloud.Models; using DatabaseTest.Models; using Microsoft.EntityFrameworkCore; using Model; namespace DatabaseTest.Services; public interface IDatabaseService { Task CheckHealthAsync(); Task GetMigrationStatusAsync(); Task ApplyMigrationsAsync(); Task EnsureDatabaseCreatedAsync(); Task ResetDatabaseAsync(); Task SeedSampleDataAsync(); // Card Notes Task> GetCardNotesAsync(string? search = null); Task GetCardNoteAsync(string cardName); Task SaveCardNoteAsync(CardNote note, bool isNew); Task DeleteCardNoteAsync(string cardName); // User Decks Task> GetUserDecksAsync(string? search = null); Task GetUserDeckAsync(Guid id); Task SaveUserDeckAsync(UserDeck deck, bool isNew); Task DeleteUserDeckAsync(Guid id); // Passkey Credentials Task> GetPasskeysAsync(string? search = null); Task GetPasskeyAsync(int id); Task SavePasskeyAsync(PasskeyCredential passkey, bool isNew); Task DeletePasskeyAsync(int id); // Automated Test Suite Task> RunAllTestsAsync(Action? progressCallback = null); } public class DatabaseService : IDatabaseService { private readonly IDatabaseConfigService _configService; private readonly ILogger _logger; public DatabaseService(IDatabaseConfigService configService, ILogger logger) { _configService = configService; _logger = logger; } public async Task CheckHealthAsync() { var status = new DatabaseHealthStatus { ProviderType = _configService.ProviderType, ProviderName = _configService.ProviderType.ToString(), ConnectionString = _configService.ConnectionString, CheckedAt = DateTime.UtcNow }; var sw = Stopwatch.StartNew(); try { await using var db = _configService.CreateDbContext(); var canConnect = await db.Database.CanConnectAsync(); sw.Stop(); status.ResponseTimeMs = sw.ElapsedMilliseconds; status.IsConnected = canConnect; if (canConnect) { if (_configService.ProviderType == DatabaseProviderType.PostgreSQL) { var pending = await db.Database.GetPendingMigrationsAsync(); var applied = await db.Database.GetAppliedMigrationsAsync(); status.PendingMigrationsCount = pending.Count(); status.AppliedMigrationsCount = applied.Count(); } try { status.CardNotesCount = await db.CardNotes.CountAsync(); status.UserDecksCount = await db.UserDecks.CountAsync(); status.PasskeysCount = await db.PasskeyCredentials.CountAsync(); } catch (Exception ex) { status.ErrorMessage = $"Connected, but table count failed (migrations may be pending): {ex.Message}"; } } else { status.ErrorMessage = "Unable to connect to database. Verify connection string and server status."; } } catch (Exception ex) { sw.Stop(); status.ResponseTimeMs = sw.ElapsedMilliseconds; status.IsConnected = false; status.ErrorMessage = ex.Message; } return status; } public async Task GetMigrationStatusAsync() { var report = new MigrationReport { ProviderType = _configService.ProviderType }; if (_configService.ProviderType != DatabaseProviderType.PostgreSQL) { report.SupportsMigrations = false; return report; } report.SupportsMigrations = true; try { await using var db = _configService.CreateDbContext(); report.AppliedMigrations = (await db.Database.GetAppliedMigrationsAsync()).ToList(); report.PendingMigrations = (await db.Database.GetPendingMigrationsAsync()).ToList(); } catch (Exception ex) { report.Error = ex.Message; } return report; } public async Task ApplyMigrationsAsync() { try { await using var db = _configService.CreateDbContext(); if (_configService.ProviderType == DatabaseProviderType.PostgreSQL) { var pending = (await db.Database.GetPendingMigrationsAsync()).ToList(); if (!pending.Any()) { return OperationResult.Ok("Database schema is already up-to-date. No pending migrations."); } await db.Database.MigrateAsync(); return OperationResult.Ok($"Successfully applied {pending.Count} migration(s): {string.Join(", ", pending)}"); } else { await db.Database.EnsureCreatedAsync(); return OperationResult.Ok("In-memory database schema ensured."); } } catch (Exception ex) { _logger.LogError(ex, "Failed to apply migrations."); return OperationResult.Fail("Failed to apply migrations: " + ex.Message, ex.ToString()); } } public async Task EnsureDatabaseCreatedAsync() { try { await using var db = _configService.CreateDbContext(); var created = await db.Database.EnsureCreatedAsync(); return OperationResult.Ok(created ? "Database created successfully." : "Database already exists."); } catch (Exception ex) { _logger.LogError(ex, "Failed to ensure database creation."); return OperationResult.Fail("Failed to ensure database created: " + ex.Message, ex.ToString()); } } public async Task ResetDatabaseAsync() { try { await using var db = _configService.CreateDbContext(); if (_configService.ProviderType == DatabaseProviderType.PostgreSQL) { // Delete rows from all tables db.CardNotes.RemoveRange(db.CardNotes); db.UserDecks.RemoveRange(db.UserDecks); db.PasskeyCredentials.RemoveRange(db.PasskeyCredentials); await db.SaveChangesAsync(); return OperationResult.Ok("All database tables cleared successfully."); } else { await db.Database.EnsureDeletedAsync(); await db.Database.EnsureCreatedAsync(); return OperationResult.Ok("In-memory database wiped and recreated successfully."); } } catch (Exception ex) { _logger.LogError(ex, "Failed to reset database."); return OperationResult.Fail("Failed to reset database: " + ex.Message, ex.ToString()); } } public async Task SeedSampleDataAsync() { var result = new SeedResult(); try { await using var db = _configService.CreateDbContext(); if (_configService.ProviderType == DatabaseProviderType.PostgreSQL) { await db.Database.MigrateAsync(); } else { await db.Database.EnsureCreatedAsync(); } // 1. Seed Card Notes var existingNoteNames = (await db.CardNotes.Select(n => n.CardName).ToListAsync()).ToHashSet(); var sampleNotes = new List { new() { CardName = "Kaelen, Arcane Weaver", Note = "Key combo enabler for control decks. High priority removal target." }, new() { CardName = "Valkyrie Ascendant", Note = "Strong mid-game aerial presence. Synergizes well with Divine Shield buffs." }, new() { CardName = "Chronos, Time Shifter", Note = "Turn rewind capability allows recovering from early tempo loss." }, new() { CardName = "Cyber Samurai", Note = "Fast aggro card with stealth. Great opener." }, new() { CardName = "Temporal Rift", Note = "Essential spell for board reset in late game." } }; foreach (var note in sampleNotes.Where(n => !existingNoteNames.Contains(n.CardName))) { db.CardNotes.Add(note); result.CardNotesSeeded++; } // 2. Seed User Decks var existingDeckNames = (await db.UserDecks.Select(d => d.Name).ToListAsync()).ToHashSet(); var sampleDecks = new List { new() { Id = Guid.NewGuid(), Name = "Chrono Temporal Control", Notes = "Built around time-shift mechanics and spell stalling.", Season = "Season 1", UpdatedAt = DateTime.UtcNow, Cards = new List { "Chronos, Time Shifter", "Chronos, Time Shifter", "Temporal Rift", "Temporal Rift", "Temporal Rift", "Kaelen, Arcane Weaver", "Kaelen, Arcane Weaver" }, Divers = new List { "Chronos, Time Shifter" } }, new() { Id = Guid.NewGuid(), Name = "Valkyrie Aggro Burst", Notes = "Fast tempo rush deck targeting rapid face damage.", Season = "Season 1", UpdatedAt = DateTime.UtcNow, Cards = new List { "Valkyrie Ascendant", "Valkyrie Ascendant", "Valkyrie Ascendant", "Cyber Samurai", "Cyber Samurai", "Cyber Samurai" }, Divers = new List() } }; foreach (var deck in sampleDecks.Where(d => !existingDeckNames.Contains(d.Name))) { db.UserDecks.Add(deck); result.UserDecksSeeded++; } // 3. Seed Passkey Credentials var existingPasskeyCount = await db.PasskeyCredentials.CountAsync(); if (existingPasskeyCount == 0) { var samplePasskeys = new List { new() { CredentialId = new byte[] { 0xAA, 0xBB, 0xCC, 0xDD, 0x01, 0x02, 0x03, 0x04, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80 }, PublicKey = new byte[] { 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07 }, UserHandle = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04 }, AaGuid = Guid.NewGuid().ToString(), SignCount = 42 }, new() { CredentialId = new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00 }, PublicKey = new byte[] { 0x04, 0x1F, 0x2E, 0x3D, 0x4C, 0x5B, 0x6A, 0x79, 0x88, 0x97, 0xA6, 0xB5, 0xC4, 0xD3, 0xE2, 0xF1 }, UserHandle = new byte[] { 0xCA, 0xFE, 0xBA, 0xBE, 0x05, 0x06, 0x07, 0x08 }, AaGuid = Guid.NewGuid().ToString(), SignCount = 100 } }; db.PasskeyCredentials.AddRange(samplePasskeys); result.PasskeysSeeded += samplePasskeys.Count; } await db.SaveChangesAsync(); result.Success = true; result.Message = $"Sample data seeded: {result.CardNotesSeeded} notes, {result.UserDecksSeeded} decks, {result.PasskeysSeeded} passkeys."; } catch (Exception ex) { _logger.LogError(ex, "Failed to seed sample data."); result.Success = false; result.Message = "Failed to seed data: " + ex.Message; } return result; } // ── Card Notes CRUD ──────────────────────────────────────────────────────── public async Task> GetCardNotesAsync(string? search = null) { await using var db = _configService.CreateDbContext(); var query = db.CardNotes.AsNoTracking().AsQueryable(); if (!string.IsNullOrWhiteSpace(search)) { var s = search.Trim().ToLower(); query = query.Where(n => n.CardName.ToLower().Contains(s) || n.Note.ToLower().Contains(s)); } return await query.OrderBy(n => n.CardName).ToListAsync(); } public async Task GetCardNoteAsync(string cardName) { await using var db = _configService.CreateDbContext(); return await db.CardNotes.AsNoTracking().FirstOrDefaultAsync(n => n.CardName == cardName); } public async Task SaveCardNoteAsync(CardNote note, bool isNew) { if (string.IsNullOrWhiteSpace(note.CardName)) return OperationResult.Fail("Card name is required."); try { await using var db = _configService.CreateDbContext(); if (isNew) { var exists = await db.CardNotes.AnyAsync(n => n.CardName == note.CardName); if (exists) return OperationResult.Fail($"A note for card '{note.CardName}' already exists."); db.CardNotes.Add(note); await db.SaveChangesAsync(); return OperationResult.Ok($"Created note for '{note.CardName}'."); } else { var existing = await db.CardNotes.FirstOrDefaultAsync(n => n.CardName == note.CardName); if (existing == null) return OperationResult.Fail($"Note for card '{note.CardName}' was not found."); existing.Note = note.Note; await db.SaveChangesAsync(); return OperationResult.Ok($"Updated note for '{note.CardName}'."); } } catch (Exception ex) { _logger.LogError(ex, "Failed to save card note {CardName}", note.CardName); return OperationResult.Fail($"Error saving card note: {ex.Message}", ex.ToString()); } } public async Task DeleteCardNoteAsync(string cardName) { try { await using var db = _configService.CreateDbContext(); var existing = await db.CardNotes.FirstOrDefaultAsync(n => n.CardName == cardName); if (existing == null) return OperationResult.Fail($"Note for '{cardName}' not found."); db.CardNotes.Remove(existing); await db.SaveChangesAsync(); return OperationResult.Ok($"Deleted note for '{cardName}'."); } catch (Exception ex) { _logger.LogError(ex, "Failed to delete card note {CardName}", cardName); return OperationResult.Fail($"Error deleting note: {ex.Message}", ex.ToString()); } } // ── User Decks CRUD ──────────────────────────────────────────────────────── public async Task> GetUserDecksAsync(string? search = null) { await using var db = _configService.CreateDbContext(); var query = db.UserDecks.AsNoTracking().AsQueryable(); if (!string.IsNullOrWhiteSpace(search)) { var s = search.Trim().ToLower(); query = query.Where(d => d.Name.ToLower().Contains(s) || (d.Notes != null && d.Notes.ToLower().Contains(s)) || (d.Season != null && d.Season.ToLower().Contains(s))); } return await query.OrderByDescending(d => d.UpdatedAt).ToListAsync(); } public async Task GetUserDeckAsync(Guid id) { await using var db = _configService.CreateDbContext(); return await db.UserDecks.AsNoTracking().FirstOrDefaultAsync(d => d.Id == id); } public async Task SaveUserDeckAsync(UserDeck deck, bool isNew) { if (string.IsNullOrWhiteSpace(deck.Name)) return OperationResult.Fail("Deck name is required."); try { await using var db = _configService.CreateDbContext(); deck.UpdatedAt = DateTime.UtcNow; if (isNew) { if (deck.Id == Guid.Empty) deck.Id = Guid.NewGuid(); db.UserDecks.Add(deck); await db.SaveChangesAsync(); return OperationResult.Ok($"Created deck '{deck.Name}'."); } else { var existing = await db.UserDecks.FirstOrDefaultAsync(d => d.Id == deck.Id); if (existing == null) return OperationResult.Fail($"Deck with ID {deck.Id} was not found."); existing.Name = deck.Name; existing.Notes = deck.Notes; existing.Season = deck.Season; existing.Cards = deck.Cards ?? []; existing.Divers = deck.Divers ?? []; existing.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(); return OperationResult.Ok($"Updated deck '{deck.Name}'."); } } catch (Exception ex) { _logger.LogError(ex, "Failed to save deck {DeckName}", deck.Name); return OperationResult.Fail($"Error saving deck: {ex.Message}", ex.ToString()); } } public async Task DeleteUserDeckAsync(Guid id) { try { await using var db = _configService.CreateDbContext(); var existing = await db.UserDecks.FirstOrDefaultAsync(d => d.Id == id); if (existing == null) return OperationResult.Fail($"Deck with ID {id} was not found."); var name = existing.Name; db.UserDecks.Remove(existing); await db.SaveChangesAsync(); return OperationResult.Ok($"Deleted deck '{name}'."); } catch (Exception ex) { _logger.LogError(ex, "Failed to delete deck {DeckId}", id); return OperationResult.Fail($"Error deleting deck: {ex.Message}", ex.ToString()); } } // ── Passkey Credentials CRUD ─────────────────────────────────────────────── public async Task> GetPasskeysAsync(string? search = null) { await using var db = _configService.CreateDbContext(); var query = db.PasskeyCredentials.AsNoTracking().AsQueryable(); if (!string.IsNullOrWhiteSpace(search)) { var s = search.Trim().ToLower(); query = query.Where(p => (p.AaGuid != null && p.AaGuid.ToLower().Contains(s)) || p.Id.ToString().Contains(s) || p.SignCount.ToString().Contains(s)); } return await query.OrderBy(p => p.Id).ToListAsync(); } public async Task GetPasskeyAsync(int id) { await using var db = _configService.CreateDbContext(); return await db.PasskeyCredentials.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id); } public async Task SavePasskeyAsync(PasskeyCredential passkey, bool isNew) { if (passkey.CredentialId == null || passkey.CredentialId.Length == 0) return OperationResult.Fail("Credential ID bytes are required."); if (passkey.PublicKey == null || passkey.PublicKey.Length == 0) return OperationResult.Fail("Public Key bytes are required."); if (passkey.UserHandle == null || passkey.UserHandle.Length == 0) return OperationResult.Fail("User Handle bytes are required."); try { await using var db = _configService.CreateDbContext(); if (isNew) { db.PasskeyCredentials.Add(passkey); await db.SaveChangesAsync(); return OperationResult.Ok($"Created Passkey credential #{passkey.Id}."); } else { var existing = await db.PasskeyCredentials.FirstOrDefaultAsync(p => p.Id == passkey.Id); if (existing == null) return OperationResult.Fail($"Passkey with ID {passkey.Id} was not found."); existing.CredentialId = passkey.CredentialId; existing.PublicKey = passkey.PublicKey; existing.UserHandle = passkey.UserHandle; existing.AaGuid = passkey.AaGuid; existing.SignCount = passkey.SignCount; await db.SaveChangesAsync(); return OperationResult.Ok($"Updated Passkey credential #{passkey.Id}."); } } catch (Exception ex) { _logger.LogError(ex, "Failed to save passkey credential {Id}", passkey.Id); return OperationResult.Fail($"Error saving passkey: {ex.Message}", ex.ToString()); } } public async Task DeletePasskeyAsync(int id) { try { await using var db = _configService.CreateDbContext(); var existing = await db.PasskeyCredentials.FirstOrDefaultAsync(p => p.Id == id); if (existing == null) return OperationResult.Fail($"Passkey with ID {id} was not found."); db.PasskeyCredentials.Remove(existing); await db.SaveChangesAsync(); return OperationResult.Ok($"Deleted Passkey credential #{id}."); } catch (Exception ex) { _logger.LogError(ex, "Failed to delete passkey credential {Id}", id); return OperationResult.Fail($"Error deleting passkey: {ex.Message}", ex.ToString()); } } // ── Automated Test Suite Runner ─────────────────────────────────────────── public async Task> RunAllTestsAsync(Action? progressCallback = null) { var results = new List(); // 1. Connection Test results.Add(await RunTestCaseAsync("DB-01", "Database Connectivity", "Connection", "Validates socket/connection pool access to the target database server.", async test => { test.AddLog($"Connecting to {_configService.GetActiveProviderDescription()}..."); await using var db = _configService.CreateDbContext(); var canConnect = await db.Database.CanConnectAsync(); if (!canConnect) throw new InvalidOperationException("Failed to establish a connection to the database."); test.AddLog("Database connection established successfully."); }, progressCallback)); // 2. Migrations & Schema Check results.Add(await RunTestCaseAsync("DB-02", "Schema & Migrations", "Schema", "Verifies applied EF Core migrations and checks that all tables exist.", async test => { await using var db = _configService.CreateDbContext(); if (_configService.ProviderType == DatabaseProviderType.PostgreSQL) { test.AddLog("Querying EF Core migration history..."); var applied = (await db.Database.GetAppliedMigrationsAsync()).ToList(); test.AddLog($"Applied migrations count: {applied.Count}"); foreach (var m in applied) test.AddLog($" - Applied: {m}"); var pending = (await db.Database.GetPendingMigrationsAsync()).ToList(); if (pending.Any()) { test.AddLog($"Warning: {pending.Count} pending migrations detected. Applying them now..."); await db.Database.MigrateAsync(); test.AddLog("Pending migrations applied successfully."); } else { test.AddLog("No pending migrations. Schema is up to date."); } } else { test.AddLog("In-memory provider: Ensuring database schema is created..."); await db.Database.EnsureCreatedAsync(); test.AddLog("In-memory schema verified."); } }, progressCallback)); // 3. CardNote CRUD Test results.Add(await RunTestCaseAsync("DB-03", "CardNote Full CRUD Lifecycle", "CardNotes", "Tests Insert, Query, Update, and Delete operations on the CardNotes entity.", async test => { var testCardName = $"__Test_Card_{Guid.NewGuid():N}"; var initialNote = "Initial test note description"; var updatedNote = "Updated test note content with extra details"; await using (var db = _configService.CreateDbContext()) { test.AddLog($"1. Creating test CardNote for '{testCardName}'..."); db.CardNotes.Add(new CardNote { CardName = testCardName, Note = initialNote }); await db.SaveChangesAsync(); test.AddLog("CardNote inserted successfully."); } await using (var db = _configService.CreateDbContext()) { test.AddLog("2. Querying inserted CardNote from database..."); var fetched = await db.CardNotes.FirstOrDefaultAsync(n => n.CardName == testCardName); if (fetched == null || fetched.Note != initialNote) throw new Exception($"Expected note '{initialNote}', but retrieved '{fetched?.Note}'"); test.AddLog("CardNote retrieved and validated."); test.AddLog("3. Updating CardNote text..."); fetched.Note = updatedNote; await db.SaveChangesAsync(); test.AddLog("CardNote updated in database."); } await using (var db = _configService.CreateDbContext()) { test.AddLog("4. Verifying persisted updated note..."); var fetched = await db.CardNotes.FirstOrDefaultAsync(n => n.CardName == testCardName); if (fetched == null || fetched.Note != updatedNote) throw new Exception($"Expected updated note '{updatedNote}', but found '{fetched?.Note}'"); test.AddLog("Update persistence verified."); test.AddLog("5. Deleting test CardNote..."); db.CardNotes.Remove(fetched); await db.SaveChangesAsync(); test.AddLog("CardNote deleted."); } await using (var db = _configService.CreateDbContext()) { test.AddLog("6. Verifying CardNote is deleted..."); var exists = await db.CardNotes.AnyAsync(n => n.CardName == testCardName); if (exists) throw new Exception("CardNote still exists after deletion."); test.AddLog("CardNote deletion verified."); } }, progressCallback)); // 4. UserDeck CRUD & JSONB Serialization Test results.Add(await RunTestCaseAsync("DB-04", "UserDeck CRUD & JSONB Mapping", "UserDecks", "Validates JSON column mapping for Cards and Divers collections in PostgreSQL/EF Core.", async test => { var testDeckId = Guid.NewGuid(); var testDeckName = $"Test Deck {Guid.NewGuid():N}"; var initialCards = new List { "Agent Alpha", "Agent Alpha", "Agent Alpha", "Spell Omega", "Spell Omega" }; var initialDivers = new List { "Agent Alpha" }; await using (var db = _configService.CreateDbContext()) { test.AddLog($"1. Inserting UserDeck '{testDeckName}' with {initialCards.Count} cards and {initialDivers.Count} divers..."); var deck = new UserDeck { Id = testDeckId, Name = testDeckName, Notes = "Automated test deck notes", Season = "Test Season 1", UpdatedAt = DateTime.UtcNow, Cards = initialCards, Divers = initialDivers }; db.UserDecks.Add(deck); await db.SaveChangesAsync(); test.AddLog("UserDeck saved with JSON structures."); } await using (var db = _configService.CreateDbContext()) { test.AddLog("2. Querying UserDeck and checking deserialized JSON lists..."); var fetched = await db.UserDecks.FirstOrDefaultAsync(d => d.Id == testDeckId); if (fetched == null) throw new Exception("UserDeck not found after insertion."); if (fetched.Cards == null || fetched.Cards.Count != 5) throw new Exception($"Expected 5 cards in deck, found {fetched.Cards?.Count ?? 0}"); if (fetched.Divers == null || fetched.Divers.Count != 1 || fetched.Divers[0] != "Agent Alpha") throw new Exception($"Diver entry deserialization mismatch."); test.AddLog("JSON properties successfully verified."); test.AddLog("3. Modifying cards list in UserDeck..."); fetched.Cards.Add("Added Card Gamma"); fetched.Divers.Add("Added Diver"); fetched.Name = testDeckName + " (Updated)"; await db.SaveChangesAsync(); test.AddLog("Updated deck with additional cards and divers."); } await using (var db = _configService.CreateDbContext()) { test.AddLog("4. Validating updated cards and divers..."); var fetched = await db.UserDecks.FirstOrDefaultAsync(d => d.Id == testDeckId); if (fetched == null || fetched.Cards?.Count != 6 || fetched.Divers?.Count != 2) throw new Exception("Updated JSON collections did not persist correctly."); test.AddLog("Updated JSON verified."); test.AddLog("5. Deleting UserDeck..."); db.UserDecks.Remove(fetched); await db.SaveChangesAsync(); test.AddLog("UserDeck deleted."); } await using (var db = _configService.CreateDbContext()) { var exists = await db.UserDecks.AnyAsync(d => d.Id == testDeckId); if (exists) throw new Exception("UserDeck still exists after deletion."); test.AddLog("UserDeck deletion confirmed."); } }, progressCallback)); // 5. PasskeyCredential Binary Handling Test results.Add(await RunTestCaseAsync("DB-05", "PasskeyCredential Binary Storage", "Passkeys", "Validates byte array (byte[]) handling for CredentialId, PublicKey, and UserHandle.", async test => { var credId = new byte[] { 0x01, 0x02, 0x03, 0x04, 0xAA, 0xBB, 0xCC, 0xDD, 0x10, 0x20, 0x30, 0x40 }; var pubKey = new byte[] { 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86 }; var userHandle = new byte[] { 0xFE, 0xDC, 0xBA, 0x98 }; var aaGuid = Guid.NewGuid().ToString(); long signCount = 100; int savedId = 0; await using (var db = _configService.CreateDbContext()) { test.AddLog("1. Creating PasskeyCredential with raw byte arrays..."); var passkey = new PasskeyCredential { CredentialId = credId, PublicKey = pubKey, UserHandle = userHandle, AaGuid = aaGuid, SignCount = signCount }; db.PasskeyCredentials.Add(passkey); await db.SaveChangesAsync(); savedId = passkey.Id; test.AddLog($"Passkey stored with generated ID: {savedId}"); } await using (var db = _configService.CreateDbContext()) { test.AddLog("2. Querying Passkey and verifying byte-for-byte exact equality..."); var fetched = await db.PasskeyCredentials.FirstOrDefaultAsync(p => p.Id == savedId); if (fetched == null) throw new Exception("Passkey not found after insertion."); if (!fetched.CredentialId.SequenceEqual(credId)) throw new Exception("CredentialId byte array does not match original bytes."); if (!fetched.PublicKey.SequenceEqual(pubKey)) throw new Exception("PublicKey byte array does not match original bytes."); if (!fetched.UserHandle.SequenceEqual(userHandle)) throw new Exception("UserHandle byte array does not match original bytes."); if (fetched.SignCount != signCount) throw new Exception($"SignCount mismatch: expected {signCount}, got {fetched.SignCount}"); test.AddLog("Binary fields verified successfully."); test.AddLog("3. Incrementing SignCount..."); fetched.SignCount += 1; await db.SaveChangesAsync(); test.AddLog($"Updated SignCount to {fetched.SignCount}."); } await using (var db = _configService.CreateDbContext()) { var fetched = await db.PasskeyCredentials.FirstOrDefaultAsync(p => p.Id == savedId); if (fetched == null || fetched.SignCount != 101) throw new Exception("SignCount increment was not persisted."); test.AddLog("4. Deleting Passkey..."); db.PasskeyCredentials.Remove(fetched); await db.SaveChangesAsync(); test.AddLog("Passkey deleted."); } await using (var db = _configService.CreateDbContext()) { var exists = await db.PasskeyCredentials.AnyAsync(p => p.Id == savedId); if (exists) throw new Exception("Passkey still exists after deletion."); test.AddLog("Passkey deletion confirmed."); } }, progressCallback)); // 6. Primary Key & Duplicate Key Constraint Test results.Add(await RunTestCaseAsync("DB-06", "Primary Key & Duplicate Constraints", "Integrity", "Verifies that attempting to insert a duplicate primary key throws DbUpdateException.", async test => { var duplicateKey = $"__DupTest_{Guid.NewGuid():N}"; await using (var db = _configService.CreateDbContext()) { test.AddLog($"1. Inserting initial record with key '{duplicateKey}'..."); db.CardNotes.Add(new CardNote { CardName = duplicateKey, Note = "First note" }); await db.SaveChangesAsync(); test.AddLog("Initial record created."); } try { await using (var db = _configService.CreateDbContext()) { test.AddLog("2. Attempting to insert duplicate record with same primary key..."); db.CardNotes.Add(new CardNote { CardName = duplicateKey, Note = "Second duplicate note" }); await db.SaveChangesAsync(); } throw new Exception("Expected exception on duplicate primary key, but operation succeeded!"); } catch (DbUpdateException dbEx) { test.AddLog($"Expected DbUpdateException caught: {dbEx.Message}"); } catch (ArgumentException argEx) { test.AddLog($"Expected ArgumentException caught: {argEx.Message}"); } catch (InvalidOperationException invEx) { test.AddLog($"Expected key constraint exception caught: {invEx.Message}"); } finally { // Cleanup await using var db = _configService.CreateDbContext(); var toClean = await db.CardNotes.FirstOrDefaultAsync(n => n.CardName == duplicateKey); if (toClean != null) { db.CardNotes.Remove(toClean); await db.SaveChangesAsync(); test.AddLog("Cleaned up test record."); } } }, progressCallback)); // 7. Transaction Rollback Verification results.Add(await RunTestCaseAsync("DB-07", "Transaction Rollback Support", "Transactions", "Verifies that uncommitted transactions roll back changes cleanly on failure.", async test => { if (_configService.ProviderType != DatabaseProviderType.PostgreSQL) { test.AddLog("Transaction testing requires relational provider (PostgreSQL). Skipping for In-Memory."); return; } var transCard = $"__Trans_{Guid.NewGuid():N}"; await using (var db = _configService.CreateDbContext()) { test.AddLog("1. Beginning transaction..."); await using var transaction = await db.Database.BeginTransactionAsync(); test.AddLog($"2. Inserting card '{transCard}' inside transaction..."); db.CardNotes.Add(new CardNote { CardName = transCard, Note = "Transaction Note" }); await db.SaveChangesAsync(); test.AddLog("3. Rolling back transaction explicitly without committing..."); await transaction.RollbackAsync(); test.AddLog("Transaction rolled back."); } await using (var db = _configService.CreateDbContext()) { test.AddLog("4. Verifying that the rolled back card note was NOT persisted..."); var exists = await db.CardNotes.AnyAsync(n => n.CardName == transCard); if (exists) throw new Exception("Record was persisted despite transaction rollback!"); test.AddLog("Rollback verified: record does not exist in database."); } }, progressCallback)); return results; } private static async Task RunTestCaseAsync( string id, string name, string category, string description, Func testAction, Action? progressCallback) { var test = new DatabaseTestCaseResult { Id = id, Name = name, Category = category, Description = description, Status = TestStatus.Running }; progressCallback?.Invoke($"Running {name}..."); var sw = Stopwatch.StartNew(); try { await testAction(test); sw.Stop(); test.DurationMs = sw.ElapsedMilliseconds; test.Status = TestStatus.Passed; test.AddLog($"Test PASSED in {test.DurationMs}ms."); progressCallback?.Invoke($"Passed {name} ({test.DurationMs}ms)"); } catch (Exception ex) { sw.Stop(); test.DurationMs = sw.ElapsedMilliseconds; test.Status = TestStatus.Failed; test.ErrorMessage = ex.Message; test.StackTrace = ex.StackTrace; test.AddLog($"Test FAILED: {ex.Message}"); progressCallback?.Invoke($"Failed {name}: {ex.Message}"); } return test; } }