using System.Net; using System.Net.Http.Json; using System.Text; using NeonSprawl.Server.Game.AbilityInput; using Xunit; namespace NeonSprawl.Server.Tests.Game.AbilityInput; public sealed class HotbarLoadoutApiTests { [Fact] public async Task GetHotbarLoadout_ShouldReturnEightSlots_WhenPlayerKnown() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.GetAsync("/game/players/dev-local-1/hotbar-loadout"); // Assert var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.Equal(HotbarLoadoutResponse.CurrentSchemaVersion, body!.SchemaVersion); Assert.Equal(HotbarLoadoutResponse.SlotCountV1, body.SlotCount); Assert.Equal(HotbarLoadoutResponse.SlotCountV1, body.Slots.Count); Assert.All(body.Slots, slot => Assert.Null(slot.AbilityId)); } [Fact] public async Task PostHotbarLoadout_ShouldPersistBindings_AndRoundTripInGet() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var post = await client.PostAsJsonAsync( "/game/players/dev-local-1/hotbar-loadout", new HotbarLoadoutUpdateRequest { SchemaVersion = HotbarLoadoutUpdateRequest.CurrentSchemaVersion, Slots = [ new HotbarSlotBindingJson { SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse }, new HotbarSlotBindingJson { SlotIndex = 3, AbilityId = PrototypeAbilityRegistry.PrototypeBurst }, ], }); // Assert var postBody = await post.Content.ReadFromJsonAsync(); var get = await client.GetFromJsonAsync("/game/players/dev-local-1/hotbar-loadout"); Assert.Equal(HttpStatusCode.OK, post.StatusCode); Assert.NotNull(postBody); Assert.True(postBody!.Updated); Assert.Null(postBody.ReasonCode); Assert.Equal(PrototypeAbilityRegistry.PrototypePulse, postBody.Loadout.Slots[0].AbilityId); Assert.Equal(PrototypeAbilityRegistry.PrototypeBurst, postBody.Loadout.Slots[3].AbilityId); Assert.NotNull(get); Assert.Equal(PrototypeAbilityRegistry.PrototypePulse, get!.Slots[0].AbilityId); Assert.Equal(PrototypeAbilityRegistry.PrototypeBurst, get.Slots[3].AbilityId); } [Fact] public async Task PostHotbarLoadout_ShouldDenyOutOfBounds_WithReasonCode() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var post = await client.PostAsJsonAsync( "/game/players/dev-local-1/hotbar-loadout", new HotbarLoadoutUpdateRequest { SchemaVersion = HotbarLoadoutUpdateRequest.CurrentSchemaVersion, Slots = [new HotbarSlotBindingJson { SlotIndex = 8, AbilityId = PrototypeAbilityRegistry.PrototypePulse }], }); // Assert var body = await post.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, post.StatusCode); Assert.NotNull(body); Assert.False(body!.Updated); Assert.Equal(HotbarLoadoutApi.ReasonSlotOutOfBounds, body.ReasonCode); } [Fact] public async Task PostHotbarLoadout_ShouldDenyNegativeSlot_WithReasonCode() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var post = await client.PostAsJsonAsync( "/game/players/dev-local-1/hotbar-loadout", new HotbarLoadoutUpdateRequest { SchemaVersion = HotbarLoadoutUpdateRequest.CurrentSchemaVersion, Slots = [new HotbarSlotBindingJson { SlotIndex = -1, AbilityId = PrototypeAbilityRegistry.PrototypePulse }], }); // Assert var body = await post.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, post.StatusCode); Assert.NotNull(body); Assert.False(body!.Updated); Assert.Equal(HotbarLoadoutApi.ReasonSlotOutOfBounds, body.ReasonCode); } [Fact] public async Task PostHotbarLoadout_ShouldDenyUnknownAbility_WithReasonCode() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var post = await client.PostAsJsonAsync( "/game/players/dev-local-1/hotbar-loadout", new HotbarLoadoutUpdateRequest { SchemaVersion = HotbarLoadoutUpdateRequest.CurrentSchemaVersion, Slots = [new HotbarSlotBindingJson { SlotIndex = 2, AbilityId = "missing_ability" }], }); // Assert var body = await post.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, post.StatusCode); Assert.NotNull(body); Assert.False(body!.Updated); Assert.Equal(HotbarLoadoutApi.ReasonUnknownAbility, body.ReasonCode); } [Fact] public async Task PostHotbarLoadout_ShouldDenyDuplicateSlots_WithReasonCode() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var post = await client.PostAsJsonAsync( "/game/players/dev-local-1/hotbar-loadout", new HotbarLoadoutUpdateRequest { SchemaVersion = HotbarLoadoutUpdateRequest.CurrentSchemaVersion, Slots = [ new HotbarSlotBindingJson { SlotIndex = 1, AbilityId = PrototypeAbilityRegistry.PrototypePulse }, new HotbarSlotBindingJson { SlotIndex = 1, AbilityId = PrototypeAbilityRegistry.PrototypeGuard }, ], }); // Assert var body = await post.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, post.StatusCode); Assert.NotNull(body); Assert.False(body!.Updated); Assert.Equal(HotbarLoadoutApi.ReasonDuplicateSlot, body.ReasonCode); } [Fact] public async Task PostHotbarLoadout_ShouldNormalizeAbilityIdCaseAndWhitespace_WhenKnownAbility() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var post = await client.PostAsJsonAsync( "/game/players/dev-local-1/hotbar-loadout", new HotbarLoadoutUpdateRequest { SchemaVersion = HotbarLoadoutUpdateRequest.CurrentSchemaVersion, Slots = [new HotbarSlotBindingJson { SlotIndex = 5, AbilityId = " PROTOTYPE_DASH " }], }); // Assert var body = await post.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, post.StatusCode); Assert.NotNull(body); Assert.True(body!.Updated); Assert.Equal(PrototypeAbilityRegistry.PrototypeDash, body.Loadout.Slots[5].AbilityId); } [Fact] public async Task PostHotbarLoadout_ShouldReturnBadRequest_WhenSchemaVersionWrong() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var post = await client.PostAsJsonAsync( "/game/players/dev-local-1/hotbar-loadout", new HotbarLoadoutUpdateRequest { SchemaVersion = 999, Slots = [new HotbarSlotBindingJson { SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse }], }); // Assert Assert.Equal(HttpStatusCode.BadRequest, post.StatusCode); } [Fact] public async Task PostHotbarLoadout_ShouldReturnBadRequest_WhenBodyMalformed() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsync( "/game/players/dev-local-1/hotbar-loadout", new StringContent("{\"schemaVersion\":1,\"slots\":[", Encoding.UTF8, "application/json")); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task GetHotbarLoadout_ShouldReturnNotFound_WhenPlayerUnknown() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.GetAsync("/game/players/missing-player/hotbar-loadout"); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task PostHotbarLoadout_ShouldReturnNotFound_WhenPlayerUnknown() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var post = await client.PostAsJsonAsync( "/game/players/missing-player/hotbar-loadout", new HotbarLoadoutUpdateRequest { SchemaVersion = HotbarLoadoutUpdateRequest.CurrentSchemaVersion, Slots = [new HotbarSlotBindingJson { SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse }], }); // Assert Assert.Equal(HttpStatusCode.NotFound, post.StatusCode); } }