Files
step3/tests/auth_session_test.gd
hz f17797becc Don't show the session-expiry warning to actively-used sessions
The warning was purely a token-expiry countdown with no concept of
user activity, so it interrupted people who had been clicking through
questions the whole time. AuthState now tracks real input activity
globally and silently refreshes the session in the background when
the user has interacted recently, only surfacing the "Stay logged in?"
dialog for sessions that are genuinely idle. A failed background
refresh still surfaces the dialog rather than silently logging the
user out.
2026-07-22 22:52:38 -05:00

120 lines
6.2 KiB
GDScript

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_active_user_silently_refreshes_instead_of_warning()
_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.last_activity_unix = Time.get_unix_time_from_system() - 3600.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_active_user_silently_refreshes_instead_of_warning() -> void:
var observed := {"warning_seconds": -1}
var warning_callback := func(seconds: int): observed["warning_seconds"] = seconds
auth.session_warning.connect(warning_callback)
# Point at a local, always-refused port so the real refresh attempt this triggers fails
# fast and never touches the real network - the point of this test is that
# request_session_refresh() gets called at all, not that it succeeds.
auth.auth_base_url = "http://127.0.0.1:1"
auth.set_session("tester", "active-token", "refresh-token", "USER", str(Time.get_unix_time_from_system() + 90.0))
auth.last_activity_unix = Time.get_unix_time_from_system()
auth._evaluate_expiry()
_assert(int(observed["warning_seconds"]) == -1 and not auth.warning_active, "an actively-used session should not show the expiry warning")
_assert(auth.refresh_in_progress, "an actively-used session nearing expiry should silently attempt a refresh instead of warning")
auth.session_warning.disconnect(warning_callback)
auth.refresh_in_progress = false
auth.auth_base_url = auth.DEFAULT_AUTH_BASE_URL
auth.clear_session()
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)