NEO-29: cover hotbar edge paths and add Bruno requests
Add server/client tests for duplicate-slot, schema, missing-player, normalization, and busy/error handling paths, and include Bruno requests for the new hotbar routes with happy and representative failure checks.pull/54/head
parent
acde9e8145
commit
b92b86a407
|
|
@ -0,0 +1,17 @@
|
||||||
|
meta {
|
||||||
|
name: hotbar-loadout get missing player
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/game/players/missing-player/hotbar-loadout
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("status 404", function () {
|
||||||
|
expect(res.getStatus()).to.equal(404);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
meta {
|
||||||
|
name: hotbar-loadout get
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/game/players/dev-local-1/hotbar-loadout
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("status 200", function () {
|
||||||
|
expect(res.getStatus()).to.equal(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("schema and slots present", function () {
|
||||||
|
const body = res.getBody();
|
||||||
|
expect(body.schemaVersion).to.equal(1);
|
||||||
|
expect(body.slotCount).to.equal(8);
|
||||||
|
expect(Array.isArray(body.slots)).to.equal(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
meta {
|
||||||
|
name: hotbar-loadout post bad schema
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/game/players/dev-local-1/hotbar-loadout
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"schemaVersion": 999,
|
||||||
|
"slots": [
|
||||||
|
{
|
||||||
|
"slotIndex": 0,
|
||||||
|
"abilityId": "prototype_pulse"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("status 400", function () {
|
||||||
|
expect(res.getStatus()).to.equal(400);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
meta {
|
||||||
|
name: hotbar-loadout post bind
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/game/players/dev-local-1/hotbar-loadout
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"slots": [
|
||||||
|
{
|
||||||
|
"slotIndex": 0,
|
||||||
|
"abilityId": "prototype_pulse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slotIndex": 3,
|
||||||
|
"abilityId": "prototype_burst"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("status 200", function () {
|
||||||
|
expect(res.getStatus()).to.equal(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("updated true and payload shape", function () {
|
||||||
|
const body = res.getBody();
|
||||||
|
expect(body.schemaVersion).to.equal(1);
|
||||||
|
expect(body.updated).to.equal(true);
|
||||||
|
expect(body.loadout.slotCount).to.equal(8);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,35 @@ class MockHttpTransport:
|
||||||
return OK
|
return OK
|
||||||
|
|
||||||
|
|
||||||
|
class HoldFirstThenAutoTransport:
|
||||||
|
extends Node
|
||||||
|
signal request_completed(
|
||||||
|
result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
|
||||||
|
)
|
||||||
|
var request_count: int = 0
|
||||||
|
var last_body: String = ""
|
||||||
|
var _first: bool = true
|
||||||
|
|
||||||
|
func request(
|
||||||
|
_url: String,
|
||||||
|
_custom_headers: PackedStringArray = PackedStringArray(),
|
||||||
|
_method: HTTPClient.Method = HTTPClient.METHOD_GET,
|
||||||
|
request_data: String = ""
|
||||||
|
) -> 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()
|
||||||
|
if _first:
|
||||||
|
_first = false
|
||||||
|
call_deferred("_emit", body_bytes)
|
||||||
|
else:
|
||||||
|
request_completed.emit(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), body_bytes)
|
||||||
|
return OK
|
||||||
|
|
||||||
|
func _emit(body_bytes: PackedByteArray) -> void:
|
||||||
|
request_completed.emit(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), body_bytes)
|
||||||
|
|
||||||
|
|
||||||
func _make_client(transport: Node) -> Node:
|
func _make_client(transport: Node) -> Node:
|
||||||
var c: Node = HotbarClient.new()
|
var c: Node = HotbarClient.new()
|
||||||
c.set("injected_http", transport)
|
c.set("injected_http", transport)
|
||||||
|
|
@ -107,3 +136,35 @@ func test_denied_update_emits_reason_signal() -> void:
|
||||||
monitor_signals(c)
|
monitor_signals(c)
|
||||||
c.call("request_bind_slot", 1, "not_real")
|
c.call("request_bind_slot", 1, "not_real")
|
||||||
assert_signal(c).is_emitted("loadout_update_denied")
|
assert_signal(c).is_emitted("loadout_update_denied")
|
||||||
|
|
||||||
|
|
||||||
|
func test_bind_slot_while_busy_is_ignored() -> void:
|
||||||
|
var transport := HoldFirstThenAutoTransport.new()
|
||||||
|
var c := _make_client(transport)
|
||||||
|
c.call("request_bind_slot", 1, "prototype_pulse")
|
||||||
|
c.call("request_bind_slot", 2, "prototype_guard")
|
||||||
|
await get_tree().process_frame
|
||||||
|
await get_tree().process_frame
|
||||||
|
assert_that(transport.request_count).is_equal(1)
|
||||||
|
var parsed: Variant = JSON.parse_string(transport.last_body)
|
||||||
|
assert_that(parsed is Dictionary).is_true()
|
||||||
|
var slots: Array = (parsed as Dictionary).get("slots", [])
|
||||||
|
assert_that(int((slots[0] as Dictionary).get("slotIndex", -1))).is_equal(1)
|
||||||
|
|
||||||
|
|
||||||
|
func test_http_failure_does_not_mutate_cached_loadout() -> void:
|
||||||
|
var transport := MockHttpTransport.new()
|
||||||
|
transport.enqueue(HTTPRequest.RESULT_CANT_CONNECT, 0, "")
|
||||||
|
var c := _make_client(transport)
|
||||||
|
c.call("request_sync_from_server")
|
||||||
|
var cached: Dictionary = c.call("cached_loadout")
|
||||||
|
assert_that(cached.is_empty()).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
func test_non_json_error_response_keeps_cached_loadout_empty() -> void:
|
||||||
|
var transport := MockHttpTransport.new()
|
||||||
|
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 500, "oops")
|
||||||
|
var c := _make_client(transport)
|
||||||
|
c.call("request_sync_from_server")
|
||||||
|
var cached: Dictionary = c.call("cached_loadout")
|
||||||
|
assert_that(cached.is_empty()).is_true()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using System.Text;
|
||||||
using NeonSprawl.Server.Game.AbilityInput;
|
using NeonSprawl.Server.Game.AbilityInput;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -97,4 +98,106 @@ public sealed class HotbarLoadoutApiTests
|
||||||
Assert.False(body!.Updated);
|
Assert.False(body!.Updated);
|
||||||
Assert.Equal(HotbarLoadoutApi.ReasonUnknownAbility, body.ReasonCode);
|
Assert.Equal(HotbarLoadoutApi.ReasonUnknownAbility, body.ReasonCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostHotbarLoadout_ShouldDenyDuplicateSlots_WithReasonCode()
|
||||||
|
{
|
||||||
|
await using var factory = new InMemoryWebApplicationFactory();
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
|
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.Equal(HttpStatusCode.OK, post.StatusCode);
|
||||||
|
|
||||||
|
var body = await post.Content.ReadFromJsonAsync<HotbarLoadoutUpdateResponse>();
|
||||||
|
Assert.NotNull(body);
|
||||||
|
Assert.False(body!.Updated);
|
||||||
|
Assert.Equal(HotbarLoadoutApi.ReasonDuplicateSlot, body.ReasonCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostHotbarLoadout_ShouldNormalizeAbilityIdCaseAndWhitespace_WhenKnownAbility()
|
||||||
|
{
|
||||||
|
await using var factory = new InMemoryWebApplicationFactory();
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
|
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.Equal(HttpStatusCode.OK, post.StatusCode);
|
||||||
|
|
||||||
|
var body = await post.Content.ReadFromJsonAsync<HotbarLoadoutUpdateResponse>();
|
||||||
|
Assert.NotNull(body);
|
||||||
|
Assert.True(body!.Updated);
|
||||||
|
Assert.Equal(PrototypeAbilityRegistry.PrototypeDash, body.Loadout.Slots[5].AbilityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostHotbarLoadout_ShouldReturnBadRequest_WhenSchemaVersionWrong()
|
||||||
|
{
|
||||||
|
await using var factory = new InMemoryWebApplicationFactory();
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
|
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.Equal(HttpStatusCode.BadRequest, post.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostHotbarLoadout_ShouldReturnBadRequest_WhenBodyMalformed()
|
||||||
|
{
|
||||||
|
await using var factory = new InMemoryWebApplicationFactory();
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
|
var response = await client.PostAsync(
|
||||||
|
"/game/players/dev-local-1/hotbar-loadout",
|
||||||
|
new StringContent("{\"schemaVersion\":1,\"slots\":[", Encoding.UTF8, "application/json"));
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetHotbarLoadout_ShouldReturnNotFound_WhenPlayerUnknown()
|
||||||
|
{
|
||||||
|
await using var factory = new InMemoryWebApplicationFactory();
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
|
var response = await client.GetAsync("/game/players/missing-player/hotbar-loadout");
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostHotbarLoadout_ShouldReturnNotFound_WhenPlayerUnknown()
|
||||||
|
{
|
||||||
|
await using var factory = new InMemoryWebApplicationFactory();
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
|
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.Equal(HttpStatusCode.NotFound, post.StatusCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue