This commit is contained in:
6d486f49
2026-08-17 00:45:08 -04:00
parent 3a2f112a73
commit 1d6207fbfe
154 changed files with 126007 additions and 47385 deletions
+316 -111
View File
@@ -1,6 +1,4 @@
using System.Diagnostics;
using System.Text.Json;
using Cloud;
using Cloud.Models;
using DatabaseTest.Models;
using Microsoft.EntityFrameworkCore;
@@ -17,11 +15,17 @@ public interface IDatabaseService
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);
Task<CardNote?> GetCardNoteAsync(string cardName);
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);
Task<OperationResult> DeleteCardNoteAsync(string cardName, string username = "");
// User Decks
Task<List<UserDeck>> GetUserDecksAsync(string? search = null);
@@ -84,10 +88,12 @@ public class DatabaseService : IDatabaseService
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}";
status.ErrorMessage =
$"Connected, but table count failed (migrations may be pending): {ex.Message}";
}
}
else
@@ -143,18 +149,15 @@ public class DatabaseService : IDatabaseService
{
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.");
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)
{
@@ -189,15 +192,14 @@ public class DatabaseService : IDatabaseService
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.");
}
else
{
await db.Database.EnsureDeletedAsync();
await db.Database.EnsureCreatedAsync();
return OperationResult.Ok("In-memory database wiped and recreated successfully.");
}
await db.Database.EnsureDeletedAsync();
await db.Database.EnsureCreatedAsync();
return OperationResult.Ok("In-memory database wiped and recreated successfully.");
}
catch (Exception ex)
{
@@ -213,26 +215,58 @@ public class DatabaseService : IDatabaseService
{
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 existingNoteNames = (await db.CardNotes.Select(n => n.CardName).ToListAsync()).ToHashSet();
var existingNotes =
(await db.CardNotes.Select(n => new { n.Username, n.CardName }).ToListAsync()).ToHashSet();
var sampleNotes = new List<CardNote>
{
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." }
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 => !existingNoteNames.Contains(n.CardName)))
foreach (var note in sampleNotes.Where(n => !existingNotes.Contains(new { n.Username, n.CardName })))
{
db.CardNotes.Add(note);
result.CardNotesSeeded++;
@@ -295,16 +329,32 @@ public class DatabaseService : IDatabaseService
{
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 },
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 },
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
@@ -317,7 +367,8 @@ public class DatabaseService : IDatabaseService
await db.SaveChangesAsync();
result.Success = true;
result.Message = $"Sample data seeded: {result.CardNotesSeeded} notes, {result.UserDecksSeeded} decks, {result.PasskeysSeeded} passkeys.";
result.Message =
$"Sample data seeded: {result.UsersSeeded} users, {result.CardNotesSeeded} notes, {result.UserDecksSeeded} decks, {result.PasskeysSeeded} passkeys.";
}
catch (Exception ex)
{
@@ -329,26 +380,109 @@ public class DatabaseService : IDatabaseService
return result;
}
// ── Card Notes CRUD ────────────────────────────────────────────────────────
// ── Users CRUD ─────────────────────────────────────────────────────────────
public async Task<List<CardNote>> GetCardNotesAsync(string? search = null)
public async Task<List<User>> GetUsersAsync(string? search = null)
{
await using var db = _configService.CreateDbContext();
var query = db.CardNotes.AsNoTracking().AsQueryable();
var query = db.Users.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));
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)
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);
return await db.CardNotes.AsNoTracking()
.FirstOrDefaultAsync(n => n.CardName == cardName && n.Username == (username ?? ""));
}
public async Task<OperationResult> SaveCardNoteAsync(CardNote note, bool isNew)
@@ -356,29 +490,35 @@ public class DatabaseService : IDatabaseService
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);
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}' already 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}'.");
}
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}'.");
}
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)
{
@@ -387,12 +527,13 @@ public class DatabaseService : IDatabaseService
}
}
public async Task<OperationResult> DeleteCardNoteAsync(string cardName)
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);
var existing =
await db.CardNotes.FirstOrDefaultAsync(n => n.CardName == cardName && n.Username == (username ?? ""));
if (existing == null)
return OperationResult.Fail($"Note for '{cardName}' not found.");
@@ -450,22 +591,20 @@ public class DatabaseService : IDatabaseService
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;
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.");
await db.SaveChangesAsync();
return OperationResult.Ok($"Updated deck '{deck.Name}'.");
}
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)
{
@@ -537,21 +676,19 @@ public class DatabaseService : IDatabaseService
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;
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.");
await db.SaveChangesAsync();
return OperationResult.Ok($"Updated Passkey credential #{passkey.Id}.");
}
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)
{
@@ -587,8 +724,8 @@ public class DatabaseService : IDatabaseService
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.",
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()}...");
@@ -601,8 +738,8 @@ public class DatabaseService : IDatabaseService
}, 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.",
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();
@@ -634,18 +771,20 @@ public class DatabaseService : IDatabaseService
}, 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.",
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}'...");
db.CardNotes.Add(new CardNote { CardName = testCardName, Note = initialNote });
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.");
}
@@ -653,7 +792,8 @@ public class DatabaseService : IDatabaseService
await using (var db = _configService.CreateDbContext())
{
test.AddLog("2. Querying inserted CardNote from database...");
var fetched = await db.CardNotes.FirstOrDefaultAsync(n => n.CardName == testCardName);
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.");
@@ -667,7 +807,8 @@ public class DatabaseService : IDatabaseService
await using (var db = _configService.CreateDbContext())
{
test.AddLog("4. Verifying persisted updated note...");
var fetched = await db.CardNotes.FirstOrDefaultAsync(n => n.CardName == testCardName);
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.");
@@ -681,7 +822,8 @@ public class DatabaseService : IDatabaseService
await using (var db = _configService.CreateDbContext())
{
test.AddLog("6. Verifying CardNote is deleted...");
var exists = await db.CardNotes.AnyAsync(n => n.CardName == testCardName);
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.");
@@ -689,8 +831,8 @@ public class DatabaseService : IDatabaseService
}, 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.",
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();
@@ -708,7 +850,8 @@ public class DatabaseService : IDatabaseService
await using (var db = _configService.CreateDbContext())
{
test.AddLog($"1. Inserting UserDeck '{testDeckName}' with {initialCards.Count} cards and {initialDivers.Count} divers...");
test.AddLog(
$"1. Inserting UserDeck '{testDeckName}' with {initialCards.Count} cards and {initialDivers.Count} divers...");
var deck = new UserDeck
{
Id = testDeckId,
@@ -732,7 +875,7 @@ public class DatabaseService : IDatabaseService
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.");
throw new Exception("Diver entry deserialization mismatch.");
test.AddLog("JSON properties successfully verified.");
test.AddLog("3. Modifying cards list in UserDeck...");
@@ -766,8 +909,8 @@ public class DatabaseService : IDatabaseService
}, 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.",
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 };
@@ -775,7 +918,7 @@ public class DatabaseService : IDatabaseService
var userHandle = new byte[] { 0xFE, 0xDC, 0xBA, 0x98 };
var aaGuid = Guid.NewGuid().ToString();
long signCount = 100;
int savedId = 0;
var savedId = 0;
await using (var db = _configService.CreateDbContext())
{
@@ -838,15 +981,17 @@ public class DatabaseService : IDatabaseService
}, 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.",
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 { CardName = duplicateKey, Note = "First note" });
db.CardNotes.Add(new CardNote
{ Username = testUser, CardName = duplicateKey, Note = "First note" });
await db.SaveChangesAsync();
test.AddLog("Initial record created.");
}
@@ -856,7 +1001,8 @@ public class DatabaseService : IDatabaseService
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" });
db.CardNotes.Add(new CardNote
{ Username = testUser, CardName = duplicateKey, Note = "Second duplicate note" });
await db.SaveChangesAsync();
}
@@ -878,7 +1024,9 @@ public class DatabaseService : IDatabaseService
{
// Cleanup
await using var db = _configService.CreateDbContext();
var toClean = await db.CardNotes.FirstOrDefaultAsync(n => n.CardName == duplicateKey);
var toClean =
await db.CardNotes.FirstOrDefaultAsync(n =>
n.CardName == duplicateKey && n.Username == testUser);
if (toClean != null)
{
db.CardNotes.Remove(toClean);
@@ -889,24 +1037,27 @@ public class DatabaseService : IDatabaseService
}, 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.",
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.");
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 { CardName = transCard, Note = "Transaction Note" });
db.CardNotes.Add(new CardNote
{ Username = testUser, CardName = transCard, Note = "Transaction Note" });
await db.SaveChangesAsync();
test.AddLog("3. Rolling back transaction explicitly without committing...");
@@ -917,13 +1068,67 @@ public class DatabaseService : IDatabaseService
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);
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;
}
@@ -969,4 +1174,4 @@ public class DatabaseService : IDatabaseService
return test;
}
}
}