NEO-29: normalize test formatting across server and client suites

Audit all test files and bring them in line with formatting/style expectations by adding explicit AAA markers across server test methods and fixing GDScript test lint/format issues.
pull/54/head
VinPropane 2026-04-25 22:57:05 -04:00
parent 3dccab8edb
commit 71af7a5942
9 changed files with 229 additions and 52 deletions

View File

@ -52,7 +52,13 @@ class HoldFirstThenAutoTransport:
) -> Error:
request_count += 1
last_body = request_data
var body_bytes: PackedByteArray = '{"schemaVersion":1,"updated":true,"loadout":{"schemaVersion":1,"playerId":"dev-local-1","slotCount":8,"slots":[]}}'.to_utf8_buffer()
var body_bytes: PackedByteArray = (
'{"schemaVersion":1,"updated":true,"loadout":'
+ (
'{"schemaVersion":1,"playerId":"dev-local-1","slotCount":8,"slots":[]}}'
. to_utf8_buffer()
)
)
if _first:
_first = false
call_deferred("_emit", body_bytes)
@ -109,7 +115,10 @@ func test_bind_slot_posts_expected_payload() -> void:
transport.enqueue(
HTTPRequest.RESULT_SUCCESS,
200,
'{"schemaVersion":1,"updated":true,"loadout":{"schemaVersion":1,"playerId":"dev-local-1","slotCount":8,"slots":[]}}'
(
'{"schemaVersion":1,"updated":true,"loadout":'
+ '{"schemaVersion":1,"playerId":"dev-local-1","slotCount":8,"slots":[]}}'
)
)
var c := _make_client(transport)
c.call("request_bind_slot", 3, "prototype_burst")
@ -130,7 +139,10 @@ func test_denied_update_emits_reason_signal() -> void:
transport.enqueue(
HTTPRequest.RESULT_SUCCESS,
200,
'{"schemaVersion":1,"updated":false,"reasonCode":"unknown_ability","loadout":{"schemaVersion":1,"playerId":"dev-local-1","slotCount":8,"slots":[]}}'
(
'{"schemaVersion":1,"updated":false,"reasonCode":"unknown_ability","loadout":'
+ '{"schemaVersion":1,"playerId":"dev-local-1","slotCount":8,"slots":[]}}'
)
)
var c := _make_client(transport)
monitor_signals(c)

View File

