using System.Net; using System.Net.Http; using System.Net.Http.Json; using System.Text; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using NeonSprawl.Server.Game.Mastery; using NeonSprawl.Server.Game.Skills; using Xunit; namespace NeonSprawl.Server.Tests.Game.Mastery; public sealed class MasteryFixtureApiTests { private const string DevPlayer = "dev-local-1"; private const string FixturePath = $"/game/players/{DevPlayer}/__dev/mastery-fixture"; private static MasteryFixtureRequest ValidFixture( bool resetPerkState = true, Dictionary? skillXp = null) => new MasteryFixtureRequest { SchemaVersion = MasteryFixtureRequest.CurrentSchemaVersion, ResetPerkState = resetPerkState, SkillXp = skillXp ?? new Dictionary { ["salvage"] = 0 }, }; private static async Task ArrangeSalvageProgressedWithTier1PickAsync(HttpClient client) { var grant = await client.PostAsJsonAsync( $"/game/players/{DevPlayer}/skill-progression", new SkillProgressionGrantRequest { SchemaVersion = SkillProgressionGrantRequest.CurrentSchemaVersion, SkillId = "salvage", Amount = 100, SourceKind = "activity", }); Assert.Equal(HttpStatusCode.OK, grant.StatusCode); var pick = await client.PostAsJsonAsync( $"/game/players/{DevPlayer}/perk-state", new PerkBranchSelectRequest { SchemaVersion = PerkBranchSelectRequest.CurrentSchemaVersion, SkillId = "salvage", TierIndex = 1, BranchId = "scrap_efficiency", }); Assert.Equal(HttpStatusCode.OK, pick.StatusCode); } [Fact] public async Task PostMasteryFixture_ShouldReturnNotFound_WhenRouteNotRegistered() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.WithWebHostBuilder(b => { b.UseEnvironment("Production"); b.UseSetting("Game:EnableMasteryFixtureApi", "false"); }).CreateClient(); // Act var response = await client.PostAsJsonAsync(FixturePath, ValidFixture()); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task PostMasteryFixture_ShouldReturnBadRequest_WhenBodyNull() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsync( FixturePath, new StringContent(string.Empty, Encoding.UTF8, "application/json")); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostMasteryFixture_ShouldReturnBadRequest_WhenSchemaVersionMismatch() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var request = ValidFixture(); request = new MasteryFixtureRequest { SchemaVersion = MasteryFixtureRequest.CurrentSchemaVersion + 99, ResetPerkState = request.ResetPerkState, SkillXp = request.SkillXp, }; // Act var response = await client.PostAsJsonAsync(FixturePath, request); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostMasteryFixture_ShouldReturnBadRequest_WhenSkillXpNegative() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsJsonAsync( FixturePath, ValidFixture(skillXp: new Dictionary { ["salvage"] = -1 })); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostMasteryFixture_ShouldReturnNotFound_WhenPlayerUnknown() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsJsonAsync( "/game/players/missing-player/__dev/mastery-fixture", ValidFixture()); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task PostMasteryFixture_ShouldResetSalvageXpAndPerkState_ThenLevelTooLowDenies() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await ArrangeSalvageProgressedWithTier1PickAsync(client); // Act var reset = await client.PostAsJsonAsync(FixturePath, ValidFixture()); var deny = await client.PostAsJsonAsync( $"/game/players/{DevPlayer}/perk-state", new PerkBranchSelectRequest { SchemaVersion = PerkBranchSelectRequest.CurrentSchemaVersion, SkillId = "salvage", TierIndex = 1, BranchId = "scrap_efficiency", }); // Assert Assert.Equal(HttpStatusCode.OK, reset.StatusCode); var resetBody = await reset.Content.ReadFromJsonAsync(); Assert.NotNull(resetBody); Assert.True(resetBody!.Applied); var progression = await client.GetFromJsonAsync( $"/game/players/{DevPlayer}/skill-progression"); var salvage = Assert.Single(progression!.Skills!, static s => s.Id == "salvage"); Assert.Equal(0, salvage.Xp); Assert.Equal(1, salvage.Level); var perkGet = await client.GetFromJsonAsync( $"/game/players/{DevPlayer}/perk-state"); Assert.Empty(perkGet!.BranchPicks); Assert.Equal(HttpStatusCode.OK, deny.StatusCode); var envelope = await deny.Content.ReadFromJsonAsync(); Assert.NotNull(envelope); Assert.False(envelope!.Selected); Assert.Equal(PerkUnlockReasonCodes.LevelTooLow, envelope.ReasonCode); } }