This commit is contained in:
6d486f49
2026-08-17 00:45:08 -04:00
parent 3a2f112a73
commit 1d6207fbfe
154 changed files with 126007 additions and 47385 deletions
+337
View File
@@ -0,0 +1,337 @@
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<AppDbContext>()
.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<Query>()
.AddMutationType<Mutation>()
.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<Exception>(async () =>
{
await query.GetNote(_db, unauthenticatedAccessor, _tokenService, "Alina, the Overflowing Cup");
});
Assert.ThrowsAsync<Exception>(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<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);
}
}
}