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")