diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f81e57..b9c7168 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,7 @@ jobs: - 'tests/integration/test_sharded_archive_atomicity.py' - 'tests/compat/**' - 'compatibility/**' + - 'tests/i3/test_scalar_http_*.py' - 'pyproject.toml' api: - 'openblade/api/**' @@ -127,6 +128,8 @@ jobs: - run: pytest tests/compat -q --tb=short # Whole safety suite (was only test_safety_regression.py) so the SAFETY_003 # import guard is enforced and cannot silently rot again. + # scalar_http real-i3 client contract tests (in-process via TestClient, no server). + - run: pytest tests/i3/test_scalar_http_session.py tests/i3/test_scalar_http_library_backend.py tests/i3/test_scalar_http_move_medium.py tests/i3/test_scalar_http_wiring.py -q --tb=short - run: pytest tests/safety -q --tb=short api-aml-integration: diff --git a/openblade/hardware/scalar_http/session.py b/openblade/hardware/scalar_http/session.py index e126ddf..25c1c87 100644 --- a/openblade/hardware/scalar_http/session.py +++ b/openblade/hardware/scalar_http/session.py @@ -50,8 +50,9 @@ def login(self) -> None: """Establish a session. Raises ``ScalarHttpError`` on failure. The AML login accepts a JSON body and aliases ``username`` to ``name``; the - session cookie lands in the client cookie jar. The bearer token in the body - is captured as a fallback for clients whose jar is not shared across calls. + session cookie lands in the client cookie jar and authenticates every + subsequent request. The emulator also returns a token in the body — retained + for diagnostics only, NOT sent as a Bearer header (see ``_headers``). """ response = self._client.post( self._login_path, @@ -65,10 +66,12 @@ def login(self) -> None: self._logged_in = True def _headers(self) -> dict[str, str]: - headers = dict(_JSON_HEADERS) - if self._token: - headers["Authorization"] = f"Bearer {self._token}" - return headers + # Cookie-session only, to match the real i3: authentication rides on the + # ``sessionID`` cookie persisted in the client's cookie jar from login. We do + # NOT send an ``Authorization: Bearer`` header — that is an emulator-only + # artifact the appliance does not accept, and relying on it would let the + # client "work" against the emulator while diverging from the real contract. + return dict(_JSON_HEADERS) def request( self, diff --git a/tests/i3/test_scalar_http_session.py b/tests/i3/test_scalar_http_session.py index 83835d8..0b491b1 100644 --- a/tests/i3/test_scalar_http_session.py +++ b/tests/i3/test_scalar_http_session.py @@ -63,3 +63,16 @@ def test_scalar_http_session_bad_credentials_raise(emulator_client: TestClient) session.login() assert exc_info.value.status_code == 401 + + +def test_scalar_http_session_authenticates_via_cookie_not_bearer( + emulator_client: TestClient, +) -> None: + # Real i3 fidelity: auth rides on the sessionID cookie, never an Authorization + # header. Guards against regressing to the emulator-only Bearer artifact. + session = _session(emulator_client) + session.login() + + assert "Authorization" not in session._headers() + assert "sessionID" in emulator_client.cookies # cookie jar carries the session + assert session.get_json("/aml/system/status") # cookie-authenticated read works