Add session expiry renewal flow
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
extends SceneTree
|
||||
|
||||
var failures := 0
|
||||
var auth
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
auth = load("res://scripts/autoload/auth_state.gd").new()
|
||||
root.add_child(auth)
|
||||
await process_frame
|
||||
_test_expiration_parsing()
|
||||
_test_warning_and_expiration()
|
||||
_test_refresh_preserves_identity()
|
||||
_test_refresh_in_flight_has_expiry_grace()
|
||||
_test_stale_responses_cannot_restore_or_end_session()
|
||||
auth.queue_free()
|
||||
await process_frame
|
||||
quit(1 if failures > 0 else 0)
|
||||
|
||||
|
||||
func _test_expiration_parsing() -> void:
|
||||
var iso_expiration: float = float(auth._parse_expiration("2030-01-02T03:04:05Z"))
|
||||
var expected_iso := float(Time.get_unix_time_from_datetime_string("2030-01-02T03:04:05"))
|
||||
_assert(is_equal_approx(iso_expiration, expected_iso), "session expiry should parse the Auth API ISO timestamp")
|
||||
var jwt_expiration := 1893456000
|
||||
var payload: String = Marshalls.raw_to_base64(JSON.stringify({"exp": jwt_expiration}).to_utf8_buffer())
|
||||
payload = payload.replace("+", "-").replace("/", "_").trim_suffix("=")
|
||||
var token := "header.%s.signature" % payload
|
||||
_assert(int(auth._parse_expiration("", token)) == jwt_expiration, "session expiry should fall back to the JWT exp claim")
|
||||
|
||||
|
||||
func _test_warning_and_expiration() -> void:
|
||||
var observed := {"warning_seconds": -1, "expiration_reason": ""}
|
||||
var warning_callback := func(seconds: int): observed["warning_seconds"] = seconds
|
||||
var expiration_callback := func(reason: String): observed["expiration_reason"] = reason
|
||||
auth.session_warning.connect(warning_callback)
|
||||
auth.session_expired.connect(expiration_callback)
|
||||
auth.set_session("tester", "warning-token", "refresh-token", "USER", str(Time.get_unix_time_from_system() + 90.0))
|
||||
auth._evaluate_expiry()
|
||||
_assert(int(observed["warning_seconds"]) > 0 and int(observed["warning_seconds"]) <= 90, "session should warn inside the two-minute renewal window")
|
||||
auth.expiration_unix = Time.get_unix_time_from_system() - 1.0
|
||||
auth._evaluate_expiry()
|
||||
_assert(str(observed["expiration_reason"]) == "expired" and not auth.is_logged_in, "an unanswered warning should clear the session at expiry")
|
||||
auth.session_warning.disconnect(warning_callback)
|
||||
auth.session_expired.disconnect(expiration_callback)
|
||||
|
||||
|
||||
func _test_refresh_preserves_identity() -> void:
|
||||
var observed := {"refreshed": false}
|
||||
var refreshed_callback := func(): observed["refreshed"] = true
|
||||
auth.session_refreshed.connect(refreshed_callback)
|
||||
auth.set_session("tester", "old-token", "persistent-refresh", "SUPER", str(Time.get_unix_time_from_system() + 60.0))
|
||||
auth.refresh_in_progress = true
|
||||
auth.refresh_revision = auth.session_revision
|
||||
var next_expiration := Time.get_unix_time_from_system() + 900.0
|
||||
var body := JSON.stringify({"accessToken": "new-token", "exp": next_expiration}).to_utf8_buffer()
|
||||
auth._on_refresh_completed(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), body)
|
||||
_assert(bool(observed["refreshed"]) and auth.access_token == "new-token", "successful renewal should install the replacement access token")
|
||||
_assert(auth.refresh_token == "persistent-refresh" and auth.username == "tester" and auth.role == "SUPER", "renewal should preserve identity and the non-rotating refresh token")
|
||||
_assert(not auth.warning_active and auth.expiration_unix > Time.get_unix_time_from_system(), "renewal should reset the warning window")
|
||||
auth.session_refreshed.disconnect(refreshed_callback)
|
||||
|
||||
|
||||
func _test_refresh_in_flight_has_expiry_grace() -> void:
|
||||
auth.set_session("tester", "old-token", "refresh-token", "USER", str(Time.get_unix_time_from_system() + 60.0))
|
||||
auth.expiration_unix = Time.get_unix_time_from_system() - 1.0
|
||||
auth.refresh_in_progress = true
|
||||
auth._evaluate_expiry()
|
||||
_assert(auth.is_logged_in, "a submitted renewal should be allowed to finish after the old access token expires")
|
||||
_assert(not auth.can_send_authenticated_request(), "expired access tokens should not be used while renewal is in progress")
|
||||
auth.refresh_in_progress = false
|
||||
auth.clear_session()
|
||||
|
||||
|
||||
func _test_stale_responses_cannot_restore_or_end_session() -> void:
|
||||
auth.set_session("tester", "old-token", "refresh-token", "USER", str(Time.get_unix_time_from_system() + 60.0))
|
||||
auth.refresh_in_progress = true
|
||||
auth.refresh_revision = auth.session_revision
|
||||
auth.clear_session()
|
||||
var body := JSON.stringify({"accessToken": "stale-token", "exp": Time.get_unix_time_from_system() + 900.0}).to_utf8_buffer()
|
||||
auth._on_refresh_completed(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), body)
|
||||
_assert(auth.access_token.is_empty(), "a delayed refresh response must not restore a logged-out session")
|
||||
auth.set_session("tester", "current-token", "refresh-token", "USER", str(Time.get_unix_time_from_system() + 900.0))
|
||||
auth.handle_unauthorized("old-token")
|
||||
_assert(auth.is_logged_in and auth.access_token == "current-token", "a stale 401 must not end a newer session")
|
||||
auth.handle_unauthorized("current-token")
|
||||
_assert(not auth.is_logged_in, "a 401 for the active token should end the invalid session")
|
||||
|
||||
|
||||
func _assert(condition: bool, message: String) -> void:
|
||||
if condition:
|
||||
return
|
||||
failures += 1
|
||||
push_error(message)
|
||||
Reference in New Issue
Block a user