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() .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 { 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 { 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 { "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 _handler; public TestHttpMessageHandler(Func handler) { _handler = handler; } protected override Task 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 InvokeAsync(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 InvokeAsync(string identifier, CancellationToken cancellationToken, object?[]? args) { return InvokeAsync(identifier, args); } } }