244 lines
7.9 KiB
GDScript
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")
|