diff --git a/horizon/tests/test_enforcer_api.py b/horizon/tests/test_enforcer_api.py index 84052175..d7fd7534 100644 --- a/horizon/tests/test_enforcer_api.py +++ b/horizon/tests/test_enforcer_api.py @@ -97,6 +97,14 @@ def test_enforcer_endpoint_invalid_token_returns_401(endpoint): assert response.json()["detail"] == "Invalid PDP token" +@pytest.mark.parametrize("endpoint", PROTECTED_ENFORCER_ENDPOINTS) +@pytest.mark.parametrize("value", ["garbage", "Bearer", "Bearer ", "Bearer a b c"]) +def test_enforcer_endpoint_malformed_header_is_401_not_500(endpoint, value): + client = TestClient(sidecar._app) + response = client.post(endpoint, headers={"authorization": value}, json={}) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + def test_health_endpoint_is_public(monkeypatch): monkeypatch.setattr(stats_manager, "_had_failure", False) client = TestClient(sidecar._app) diff --git a/horizon/tests/test_legacy_update_routes.py b/horizon/tests/test_legacy_update_routes.py index 8cbcdc68..d38657c6 100644 --- a/horizon/tests/test_legacy_update_routes.py +++ b/horizon/tests/test_legacy_update_routes.py @@ -39,19 +39,26 @@ def test_update_policy_rejects_unauthenticated(pdp: MockPermitPDP, monkeypatch): monkeypatch.setattr(pdp._opal.policy_updater, "trigger_update_policy", trigger) client = TestClient(pdp._app) - # A missing header is rejected before the handler runs: 422 while the - # `authorization` param has no default (FastAPI required-param validation), - # 401 once enforce_pdp_token gains `= None` (PER-15244 / #317). Accept both - # so this survives either merge order, while still failing on an accidental - # 200 (auth bypass) or 500. An invalid token is 401 in both regimes, and the - # updater must never run for an unauthenticated caller either way. missing = client.post("/update_policy", follow_redirects=False) - assert missing.status_code in (401, 422) + assert missing.status_code == 401 + assert missing.json()["detail"] == "Missing Authorization header" invalid = client.post("/update_policy", headers={"authorization": "Bearer wrong"}, follow_redirects=False) assert invalid.status_code == 401 trigger.assert_not_awaited() +@pytest.mark.parametrize("value", ["garbage", "Bearer", "Bearer ", "Bearer a b c"]) +def test_update_policy_malformed_header_is_401_not_500(pdp: MockPermitPDP, monkeypatch, value: str): + trigger = AsyncMock() + monkeypatch.setattr(pdp._opal.policy_updater, "trigger_update_policy", trigger) + client = TestClient(pdp._app) + + response = client.post("/update_policy", headers={"authorization": value}, follow_redirects=False) + + assert response.status_code == 401 + trigger.assert_not_awaited() + + def test_update_policy_data_triggers_updater(pdp: MockPermitPDP, auth: dict[str, str], monkeypatch): get_base = AsyncMock() monkeypatch.setattr(pdp._opal.data_updater, "get_base_policy_data", get_base) @@ -78,14 +85,26 @@ def test_update_policy_data_rejects_unauthenticated(pdp: MockPermitPDP, monkeypa monkeypatch.setattr(pdp._opal.data_updater, "get_base_policy_data", get_base) client = TestClient(pdp._app) - # See test_update_policy_rejects_unauthenticated for the 401/422 dual regime. missing = client.post("/update_policy_data", follow_redirects=False) - assert missing.status_code in (401, 422) + assert missing.status_code == 401 + assert missing.json()["detail"] == "Missing Authorization header" invalid = client.post("/update_policy_data", headers={"authorization": "Bearer wrong"}, follow_redirects=False) assert invalid.status_code == 401 get_base.assert_not_awaited() +@pytest.mark.parametrize("value", ["garbage", "Bearer", "Bearer ", "Bearer a b c"]) +def test_update_policy_data_malformed_header_is_401_not_500(pdp: MockPermitPDP, monkeypatch, value: str): + get_base = AsyncMock() + monkeypatch.setattr(pdp._opal.data_updater, "get_base_policy_data", get_base) + client = TestClient(pdp._app) + + response = client.post("/update_policy_data", headers={"authorization": value}, follow_redirects=False) + + assert response.status_code == 401 + get_base.assert_not_awaited() + + def test_legacy_routes_do_not_redirect(pdp: MockPermitPDP, auth: dict[str, str], monkeypatch): # Lock in the fix: the aliases must call the updaters directly, never redirect # to the canonical routes. Clients drop Authorization on redirects, and diff --git a/horizon/tests/test_opal_trigger_auth.py b/horizon/tests/test_opal_trigger_auth.py index 9d0cad7c..d8c9608b 100644 --- a/horizon/tests/test_opal_trigger_auth.py +++ b/horizon/tests/test_opal_trigger_auth.py @@ -7,10 +7,13 @@ The TestClient is used WITHOUT a context manager, so the app lifespan never runs (no OPAL policy/data fetch, no OPA process, no control-plane connection). ``raise_server_exceptions =False`` means a request the auth dependency *allows* through but which then fails on real -offline I/O surfaces as a 500 response instead of raising - so an authenticated trigger -call asserts only that it is not blocked (status != 401), not that the handler succeeds. +offline I/O surfaces as a 500 response instead of raising. Valid-token tests boundary-mock +the real (unstarted) updater instances hanging off ``_sidecar._opal`` with an ``AsyncMock``, +so the handler runs to completion and the test asserts an actual 200, not just ``!= 401``. """ +from unittest.mock import AsyncMock + import pytest from fastapi import FastAPI from fastapi.testclient import TestClient @@ -57,11 +60,26 @@ def test_trigger_route_without_token_is_401(client: TestClient, path: str): assert resp.json()["detail"] == "Missing Authorization header" -@pytest.mark.parametrize("path", TRIGGER_ROUTES) -def test_trigger_route_with_valid_token_is_not_blocked(client: TestClient, path: str): - # The dependency passes; the handler may 500 offline, but it must not be a 401. - resp = client.post(path, headers=_auth(VALID_TOKEN)) - assert resp.status_code != status.HTTP_401_UNAUTHORIZED +def test_policy_updater_trigger_route_with_valid_token_returns_200(client: TestClient, monkeypatch): + trigger = AsyncMock() + monkeypatch.setattr(_sidecar._opal.policy_updater, "trigger_update_policy", trigger) + + resp = client.post("/policy-updater/trigger", headers=_auth(VALID_TOKEN)) + + assert resp.status_code == status.HTTP_200_OK + assert resp.json() == {"status": "ok"} + trigger.assert_awaited_once_with(force_full_update=True) + + +def test_data_updater_trigger_route_with_valid_token_returns_200(client: TestClient, monkeypatch): + get_base = AsyncMock() + monkeypatch.setattr(_sidecar._opal.data_updater, "get_base_policy_data", get_base) + + resp = client.post("/data-updater/trigger", headers=_auth(VALID_TOKEN)) + + assert resp.status_code == status.HTTP_200_OK + assert resp.json() == {"status": "ok"} + get_base.assert_awaited_once_with(data_fetch_reason="request from sdk") @pytest.mark.parametrize("path", TRIGGER_ROUTES)