extends GdUnitTestSuite ## NEO-122: `QuestProgressClient` GET parse, accept POST, and failure signals. const QuestProgressClient := preload("res://scripts/quest_progress_client.gd") const PROTOTYPE_QUEST_GATHER_ID := "prototype_quest_gather_intro" var _progress_capture: Dictionary = {} var _accept_capture: Dictionary = {} var _accept_failed_capture: Dictionary = {} func _capture_progress(snapshot: Dictionary) -> void: _progress_capture = snapshot func _capture_accept(quest_id: String, result: Dictionary) -> void: _accept_capture = {"questId": quest_id, "result": result} func _capture_accept_failed(quest_id: String, reason: String) -> void: _accept_failed_capture = {"questId": quest_id, "reason": 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 static func _not_started_progress_json() -> String: return ( '{"schemaVersion":1,"playerId":"dev-local-1","quests":' + '[{"questId":"prototype_quest_gather_intro","status":"not_started",' + '"currentStepIndex":0,"objectiveCounters":{}}]}' ) static func _active_partial_json() -> String: return ( '{"schemaVersion":1,"playerId":"dev-local-1","quests":' + '[{"questId":"prototype_quest_gather_intro","status":"active",' + '"currentStepIndex":0,"objectiveCounters":{"gather_intro_obj_scrap":2}}]}' ) static func _completed_json() -> String: return ( '{"schemaVersion":1,"playerId":"dev-local-1","quests":' + '[{"questId":"prototype_quest_gather_intro","status":"completed",' + '"currentStepIndex":0,"objectiveCounters":{},' + '"completedAt":"2026-06-07T12:00:00Z"}]}' ) static func _accept_success_json() -> String: return ( '{"schemaVersion":1,"accepted":true,"quest":' + '{"questId":"prototype_quest_gather_intro","status":"active",' + '"currentStepIndex":0,"objectiveCounters":{}}}' ) static func _accept_deny_json() -> String: return ( '{"schemaVersion":1,"accepted":false,"reasonCode":"prerequisite_incomplete"}' ) func _make_client(sync_transport: Node, accept_transport: Node = null) -> Node: var c: Node = QuestProgressClient.new() c.set("injected_sync_http", sync_transport) var accept_http: Node = accept_transport if accept_transport != null else sync_transport c.set("injected_accept_http", accept_http) auto_free(sync_transport) if accept_transport != null and accept_transport != sync_transport: auto_free(accept_transport) auto_free(c) add_child(c) return c 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 class PendingAcceptHttpTransport: extends Node signal request_completed( result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray ) var last_url: String = "" var response_code: int = 200 var body_json: String = "" func request( url: String, _custom_headers: PackedStringArray = PackedStringArray(), _method: HTTPClient.Method = HTTPClient.METHOD_POST, _request_data: String = "" ) -> Error: last_url = url return OK func complete_pending() -> void: request_completed.emit( HTTPRequest.RESULT_SUCCESS, response_code, PackedStringArray(), body_json.to_utf8_buffer() ) func test_parse_not_started_quest_row() -> void: # Arrange var json := _not_started_progress_json() # Act var snapshot: Variant = QuestProgressClient.parse_quest_progress_json(json) # Assert assert_that(snapshot is Dictionary).is_true() var c: Node = QuestProgressClient.new() auto_free(c) var row: Dictionary = c.call("quest_row", PROTOTYPE_QUEST_GATHER_ID, snapshot as Dictionary) assert_that(str(row.get("status", ""))).is_equal("not_started") func test_parse_active_row_has_objective_counters() -> void: # Arrange var json := _active_partial_json() # Act var snapshot: Variant = QuestProgressClient.parse_quest_progress_json(json) # Assert assert_that(snapshot is Dictionary).is_true() var c: Node = QuestProgressClient.new() auto_free(c) var row: Dictionary = c.call("quest_row", PROTOTYPE_QUEST_GATHER_ID, snapshot as Dictionary) assert_that(str(row.get("status", ""))).is_equal("active") var counters: Variant = row.get("objectiveCounters", null) assert_that(counters is Dictionary).is_true() assert_that(int((counters as Dictionary).get("gather_intro_obj_scrap", 0))).is_equal(2) func test_parse_completed_row_has_completed_at() -> void: # Arrange var json := _completed_json() # Act var snapshot: Variant = QuestProgressClient.parse_quest_progress_json(json) # Assert assert_that(snapshot is Dictionary).is_true() var c: Node = QuestProgressClient.new() auto_free(c) var row: Dictionary = c.call("quest_row", PROTOTYPE_QUEST_GATHER_ID, snapshot as Dictionary) assert_that(str(row.get("status", ""))).is_equal("completed") assert_that(str(row.get("completedAt", ""))).contains("2026-06-07") func test_request_sync_emits_quest_progress_received() -> void: # Arrange _progress_capture = {} var transport := MockHttpTransport.new() transport.body_json = _not_started_progress_json() var noop := NoopHttpTransport.new() var c := _make_client(transport, noop) c.connect("quest_progress_received", Callable(self, "_capture_progress")) monitor_signals(c) # Act c.call("request_sync_from_server") # Assert await assert_signal(c).is_emitted("quest_progress_received", any()) assert_that(_progress_capture.get("playerId", "")).is_equal("dev-local-1") assert_that(transport.last_url).contains("/quest-progress") func test_http_404_emits_quest_sync_failed() -> void: # Arrange var transport := MockHttpTransport.new() transport.response_code = 404 var noop := NoopHttpTransport.new() var c := _make_client(transport, noop) monitor_signals(c) # Act c.call("request_sync_from_server") # Assert await assert_signal(c).is_emitted("quest_sync_failed", "HTTP 404 (player unknown)") func test_parse_quest_progress_json_returns_null_for_schema_mismatch() -> void: # Arrange var json := '{"schemaVersion":2,"playerId":"dev-local-1","quests":[]}' # Act var snapshot: Variant = QuestProgressClient.parse_quest_progress_json(json) # Assert assert_that(snapshot).is_null() func test_request_accept_emits_quest_accept_result_received() -> void: # Arrange _accept_capture = {} var transport := MockHttpTransport.new() transport.body_json = _accept_success_json() var noop := NoopHttpTransport.new() var c := _make_client(noop, transport) c.connect("quest_accept_result_received", Callable(self, "_capture_accept")) # Act var started: bool = bool(c.call("request_accept", PROTOTYPE_QUEST_GATHER_ID)) # Assert assert_that(started).is_true() assert_that(_accept_capture.get("questId", "")).is_equal(PROTOTYPE_QUEST_GATHER_ID) var accept_result: Dictionary = _accept_capture.get("result", {}) as Dictionary assert_that(bool(accept_result.get("accepted", false))).is_true() assert_that(transport.last_url).contains("/accept") assert_that(transport.last_method).is_equal(HTTPClient.METHOD_POST) func test_request_accept_emits_deny_with_reason_code() -> void: # Arrange _accept_capture = {} var transport := MockHttpTransport.new() transport.body_json = _accept_deny_json() var noop := NoopHttpTransport.new() var c := _make_client(noop, transport) c.connect("quest_accept_result_received", Callable(self, "_capture_accept")) # Act c.call("request_accept", "prototype_quest_refine_intro") # Assert var result: Dictionary = _accept_capture.get("result", {}) as Dictionary assert_that(bool(result.get("accepted", true))).is_false() assert_that(str(result.get("reasonCode", ""))).is_equal("prerequisite_incomplete") func test_http_404_on_accept_emits_quest_accept_failed() -> void: # Arrange _accept_failed_capture = {} var transport := MockHttpTransport.new() transport.response_code = 404 var noop := NoopHttpTransport.new() var c := _make_client(noop, transport) c.connect("quest_accept_failed", Callable(self, "_capture_accept_failed")) # Act c.call("request_accept", PROTOTYPE_QUEST_GATHER_ID) # Assert assert_that(_accept_failed_capture.get("questId", "")).is_equal(PROTOTYPE_QUEST_GATHER_ID) assert_that(str(_accept_failed_capture.get("reason", ""))).is_equal("HTTP 404 (player unknown)") func test_request_accept_returns_false_when_accept_busy() -> void: # Arrange var pending := PendingAcceptHttpTransport.new() pending.body_json = _accept_success_json() var noop := NoopHttpTransport.new() var c := _make_client(noop, pending) # Act var first: bool = bool(c.call("request_accept", PROTOTYPE_QUEST_GATHER_ID)) var second: bool = bool(c.call("request_accept", "prototype_quest_refine_intro")) # Assert assert_that(first).is_true() assert_that(second).is_false() pending.complete_pending()