1177 lines
50 KiB
C#
1177 lines
50 KiB
C#
using System.Diagnostics;
|
|
using Cloud.Models;
|
|
using DatabaseTest.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Model;
|
|
|
|
namespace DatabaseTest.Services;
|
|
|
|
public interface IDatabaseService
|
|
{
|
|
Task<DatabaseHealthStatus> CheckHealthAsync();
|
|
Task<MigrationReport> GetMigrationStatusAsync();
|
|
Task<OperationResult> ApplyMigrationsAsync();
|
|
Task<OperationResult> EnsureDatabaseCreatedAsync();
|
|
Task<OperationResult> ResetDatabaseAsync();
|
|
Task<SeedResult> SeedSampleDataAsync();
|
|
|
|
// Users
|
|
Task<List<User>> GetUsersAsync(string? search = null);
|
|
Task<User?> GetUserAsync(string username);
|
|
Task<OperationResult> SaveUserAsync(User user, bool isNew);
|
|
Task<OperationResult> DeleteUserAsync(string username);
|
|
|
|
// Card Notes
|
|
Task<List<CardNote>> GetCardNotesAsync(string? search = null, string? username = null);
|
|
Task<CardNote?> GetCardNoteAsync(string cardName, string username = "");
|
|
Task<OperationResult> SaveCardNoteAsync(CardNote note, bool isNew);
|
|
Task<OperationResult> DeleteCardNoteAsync(string cardName, string username = "");
|
|
|
|
// User Decks
|
|
Task<List<UserDeck>> GetUserDecksAsync(string? search = null);
|
|
Task<UserDeck?> GetUserDeckAsync(Guid id);
|
|
Task<OperationResult> SaveUserDeckAsync(UserDeck deck, bool isNew);
|
|
Task<OperationResult> DeleteUserDeckAsync(Guid id);
|
|
|
|
// Passkey Credentials
|
|
Task<List<PasskeyCredential>> GetPasskeysAsync(string? search = null);
|
|
Task<PasskeyCredential?> GetPasskeyAsync(int id);
|
|
Task<OperationResult> SavePasskeyAsync(PasskeyCredential passkey, bool isNew);
|
|
Task<OperationResult> DeletePasskeyAsync(int id);
|
|
|
|
// Automated Test Suite
|
|
Task<List<DatabaseTestCaseResult>> RunAllTestsAsync(Action<string>? progressCallback = null);
|
|
}
|
|
|
|
public class DatabaseService : IDatabaseService
|
|
{
|
|
private readonly IDatabaseConfigService _configService;
|
|
private readonly ILogger<DatabaseService> _logger;
|
|
|
|
public DatabaseService(IDatabaseConfigService configService, ILogger<DatabaseService> logger)
|
|
{
|
|
_configService = configService;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<DatabaseHealthStatus> 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();
|
|
status.UsersCount = await db.Users.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<MigrationReport> 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<OperationResult> 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)}");
|
|
}
|
|
|
|
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<OperationResult> 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<OperationResult> 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);
|
|
db.Users.RemoveRange(db.Users);
|
|
await db.SaveChangesAsync();
|
|
return OperationResult.Ok("All database tables cleared successfully.");
|
|
}
|
|
|
|
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<SeedResult> 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();
|
|
|
|
// 0. Seed Users
|
|
var existingUsernames = (await db.Users.Select(u => u.Username).ToListAsync()).ToHashSet();
|
|
var sampleUsers = new List<User>
|
|
{
|
|
new() { Username = "admin", Password = "password123", CreatedAt = DateTime.UtcNow },
|
|
new() { Username = "testuser", Password = "password123", CreatedAt = DateTime.UtcNow },
|
|
new() { Username = "player1", Password = "password123", CreatedAt = DateTime.UtcNow }
|
|
};
|
|
|
|
foreach (var user in sampleUsers.Where(u => !existingUsernames.Contains(u.Username)))
|
|
{
|
|
db.Users.Add(user);
|
|
result.UsersSeeded++;
|
|
}
|
|
|
|
// 1. Seed Card Notes
|
|
var existingNotes =
|
|
(await db.CardNotes.Select(n => new { n.Username, n.CardName }).ToListAsync()).ToHashSet();
|
|
var sampleNotes = new List<CardNote>
|
|
{
|
|
new()
|
|
{
|
|
Username = "admin", CardName = "Kaelen, Arcane Weaver",
|
|
Note = "Key combo enabler for control decks. High priority removal target."
|
|
},
|
|
new()
|
|
{
|
|
Username = "admin", CardName = "Valkyrie Ascendant",
|
|
Note = "Strong mid-game aerial presence. Synergizes well with Divine Shield buffs."
|
|
},
|
|
new()
|
|
{
|
|
Username = "testuser", CardName = "Chronos, Time Shifter",
|
|
Note = "Turn rewind capability allows recovering from early tempo loss."
|
|
},
|
|
new()
|
|
{
|
|
Username = "testuser", CardName = "Cyber Samurai",
|
|
Note = "Fast aggro card with stealth. Great opener."
|
|
},
|
|
new()
|
|
{
|
|
Username = "player1", CardName = "Temporal Rift",
|
|
Note = "Essential spell for board reset in late game."
|
|
}
|
|
};
|
|
|
|
foreach (var note in sampleNotes.Where(n => !existingNotes.Contains(new { n.Username, 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<UserDeck>
|
|
{
|
|
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<string>
|
|
{
|
|
"Chronos, Time Shifter",
|
|
"Chronos, Time Shifter",
|
|
"Temporal Rift",
|
|
"Temporal Rift",
|
|
"Temporal Rift",
|
|
"Kaelen, Arcane Weaver",
|
|
"Kaelen, Arcane Weaver"
|
|
},
|
|
Divers = new List<string> { "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<string>
|
|
{
|
|
"Valkyrie Ascendant",
|
|
"Valkyrie Ascendant",
|
|
"Valkyrie Ascendant",
|
|
"Cyber Samurai",
|
|
"Cyber Samurai",
|
|
"Cyber Samurai"
|
|
},
|
|
Divers = new List<string>()
|
|
}
|
|
};
|
|
|
|
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<PasskeyCredential>
|
|
{
|
|
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.UsersSeeded} users, {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;
|
|
}
|
|
|
|
// ── Users CRUD ─────────────────────────────────────────────────────────────
|
|
|
|
public async Task<List<User>> GetUsersAsync(string? search = null)
|
|
{
|
|
await using var db = _configService.CreateDbContext();
|
|
var query = db.Users.AsNoTracking().AsQueryable();
|
|
|
|
if (!string.IsNullOrWhiteSpace(search))
|
|
{
|
|
var s = search.Trim().ToLower();
|
|
query = query.Where(u => u.Username.ToLower().Contains(s));
|
|
}
|
|
|
|
return await query.OrderBy(u => u.Username).ToListAsync();
|
|
}
|
|
|
|
public async Task<User?> GetUserAsync(string username)
|
|
{
|
|
await using var db = _configService.CreateDbContext();
|
|
return await db.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Username == username);
|
|
}
|
|
|
|
public async Task<OperationResult> SaveUserAsync(User user, bool isNew)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(user.Username))
|
|
return OperationResult.Fail("Username is required.");
|
|
if (string.IsNullOrWhiteSpace(user.Password))
|
|
return OperationResult.Fail("Password is required.");
|
|
|
|
try
|
|
{
|
|
await using var db = _configService.CreateDbContext();
|
|
if (isNew)
|
|
{
|
|
var exists = await db.Users.AnyAsync(u => u.Username == user.Username);
|
|
if (exists)
|
|
return OperationResult.Fail($"User '{user.Username}' already exists.");
|
|
|
|
user.CreatedAt = DateTime.UtcNow;
|
|
db.Users.Add(user);
|
|
await db.SaveChangesAsync();
|
|
return OperationResult.Ok($"Created user '{user.Username}'.");
|
|
}
|
|
|
|
var existing = await db.Users.FirstOrDefaultAsync(u => u.Username == user.Username);
|
|
if (existing == null)
|
|
return OperationResult.Fail($"User '{user.Username}' was not found.");
|
|
|
|
existing.Password = user.Password;
|
|
await db.SaveChangesAsync();
|
|
return OperationResult.Ok($"Updated user '{user.Username}'.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to save user {Username}", user.Username);
|
|
return OperationResult.Fail($"Error saving user: {ex.Message}", ex.ToString());
|
|
}
|
|
}
|
|
|
|
public async Task<OperationResult> DeleteUserAsync(string username)
|
|
{
|
|
try
|
|
{
|
|
await using var db = _configService.CreateDbContext();
|
|
var existing = await db.Users.FirstOrDefaultAsync(u => u.Username == username);
|
|
if (existing == null)
|
|
return OperationResult.Fail($"User '{username}' not found.");
|
|
|
|
db.Users.Remove(existing);
|
|
await db.SaveChangesAsync();
|
|
return OperationResult.Ok($"Deleted user '{username}'.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to delete user {Username}", username);
|
|
return OperationResult.Fail($"Error deleting user: {ex.Message}", ex.ToString());
|
|
}
|
|
}
|
|
|
|
// ── Card Notes CRUD ────────────────────────────────────────────────────────
|
|
|
|
public async Task<List<CardNote>> GetCardNotesAsync(string? search = null, string? username = null)
|
|
{
|
|
await using var db = _configService.CreateDbContext();
|
|
var query = db.CardNotes.AsNoTracking().AsQueryable();
|
|
|
|
if (!string.IsNullOrWhiteSpace(username)) query = query.Where(n => n.Username == username);
|
|
|
|
if (!string.IsNullOrWhiteSpace(search))
|
|
{
|
|
var s = search.Trim().ToLower();
|
|
query = query.Where(n =>
|
|
n.CardName.ToLower().Contains(s) || n.Note.ToLower().Contains(s) || n.Username.ToLower().Contains(s));
|
|
}
|
|
|
|
return await query.OrderBy(n => n.CardName).ToListAsync();
|
|
}
|
|
|
|
public async Task<CardNote?> GetCardNoteAsync(string cardName, string username = "")
|
|
{
|
|
await using var db = _configService.CreateDbContext();
|
|
return await db.CardNotes.AsNoTracking()
|
|
.FirstOrDefaultAsync(n => n.CardName == cardName && n.Username == (username ?? ""));
|
|
}
|
|
|
|
public async Task<OperationResult> SaveCardNoteAsync(CardNote note, bool isNew)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(note.CardName))
|
|
return OperationResult.Fail("Card name is required.");
|
|
|
|
note.Username ??= "";
|
|
|
|
try
|
|
{
|
|
await using var db = _configService.CreateDbContext();
|
|
if (isNew)
|
|
{
|
|
var exists =
|
|
await db.CardNotes.AnyAsync(n => n.CardName == note.CardName && n.Username == note.Username);
|
|
if (exists)
|
|
return OperationResult.Fail(
|
|
$"A note for card '{note.CardName}' for user '{note.Username}' already exists.");
|
|
|
|
note.UpdatedAt = DateTime.UtcNow;
|
|
db.CardNotes.Add(note);
|
|
await db.SaveChangesAsync();
|
|
return OperationResult.Ok($"Created note for '{note.CardName}'.");
|
|
}
|
|
|
|
var existing =
|
|
await db.CardNotes.FirstOrDefaultAsync(n => n.CardName == note.CardName && n.Username == note.Username);
|
|
if (existing == null)
|
|
return OperationResult.Fail(
|
|
$"Note for card '{note.CardName}' for user '{note.Username}' was not found.");
|
|
|
|
existing.Note = note.Note;
|
|
existing.UpdatedAt = DateTime.UtcNow;
|
|
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<OperationResult> DeleteCardNoteAsync(string cardName, string username = "")
|
|
{
|
|
try
|
|
{
|
|
await using var db = _configService.CreateDbContext();
|
|
var existing =
|
|
await db.CardNotes.FirstOrDefaultAsync(n => n.CardName == cardName && n.Username == (username ?? ""));
|
|
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<List<UserDeck>> 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<UserDeck?> GetUserDeckAsync(Guid id)
|
|
{
|
|
await using var db = _configService.CreateDbContext();
|
|
return await db.UserDecks.AsNoTracking().FirstOrDefaultAsync(d => d.Id == id);
|
|
}
|
|
|
|
public async Task<OperationResult> 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}'.");
|
|
}
|
|
|
|
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<OperationResult> 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<List<PasskeyCredential>> 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<PasskeyCredential?> GetPasskeyAsync(int id)
|
|
{
|
|
await using var db = _configService.CreateDbContext();
|
|
return await db.PasskeyCredentials.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id);
|
|
}
|
|
|
|
public async Task<OperationResult> 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}.");
|
|
}
|
|
|
|
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<OperationResult> 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<List<DatabaseTestCaseResult>> RunAllTestsAsync(Action<string>? progressCallback = null)
|
|
{
|
|
var results = new List<DatabaseTestCaseResult>();
|
|
|
|
// 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 testUsername = "testuser";
|
|
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}' (user: '{testUsername}')...");
|
|
db.CardNotes.Add(new CardNote
|
|
{ Username = testUsername, 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 && n.Username == testUsername);
|
|
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 && n.Username == testUsername);
|
|
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 && n.Username == testUsername);
|
|
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<string>
|
|
{
|
|
"Agent Alpha",
|
|
"Agent Alpha",
|
|
"Agent Alpha",
|
|
"Spell Omega",
|
|
"Spell Omega"
|
|
};
|
|
var initialDivers = new List<string> { "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;
|
|
var 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}";
|
|
var testUser = "testuser";
|
|
await using (var db = _configService.CreateDbContext())
|
|
{
|
|
test.AddLog($"1. Inserting initial record with key '{duplicateKey}'...");
|
|
db.CardNotes.Add(new CardNote
|
|
{ Username = testUser, 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
|
|
{ Username = testUser, 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 && n.Username == testUser);
|
|
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}";
|
|
var testUser = "testuser";
|
|
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
|
|
{ Username = testUser, 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 && n.Username == testUser);
|
|
if (exists)
|
|
throw new Exception("Record was persisted despite transaction rollback!");
|
|
test.AddLog("Rollback verified: record does not exist in database.");
|
|
}
|
|
}, progressCallback));
|
|
|
|
// 8. User Management CRUD Test
|
|
results.Add(await RunTestCaseAsync("DB-08", "User Entity CRUD & Querying", "Users",
|
|
"Tests creating, retrieving, updating password, and deleting User records.",
|
|
async test =>
|
|
{
|
|
var testUsername = $"user_{Guid.NewGuid():N}";
|
|
var initialPassword = "initialPassword123";
|
|
var updatedPassword = "newSecurePassword456";
|
|
|
|
await using (var db = _configService.CreateDbContext())
|
|
{
|
|
test.AddLog($"1. Creating user '{testUsername}'...");
|
|
db.Users.Add(new User
|
|
{ Username = testUsername, Password = initialPassword, CreatedAt = DateTime.UtcNow });
|
|
await db.SaveChangesAsync();
|
|
test.AddLog("User created.");
|
|
}
|
|
|
|
await using (var db = _configService.CreateDbContext())
|
|
{
|
|
test.AddLog($"2. Querying user '{testUsername}'...");
|
|
var user = await db.Users.FirstOrDefaultAsync(u => u.Username == testUsername);
|
|
if (user == null || user.Password != initialPassword)
|
|
throw new Exception("User not found or password mismatch.");
|
|
test.AddLog("User query verified.");
|
|
|
|
test.AddLog("3. Updating user password...");
|
|
user.Password = updatedPassword;
|
|
await db.SaveChangesAsync();
|
|
test.AddLog("User password updated.");
|
|
}
|
|
|
|
await using (var db = _configService.CreateDbContext())
|
|
{
|
|
test.AddLog("4. Verifying updated password...");
|
|
var user = await db.Users.FirstOrDefaultAsync(u => u.Username == testUsername);
|
|
if (user == null || user.Password != updatedPassword)
|
|
throw new Exception("Updated password did not persist.");
|
|
test.AddLog("Password update verified.");
|
|
|
|
test.AddLog("5. Deleting test user...");
|
|
db.Users.Remove(user);
|
|
await db.SaveChangesAsync();
|
|
test.AddLog("User deleted.");
|
|
}
|
|
|
|
await using (var db = _configService.CreateDbContext())
|
|
{
|
|
var exists = await db.Users.AnyAsync(u => u.Username == testUsername);
|
|
if (exists) throw new Exception("User still exists after deletion.");
|
|
test.AddLog("User deletion confirmed.");
|
|
}
|
|
}, progressCallback));
|
|
|
|
return results;
|
|
}
|
|
|
|
private static async Task<DatabaseTestCaseResult> RunTestCaseAsync(
|
|
string id,
|
|
string name,
|
|
string category,
|
|
string description,
|
|
Func<DatabaseTestCaseResult, Task> testAction,
|
|
Action<string>? 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;
|
|
}
|
|
} |