413 lines
16 KiB
C#
413 lines
16 KiB
C#
using API.Services;
|
|
using Cloud.Models;
|
|
using DatabaseTest.Models;
|
|
using DatabaseTest.Services;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Model;
|
|
|
|
namespace Tests;
|
|
|
|
[TestFixture]
|
|
public class DatabaseServiceTests
|
|
{
|
|
[SetUp]
|
|
public async Task Setup()
|
|
{
|
|
_testDbName = $"chrono_test_{Guid.NewGuid():N}";
|
|
var config = new ConfigurationBuilder().Build();
|
|
var configService = new DatabaseConfigService(config);
|
|
configService.SetProvider(DatabaseProviderType.InMemory);
|
|
configService.SetInMemoryDbName(_testDbName);
|
|
|
|
_configService = configService;
|
|
_dbService = new DatabaseService(configService, NullLogger<DatabaseService>.Instance);
|
|
|
|
await _dbService.EnsureDatabaseCreatedAsync();
|
|
}
|
|
|
|
private IDatabaseConfigService _configService = null!;
|
|
private IDatabaseService _dbService = null!;
|
|
private string _testDbName = null!;
|
|
|
|
[Test]
|
|
public async Task CheckHealth_InMemory_ReturnsConnected()
|
|
{
|
|
var health = await _dbService.CheckHealthAsync();
|
|
Assert.That(health.IsConnected, Is.True);
|
|
Assert.That(health.ProviderType, Is.EqualTo(DatabaseProviderType.InMemory));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Users_FullCrudLifecycle()
|
|
{
|
|
var username = "player_" + Guid.NewGuid().ToString("N")[..8];
|
|
var initialUser = new User { Username = username, Password = "password123", CreatedAt = DateTime.UtcNow };
|
|
|
|
// 1. Create
|
|
var createResult = await _dbService.SaveUserAsync(initialUser, true);
|
|
Assert.That(createResult.Success, Is.True);
|
|
|
|
// 2. Read
|
|
var fetched = await _dbService.GetUserAsync(username);
|
|
Assert.That(fetched, Is.Not.Null);
|
|
Assert.That(fetched!.Password, Is.EqualTo("password123"));
|
|
|
|
// 3. Update
|
|
fetched.Password = "newpassword456";
|
|
var updateResult = await _dbService.SaveUserAsync(fetched, false);
|
|
Assert.That(updateResult.Success, Is.True);
|
|
|
|
var updated = await _dbService.GetUserAsync(username);
|
|
Assert.That(updated!.Password, Is.EqualTo("newpassword456"));
|
|
|
|
// 4. Delete
|
|
var deleteResult = await _dbService.DeleteUserAsync(username);
|
|
Assert.That(deleteResult.Success, Is.True);
|
|
|
|
var deleted = await _dbService.GetUserAsync(username);
|
|
Assert.That(deleted, Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public async Task CardNotes_FullCrudLifecycle()
|
|
{
|
|
var cardName = "Test Hero " + Guid.NewGuid().ToString("N");
|
|
var username = "user1";
|
|
var initialNote = new CardNote { Username = username, CardName = cardName, Note = "Initial note text" };
|
|
|
|
// 1. Create
|
|
var createResult = await _dbService.SaveCardNoteAsync(initialNote, true);
|
|
Assert.That(createResult.Success, Is.True);
|
|
|
|
// 2. Read
|
|
var fetched = await _dbService.GetCardNoteAsync(cardName, username);
|
|
Assert.That(fetched, Is.Not.Null);
|
|
Assert.That(fetched!.Note, Is.EqualTo("Initial note text"));
|
|
|
|
// 3. Update
|
|
fetched.Note = "Updated note text";
|
|
var updateResult = await _dbService.SaveCardNoteAsync(fetched, false);
|
|
Assert.That(updateResult.Success, Is.True);
|
|
|
|
var updated = await _dbService.GetCardNoteAsync(cardName, username);
|
|
Assert.That(updated!.Note, Is.EqualTo("Updated note text"));
|
|
|
|
// 4. Delete
|
|
var deleteResult = await _dbService.DeleteCardNoteAsync(cardName, username);
|
|
Assert.That(deleteResult.Success, Is.True);
|
|
|
|
var deleted = await _dbService.GetCardNoteAsync(cardName, username);
|
|
Assert.That(deleted, Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public async Task DuplicateCardNote_ReturnsFailureResult()
|
|
{
|
|
var cardName = "Unique Card " + Guid.NewGuid().ToString("N");
|
|
var username = "user1";
|
|
var note1 = new CardNote { Username = username, CardName = cardName, Note = "First" };
|
|
var note2 = new CardNote { Username = username, CardName = cardName, Note = "Second" };
|
|
|
|
var res1 = await _dbService.SaveCardNoteAsync(note1, true);
|
|
Assert.That(res1.Success, Is.True);
|
|
|
|
var res2 = await _dbService.SaveCardNoteAsync(note2, true);
|
|
Assert.That(res2.Success, Is.False);
|
|
Assert.That(res2.Message, Does.Contain("already exists"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task UserDecks_FullCrudLifecycle_WithJsonCollections()
|
|
{
|
|
var deckId = Guid.NewGuid();
|
|
var deck = new UserDeck
|
|
{
|
|
Id = deckId,
|
|
Name = "Control Deck",
|
|
Season = "Season 1",
|
|
Notes = "Strategic combo control",
|
|
Cards = new List<string> { "Chronos, Time Shifter", "Temporal Rift", "Chronos, Time Shifter" },
|
|
Divers = new List<string> { "Chronos, Time Shifter" }
|
|
};
|
|
|
|
// 1. Create
|
|
var createResult = await _dbService.SaveUserDeckAsync(deck, true);
|
|
Assert.That(createResult.Success, Is.True);
|
|
|
|
// 2. Read
|
|
var fetched = await _dbService.GetUserDeckAsync(deckId);
|
|
Assert.That(fetched, Is.Not.Null);
|
|
Assert.That(fetched!.Name, Is.EqualTo("Control Deck"));
|
|
Assert.That(fetched.Cards.Count, Is.EqualTo(3));
|
|
Assert.That(fetched.Divers.Count, Is.EqualTo(1));
|
|
|
|
// 3. Update
|
|
fetched.Cards.Add("Added Card");
|
|
fetched.Name = "Control Deck v2";
|
|
var updateResult = await _dbService.SaveUserDeckAsync(fetched, false);
|
|
Assert.That(updateResult.Success, Is.True);
|
|
|
|
var updated = await _dbService.GetUserDeckAsync(deckId);
|
|
Assert.That(updated!.Name, Is.EqualTo("Control Deck v2"));
|
|
Assert.That(updated.Cards.Count, Is.EqualTo(4));
|
|
|
|
// 4. Delete
|
|
var deleteResult = await _dbService.DeleteUserDeckAsync(deckId);
|
|
Assert.That(deleteResult.Success, Is.True);
|
|
|
|
var deleted = await _dbService.GetUserDeckAsync(deckId);
|
|
Assert.That(deleted, Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public async Task PasskeyCredentials_FullCrudLifecycle_WithBinaryData()
|
|
{
|
|
var credId = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0xAA, 0xBB, 0xCC };
|
|
var pubKey = new byte[] { 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80 };
|
|
var userHandle = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF };
|
|
|
|
var passkey = new PasskeyCredential
|
|
{
|
|
CredentialId = credId,
|
|
PublicKey = pubKey,
|
|
UserHandle = userHandle,
|
|
AaGuid = Guid.NewGuid().ToString(),
|
|
SignCount = 5
|
|
};
|
|
|
|
// 1. Create
|
|
var createResult = await _dbService.SavePasskeyAsync(passkey, true);
|
|
Assert.That(createResult.Success, Is.True);
|
|
Assert.That(passkey.Id, Is.GreaterThan(0));
|
|
|
|
// 2. Read & Binary Verification
|
|
var fetched = await _dbService.GetPasskeyAsync(passkey.Id);
|
|
Assert.That(fetched, Is.Not.Null);
|
|
Assert.That(fetched!.CredentialId, Is.EqualTo(credId));
|
|
Assert.That(fetched.PublicKey, Is.EqualTo(pubKey));
|
|
Assert.That(fetched.UserHandle, Is.EqualTo(userHandle));
|
|
Assert.That(fetched.SignCount, Is.EqualTo(5));
|
|
|
|
// 3. Update
|
|
fetched.SignCount = 10;
|
|
var updateResult = await _dbService.SavePasskeyAsync(fetched, false);
|
|
Assert.That(updateResult.Success, Is.True);
|
|
|
|
var updated = await _dbService.GetPasskeyAsync(passkey.Id);
|
|
Assert.That(updated!.SignCount, Is.EqualTo(10));
|
|
|
|
// 4. Delete
|
|
var deleteResult = await _dbService.DeletePasskeyAsync(passkey.Id);
|
|
Assert.That(deleteResult.Success, Is.True);
|
|
|
|
var deleted = await _dbService.GetPasskeyAsync(passkey.Id);
|
|
Assert.That(deleted, Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public async Task SeedAndResetDatabase_WorksCorrectly()
|
|
{
|
|
// 1. Seed
|
|
var seedResult = await _dbService.SeedSampleDataAsync();
|
|
Assert.That(seedResult.Success, Is.True);
|
|
Assert.That(seedResult.UsersSeeded, Is.GreaterThan(0));
|
|
Assert.That(seedResult.CardNotesSeeded, Is.GreaterThan(0));
|
|
Assert.That(seedResult.UserDecksSeeded, Is.GreaterThan(0));
|
|
Assert.That(seedResult.PasskeysSeeded, Is.GreaterThan(0));
|
|
|
|
var users = await _dbService.GetUsersAsync();
|
|
var notes = await _dbService.GetCardNotesAsync();
|
|
var decks = await _dbService.GetUserDecksAsync();
|
|
var passkeys = await _dbService.GetPasskeysAsync();
|
|
|
|
Assert.That(users.Count, Is.GreaterThan(0));
|
|
Assert.That(notes.Count, Is.GreaterThan(0));
|
|
Assert.That(decks.Count, Is.GreaterThan(0));
|
|
Assert.That(passkeys.Count, Is.GreaterThan(0));
|
|
|
|
// 2. Reset
|
|
var resetResult = await _dbService.ResetDatabaseAsync();
|
|
Assert.That(resetResult.Success, Is.True);
|
|
|
|
var usersAfterReset = await _dbService.GetUsersAsync();
|
|
var notesAfterReset = await _dbService.GetCardNotesAsync();
|
|
var decksAfterReset = await _dbService.GetUserDecksAsync();
|
|
var passkeysAfterReset = await _dbService.GetPasskeysAsync();
|
|
|
|
Assert.That(usersAfterReset, Is.Empty);
|
|
Assert.That(notesAfterReset, Is.Empty);
|
|
Assert.That(decksAfterReset, Is.Empty);
|
|
Assert.That(passkeysAfterReset, Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public async Task RunAllTestsAsync_ExecutesAndPassesCoreTests()
|
|
{
|
|
var testResults = await _dbService.RunAllTestsAsync();
|
|
Assert.That(testResults, Is.Not.Empty);
|
|
|
|
var failed = testResults.Where(t => t.Status == TestStatus.Failed).ToList();
|
|
Assert.That(failed, Is.Empty,
|
|
() => $"Failed tests: {string.Join(", ", failed.Select(f => $"{f.Name}: {f.ErrorMessage}"))}");
|
|
|
|
var passedCount = testResults.Count(t => t.Status == TestStatus.Passed);
|
|
Assert.That(passedCount, Is.GreaterThanOrEqualTo(7));
|
|
}
|
|
|
|
[Test]
|
|
public void DatabaseConfigService_WithPostgreSQL_Password_EnvironmentVariable_AppliesPassword()
|
|
{
|
|
var testPassword = "test_custom_env_password_123";
|
|
Environment.SetEnvironmentVariable("PostgreSQL_Password", testPassword);
|
|
try
|
|
{
|
|
var config = new ConfigurationBuilder().Build();
|
|
var service = new DatabaseConfigService(config);
|
|
|
|
Assert.That(service.ConnectionString, Does.Contain(testPassword));
|
|
}
|
|
finally
|
|
{
|
|
Environment.SetEnvironmentVariable("PostgreSQL_Password", null);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void DatabaseConfigService_MasksPassword_InDisplay()
|
|
{
|
|
var config = new ConfigurationBuilder().Build();
|
|
var service = new DatabaseConfigService(config);
|
|
service.SetConnectionString("Host=localhost;Database=chrono;Username=postgres;Password=secret_password;");
|
|
|
|
var description = service.GetActiveProviderDescription();
|
|
Assert.That(description, Does.Contain("Password=******"));
|
|
Assert.That(description, Does.Not.Contain("secret_password"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task AddNewUser_ValidationAndTrimming_WorksCorrectly()
|
|
{
|
|
// Empty username fails
|
|
var emptyUser = new User { Username = " ", Password = "valid_password" };
|
|
var emptyRes = await _dbService.SaveUserAsync(emptyUser, true);
|
|
Assert.That(emptyRes.Success, Is.False);
|
|
Assert.That(emptyRes.Message, Does.Contain("Username is required"));
|
|
|
|
// Empty password fails
|
|
var noPassUser = new User { Username = "valid_user", Password = " " };
|
|
var noPassRes = await _dbService.SaveUserAsync(noPassUser, true);
|
|
Assert.That(noPassRes.Success, Is.False);
|
|
Assert.That(noPassRes.Message, Does.Contain("Password is required"));
|
|
|
|
// Valid user with padding gets trimmed and created
|
|
var paddedUser = new User { Username = " trimmed_user ", Password = " pass123 " };
|
|
var okRes = await _dbService.SaveUserAsync(paddedUser, true);
|
|
Assert.That(okRes.Success, Is.True);
|
|
|
|
var savedUser = await _dbService.GetUserAsync("trimmed_user");
|
|
Assert.That(savedUser, Is.Not.Null);
|
|
Assert.That(savedUser!.Username, Is.EqualTo("trimmed_user"));
|
|
Assert.That(savedUser.Password, Is.EqualTo("pass123"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task AddNewNote_ValidationAndTrimming_WorksCorrectly()
|
|
{
|
|
// Empty card name fails
|
|
var emptyCard = new CardNote { CardName = " ", Username = "user1", Note = "Some note" };
|
|
var emptyRes = await _dbService.SaveCardNoteAsync(emptyCard, true);
|
|
Assert.That(emptyRes.Success, Is.False);
|
|
Assert.That(emptyRes.Message, Does.Contain("Card name is required"));
|
|
|
|
// Valid note with padding gets trimmed and created
|
|
var paddedNote = new CardNote { CardName = " Arcane Spell ", Username = " player2 ", Note = " Important note " };
|
|
var okRes = await _dbService.SaveCardNoteAsync(paddedNote, true);
|
|
Assert.That(okRes.Success, Is.True);
|
|
|
|
var savedNote = await _dbService.GetCardNoteAsync("Arcane Spell", "player2");
|
|
Assert.That(savedNote, Is.Not.Null);
|
|
Assert.That(savedNote!.CardName, Is.EqualTo("Arcane Spell"));
|
|
Assert.That(savedNote.Username, Is.EqualTo("player2"));
|
|
Assert.That(savedNote.Note, Is.EqualTo("Important note"));
|
|
}
|
|
|
|
[Test]
|
|
public void DatabaseTest_Migrations_IncludesAddUsersAndSyncCardNotes()
|
|
{
|
|
var migrations = typeof(Cloud.AppDbContext).Assembly.GetTypes()
|
|
.Where(t => typeof(Microsoft.EntityFrameworkCore.Migrations.Migration).IsAssignableFrom(t) && !t.IsAbstract)
|
|
.Select(t => t.Name)
|
|
.ToList();
|
|
|
|
Assert.That(migrations, Does.Contain("AddUsersAndSyncCardNotes"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task CreateAccount_WithRandomCredentials_AgainstPostgreSql_AndTestAgainstLoginApi()
|
|
{
|
|
var config = new ConfigurationBuilder().Build();
|
|
var postgresConfigService = new DatabaseConfigService(config);
|
|
postgresConfigService.SetProvider(DatabaseProviderType.PostgreSQL);
|
|
|
|
var postgresDbService = new DatabaseService(postgresConfigService, NullLogger<DatabaseService>.Instance);
|
|
var health = await postgresDbService.CheckHealthAsync();
|
|
if (!health.IsConnected)
|
|
{
|
|
Assert.Ignore($"PostgreSQL database is not reachable at {postgresConfigService.ConnectionString}. Error: {health.ErrorMessage}");
|
|
}
|
|
|
|
await postgresDbService.ApplyMigrationsAsync();
|
|
|
|
var randomUsername = $"user_{Guid.NewGuid():N}";
|
|
var randomPassword = $"P@ss_{Guid.NewGuid():N}";
|
|
var newUser = new User
|
|
{
|
|
Username = randomUsername,
|
|
Password = randomPassword,
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
try
|
|
{
|
|
// 1. Add account to PostgreSQL database using DatabaseTest service
|
|
var saveResult = await postgresDbService.SaveUserAsync(newUser, true);
|
|
Assert.That(saveResult.Success, Is.True, $"Failed to create user in PostgreSQL: {saveResult.Message}");
|
|
|
|
// 2. Verify account exists in the database
|
|
var fetchedUser = await postgresDbService.GetUserAsync(randomUsername);
|
|
Assert.That(fetchedUser, Is.Not.Null);
|
|
Assert.That(fetchedUser!.Username, Is.EqualTo(randomUsername));
|
|
Assert.That(fetchedUser.Password, Is.EqualTo(randomPassword));
|
|
|
|
// 3. Test against Login API using API's AppDbContext connected to the same PostgreSQL DB
|
|
var apiDbOptions = new DbContextOptionsBuilder<API.Data.AppDbContext>()
|
|
.UseNpgsql(postgresConfigService.ConnectionString)
|
|
.Options;
|
|
await using var apiDb = new API.Data.AppDbContext(apiDbOptions);
|
|
|
|
var tokenService = new TokenService();
|
|
var mutation = new API.GraphQL.Mutation();
|
|
|
|
// Test login with incorrect password
|
|
var invalidLogin = await mutation.Login(apiDb, tokenService, randomUsername, "wrong_" + randomPassword);
|
|
Assert.That(invalidLogin.Success, Is.False, "Login should fail with invalid password");
|
|
Assert.That(invalidLogin.Token, Is.Null);
|
|
|
|
// Test login with correct password
|
|
var validLogin = await mutation.Login(apiDb, tokenService, randomUsername, randomPassword);
|
|
Assert.That(validLogin.Success, Is.True, $"Login failed: {validLogin.Message}");
|
|
Assert.That(validLogin.Username, Is.EqualTo(randomUsername));
|
|
Assert.That(validLogin.Token, Is.Not.Null.And.Not.Empty);
|
|
|
|
var validatedUsername = tokenService.ValidateToken(validLogin.Token);
|
|
Assert.That(validatedUsername, Is.EqualTo(randomUsername));
|
|
}
|
|
finally
|
|
{
|
|
// Cleanup test user
|
|
await postgresDbService.DeleteUserAsync(randomUsername);
|
|
}
|
|
}
|
|
} |