238 lines
8.0 KiB
GDScript
238 lines
8.0 KiB
GDScript
extends GdUnitTestSuite
|
|
|
|
# main.gd sets base_url/dev_player_id on AbilityCastClient from the same authority block as
|
|
# HotbarLoadoutClient (NEO-31).
|
|
# NEO-30: Slice 3 ability_cast_denied dev warning includes reasonCode from AbilityCastResponse;
|
|
# server deny hook sites are documented in AbilityCastApi.cs (gdlint: script doc lines <= 100 cols).
|
|
|
|
const CastClient := preload("res://scripts/ability_cast_client.gd")
|
|
|
|
|
|
class MockHttpTransport:
|
|
extends Node
|
|
signal request_completed(
|
|
result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
|
|
)
|
|
var last_url: String = ""
|
|
var last_body: String = ""
|
|
var last_method: HTTPClient.Method = HTTPClient.METHOD_GET
|
|
var _queue: Array[Dictionary] = []
|
|
|
|
func enqueue(result: int, code: int, body: String) -> void:
|
|
_queue.append({"result": result, "code": code, "body": body})
|
|
|
|
func request(
|
|
url: String,
|
|
_custom_headers: PackedStringArray = PackedStringArray(),
|
|
method: HTTPClient.Method = HTTPClient.METHOD_GET,
|
|
request_data: String = ""
|
|
) -> Error:
|
|
last_url = url
|
|
last_method = method
|
|
last_body = request_data
|
|
if _queue.is_empty():
|
|
return ERR_UNAVAILABLE
|
|
var r: Dictionary = _queue.pop_front()
|
|
request_completed.emit(
|
|
r["result"], r["code"], PackedStringArray(), str(r["body"]).to_utf8_buffer()
|
|
)
|
|
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_json := '{"schemaVersion":1,"accepted":true}'
|
|
var body_bytes: PackedByteArray = body_json.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:
|
|
var c: Node = CastClient.new()
|
|
c.set("injected_http", transport)
|
|
auto_free(transport)
|
|
auto_free(c)
|
|
add_child(c)
|
|
return c
|
|
|
|
|
|
func test_request_cast_posts_expected_payload_with_null_target() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"schemaVersion":1,"accepted":true}')
|
|
var c := _make_client(transport)
|
|
# Act
|
|
var started: bool = bool(c.call("request_cast", 2, "prototype_guard", null))
|
|
# Assert
|
|
assert_that(started).is_true()
|
|
assert_that(transport.last_url).contains("/ability-cast")
|
|
assert_that(transport.last_method).is_equal(HTTPClient.METHOD_POST)
|
|
var parsed: Variant = JSON.parse_string(transport.last_body)
|
|
assert_that(parsed is Dictionary).is_true()
|
|
var body: Dictionary = parsed
|
|
assert_that(int(body.get("schemaVersion", 0))).is_equal(1)
|
|
assert_that(int(body.get("slotIndex", -1))).is_equal(2)
|
|
assert_that(body.get("abilityId")).is_equal("prototype_guard")
|
|
assert_that(body.get("targetId", "missing")).is_equal(null)
|
|
|
|
|
|
func test_request_cast_posts_target_id_string_when_set() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"schemaVersion":1,"accepted":true}')
|
|
var c := _make_client(transport)
|
|
# Act
|
|
var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", "prototype_target_alpha"))
|
|
# Assert
|
|
assert_that(started).is_true()
|
|
var parsed: Variant = JSON.parse_string(transport.last_body)
|
|
var body: Dictionary = parsed
|
|
assert_that(body.get("targetId")).is_equal("prototype_target_alpha")
|
|
|
|
|
|
func test_request_cast_returns_false_when_http_request_fails_to_start() -> void:
|
|
# Arrange
|
|
# Empty queue → transport.request returns ERR_UNAVAILABLE (same as failed start).
|
|
var transport := MockHttpTransport.new()
|
|
var c := _make_client(transport)
|
|
# Act
|
|
var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", null))
|
|
# Assert
|
|
assert_that(started).is_false()
|
|
|
|
|
|
func test_request_cast_while_busy_is_ignored() -> void:
|
|
# Arrange
|
|
var transport := HoldFirstThenAutoTransport.new()
|
|
var c := _make_client(transport)
|
|
# Act
|
|
var first_started: bool = bool(c.call("request_cast", 0, "prototype_pulse", null))
|
|
var second_started: bool = bool(c.call("request_cast", 1, "prototype_guard", null))
|
|
await get_tree().process_frame
|
|
await get_tree().process_frame
|
|
# Assert
|
|
assert_that(first_started).is_true()
|
|
assert_that(second_started).is_false()
|
|
assert_that(transport.request_count).is_equal(1)
|
|
var parsed: Variant = JSON.parse_string(transport.last_body)
|
|
var body: Dictionary = parsed
|
|
assert_that(int(body.get("slotIndex", -1))).is_equal(0)
|
|
|
|
|
|
func test_cast_result_received_emits_false_with_reason_on_denied() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS,
|
|
200,
|
|
'{"schemaVersion":1,"accepted":false,"reasonCode":"invalid_target"}'
|
|
)
|
|
var c := _make_client(transport)
|
|
var got: Array = []
|
|
c.connect(
|
|
"cast_result_received",
|
|
func(accepted: bool, reason: String, resolution: Dictionary) -> void:
|
|
got.append([accepted, reason, resolution])
|
|
)
|
|
# Act
|
|
var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", "prototype_target_alpha"))
|
|
# Assert
|
|
assert_that(started).is_true()
|
|
assert_that(got.size()).is_equal(1)
|
|
assert_that(bool(got[0][0])).is_false()
|
|
assert_that(str(got[0][1])).is_equal("invalid_target")
|
|
assert_that((got[0][2] as Dictionary).is_empty()).is_true()
|
|
|
|
|
|
func test_cast_result_received_emits_true_with_empty_reason_on_accepted() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"schemaVersion":1,"accepted":true}')
|
|
var c := _make_client(transport)
|
|
var got: Array = []
|
|
c.connect(
|
|
"cast_result_received",
|
|
func(accepted: bool, reason: String, resolution: Dictionary) -> void:
|
|
got.append([accepted, reason, resolution])
|
|
)
|
|
# Act
|
|
var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", null))
|
|
# Assert
|
|
assert_that(started).is_true()
|
|
assert_that(got.size()).is_equal(1)
|
|
assert_that(bool(got[0][0])).is_true()
|
|
assert_that(str(got[0][1])).is_equal("")
|
|
assert_that((got[0][2] as Dictionary).is_empty()).is_true()
|
|
|
|
|
|
func test_cast_result_received_emits_combat_resolution_on_accept() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS,
|
|
200,
|
|
(
|
|
'{"schemaVersion":1,"accepted":true,"combatResolution":'
|
|
+ '{"abilityId":"prototype_pulse","targetId":"prototype_target_alpha",'
|
|
+ '"damageDealt":25,"targetRemainingHp":75,"targetDefeated":false}}'
|
|
)
|
|
)
|
|
var c := _make_client(transport)
|
|
var got: Array = []
|
|
c.connect(
|
|
"cast_result_received",
|
|
func(accepted: bool, reason: String, resolution: Dictionary) -> void:
|
|
got.append([accepted, reason, resolution])
|
|
)
|
|
# Act
|
|
var started: bool = bool(
|
|
c.call("request_cast", 0, "prototype_pulse", "prototype_target_alpha")
|
|
)
|
|
# Assert
|
|
assert_that(started).is_true()
|
|
assert_that(got.size()).is_equal(1)
|
|
assert_that(bool(got[0][0])).is_true()
|
|
var resolution: Dictionary = got[0][2]
|
|
assert_that(int(resolution.get("damageDealt", 0))).is_equal(25)
|
|
assert_that(int(resolution.get("targetRemainingHp", 0))).is_equal(75)
|
|
assert_that(bool(resolution.get("targetDefeated", false))).is_false()
|
|
|
|
|
|
func test_parse_cast_response_json_maps_combat_resolution() -> void:
|
|
# Arrange
|
|
var body := (
|
|
'{"schemaVersion":1,"accepted":true,"combatResolution":'
|
|
+ '{"abilityId":"prototype_pulse","targetId":"prototype_target_alpha",'
|
|
+ '"damageDealt":25,"targetRemainingHp":75,"targetDefeated":false}}'
|
|
)
|
|
# Act
|
|
var parsed: Variant = CastClient.parse_cast_response_json(body)
|
|
# Assert
|
|
assert_that(parsed is Dictionary).is_true()
|
|
var data: Dictionary = parsed
|
|
assert_that(bool(data.get("accepted", false))).is_true()
|
|
var resolution: Dictionary = data.get("resolution", {}) as Dictionary
|
|
assert_that(int(resolution.get("damageDealt", 0))).is_equal(25)
|