242 lines
9.2 KiB
C#
242 lines
9.2 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text;
|
|
using Microsoft.AspNetCore.TestHost;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using NeonSprawl.Server.Game.AbilityInput;
|
|
using NeonSprawl.Server.Game.Npc;
|
|
using NeonSprawl.Server.Game.PositionState;
|
|
using NeonSprawl.Server.Game.Targeting;
|
|
using NeonSprawl.Server.Tests;
|
|
using Xunit;
|
|
|
|
namespace NeonSprawl.Server.Tests.Game.PositionState;
|
|
|
|
/// <summary>
|
|
/// NS-16 / NS-19: integration tests for <c>POST /game/players/{{id}}/move</c> on <see cref="NeonSprawl.Server.Game.PositionState.PositionStateApi"/>.
|
|
/// Isolated host per test (fresh in-memory store).
|
|
/// </summary>
|
|
public class MoveCommandApiTests
|
|
{
|
|
[Fact]
|
|
public async Task PostMove_ShouldPersistTargetAndIncrementSequence_WhenDevPlayerPostsValidMove()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var client = factory.CreateClient();
|
|
var cmd = new MoveCommandRequest
|
|
{
|
|
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
|
|
Target = new PositionVector { X = 1.5, Y = 0.0, Z = 2.25 },
|
|
};
|
|
var beforeMove = await client.GetFromJsonAsync<PositionStateResponse>("/game/players/dev-local-1/position");
|
|
|
|
// Act
|
|
var postResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/move", cmd);
|
|
|
|
// Assert
|
|
Assert.NotNull(beforeMove);
|
|
var seq0 = beforeMove!.Sequence;
|
|
|
|
Assert.Equal(HttpStatusCode.OK, postResponse.StatusCode);
|
|
var postBody = await postResponse.Content.ReadFromJsonAsync<PositionStateResponse>();
|
|
var afterMove = await client.GetFromJsonAsync<PositionStateResponse>("/game/players/dev-local-1/position");
|
|
Assert.NotNull(postBody);
|
|
Assert.Equal(PositionStateResponse.CurrentSchemaVersion, postBody!.SchemaVersion);
|
|
Assert.Equal("dev-local-1", postBody.PlayerId);
|
|
Assert.Equal(seq0 + 1, postBody.Sequence);
|
|
Assert.Equal(1.5, postBody.Position.X);
|
|
Assert.Equal(0.0, postBody.Position.Y);
|
|
Assert.Equal(2.25, postBody.Position.Z);
|
|
|
|
Assert.NotNull(afterMove);
|
|
Assert.Equal(postBody.Position.X, afterMove!.Position.X);
|
|
Assert.Equal(postBody.Position.Y, afterMove.Position.Y);
|
|
Assert.Equal(postBody.Position.Z, afterMove.Position.Z);
|
|
Assert.Equal(postBody.Sequence, afterMove.Sequence);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostMove_ShouldReturnNotFound_WhenPlayerIsUnknown()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var client = factory.CreateClient();
|
|
var cmd = new MoveCommandRequest
|
|
{
|
|
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
|
|
Target = new PositionVector { X = 0, Y = 0, Z = 0 },
|
|
};
|
|
|
|
// Act
|
|
var response = await client.PostAsJsonAsync("/game/players/unknown-player/move", cmd);
|
|
|
|
// Assert
|
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostMove_ShouldReturnBadRequest_WhenSchemaVersionIsWrong()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var client = factory.CreateClient();
|
|
var cmd = new MoveCommandRequest
|
|
{
|
|
SchemaVersion = 999,
|
|
Target = new PositionVector { X = 0, Y = 0, Z = 0 },
|
|
};
|
|
|
|
// Act
|
|
var response = await client.PostAsJsonAsync("/game/players/dev-local-1/move", cmd);
|
|
|
|
// Assert
|
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostMove_ShouldReturnBadRequest_WhenTargetIsMissing()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var client = factory.CreateClient();
|
|
var content = new StringContent(
|
|
"{\"schemaVersion\":1}",
|
|
Encoding.UTF8,
|
|
"application/json");
|
|
|
|
// Act
|
|
var response = await client.PostAsync("/game/players/dev-local-1/move", content);
|
|
|
|
// Assert
|
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostMove_ShouldReturn400WithReason_WhenHorizontalStepTooLarge()
|
|
{
|
|
// Arrange
|
|
|
|
await using var factory = new InMemoryWebApplicationFactory().WithWebHostBuilder(b =>
|
|
{
|
|
b.ConfigureTestServices(services =>
|
|
{
|
|
services.PostConfigure<GamePositionOptions>(o =>
|
|
{
|
|
o.MovementValidation.HorizontalStepEnabled = true;
|
|
o.MovementValidation.MaxHorizontalStep = 1.0;
|
|
});
|
|
});
|
|
});
|
|
var client = factory.CreateClient();
|
|
var cmd = new MoveCommandRequest
|
|
{
|
|
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
|
|
Target = new PositionVector { X = 0, Y = 0.9, Z = 0 },
|
|
};
|
|
// Act
|
|
var response = await client.PostAsJsonAsync("/game/players/dev-local-1/move", cmd);
|
|
|
|
// Assert
|
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
|
var body = await response.Content.ReadFromJsonAsync<MoveCommandRejectedResponse>();
|
|
Assert.NotNull(body);
|
|
Assert.Equal(MoveCommandRejectedResponse.CurrentSchemaVersion, body!.SchemaVersion);
|
|
Assert.Equal(MoveCommandReasonCodes.HorizontalStepExceeded, body.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostMove_ShouldReturn400WithReason_WhenVerticalStepTooLarge()
|
|
{
|
|
// Arrange
|
|
|
|
await using var factory = new InMemoryWebApplicationFactory().WithWebHostBuilder(b =>
|
|
{
|
|
b.ConfigureTestServices(services =>
|
|
{
|
|
services.PostConfigure<GamePositionOptions>(o => o.MovementValidation.MaxVerticalStep = 0.5);
|
|
});
|
|
});
|
|
var client = factory.CreateClient();
|
|
var cmd = new MoveCommandRequest
|
|
{
|
|
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
|
|
Target = new PositionVector { X = -5, Y = 2.0, Z = -5 },
|
|
};
|
|
// Act
|
|
var response = await client.PostAsJsonAsync("/game/players/dev-local-1/move", cmd);
|
|
|
|
// Assert
|
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
|
var body = await response.Content.ReadFromJsonAsync<MoveCommandRejectedResponse>();
|
|
Assert.NotNull(body);
|
|
Assert.Equal(MoveCommandReasonCodes.VerticalStepExceeded, body!.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostMove_ShouldReturn400WithoutReasonBody_WhenSchemaInvalid()
|
|
{
|
|
// Arrange
|
|
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var client = factory.CreateClient();
|
|
// Act
|
|
var response = await client.PostAsync(
|
|
"/game/players/dev-local-1/move",
|
|
new StringContent("{\"schemaVersion\":999,\"target\":{\"x\":0,\"y\":0,\"z\":0}}", Encoding.UTF8, "application/json"));
|
|
// Assert
|
|
|
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
|
var text = await response.Content.ReadAsStringAsync();
|
|
Assert.DoesNotContain("reasonCode", text, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostMove_ShouldClearAggroHolder_WhenPlayerMovesBeyondLeashRadius()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var client = factory.CreateClient();
|
|
var threatStore = factory.Services.GetRequiredService<IThreatStateStore>();
|
|
await client.PostAsJsonAsync(
|
|
"/game/players/dev-local-1/hotbar-loadout",
|
|
new HotbarLoadoutUpdateRequest
|
|
{
|
|
SchemaVersion = HotbarLoadoutUpdateRequest.CurrentSchemaVersion,
|
|
Slots = [new HotbarSlotBindingJson { SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse }],
|
|
});
|
|
await client.PostAsJsonAsync(
|
|
"/game/players/dev-local-1/target/select",
|
|
new TargetSelectRequest
|
|
{
|
|
SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
|
|
TargetId = PrototypeNpcRegistry.PrototypeNpcMeleeId,
|
|
});
|
|
var cast = await client.PostAsJsonAsync(
|
|
"/game/players/dev-local-1/ability-cast",
|
|
new AbilityCastRequest
|
|
{
|
|
SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
|
|
SlotIndex = 0,
|
|
AbilityId = PrototypeAbilityRegistry.PrototypePulse,
|
|
TargetId = PrototypeNpcRegistry.PrototypeNpcMeleeId,
|
|
});
|
|
cast.EnsureSuccessStatusCode();
|
|
threatStore.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var afterCast);
|
|
var cmd = new MoveCommandRequest
|
|
{
|
|
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
|
|
Target = new PositionVector { X = 12, Y = 0, Z = 12 },
|
|
};
|
|
|
|
// Act
|
|
var moveResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/move", cmd);
|
|
threatStore.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var afterMove);
|
|
|
|
// Assert
|
|
Assert.Equal(HttpStatusCode.OK, moveResponse.StatusCode);
|
|
Assert.Equal("dev-local-1", afterCast.AggroHolderPlayerId);
|
|
Assert.Null(afterMove.AggroHolderPlayerId);
|
|
}
|
|
}
|