neon-sprawl/client/test/contract_client_test.gd

244 lines
7.9 KiB
GDScript

extends GdUnitTestSuite
## NEO-153: `ContractClient` GET parse, issue POST, and failure signals.
const ContractClient := preload("res://scripts/contract_client.gd")
const INSTANCE_ID := "ci_test_instance"
const TEMPLATE_ID := "prototype_contract_clear_combat_pocket"
const ENCOUNTER_ID := "prototype_combat_pocket"
var _contracts_capture: Dictionary = {}
var _issue_capture: Dictionary = {}
var _issue_failed_capture: String = ""
func _capture_contracts(snapshot: Dictionary) -> void:
_contracts_capture = snapshot
func _capture_issue(result: Dictionary) -> void:
_issue_capture = result
func _capture_issue_failed(reason: String) -> void:
_issue_failed_capture = reason
class MockHttpTransport:
extends Node
signal request_completed(
result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
)
var last_url: String = ""
var last_method: int = HTTPClient.METHOD_GET
var response_code: int = 200
var body_json: String = ""
func request(
url: String,
_custom_headers: PackedStringArray = PackedStringArray(),
method: HTTPClient.Method = HTTPClient.METHOD_GET,
_request_data: String = ""
) -> Error:
last_url = url
last_method = method
request_completed.emit(
HTTPRequest.RESULT_SUCCESS,
response_code,
PackedStringArray(),
body_json.to_utf8_buffer()
)
return OK
class NoopHttpTransport:
extends Node
signal request_completed(
result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
)
func request(
_url: String,
_custom_headers: PackedStringArray = PackedStringArray(),
_method: HTTPClient.Method = HTTPClient.METHOD_GET,
_request_data: String = ""
) -> Error:
return OK
static func _active_contracts_json() -> String:
var body := (
'{"schemaVersion":1,"playerId":"dev-local-1","contracts":'
+ '[{"contractInstanceId":"%s","templateId":"%s",'
+ '"templateDisplayName":"Clear Combat Pocket (Repeat)","status":"active",'
+ '"encounterTemplateId":"%s","seedBucket":"prototype_contract_dev_seed",'
+ '"issuedAt":"2026-06-28T12:00:00Z"}]}'
)
return body % [INSTANCE_ID, TEMPLATE_ID, ENCOUNTER_ID]
static func _completed_with_summary_json() -> String:
var body := (
'{"schemaVersion":1,"playerId":"dev-local-1","contracts":'
+ '[{"contractInstanceId":"%s","templateId":"%s",'
+ '"templateDisplayName":"Clear Combat Pocket (Repeat)","status":"completed",'
+ '"encounterTemplateId":"%s","seedBucket":"prototype_contract_dev_seed",'
+ '"issuedAt":"2026-06-28T12:00:00Z","completedAt":"2026-06-28T13:00:00Z",'
+ '"completionRewardSummary":{"itemGrants":[{"itemId":"scrap_metal_bulk","quantity":5}],'
+ '"skillXpGrants":[{"skillId":"salvage","amount":15}]}}]}'
)
return body % [INSTANCE_ID, TEMPLATE_ID, ENCOUNTER_ID]
static func _issue_success_json() -> String:
var body := (
'{"schemaVersion":1,"issued":true,"contract":'
+ '{"contractInstanceId":"%s","templateId":"%s",'
+ '"templateDisplayName":"Clear Combat Pocket (Repeat)","status":"active",'
+ '"encounterTemplateId":"%s","seedBucket":"prototype_contract_dev_seed",'
+ '"issuedAt":"2026-06-28T12:00:00Z"}}'
)
return body % [INSTANCE_ID, TEMPLATE_ID, ENCOUNTER_ID]
static func _issue_deny_json(reason_code: String) -> String:
return '{"schemaVersion":1,"issued":false,"reasonCode":"%s"}' % reason_code
func _make_client(sync_transport: Node, issue_transport: Node = null) -> Node:
var c: Node = ContractClient.new()
c.set("injected_sync_http", sync_transport)
var issue_http: Node = issue_transport if issue_transport != null else sync_transport
c.set("injected_issue_http", issue_http)
auto_free(sync_transport)
if issue_transport != null and issue_transport != sync_transport:
auto_free(issue_transport)
auto_free(c)
add_child(c)
return c
func test_parse_active_contract_row_includes_encounter_id() -> void:
# Arrange
var json := _active_contracts_json()
# Act
var snapshot: Variant = ContractClient.parse_contracts_json(json)
# Assert
assert_that(snapshot is Dictionary).is_true()
var row: Dictionary = ContractClient.active_contract_row(snapshot as Dictionary)
assert_that(str(row.get("status", ""))).is_equal("active")
assert_that(str(row.get("encounterTemplateId", ""))).is_equal(ENCOUNTER_ID)
func test_parse_completed_row_has_completion_reward_summary() -> void:
# Arrange
var json := _completed_with_summary_json()
# Act
var snapshot: Variant = ContractClient.parse_contracts_json(json)
# Assert
assert_that(snapshot is Dictionary).is_true()
var summary: Dictionary = ContractClient.completion_reward_summary(
INSTANCE_ID, snapshot as Dictionary
)
var items: Variant = summary.get("itemGrants", null)
assert_that(items is Array).is_true()
assert_that((items as Array).size()).is_equal(1)
func test_parse_contracts_json_rejects_schema_mismatch() -> void:
# Arrange
var json := '{"schemaVersion":99,"playerId":"dev-local-1","contracts":[]}'
# Act
var snapshot: Variant = ContractClient.parse_contracts_json(json)
# Assert
assert_that(snapshot).is_null()
func test_request_sync_emits_contracts_received() -> void:
# Arrange
_contracts_capture = {}
var transport := MockHttpTransport.new()
transport.body_json = _active_contracts_json()
var c := _make_client(transport, NoopHttpTransport.new())
c.connect("contracts_received", Callable(self, "_capture_contracts"))
monitor_signals(c)
# Act
c.call("request_sync_from_server")
# Assert
await assert_signal(c).is_emitted("contracts_received", any())
assert_that(_contracts_capture.get("playerId", "")).is_equal("dev-local-1")
assert_that(transport.last_url).contains("/contracts")
func test_http_404_emits_contracts_sync_failed() -> void:
# Arrange
_issue_failed_capture = ""
var transport := MockHttpTransport.new()
transport.response_code = 404
var c := _make_client(transport, NoopHttpTransport.new())
c.connect("contracts_sync_failed", Callable(self, "_capture_issue_failed"))
monitor_signals(c)
# Act
c.call("request_sync_from_server")
# Assert
await assert_signal(c).is_emitted("contracts_sync_failed", any())
assert_that(_issue_failed_capture).contains("404")
func test_issue_success_emits_contract_issue_result_received() -> void:
# Arrange
_issue_capture = {}
var sync := NoopHttpTransport.new()
var issue := MockHttpTransport.new()
issue.body_json = _issue_success_json()
var c := _make_client(sync, issue)
c.connect("contract_issue_result_received", Callable(self, "_capture_issue"))
monitor_signals(c)
# Act
c.call("request_issue", TEMPLATE_ID, "prototype_contract_dev_seed")
# Assert
await assert_signal(c).is_emitted("contract_issue_result_received", any())
assert_bool(_issue_capture.get("issued", false)).is_true()
assert_that(issue.last_url).contains("/contracts/issue")
assert_that(issue.last_method).is_equal(HTTPClient.METHOD_POST)
func test_issue_deny_emits_result_with_reason_code() -> void:
# Arrange
_issue_capture = {}
var sync := NoopHttpTransport.new()
var issue := MockHttpTransport.new()
issue.body_json = _issue_deny_json("economy_cap_exceeded")
var c := _make_client(sync, issue)
c.connect("contract_issue_result_received", Callable(self, "_capture_issue"))
monitor_signals(c)
# Act
c.call("request_issue", TEMPLATE_ID, "prototype_contract_dev_seed")
# Assert
await assert_signal(c).is_emitted("contract_issue_result_received", any())
assert_bool(_issue_capture.get("issued", true)).is_false()
assert_that(str(_issue_capture.get("reasonCode", ""))).is_equal("economy_cap_exceeded")
func test_merge_contract_row_replaces_matching_instance_id() -> void:
# Arrange
var snapshot := {
"schemaVersion": 1,
"playerId": "dev-local-1",
"contracts":
[
{
"contractInstanceId": "ci_old",
"status": "active",
}
],
}
var row := {"contractInstanceId": "ci_old", "status": "completed"}
# Act
var merged: Dictionary = ContractClient.merge_contract_row_into_snapshot(snapshot, row)
# Assert
var contracts: Array = merged.get("contracts", []) as Array
assert_that(contracts.size()).is_equal(1)
assert_that(str((contracts[0] as Dictionary).get("status", ""))).is_equal("completed")