Files
ChronoCCG/Chrono/Tests/DeckBuilderServiceTests.cs
6d486f49 ffaab59585 ...
2026-09-09 17:19:03 -04:00

291 lines
11 KiB
C#

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);
}
}
}