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 AbilityCastApiTests { private static async Task BindSlot0PulseAsync(HttpClient client) { 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 }], }); post.EnsureSuccessStatusCode(); } [Fact] public async Task PostAbilityCast_ShouldAccept_WhenLoadoutMatches() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = null, }); var body = await response.Content.ReadFromJsonAsync(); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.True(body!.Accepted); Assert.Null(body.ReasonCode); } [Fact] public async Task PostAbilityCast_ShouldAccept_WithTargetId_WhenLoadoutMatches() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = "prototype_target_alpha", }); var body = await response.Content.ReadFromJsonAsync(); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.True(body!.Accepted); } [Fact] public async Task PostAbilityCast_ShouldDenySlotUnbound_WhenSlotEmpty() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 2, AbilityId = PrototypeAbilityRegistry.PrototypeGuard, TargetId = null, }); var body = await response.Content.ReadFromJsonAsync(); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(AbilityCastApi.ReasonSlotUnbound, body.ReasonCode); } [Fact] public async Task PostAbilityCast_ShouldDenyLoadoutMismatch_WhenAbilityDoesNotMatchSlot() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypeBurst, TargetId = null, }); var body = await response.Content.ReadFromJsonAsync(); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(AbilityCastApi.ReasonLoadoutMismatch, body.ReasonCode); } [Fact] public async Task PostAbilityCast_ShouldDenySlotOutOfBounds() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 8, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = null, }); var body = await response.Content.ReadFromJsonAsync(); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(AbilityCastApi.ReasonSlotOutOfBounds, body.ReasonCode); } [Fact] public async Task PostAbilityCast_ShouldDenyUnknownAbility_WhenAbilityIdNotInRegistry() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = "not_a_real_ability", TargetId = null, }); var body = await response.Content.ReadFromJsonAsync(); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(AbilityCastApi.ReasonUnknownAbility, body.ReasonCode); } [Fact] public async Task PostAbilityCast_ShouldReturnBadRequest_WhenBodyMalformed() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var malformed = new StringContent( "{\"schemaVersion\":1,\"slotIndex\":", Encoding.UTF8, "application/json"); // Act var response = await client.PostAsync("/game/players/dev-local-1/ability-cast", malformed); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostAbilityCast_ShouldReturnBadRequest_WhenSchemaVersionWrong() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var request = new AbilityCastRequest { SchemaVersion = 999, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, }; // Act var response = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", request); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostAbilityCast_ShouldReturnNotFound_WhenPlayerUnknown() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var request = new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, }; // Act var response = await client.PostAsJsonAsync("/game/players/missing-player/ability-cast", request); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } }