...
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
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;
|
||||
@@ -283,4 +285,129 @@ public class DatabaseServiceTests
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
using System.Net;
|
||||
using API.Data;
|
||||
using API.GraphQL;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.JSInterop;
|
||||
using Model;
|
||||
using Web.Services;
|
||||
|
||||
namespace Tests;
|
||||
|
||||
[TestFixture]
|
||||
public class DeckBuilderServiceTests
|
||||
{
|
||||
private AppDbContext _db = null!;
|
||||
private ITokenService _tokenService = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
|
||||
_db = new AppDbContext(options);
|
||||
_tokenService = new TokenService();
|
||||
|
||||
_db.Users.Add(new User { Username = "player1", Password = "password123", CreatedAt = DateTime.UtcNow });
|
||||
_db.SaveChanges();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
_db.Database.EnsureDeleted();
|
||||
_db.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UserDeck_ValidationRules_AreEnforcedCorrectly()
|
||||
{
|
||||
var deck = new UserDeck
|
||||
{
|
||||
Name = "My Test Deck",
|
||||
Season = "Season 1",
|
||||
Notes = "Test notes"
|
||||
};
|
||||
|
||||
// Initially empty -> invalid
|
||||
Assert.That(deck.IsValid, Is.False);
|
||||
Assert.That(deck.ValidationMessage, Does.Contain("needs 40 more cards"));
|
||||
|
||||
// Add 40 cards (13x3 + 1x1)
|
||||
for (int i = 0; i < 13; i++)
|
||||
{
|
||||
deck.Cards.Add($"Card_{i}");
|
||||
deck.Cards.Add($"Card_{i}");
|
||||
deck.Cards.Add($"Card_{i}");
|
||||
}
|
||||
deck.Cards.Add("Card_13");
|
||||
|
||||
Assert.That(deck.Cards.Count, Is.EqualTo(40));
|
||||
Assert.That(deck.IsValid, Is.False); // Still needs 2 divers
|
||||
Assert.That(deck.ValidationMessage, Does.Contain("needs 2 more divers"));
|
||||
|
||||
// Add 1 diver
|
||||
deck.Divers.Add("Diver_1");
|
||||
Assert.That(deck.IsValid, Is.False);
|
||||
Assert.That(deck.ValidationMessage, Does.Contain("needs 1 more diver"));
|
||||
|
||||
// Add duplicate diver -> invalid
|
||||
deck.Divers.Add("Diver_1");
|
||||
Assert.That(deck.IsValid, Is.False);
|
||||
Assert.That(deck.ValidationMessage, Does.Contain("must be unique"));
|
||||
|
||||
// Change second diver to a card that is in the deck -> invalid
|
||||
deck.Divers[1] = "Card_0";
|
||||
Assert.That(deck.IsValid, Is.False);
|
||||
Assert.That(deck.ValidationMessage, Does.Contain("cannot be in the main deck"));
|
||||
|
||||
// Change second diver to a unique card not in deck -> valid!
|
||||
deck.Divers[1] = "Diver_2";
|
||||
Assert.That(deck.IsValid, Is.True);
|
||||
Assert.That(deck.ValidationMessage, Is.EqualTo("Deck is valid."));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GraphQL_SaveDeck_And_GetDecks_WorksEndToEnd()
|
||||
{
|
||||
var token = _tokenService.GenerateToken("player1");
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Headers["Authorization"] = $"Bearer {token}";
|
||||
|
||||
var accessor = new HttpContextAccessor { HttpContext = httpContext };
|
||||
var mutation = new Mutation();
|
||||
var query = new Query();
|
||||
|
||||
var input = new UserDeckInput(
|
||||
Id: Guid.NewGuid(),
|
||||
Name: "Aggro Champions",
|
||||
Cards: ["CardA", "CardA", "CardA", "CardB"],
|
||||
Divers: ["DiverA", "DiverB"],
|
||||
Notes: "Early aggression deck notes",
|
||||
Season: "Season 1"
|
||||
);
|
||||
|
||||
var savedDeck = await mutation.SaveDeck(_db, accessor, _tokenService, input);
|
||||
Assert.That(savedDeck, Is.Not.Null);
|
||||
Assert.That(savedDeck.Name, Is.EqualTo("Aggro Champions"));
|
||||
Assert.That(savedDeck.Notes, Is.EqualTo("Early aggression deck notes"));
|
||||
Assert.That(savedDeck.Season, Is.EqualTo("Season 1"));
|
||||
Assert.That(savedDeck.Cards.Count, Is.EqualTo(4));
|
||||
Assert.That(savedDeck.Divers.Count, Is.EqualTo(2));
|
||||
|
||||
var fetchedDecks = await query.GetDecks(_db, accessor, _tokenService);
|
||||
Assert.That(fetchedDecks.Count, Is.EqualTo(1));
|
||||
Assert.That(fetchedDecks[0].Name, Is.EqualTo("Aggro Champions"));
|
||||
|
||||
var singleDeck = await query.GetDeck(_db, accessor, _tokenService, savedDeck.Id);
|
||||
Assert.That(singleDeck, Is.Not.Null);
|
||||
Assert.That(singleDeck!.Name, Is.EqualTo("Aggro Champions"));
|
||||
|
||||
var deleted = await mutation.DeleteDeck(_db, accessor, _tokenService, savedDeck.Id);
|
||||
Assert.That(deleted, Is.True);
|
||||
|
||||
var remainingDecks = await query.GetDecks(_db, accessor, _tokenService);
|
||||
Assert.That(remainingDecks, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UserDecksService_SaveAndGetDecks_WithMockHttp()
|
||||
{
|
||||
var mockHttp = new HttpClient(new TestHttpMessageHandler(req =>
|
||||
{
|
||||
var content = req.Content?.ReadAsStringAsync().Result ?? "";
|
||||
if (content.Contains("SaveDeck") || (req.Method == HttpMethod.Post && req.RequestUri?.AbsolutePath == "/api/decks"))
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(@"{
|
||||
""data"": {
|
||||
""saveDeck"": {
|
||||
""id"": ""3fa85f64-5717-4562-b3fc-2c963f66afa6"",
|
||||
""name"": ""Test GraphQL Deck"",
|
||||
""cards"": [""Card1"", ""Card2""],
|
||||
""divers"": [""Diver1"", ""Diver2""],
|
||||
""notes"": ""Great deck"",
|
||||
""season"": ""Season 1"",
|
||||
""updatedAt"": ""2026-08-17T00:00:00Z""
|
||||
}
|
||||
}
|
||||
}")
|
||||
};
|
||||
}
|
||||
if (content.Contains("GetDecks") || (req.Method == HttpMethod.Get && req.RequestUri?.AbsolutePath == "/api/decks"))
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(@"{
|
||||
""data"": {
|
||||
""decks"": [
|
||||
{
|
||||
""id"": ""3fa85f64-5717-4562-b3fc-2c963f66afa6"",
|
||||
""name"": ""Test GraphQL Deck"",
|
||||
""cards"": [""Card1"", ""Card2""],
|
||||
""divers"": [""Diver1"", ""Diver2""],
|
||||
""notes"": ""Great deck"",
|
||||
""season"": ""Season 1"",
|
||||
""updatedAt"": ""2026-08-17T00:00:00Z""
|
||||
}
|
||||
]
|
||||
}
|
||||
}")
|
||||
};
|
||||
}
|
||||
return new HttpResponseMessage(HttpStatusCode.BadRequest);
|
||||
}));
|
||||
|
||||
var networkStatus = new NetworkStatusService(new DummyJsRuntime(true));
|
||||
await networkStatus.InitializeAsync();
|
||||
|
||||
var sp = new ServiceCollection().BuildServiceProvider();
|
||||
var authService = new AuthService(mockHttp, networkStatus, sp);
|
||||
authService.SetAuth("player1", "test-token");
|
||||
|
||||
var service = new UserDecksService(mockHttp, authService, networkStatus);
|
||||
|
||||
var saved = await service.SaveDeckAsync(new UserDeck
|
||||
{
|
||||
Id = Guid.Parse("3fa85f64-5717-4562-b3fc-2c963f66afa6"),
|
||||
Name = "Test GraphQL Deck",
|
||||
Cards = ["Card1", "Card2"],
|
||||
Divers = ["Diver1", "Diver2"],
|
||||
Notes = "Great deck",
|
||||
Season = "Season 1"
|
||||
});
|
||||
|
||||
Assert.That(saved, Is.Not.Null);
|
||||
Assert.That(saved!.Name, Is.EqualTo("Test GraphQL Deck"));
|
||||
Assert.That(saved.Season, Is.EqualTo("Season 1"));
|
||||
Assert.That(saved.Notes, Is.EqualTo("Great deck"));
|
||||
|
||||
var decks = await service.GetDecksAsync();
|
||||
Assert.That(decks, Is.Not.Empty);
|
||||
Assert.That(decks[0].Name, Is.EqualTo("Test GraphQL Deck"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DeckBuilder_CardSelection_ExcludesImmortalizedCards()
|
||||
{
|
||||
var sampleCards = new List<CardData>
|
||||
{
|
||||
new() { Name = "Card1", Category = "Agent" },
|
||||
new() { Name = "Card2", Category = "Spell" },
|
||||
new() { Name = "Card3", Category = "Immortalized" },
|
||||
new() { Name = "Card4", Category = "Agent", ImmortalizeFrom = "BaseCard" },
|
||||
new() { Name = "Card5", Category = "Artifact" }
|
||||
};
|
||||
|
||||
var selectableCards = sampleCards
|
||||
.Where(c => (c.Category is "Agent" or "Spell") && !c.IsImmortalized)
|
||||
.ToList();
|
||||
|
||||
Assert.That(selectableCards.Select(c => c.Name), Is.EquivalentTo(new[] { "Card1", "Card2" }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DeckBuilder_DiverSelection_ExcludesImmortalizedCardsAndDeckCards()
|
||||
{
|
||||
var sampleCards = new List<CardData>
|
||||
{
|
||||
new() { Name = "Card1", Category = "Agent" },
|
||||
new() { Name = "Card2", Category = "Spell" },
|
||||
new() { Name = "Card3", Category = "Immortalized" },
|
||||
new() { Name = "Card4", Category = "Agent", ImmortalizeFrom = "BaseCard" },
|
||||
new() { Name = "Card5", Category = "Agent" }
|
||||
};
|
||||
var deckCards = new List<string> { "Card1" };
|
||||
var otherDiver = "Card5";
|
||||
|
||||
var availableDivers = sampleCards
|
||||
.Where(c => (c.Category is "Agent" or "Spell") && !c.IsImmortalized)
|
||||
.Where(c => !deckCards.Contains(c.Name))
|
||||
.Where(c => c.Name != otherDiver)
|
||||
.ToList();
|
||||
|
||||
Assert.That(availableDivers.Select(c => c.Name), Is.EquivalentTo(new[] { "Card2" }));
|
||||
}
|
||||
|
||||
private class TestHttpMessageHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly Func<HttpRequestMessage, HttpResponseMessage> _handler;
|
||||
|
||||
public TestHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> handler)
|
||||
{
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(_handler(request));
|
||||
}
|
||||
}
|
||||
|
||||
private class DummyJsRuntime : IJSRuntime
|
||||
{
|
||||
private readonly bool _online;
|
||||
|
||||
public DummyJsRuntime(bool online)
|
||||
{
|
||||
_online = online;
|
||||
}
|
||||
|
||||
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object?[]? args)
|
||||
{
|
||||
if (identifier == "networkStatus.initialize" && typeof(TValue) == typeof(bool))
|
||||
return ValueTask.FromResult((TValue)(object)_online);
|
||||
return ValueTask.FromResult(default(TValue)!);
|
||||
}
|
||||
|
||||
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken,
|
||||
object?[]? args)
|
||||
{
|
||||
return InvokeAsync<TValue>(identifier, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,6 +271,35 @@ public class GraphQLTests
|
||||
Assert.That(saved, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AuthService_LoginAsync_SendsRequestToPort7266Url()
|
||||
{
|
||||
Uri? requestedUri = null;
|
||||
var mockHttp = new HttpClient(new TestHttpMessageHandler(req =>
|
||||
{
|
||||
requestedUri = req.RequestUri;
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(
|
||||
@"{""data"":{""login"":{""success"":true,""token"":""test_jwt"",""username"":""player1"",""message"":""Login successful""}}}")
|
||||
};
|
||||
}));
|
||||
|
||||
var networkStatus = new NetworkStatusService(new DummyJsRuntime(true));
|
||||
await networkStatus.InitializeAsync();
|
||||
|
||||
var sp = new ServiceCollection().BuildServiceProvider();
|
||||
var authService = new AuthService(mockHttp, networkStatus, sp);
|
||||
|
||||
var result = await authService.LoginAsync("player1", "password123");
|
||||
|
||||
Assert.That(result.Success, Is.True);
|
||||
Assert.That(authService.IsLoggedIn, Is.True);
|
||||
Assert.That(requestedUri, Is.Not.Null);
|
||||
Assert.That(requestedUri!.Port, Is.EqualTo(7266));
|
||||
Assert.That(requestedUri.ToString(), Does.Contain("7266/graphql"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AppDbContext_Model_ConfiguresRequiredEntities()
|
||||
{
|
||||
@@ -280,7 +309,7 @@ public class GraphQLTests
|
||||
var usersEntity = _db.Model.FindEntityType(typeof(User));
|
||||
Assert.That(usersEntity, Is.Not.Null);
|
||||
|
||||
var userDecksEntity = _db.Model.FindEntityType(typeof(API.Models.UserDeck));
|
||||
var userDecksEntity = _db.Model.FindEntityType(typeof(Model.UserDeck));
|
||||
Assert.That(userDecksEntity, Is.Not.Null);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user