483 lines
18 KiB
C#
483 lines
18 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text;
|
|
using NeonSprawl.Server.Game.AbilityInput;
|
|
using NeonSprawl.Server.Game.PositionState;
|
|
using NeonSprawl.Server.Game.Targeting;
|
|
using Xunit;
|
|
|
|
namespace NeonSprawl.Server.Tests.Game.AbilityInput;
|
|
|
|
/// <summary>HTTP integration tests for <see cref="AbilityCastApi"/>.</summary>
|
|
/// <remarks>
|
|
/// NEO-30: every JSON deny (<see cref="AbilityCastResponse.Accepted"/> <c>false</c>) asserts a non-whitespace
|
|
/// <see cref="AbilityCastResponse.ReasonCode"/> — matches Slice 3 <c>ability_cast_denied</c> payload expectations;
|
|
/// authoritative hook-site comments live on <see cref="AbilityCastApi"/> (cast route: <c>MapAbilityCastApi</c> extension in the same source file).
|
|
/// </remarks>
|
|
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();
|
|
}
|
|
|
|
private static async Task LockPrototypeTargetAlphaAsync(HttpClient client)
|
|
{
|
|
var response = await client.PostAsJsonAsync(
|
|
"/game/players/dev-local-1/target/select",
|
|
new TargetSelectRequest
|
|
{
|
|
SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
|
|
TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
|
|
});
|
|
response.EnsureSuccessStatusCode();
|
|
var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();
|
|
Assert.NotNull(body);
|
|
Assert.True(body!.SelectionApplied);
|
|
}
|
|
|
|
private static async Task TeleportDevPlayerAsync(HttpClient client, double x, double y, double z)
|
|
{
|
|
var response = await client.PostAsJsonAsync(
|
|
"/game/players/dev-local-1/move-stream",
|
|
new MoveStreamRequest
|
|
{
|
|
SchemaVersion = MoveStreamRequest.CurrentSchemaVersion,
|
|
Targets = [new PositionVector { X = x, Y = y, Z = z }],
|
|
});
|
|
response.EnsureSuccessStatusCode();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostAbilityCast_ShouldAccept_WhenLoadoutMatchesAndLockInRange()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var client = factory.CreateClient();
|
|
await BindSlot0PulseAsync(client);
|
|
await LockPrototypeTargetAlphaAsync(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 = PrototypeTargetRegistry.PrototypeTargetAlphaId,
|
|
});
|
|
|
|
// Assert
|
|
var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
|
|
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);
|
|
await LockPrototypeTargetAlphaAsync(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",
|
|
});
|
|
|
|
// Assert
|
|
var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
Assert.NotNull(body);
|
|
Assert.True(body!.Accepted);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostAbilityCast_ShouldDenyInvalidTarget_WhenTargetIdMissing()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var client = factory.CreateClient();
|
|
await BindSlot0PulseAsync(client);
|
|
await LockPrototypeTargetAlphaAsync(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,
|
|
});
|
|
|
|
// Assert
|
|
var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
Assert.NotNull(body);
|
|
Assert.False(body!.Accepted);
|
|
Assert.Equal(AbilityCastApi.ReasonInvalidTarget, body.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostAbilityCast_ShouldDenyInvalidTarget_WhenUnknownTargetId()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var client = factory.CreateClient();
|
|
await BindSlot0PulseAsync(client);
|
|
await LockPrototypeTargetAlphaAsync(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 = "not_a_registered_combat_target",
|
|
});
|
|
|
|
// Assert
|
|
var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
Assert.NotNull(body);
|
|
Assert.False(body!.Accepted);
|
|
Assert.Equal(AbilityCastApi.ReasonInvalidTarget, body.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostAbilityCast_ShouldDenyInvalidTarget_WhenTargetDoesNotMatchServerLock()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var client = factory.CreateClient();
|
|
await BindSlot0PulseAsync(client);
|
|
await LockPrototypeTargetAlphaAsync(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 = PrototypeTargetRegistry.PrototypeTargetBetaId,
|
|
});
|
|
|
|
// Assert
|
|
var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
Assert.NotNull(body);
|
|
Assert.False(body!.Accepted);
|
|
Assert.Equal(AbilityCastApi.ReasonInvalidTarget, body.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostAbilityCast_ShouldDenyInvalidTarget_WhenNoServerLock()
|
|
{
|
|
// 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 = PrototypeTargetRegistry.PrototypeTargetAlphaId,
|
|
});
|
|
|
|
// Assert
|
|
var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
Assert.NotNull(body);
|
|
Assert.False(body!.Accepted);
|
|
Assert.Equal(AbilityCastApi.ReasonInvalidTarget, body.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostAbilityCast_ShouldDenyOutOfRange_WhenAuthoritativePositionTooFarFromLock()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var client = factory.CreateClient();
|
|
await BindSlot0PulseAsync(client);
|
|
await LockPrototypeTargetAlphaAsync(client);
|
|
await TeleportDevPlayerAsync(client, 50.0, 0.9, 50.0);
|
|
|
|
// Act
|
|
var response = await client.PostAsJsonAsync(
|
|
"/game/players/dev-local-1/ability-cast",
|
|
new AbilityCastRequest
|
|
{
|
|
SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
|
|
SlotIndex = 0,
|
|
AbilityId = PrototypeAbilityRegistry.PrototypePulse,
|
|
TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
|
|
});
|
|
|
|
// Assert
|
|
var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
Assert.NotNull(body);
|
|
Assert.False(body!.Accepted);
|
|
Assert.Equal(AbilityCastApi.ReasonOutOfRange, body.ReasonCode);
|
|
}
|
|
|
|
[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,
|
|
});
|
|
|
|
// Assert
|
|
var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
|
|
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,
|
|
});
|
|
|
|
// Assert
|
|
var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
|
|
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,
|
|
});
|
|
|
|
// Assert
|
|
var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
|
|
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,
|
|
});
|
|
|
|
// Assert
|
|
var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
|
|
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);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostAbilityCast_ShouldDenyOnCooldown_WhenRepeatedWhileCooling()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var client = factory.CreateClient();
|
|
await BindSlot0PulseAsync(client);
|
|
await LockPrototypeTargetAlphaAsync(client);
|
|
var cast = new AbilityCastRequest
|
|
{
|
|
SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
|
|
SlotIndex = 0,
|
|
AbilityId = PrototypeAbilityRegistry.PrototypePulse,
|
|
TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
|
|
};
|
|
|
|
// Act
|
|
var first = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast);
|
|
var second = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast);
|
|
|
|
// Assert
|
|
var body1 = await first.Content.ReadFromJsonAsync<AbilityCastResponse>();
|
|
var body2 = await second.Content.ReadFromJsonAsync<AbilityCastResponse>();
|
|
Assert.Equal(HttpStatusCode.OK, first.StatusCode);
|
|
Assert.Equal(HttpStatusCode.OK, second.StatusCode);
|
|
Assert.NotNull(body1);
|
|
Assert.NotNull(body2);
|
|
Assert.True(body1!.Accepted);
|
|
Assert.False(body2!.Accepted);
|
|
Assert.Equal(AbilityCastApi.ReasonOnCooldown, body2.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostAbilityCast_ShouldAcceptAgain_WhenCooldownElapsed()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var client = factory.CreateClient();
|
|
Assert.NotNull(factory.FakeClock);
|
|
await BindSlot0PulseAsync(client);
|
|
await LockPrototypeTargetAlphaAsync(client);
|
|
var cast = new AbilityCastRequest
|
|
{
|
|
SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
|
|
SlotIndex = 0,
|
|
AbilityId = PrototypeAbilityRegistry.PrototypePulse,
|
|
TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
|
|
};
|
|
|
|
// Act
|
|
var first = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast);
|
|
factory.FakeClock!.Advance(AbilityPrototypeCooldown.GlobalDuration + TimeSpan.FromMilliseconds(50));
|
|
var second = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast);
|
|
|
|
// Assert
|
|
var body1 = await first.Content.ReadFromJsonAsync<AbilityCastResponse>();
|
|
var body2 = await second.Content.ReadFromJsonAsync<AbilityCastResponse>();
|
|
Assert.Equal(HttpStatusCode.OK, first.StatusCode);
|
|
Assert.Equal(HttpStatusCode.OK, second.StatusCode);
|
|
Assert.NotNull(body1);
|
|
Assert.NotNull(body2);
|
|
Assert.True(body1!.Accepted);
|
|
Assert.True(body2!.Accepted);
|
|
}
|
|
}
|