@ -9,35 +9,50 @@ public class HorizontalReachTests
public void HorizontalDistance_IgnoresY()
{
// Y is not a parameter; identical X/Z → zero horizontal separation regardless of implied height difference.
// Act
// Arrange
var d = HorizontalReach.HorizontalDistance(1, 2, 1, 2);
// Assert
Assert.Equal(0, d);
}
[Fact]
public void IsWithinHorizontalRadius_AtExactBoundary_ReturnsTrue()
{
{ // Assert
// Act
// Arrange
Assert.True(HorizontalReach.IsWithinHorizontalRadius(0, 0, 3, 0, 3));
}
[Fact]
public void IsWithinHorizontalRadius_JustInside_ReturnsTrue()
{
{ // Act
// Arrange
const double radius = 3.0;
const double eps = 1e-10;
// Assert
Assert.True(HorizontalReach.IsWithinHorizontalRadius(0, 0, radius - eps, 0, radius));
}
[Fact]
public void IsWithinHorizontalRadius_JustOutside_ReturnsFalse()
{
{ // Act
// Arrange
const double radius = 3.0;
const double eps = 1e-7;
// Assert
Assert.False(HorizontalReach.IsWithinHorizontalRadius(0, 0, radius + eps, 0, radius));
}
[Fact]
public void IsWithinHorizontalRadius_NegativeRadius_ThrowsArgumentOutOfRange()
{
{ // Assert
// Act
// Arrange
Assert.Throws<ArgumentOutOfRangeException>(() =>
HorizontalReach.IsWithinHorizontalRadius(0, 0, 0, 0, -1.0));
}

View File

@ -10,11 +10,14 @@ public class InteractablesWorldApiTests
{
[Fact]
public async Task GetInteractables_ShouldReturnOrderedDescriptors_WithSchemaV1()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.GetAsync("/game/world/interactables");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<InteractablesListResponse>();

View File

@ -12,7 +12,9 @@ public class InteractionApiTests
{
[Fact]
public async Task PostInteract_ShouldAllow_WhenExactlyAtHorizontalRadius()
{
{ // Act
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var move = new MoveCommandRequest
@ -20,6 +22,7 @@ public class InteractionApiTests
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
Target = new PositionVector { X = 3, Y = 0, Z = 0 },
};
// Assert
Assert.Equal(HttpStatusCode.OK, (await client.PostAsJsonAsync("/game/players/dev-local-1/move", move)).StatusCode);
var response = await client.PostAsJsonAsync(
@ -38,7 +41,8 @@ public class InteractionApiTests
[Fact]
public async Task PostInteract_ShouldAllow_WhenPlayerInHorizontalRange()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var move = new MoveCommandRequest
@ -46,7 +50,9 @@ public class InteractionApiTests
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
Target = new PositionVector { X = 0, Y = 0.9, Z = 0 },
};
// Act
var moveResp = await client.PostAsJsonAsync("/game/players/dev-local-1/move", move);
// Assert
Assert.Equal(HttpStatusCode.OK, moveResp.StatusCode);
var req = new InteractionRequest
@ -69,7 +75,8 @@ public class InteractionApiTests
[Fact]
public async Task PostInteract_ShouldDenyOutOfRange_WhenPlayerSeededFarFromTerminal()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var req = new InteractionRequest
@ -77,8 +84,10 @@ public class InteractionApiTests
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId,
};
// Act
var response = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", req);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<InteractionResponse>();
@ -89,7 +98,8 @@ public class InteractionApiTests
[Fact]
public async Task PostInteract_ShouldDenyUnknownInteractable_WhenIdNotInRegistry()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var req = new InteractionRequest
@ -97,8 +107,10 @@ public class InteractionApiTests
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
InteractableId = "no_such_thing",
};
// Act
var response = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", req);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<InteractionResponse>();
@ -109,7 +121,9 @@ public class InteractionApiTests
[Fact]
public async Task PostInteract_ShouldBeCaseInsensitive_ForInteractableId()
{
{ // Act
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var move = new MoveCommandRequest
@ -117,6 +131,7 @@ public class InteractionApiTests
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
Target = new PositionVector { X = 0, Y = 0, Z = 0 },
};
// Assert
Assert.Equal(HttpStatusCode.OK, (await client.PostAsJsonAsync("/game/players/dev-local-1/move", move)).StatusCode);
var req = new InteractionRequest
@ -135,7 +150,8 @@ public class InteractionApiTests
[Fact]
public async Task PostInteract_ShouldReturnBadRequest_WhenSchemaVersionWrong()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var req = new InteractionRequest
@ -143,30 +159,36 @@ public class InteractionApiTests
SchemaVersion = 999,
InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId,
};
// Act
var response = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", req);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task PostInteract_ShouldReturnBadRequest_WhenInteractableIdMissing()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var content = new StringContent(
$"{{\"schemaVersion\":{InteractionRequest.CurrentSchemaVersion}}}",
Encoding.UTF8,
"application/json");
// Act
var response = await client.PostAsync("/game/players/dev-local-1/interact", content);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task PostInteract_ShouldReturnBadRequest_WhenInteractableIdWhitespaceOnly()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var req = new InteractionRequest
@ -174,15 +196,18 @@ public class InteractionApiTests
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
InteractableId = " \t ",
};
// Act
var response = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", req);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task PostInteract_ShouldReturnNotFound_WhenPlayerUnknown()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var req = new InteractionRequest
@ -190,17 +215,21 @@ public class InteractionApiTests
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId,
};
// Act
var response = await client.PostAsJsonAsync("/game/players/nobody-here/interact", req);
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task PostInteract_ShouldReflectNewPosition_AfterMove()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var far = await client.PostAsJsonAsync(
"/game/players/dev-local-1/interact",
@ -210,6 +239,7 @@ public class InteractionApiTests
InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId,
});
var farBody = await far.Content.ReadFromJsonAsync<InteractionResponse>();
// Assert
Assert.False(farBody!.Allowed);
Assert.Equal(InteractionApi.ReasonOutOfRange, farBody.ReasonCode);
@ -233,7 +263,9 @@ public class InteractionApiTests
[Fact]
public async Task PostInteract_ShouldAllowResourceNode_WhenPlayerInRangeOfResourceAnchor()
{
{ // Act
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var move = new MoveCommandRequest
@ -241,6 +273,7 @@ public class InteractionApiTests
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
Target = new PositionVector { X = 12, Y = 0.9, Z = -6 },
};
// Assert
Assert.Equal(HttpStatusCode.OK, (await client.PostAsJsonAsync("/game/players/dev-local-1/move", move)).StatusCode);
var response = await client.PostAsJsonAsync(
@ -260,9 +293,11 @@ public class InteractionApiTests
[Fact]
public async Task PostInteract_ShouldDenyOutOfRange_ForResourceNode_WhenPlayerFar()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.PostAsJsonAsync(
"/game/players/dev-local-1/interact",
new InteractionRequest
@ -270,6 +305,7 @@ public class InteractionApiTests
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId,
});
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<InteractionResponse>();

View File

@ -110,7 +110,8 @@ public class MoveCommandApiTests
[Fact]
public async Task PostMove_ShouldReturn400WithReason_WhenHorizontalStepTooLarge()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory().WithWebHostBuilder(b =>
{
b.ConfigureTestServices(services =>
@ -128,8 +129,10 @@ public class MoveCommandApiTests
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>();
@ -140,7 +143,8 @@ public class MoveCommandApiTests
[Fact]
public async Task PostMove_ShouldReturn400WithReason_WhenVerticalStepTooLarge()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory().WithWebHostBuilder(b =>
{
b.ConfigureTestServices(services =>
@ -154,8 +158,10 @@ public class MoveCommandApiTests
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>();
@ -165,12 +171,15 @@ public class MoveCommandApiTests
[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();

View File

@ -23,79 +23,106 @@ public class MoveCommandValidationTests
[Fact]
public void TryValidate_ShouldAllow_WhenHorizontalExactlyAtMax()
{
{ // Act
// Arrange
var from = new PositionSnapshot(0, 0, 0, 0);
var to = new PositionVector { X = 5, Y = 0, Z = 0 };
// Assert
Assert.True(MoveCommandValidation.TryValidate(from, to, Rules(5, 10), out var code));
Assert.Equal("", code);
}
[Fact]
public void TryValidate_ShouldRejectHorizontal_WhenJustOutsideMax()
{
{ // Act
// Arrange
var from = new PositionSnapshot(0, 0, 0, 0);
var to = new PositionVector { X = 5 + 1e-6, Y = 0, Z = 0 };
// Assert
Assert.False(MoveCommandValidation.TryValidate(from, to, Rules(5, 10), out var code));
Assert.Equal(MoveCommandReasonCodes.HorizontalStepExceeded, code);
}
[Fact]
public void TryValidate_ShouldAllow_WhenVerticalExactlyAtMax()
{
{ // Act
// Arrange
var from = new PositionSnapshot(0, 1, 0, 0);
var to = new PositionVector { X = 0, Y = 2.5, Z = 0 };
// Assert
Assert.True(MoveCommandValidation.TryValidate(from, to, Rules(10, 1.5), out _));
}
[Fact]
public void TryValidate_ShouldRejectVertical_WhenJustOutsideMax()
{
{ // Act
// Arrange
var from = new PositionSnapshot(0, 1, 0, 0);
var to = new PositionVector { X = 0, Y = 2.5 + 1e-9, Z = 0 };
// Assert
Assert.False(MoveCommandValidation.TryValidate(from, to, Rules(10, 1.5), out var code));
Assert.Equal(MoveCommandReasonCodes.VerticalStepExceeded, code);
}
[Fact]
public void TryValidate_ShouldPreferHorizontalReason_WhenBothWouldFail()
{
{ // Act
// Arrange
var from = new PositionSnapshot(0, 0, 0, 0);
var to = new PositionVector { X = 100, Y = 100, Z = 0 };
// Assert
Assert.False(MoveCommandValidation.TryValidate(from, to, Rules(1, 1), out var code));
Assert.Equal(MoveCommandReasonCodes.HorizontalStepExceeded, code);
}
[Fact]
public void TryValidate_ShouldAllow_WhenHorizontalDisabled()
{
{ // Act
// Arrange
var from = new PositionSnapshot(0, 0, 0, 0);
var to = new PositionVector { X = 1000, Y = 0, Z = 1000 };
// Assert
Assert.True(MoveCommandValidation.TryValidate(from, to, Rules(1, 10, horizontalEnabled: false), out var code));
Assert.Equal("", code);
}
[Fact]
public void TryValidate_ShouldIgnoreBounds_WhenDistrictDisabled()
{
{ // Act
// Arrange
var from = new PositionSnapshot(0, 0, 0, 0);
var to = new PositionVector { X = -50, Y = 0, Z = 0 };
// Assert
Assert.True(MoveCommandValidation.TryValidate(from, to, Rules(100, 100, bounds: false), out _));
}
[Fact]
public void TryValidate_ShouldRejectOutOfBounds_WhenDistrictEnabled()
{
{ // Act
// Arrange
var from = new PositionSnapshot(5, 5, 5, 0);
var to = new PositionVector { X = 5, Y = 5, Z = 11 };
// Assert
Assert.False(MoveCommandValidation.TryValidate(from, to, Rules(20, 20, bounds: true), out var code));
Assert.Equal(MoveCommandReasonCodes.OutOfBounds, code);
}
[Fact]
public void TryValidate_ShouldAllowOnBoundsSurface_WhenDistrictEnabled()
{
{ // Act
// Arrange
var from = new PositionSnapshot(5, 5, 5, 0);
var to = new PositionVector { X = 10, Y = 10, Z = 10 };
// Assert
Assert.True(MoveCommandValidation.TryValidate(from, to, Rules(20, 20, bounds: true), out _));
}
}

View File

@ -12,7 +12,8 @@ public sealed class MoveStreamApiTests
{
[Fact]
public async Task PostMoveStream_ShouldApplyChainAndIncrementSequence_WhenTwoSmallSteps()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory().WithWebHostBuilder(b =>
{
b.ConfigureTestServices(services =>
@ -25,7 +26,9 @@ public sealed class MoveStreamApiTests
});
});
var client = factory.CreateClient();
// Act
var before = await client.GetFromJsonAsync<PositionStateResponse>("/game/players/dev-local-1/position");
// Assert
Assert.NotNull(before);
var seq0 = before!.Sequence;
@ -52,7 +55,8 @@ public sealed class MoveStreamApiTests
[Fact]
public async Task PostMoveStream_ShouldReturnNotFound_WhenPlayerIsUnknown()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var body = new MoveStreamRequest
@ -60,30 +64,36 @@ public sealed class MoveStreamApiTests
SchemaVersion = MoveStreamRequest.CurrentSchemaVersion,
Targets = [new PositionVector { X = 0, Y = 0.9, Z = 0 }],
};
// Act
var response = await client.PostAsJsonAsync("/game/players/unknown-player/move-stream", body);
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task PostMoveStream_ShouldReturn400_WhenTargetsEmpty()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var content = new StringContent(
"{\"schemaVersion\":1,\"targets\":[]}",
Encoding.UTF8,
"application/json");
// Act
var response = await client.PostAsync("/game/players/dev-local-1/move-stream", content);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task PostMoveStream_ShouldReturn400WithReason_WhenSecondStepExceedsHorizontalLimit()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory().WithWebHostBuilder(b =>
{
b.ConfigureTestServices(services =>
@ -105,8 +115,10 @@ public sealed class MoveStreamApiTests
new PositionVector { X = -4.0, Y = 0.9, Z = -5.0 },
],
};
// Act
var response = await client.PostAsJsonAsync("/game/players/dev-local-1/move-stream", body);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var rej = await response.Content.ReadFromJsonAsync<MoveCommandRejectedResponse>();

View File

@ -8,15 +8,21 @@ public class PlayerTargetStateReaderTests
{
[Fact]
public void ComputeValidity_ShouldReturnInvalidTarget_WhenLockIdNotInRegistry()
{
{ // Act
// Arrange
var snap = new PositionSnapshot(0, 0, 0, 0);
// Assert
Assert.Equal(TargetValidity.InvalidTarget, PlayerTargetStateReader.ComputeValidity("not_in_registry", in snap));
}
[Fact]
public void ComputeValidity_ShouldReturnNone_WhenLockNull()
{
{ // Act
// Arrange
var snap = new PositionSnapshot(0, 0, 0, 0);
// Assert
Assert.Equal(TargetValidity.None, PlayerTargetStateReader.ComputeValidity(null, in snap));
}
@ -24,7 +30,10 @@ public class PlayerTargetStateReaderTests
public void ComputeValidity_AtExactLockRadius_ReturnsOk()
{
// Alpha anchor XZ (-3, -3), lockRadius 6 → horizontal distance 6 from (3, -3) is on boundary (inclusive).
// Act
// Arrange
var snap = new PositionSnapshot(3, 0, -3, 0);
// Assert
Assert.Equal(
TargetValidity.Ok,
PlayerTargetStateReader.ComputeValidity(PrototypeTargetRegistry.PrototypeTargetAlphaId, in snap));
@ -32,9 +41,12 @@ public class PlayerTargetStateReaderTests
[Fact]
public void ComputeValidity_JustOutsideLockRadius_ReturnsOutOfRange()
{
{ // Act
// Arrange
const double eps = 1e-6;
var snap = new PositionSnapshot(3 + eps, 0, -3, 0);
// Assert
Assert.Equal(
TargetValidity.OutOfRange,
PlayerTargetStateReader.ComputeValidity(PrototypeTargetRegistry.PrototypeTargetAlphaId, in snap));
@ -45,7 +57,10 @@ public class PlayerTargetStateReaderTests
{
// NEO-24 post-merge: alpha (-3,-3) r=6 and beta (3,3) r=6 overlap at origin so Tab can
// flip without locomotion. Origin is ~4.24 m from each anchor (< 6 m).
// Act
// Arrange
var snap = new PositionSnapshot(0, 0, 0, 0);
// Assert
Assert.Equal(
TargetValidity.Ok,
PlayerTargetStateReader.ComputeValidity(PrototypeTargetRegistry.PrototypeTargetAlphaId, in snap));

View File

@ -12,11 +12,14 @@ public class TargetingApiTests
{
[Fact]
public async Task GetTarget_ShouldReturnNone_WhenNoLock()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.GetAsync("/game/players/dev-local-1/target");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<PlayerTargetStateResponse>();
Assert.NotNull(body);
@ -29,9 +32,11 @@ public class TargetingApiTests
[Fact]
public async Task PostSelect_ShouldApplyLock_WhenInRange()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.PostAsJsonAsync(
"/game/players/dev-local-1/target/select",
@ -40,6 +45,7 @@ public class TargetingApiTests
SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
});
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();
@ -53,9 +59,11 @@ public class TargetingApiTests
[Fact]
public async Task PostSelect_ShouldDenyUnknownTarget_WithReasonCode()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.PostAsJsonAsync(
"/game/players/dev-local-1/target/select",
@ -64,6 +72,7 @@ public class TargetingApiTests
SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
TargetId = "not_a_real_target",
});
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();
@ -76,9 +85,11 @@ public class TargetingApiTests
[Fact]
public async Task PostSelect_ShouldDenyOutOfRange_WhenBetaFarFromSpawn()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.PostAsJsonAsync(
"/game/players/dev-local-1/target/select",
@ -87,6 +98,7 @@ public class TargetingApiTests
SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
TargetId = PrototypeTargetRegistry.PrototypeTargetBetaId,
});
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();
@ -102,8 +114,10 @@ public class TargetingApiTests
// NEO-24 follow-up #5: without the hint, spawn's stored position (-5, -5) denies beta (r=6,
// anchor (3, 3)) — ~11.3 m out. With an advisory `positionHint` pinning the capsule inside
// beta's ring, the server must accept. This is exactly the race-free path the client uses.
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.PostAsJsonAsync(
"/game/players/dev-local-1/target/select",
@ -113,6 +127,7 @@ public class TargetingApiTests
TargetId = PrototypeTargetRegistry.PrototypeTargetBetaId,
PositionHint = new PositionVector { X = 3.0, Y = 0.5, Z = 5.0 },
});
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();
@ -127,8 +142,10 @@ public class TargetingApiTests
{
// Hint that places the capsule outside beta's ring must deny — hint is advisory only;
// it does not bypass the radius check.
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.PostAsJsonAsync(
"/game/players/dev-local-1/target/select",
@ -138,6 +155,7 @@ public class TargetingApiTests
TargetId = PrototypeTargetRegistry.PrototypeTargetBetaId,
PositionHint = new PositionVector { X = 50.0, Y = 0.0, Z = 50.0 },
});
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();
@ -151,10 +169,13 @@ public class TargetingApiTests
{
// Hint must be purely advisory for the range check. Confirm the stored `PositionState`
// is untouched by a select-with-hint by reading it back through `GET /position`.
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var before = await client.GetFromJsonAsync<PositionStateResponse>("/game/players/dev-local-1/position");
// Assert
Assert.NotNull(before);
Assert.Equal(
@ -178,9 +199,12 @@ public class TargetingApiTests
[Fact]
public async Task PostSelect_ShouldClearLock_WhenTargetIdOmitted()
{
{ // Act
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Assert
Assert.Equal(
HttpStatusCode.OK,
@ -209,9 +233,12 @@ public class TargetingApiTests
[Fact]
public async Task GetTarget_ShouldShowOutOfRange_AfterMoveWithoutSelect()
{
{ // Act
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Assert
Assert.Equal(
HttpStatusCode.OK,
@ -240,19 +267,24 @@ public class TargetingApiTests
[Fact]
public async Task GetTarget_ShouldReturnNotFound_WhenPlayerUnknown()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.GetAsync("/game/players/missing-player/target");
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task PostSelect_ShouldReturnNotFound_WhenPlayerUnknown()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.PostAsJsonAsync(
"/game/players/missing-player/target/select",
@ -261,15 +293,18 @@ public class TargetingApiTests
SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
});
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task PostSelect_ShouldReturnBadRequest_WhenSchemaVersionWrong()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.PostAsJsonAsync(
"/game/players/dev-local-1/target/select",
@ -278,15 +313,18 @@ public class TargetingApiTests
SchemaVersion = 999,
TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
});
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task PostSelect_ShouldReturnBadRequest_WhenTargetIdWhitespaceOnly()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.PostAsJsonAsync(
"/game/players/dev-local-1/target/select",
@ -295,15 +333,18 @@ public class TargetingApiTests
SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
TargetId = " \t ",
});
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task PostSelect_ShouldBeCaseInsensitive_ForTargetId()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.PostAsJsonAsync(
"/game/players/dev-local-1/target/select",
@ -312,6 +353,7 @@ public class TargetingApiTests
SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
TargetId = " PROTOTYPE_TARGET_ALPHA ",
});
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();
@ -322,7 +364,8 @@ public class TargetingApiTests
[Fact]
public async Task PostSelect_ShouldStayIdempotent_WhenSameTargetReselect()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
@ -331,8 +374,10 @@ public class TargetingApiTests
SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
};
// Act
var first = await client.PostAsJsonAsync("/game/players/dev-local-1/target/select", req);
var second = await client.PostAsJsonAsync("/game/players/dev-local-1/target/select", req);
// Assert
Assert.Equal(HttpStatusCode.OK, first.StatusCode);
Assert.Equal(HttpStatusCode.OK, second.StatusCode);
var b1 = await first.Content.ReadFromJsonAsync<TargetSelectResponse>();
@ -342,9 +387,11 @@ public class TargetingApiTests
[Fact]
public async Task PostSelect_ShouldDenyOutOfRange_WhenReselectingSameLockWhileOutOfRange()
{
{ // Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
await client.PostAsJsonAsync(
"/game/players/dev-local-1/target/select",
@ -368,6 +415,7 @@ public class TargetingApiTests
SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
});
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();