using System.Net; using API.Data; using API.GraphQL; using API.Services; using HotChocolate.Execution; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.JSInterop; using Model; using Web.Services; namespace Tests; [TestFixture] public class GraphQLTests { [SetUp] public void Setup() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"TestChronoDb_{Guid.NewGuid()}") .Options; _db = new AppDbContext(options); _tokenService = new TokenService(); } [TearDown] public void TearDown() { _db.Dispose(); } private AppDbContext _db = null!; private ITokenService _tokenService = null!; private class MockHttpContextAccessor : IHttpContextAccessor { public HttpContext? HttpContext { get; set; } } private IHttpContextAccessor CreateMockHttpContextAccessor(string? token = null) { var context = new DefaultHttpContext(); if (!string.IsNullOrEmpty(token)) context.Request.Headers["Authorization"] = $"Bearer {token}"; return new MockHttpContextAccessor { HttpContext = context }; } [Test] public async Task HotChocolateSchema_BuildsSuccessfully_AndContainsQueriesAndMutations() { var schema = await new ServiceCollection() .AddSingleton(_tokenService) .AddHttpContextAccessor() .AddGraphQLServer() .AddQueryType() .AddMutationType() .BuildSchemaAsync(); Assert.That(schema, Is.Not.Null); var schemaString = schema.ToString(); Assert.That(schemaString, Does.Contain("login(")); Assert.That(schemaString, Does.Contain("note(")); Assert.That(schemaString, Does.Contain("saveNote(")); Assert.That(schemaString, Does.Contain("deleteNote(")); Assert.That(schemaString, Does.Contain("decks")); Assert.That(schemaString, Does.Contain("saveDeck(")); Assert.That(schemaString, Does.Contain("deleteDeck(")); } [Test] public async Task Mutation_Login_AutoCreatesNewUser_AndReturnsValidToken() { var mutation = new Mutation(); var result = await mutation.Login(_db, _tokenService, "newplayer", "password123"); Assert.That(result.Success, Is.True); Assert.That(result.Username, Is.EqualTo("newplayer")); Assert.That(result.Token, Is.Not.Null.And.Not.Empty); var validatedUser = _tokenService.ValidateToken(result.Token); Assert.That(validatedUser, Is.EqualTo("newplayer")); } [Test] public async Task Mutation_Login_ExistingUser_ValidatesPassword() { _db.Users.Add(new User { Username = "existinguser", Password = "correctpassword", CreatedAt = DateTime.UtcNow }); await _db.SaveChangesAsync(); var mutation = new Mutation(); // 1. Invalid password var failedResult = await mutation.Login(_db, _tokenService, "existinguser", "wrongpassword"); Assert.That(failedResult.Success, Is.False); Assert.That(failedResult.Token, Is.Null); // 2. Valid password var successResult = await mutation.Login(_db, _tokenService, "existinguser", "correctpassword"); Assert.That(successResult.Success, Is.True); Assert.That(successResult.Username, Is.EqualTo("existinguser")); Assert.That(successResult.Token, Is.Not.Null.And.Not.Empty); } [Test] public void Query_And_Mutation_WithoutAuth_ThrowsException() { var query = new Query(); var mutation = new Mutation(); var unauthenticatedAccessor = CreateMockHttpContextAccessor(null); Assert.ThrowsAsync(async () => { await query.GetNote(_db, unauthenticatedAccessor, _tokenService, "Alina, the Overflowing Cup"); }); Assert.ThrowsAsync(async () => { await mutation.SaveNote(_db, unauthenticatedAccessor, _tokenService, "Alina, the Overflowing Cup", "note"); }); } [Test] public async Task CardNotes_UserIsolation_EnsuresUserOnlySeesAndWritesOwnNote() { var mutation = new Mutation(); var query = new Query(); var aliceLogin = await mutation.Login(_db, _tokenService, "alice", "alicepass"); var bobLogin = await mutation.Login(_db, _tokenService, "bob", "bobpass"); var aliceAccessor = CreateMockHttpContextAccessor(aliceLogin.Token); var bobAccessor = CreateMockHttpContextAccessor(bobLogin.Token); var cardName = "Chronos, Time Shifter"; // 1. Alice saves note on card var aliceSaved = await mutation.SaveNote(_db, aliceAccessor, _tokenService, cardName, "Alice's Secret Combo"); Assert.That(aliceSaved.Note, Is.EqualTo("Alice's Secret Combo")); Assert.That(aliceSaved.Username, Is.EqualTo("alice")); // 2. Bob queries same card -> gets empty note var bobQuery1 = await query.GetNote(_db, bobAccessor, _tokenService, cardName); Assert.That(bobQuery1, Is.Not.Null); Assert.That(bobQuery1!.Note, Is.EqualTo("")); Assert.That(bobQuery1.Username, Is.EqualTo("bob")); // 3. Bob saves his own note on the same card var bobSaved = await mutation.SaveNote(_db, bobAccessor, _tokenService, cardName, "Bob's Fast Aggro Note"); Assert.That(bobSaved.Note, Is.EqualTo("Bob's Fast Aggro Note")); Assert.That(bobSaved.Username, Is.EqualTo("bob")); // 4. Verify Alice still sees only her note var aliceQuery = await query.GetNote(_db, aliceAccessor, _tokenService, cardName); Assert.That(aliceQuery!.Note, Is.EqualTo("Alice's Secret Combo")); Assert.That(aliceQuery.Username, Is.EqualTo("alice")); // 5. Verify Bob sees only his note var bobQuery2 = await query.GetNote(_db, bobAccessor, _tokenService, cardName); Assert.That(bobQuery2!.Note, Is.EqualTo("Bob's Fast Aggro Note")); Assert.That(bobQuery2.Username, Is.EqualTo("bob")); // 6. Alice deletes her note -> Bob's note is untouched var aliceDeleted = await mutation.DeleteNote(_db, aliceAccessor, _tokenService, cardName); Assert.That(aliceDeleted, Is.True); var aliceQueryAfterDelete = await query.GetNote(_db, aliceAccessor, _tokenService, cardName); Assert.That(aliceQueryAfterDelete!.Note, Is.EqualTo("")); var bobQueryAfterAliceDelete = await query.GetNote(_db, bobAccessor, _tokenService, cardName); Assert.That(bobQueryAfterAliceDelete!.Note, Is.EqualTo("Bob's Fast Aggro Note")); } [Test] public async Task Deck_MutationsAndQueries_WorkWithAuthentication() { var mutation = new Mutation(); var query = new Query(); var token = _tokenService.GenerateToken("player1"); var accessor = CreateMockHttpContextAccessor(token); var deckId = Guid.NewGuid(); var input = new UserDeckInput(deckId, "My Custom Deck", ["Card1", "Card2"], ["Diver1"], "Some notes", "Season 1"); var savedDeck = await mutation.SaveDeck(_db, accessor, _tokenService, input); Assert.That(savedDeck.Id, Is.EqualTo(deckId)); Assert.That(savedDeck.Name, Is.EqualTo("My Custom Deck")); var retrieved = await query.GetDeck(_db, accessor, _tokenService, deckId); Assert.That(retrieved, Is.Not.Null); Assert.That(retrieved!.Name, Is.EqualTo("My Custom Deck")); var allDecks = await query.GetDecks(_db, accessor, _tokenService); Assert.That(allDecks.Any(d => d.Id == deckId), Is.True); var deleted = await mutation.DeleteDeck(_db, accessor, _tokenService, deckId); Assert.That(deleted, Is.True); } [Test] public async Task CardNotesGraphQLService_WhenOffline_ReturnsEmptyWithoutCallingNetwork() { var mockHttp = new HttpClient(new TestHttpMessageHandler(_ => throw new InvalidOperationException("Network should not be called when offline"))); var networkStatus = new NetworkStatusService(new DummyJsRuntime(false)); await networkStatus.InitializeAsync(); var sp = new ServiceCollection().BuildServiceProvider(); var authService = new AuthService(mockHttp, networkStatus, sp); authService.SetAuth("player1", "dummy_token"); var service = new CardNotesGraphQLService(mockHttp, networkStatus, authService); var note = await service.GetNoteAsync("Alina, the Overflowing Cup"); Assert.That(note, Is.EqualTo(string.Empty)); var saved = await service.SaveNoteAsync("Alina, the Overflowing Cup", "test note"); Assert.That(saved, Is.False); } [Test] public async Task CardNotesGraphQLService_WhenOnlineAndAuthenticated_SendsAuthorizationHeader() { string? capturedAuthHeader = null; var mockHttp = new HttpClient(new TestHttpMessageHandler(req => { capturedAuthHeader = req.Headers.Authorization?.ToString(); var content = req.Content?.ReadAsStringAsync().Result ?? ""; if (content.Contains("GetCardNote")) return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent( @"{""data"":{""note"":{""username"":""player1"",""cardName"":""Alina, the Overflowing Cup"",""note"":""Awesome!""}}}") }; if (content.Contains("SaveCardNote")) return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent( @"{""data"":{""saveNote"":{""username"":""player1"",""cardName"":""Alina, the Overflowing Cup"",""note"":""Saved!""}}}") }; if (content.Contains("__typename")) return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(@"{""data"":{""__typename"":""Query""}}") }; 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", "my_test_token_123"); var service = new CardNotesGraphQLService(mockHttp, networkStatus, authService); var isApiOnline = await service.IsApiOnlineAsync(); Assert.That(isApiOnline, Is.True); var note = await service.GetNoteAsync("Alina, the Overflowing Cup"); Assert.That(note, Is.EqualTo("Awesome!")); Assert.That(capturedAuthHeader, Is.EqualTo("Bearer my_test_token_123")); var saved = await service.SaveNoteAsync("Alina, the Overflowing Cup", "Saved!"); Assert.That(saved, Is.True); } [Test] public void AppDbContext_Model_ConfiguresRequiredEntities() { var cardNotesEntity = _db.Model.FindEntityType(typeof(CardNote)); Assert.That(cardNotesEntity, Is.Not.Null); var usersEntity = _db.Model.FindEntityType(typeof(User)); Assert.That(usersEntity, Is.Not.Null); var userDecksEntity = _db.Model.FindEntityType(typeof(API.Models.UserDeck)); Assert.That(userDecksEntity, Is.Not.Null); } [Test] public void AppDbContext_Migrations_ArePresentInAssembly() { var migrationsAssembly = typeof(AppDbContext).Assembly; var migrationTypes = migrationsAssembly.GetTypes() .Where(t => typeof(Microsoft.EntityFrameworkCore.Migrations.Migration).IsAssignableFrom(t) && !t.IsAbstract) .ToList(); Assert.That(migrationTypes, Is.Not.Empty, "Expected EF Core migrations to be present in API project."); Assert.That(migrationTypes.Any(m => m.Name.Contains("InitialCreate")), Is.True); } 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); } } }