API Test
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Cloud;
|
||||
using DatabaseTest.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json;
|
||||
using Cloud.Models;
|
||||
using Npgsql;
|
||||
|
||||
namespace DatabaseTest.Services;
|
||||
|
||||
@@ -23,89 +23,118 @@ public interface IDatabaseConfigService
|
||||
public class DatabaseConfigService : IDatabaseConfigService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private DatabaseProviderType _providerType;
|
||||
private string _connectionString;
|
||||
private string _inMemoryDbName = "chrono_database_test";
|
||||
|
||||
public event Action? OnConfigurationChanged;
|
||||
|
||||
public DatabaseConfigService(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
|
||||
|
||||
var envPassword = Environment.GetEnvironmentVariable("PostgreSQL_Password")
|
||||
?? (OperatingSystem.IsWindows()
|
||||
? Environment.GetEnvironmentVariable("PostgreSQL_Password",
|
||||
EnvironmentVariableTarget.User)
|
||||
: null)
|
||||
?? (OperatingSystem.IsWindows()
|
||||
? Environment.GetEnvironmentVariable("PostgreSQL_Password",
|
||||
EnvironmentVariableTarget.Machine)
|
||||
: null)
|
||||
?? Environment.GetEnvironmentVariable("POSTGRESQL_PASSWORD")
|
||||
?? Environment.GetEnvironmentVariable("POSTGRES_PASSWORD");
|
||||
|
||||
var configuredConn = _configuration.GetConnectionString("DefaultConnection")
|
||||
?? _configuration.GetConnectionString("Postgres")
|
||||
?? Environment.GetEnvironmentVariable("POSTGRES_CONNECTION_STRING")
|
||||
?? "Host=localhost;Port=5432;Database=chrono;Username=postgres;Password=postgres";
|
||||
?? Environment.GetEnvironmentVariable("POSTGRES_CONNECTION_STRING");
|
||||
|
||||
_connectionString = configuredConn;
|
||||
_providerType = DatabaseProviderType.PostgreSQL;
|
||||
if (string.IsNullOrWhiteSpace(configuredConn))
|
||||
{
|
||||
var csb = new NpgsqlConnectionStringBuilder
|
||||
{
|
||||
Host = "localhost",
|
||||
Database = "chrono",
|
||||
Username = "postgres",
|
||||
Password = envPassword
|
||||
};
|
||||
configuredConn = csb.ConnectionString;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(envPassword))
|
||||
{
|
||||
try
|
||||
{
|
||||
var csb = new NpgsqlConnectionStringBuilder(configuredConn)
|
||||
{
|
||||
Password = envPassword
|
||||
};
|
||||
configuredConn = csb.ConnectionString;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
ConnectionString = configuredConn;
|
||||
ProviderType = DatabaseProviderType.PostgreSQL;
|
||||
}
|
||||
|
||||
public DatabaseProviderType ProviderType => _providerType;
|
||||
public string ConnectionString => _connectionString;
|
||||
public string InMemoryDbName => _inMemoryDbName;
|
||||
public event Action? OnConfigurationChanged;
|
||||
|
||||
public DatabaseProviderType ProviderType { get; private set; }
|
||||
|
||||
public string ConnectionString { get; private set; }
|
||||
|
||||
public string InMemoryDbName { get; private set; } = "chrono_database_test";
|
||||
|
||||
public void SetProvider(DatabaseProviderType providerType)
|
||||
{
|
||||
if (_providerType != providerType)
|
||||
if (ProviderType != providerType)
|
||||
{
|
||||
_providerType = providerType;
|
||||
ProviderType = providerType;
|
||||
OnConfigurationChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetConnectionString(string connectionString)
|
||||
{
|
||||
if (_connectionString != connectionString)
|
||||
if (ConnectionString != connectionString)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
ConnectionString = connectionString;
|
||||
OnConfigurationChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetInMemoryDbName(string name)
|
||||
{
|
||||
if (_inMemoryDbName != name)
|
||||
if (InMemoryDbName != name)
|
||||
{
|
||||
_inMemoryDbName = name;
|
||||
InMemoryDbName = name;
|
||||
OnConfigurationChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public string GetActiveProviderDescription()
|
||||
{
|
||||
return _providerType switch
|
||||
return ProviderType switch
|
||||
{
|
||||
DatabaseProviderType.PostgreSQL => $"PostgreSQL ({GetMaskedConnectionString(_connectionString)})",
|
||||
DatabaseProviderType.InMemory => $"In-Memory Test Database ({_inMemoryDbName})",
|
||||
_ => _providerType.ToString()
|
||||
DatabaseProviderType.PostgreSQL => $"PostgreSQL ({GetMaskedConnectionString(ConnectionString)})",
|
||||
DatabaseProviderType.InMemory => $"In-Memory Test Database ({InMemoryDbName})",
|
||||
_ => ProviderType.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetMaskedConnectionString(string conn)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(conn)) return "Not configured";
|
||||
// Hide password in display
|
||||
return System.Text.RegularExpressions.Regex.Replace(conn, @"(?i)Password=[^;]+", "Password=******");
|
||||
}
|
||||
|
||||
public AppDbContext CreateDbContext()
|
||||
{
|
||||
var builder = new DbContextOptionsBuilder<AppDbContext>();
|
||||
|
||||
if (_providerType == DatabaseProviderType.PostgreSQL)
|
||||
{
|
||||
builder.UseNpgsql(_connectionString, npgsqlOptions =>
|
||||
{
|
||||
npgsqlOptions.CommandTimeout(15);
|
||||
});
|
||||
}
|
||||
if (ProviderType == DatabaseProviderType.PostgreSQL)
|
||||
builder.UseNpgsql(ConnectionString, npgsqlOptions => { npgsqlOptions.CommandTimeout(15); });
|
||||
else
|
||||
{
|
||||
builder.UseInMemoryDatabase(_inMemoryDbName);
|
||||
}
|
||||
builder.UseInMemoryDatabase(InMemoryDbName);
|
||||
|
||||
return new AppDbContext(builder.Options);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetMaskedConnectionString(string conn)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(conn)) return "Not configured";
|
||||
// Hide password in display
|
||||
return Regex.Replace(conn, @"(?i)Password=[^;]+", "Password=******");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user