Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/**'
Expand Down Expand Up @@ -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:
Expand Down
15 changes: 9 additions & 6 deletions openblade/hardware/scalar_http/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions tests/i3/test_scalar_http_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +68 to +78
Loading