173 lines
5.3 KiB
GDScript
173 lines
5.3 KiB
GDScript
extends GdUnitTestSuite
|
|
|
|
const PositionAuthorityTestDouble := preload("res://test/position_authority_test_double.gd")
|
|
|
|
|
|
# In-process transport: not an HTTPRequest subclass (Godot 4 won't call script request() on those).
|
|
class MockHttpTransport:
|
|
extends Node
|
|
signal request_completed(
|
|
result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
|
|
)
|
|
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:
|
|
if _queue.is_empty():
|
|
return ERR_UNAVAILABLE
|
|
var r: Dictionary = _queue.pop_front()
|
|
var body_bytes: PackedByteArray = str(r["body"]).to_utf8_buffer()
|
|
request_completed.emit(r["result"], r["code"], PackedStringArray(), body_bytes)
|
|
return OK
|
|
|
|
|
|
# Holds _busy without completing (never emits request_completed).
|
|
class HangingHttpTransport:
|
|
extends Node
|
|
@warning_ignore("unused_signal")
|
|
signal request_completed(
|
|
result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
|
|
)
|
|
var request_count: int = 0
|
|
|
|
func request(
|
|
_url: String,
|
|
_custom_headers: PackedStringArray = PackedStringArray(),
|
|
_method: HTTPClient.Method = HTTPClient.METHOD_GET,
|
|
_request_data: String = ""
|
|
) -> Error:
|
|
request_count += 1
|
|
return OK
|
|
|
|
|
|
func _make_client(http_transport: Node) -> Node:
|
|
var c: Node = PositionAuthorityTestDouble.new()
|
|
c.set("injected_http", http_transport)
|
|
auto_free(http_transport)
|
|
auto_free(c)
|
|
add_child(c)
|
|
return c
|
|
|
|
|
|
func test_sync_boot_get_200_emits_snap() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"position":{"x":1,"y":0.9,"z":-5}}')
|
|
var c := _make_client(transport)
|
|
monitor_signals(c)
|
|
# Act
|
|
c.sync_from_server()
|
|
# Assert
|
|
assert_signal(c).is_emitted("authoritative_position_received", Vector3(1.0, 0.9, -5.0), true)
|
|
|
|
|
|
# NEO-24: boot sync also emits the cooldown-throttled heartbeat.
|
|
func test_sync_boot_get_200_emits_authoritative_ack() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"position":{"x":1,"y":0.9,"z":-5}}')
|
|
var c := _make_client(transport)
|
|
monitor_signals(c)
|
|
# Act
|
|
c.sync_from_server()
|
|
# Assert
|
|
assert_signal(c).is_emitted("authoritative_ack", Vector3(1.0, 0.9, -5.0))
|
|
|
|
|
|
func test_sync_boot_get_200_malformed_position_does_not_emit() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"position":"invalid"}')
|
|
var c := _make_client(transport)
|
|
monitor_signals(c)
|
|
# Act
|
|
c.sync_from_server()
|
|
# Assert
|
|
assert_signal(c).wait_until(400).is_not_emitted("authoritative_position_received")
|
|
|
|
|
|
func test_move_stream_post_400_emits_move_rejected_then_boot_get() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 400, '{"reasonCode":"vertical_step_exceeded"}')
|
|
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"position":{"x":0,"y":0.9,"z":0}}')
|
|
var c := _make_client(transport)
|
|
monitor_signals(c)
|
|
# Act
|
|
c.submit_stream_targets([Vector3(1.0, 2.0, 3.0)])
|
|
# Assert
|
|
assert_signal(c).is_emitted("move_rejected", "vertical_step_exceeded")
|
|
assert_signal(c).is_emitted("authoritative_position_received", Vector3(0.0, 0.9, 0.0), true)
|
|
|
|
|
|
func test_move_stream_post_400_without_reason_emits_unknown() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 400, "{}")
|
|
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"position":{"x":1,"y":0.9,"z":1}}')
|
|
var c := _make_client(transport)
|
|
monitor_signals(c)
|
|
# Act
|
|
c.submit_stream_targets([Vector3.ZERO])
|
|
# Assert
|
|
assert_signal(c).is_emitted("move_rejected", "unknown")
|
|
assert_signal(c).is_emitted("authoritative_position_received", Vector3(1.0, 0.9, 1.0), true)
|
|
|
|
|
|
func test_second_sync_while_busy_is_ignored() -> void:
|
|
# Arrange
|
|
var transport := HangingHttpTransport.new()
|
|
var c := _make_client(transport)
|
|
# Act
|
|
c.sync_from_server()
|
|
c.sync_from_server()
|
|
# Assert
|
|
assert_that(transport.request_count).is_equal(1)
|
|
|
|
|
|
func test_move_stream_post_200_does_not_emit_position() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS,
|
|
200,
|
|
(
|
|
'{"schemaVersion":1,"playerId":"dev-local-1","position":{"x":-4.9,"y":0.9,"z":-5},'
|
|
+ '"sequence":3}'
|
|
)
|
|
)
|
|
var c := _make_client(transport)
|
|
monitor_signals(c)
|
|
# Act
|
|
c.submit_stream_targets([Vector3(-4.9, 0.9, -5.0)])
|
|
# Assert
|
|
assert_signal(c).wait_until(400).is_not_emitted("authoritative_position_received")
|
|
|
|
|
|
# NEO-24: move-stream 200 must emit `authoritative_ack` so a held target lock can re-validate
|
|
# during normal locomotion (without re-introducing the snap rubber-band).
|
|
func test_move_stream_post_200_emits_authoritative_ack() -> void:
|
|
# Arrange
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS,
|
|
200,
|
|
(
|
|
'{"schemaVersion":1,"playerId":"dev-local-1","position":{"x":-4.9,"y":0.9,"z":-5},'
|
|
+ '"sequence":3}'
|
|
)
|
|
)
|
|
var c := _make_client(transport)
|
|
monitor_signals(c)
|
|
# Act
|
|
c.submit_stream_targets([Vector3(-4.9, 0.9, -5.0)])
|
|
# Assert
|
|
assert_signal(c).is_emitted("authoritative_ack", Vector3(-4.9, 0.9, -5.0))
|