Files
jarca-ledger-mcp/tests/test_dashboard_authentication.py

132 lines
4.5 KiB
Python

from __future__ import annotations
from pathlib import Path
import sys
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from dashboard import authentication
ADMIN_ID = "test-admin"
ADMIN_PASSWORD = "test-password"
def test_load_admin_credentials_uses_only_required_environment_variables():
credentials = authentication.load_admin_credentials(
{
"JARCA_DASHBOARD_ADMIN_ID": ADMIN_ID,
"JARCA_DASHBOARD_ADMIN_PASSWORD": ADMIN_PASSWORD,
"auth.admin_id": "ignored-streamlit-id",
"auth.admin_password": "ignored-streamlit-password",
}
)
assert credentials.admin_id == ADMIN_ID
assert credentials.password == ADMIN_PASSWORD
@pytest.mark.parametrize(
"environ",
[
{},
{"JARCA_DASHBOARD_ADMIN_ID": ADMIN_ID},
{"JARCA_DASHBOARD_ADMIN_PASSWORD": ADMIN_PASSWORD},
{"JARCA_DASHBOARD_ADMIN_ID": "", "JARCA_DASHBOARD_ADMIN_PASSWORD": ADMIN_PASSWORD},
{"JARCA_DASHBOARD_ADMIN_ID": ADMIN_ID, "JARCA_DASHBOARD_ADMIN_PASSWORD": ""},
{"JARCA_DASHBOARD_ADMIN_ID": 123, "JARCA_DASHBOARD_ADMIN_PASSWORD": ADMIN_PASSWORD},
{"JARCA_DASHBOARD_ADMIN_ID": ADMIN_ID, "JARCA_DASHBOARD_ADMIN_PASSWORD": object()},
],
)
def test_load_admin_credentials_fails_closed_for_missing_empty_or_nonstring_values(environ):
with pytest.raises(authentication.AuthenticationConfigurationError) as exc_info:
authentication.load_admin_credentials(environ)
assert str(exc_info.value) == authentication.CONFIGURATION_ERROR_MESSAGE
assert ADMIN_ID not in str(exc_info.value)
assert ADMIN_PASSWORD not in str(exc_info.value)
def test_load_admin_credentials_fails_closed_when_environment_cannot_be_read():
class UnreadableEnvironment:
def get(self, key):
raise OSError("secret backend unavailable")
with pytest.raises(authentication.AuthenticationConfigurationError) as exc_info:
authentication.load_admin_credentials(UnreadableEnvironment())
assert str(exc_info.value) == authentication.CONFIGURATION_ERROR_MESSAGE
assert "secret backend" not in str(exc_info.value)
def test_verify_credentials_uses_constant_time_comparison_for_both_fields(monkeypatch):
credentials = authentication.AdminCredentials(ADMIN_ID, ADMIN_PASSWORD)
comparisons: list[tuple[bytes, bytes]] = []
def record_compare(left: bytes, right: bytes) -> bool:
comparisons.append((left, right))
return left == right
monkeypatch.setattr(authentication.hmac, "compare_digest", record_compare)
assert authentication.verify_credentials(credentials, ADMIN_ID, ADMIN_PASSWORD) is True
assert comparisons == [
(ADMIN_ID.encode("utf-8"), ADMIN_ID.encode("utf-8")),
(ADMIN_PASSWORD.encode("utf-8"), ADMIN_PASSWORD.encode("utf-8")),
]
comparisons.clear()
assert authentication.verify_credentials(credentials, "wrong-id", "wrong-password") is False
assert comparisons == [
(b"wrong-id", ADMIN_ID.encode("utf-8")),
(b"wrong-password", ADMIN_PASSWORD.encode("utf-8")),
]
def test_failed_login_delay_waits_only_for_remaining_minimum_duration():
sleeps: list[float] = []
authentication.enforce_failed_login_delay(
started_at=10.0,
minimum_seconds=1.0,
monotonic=lambda: 10.25,
sleep=sleeps.append,
)
assert sleeps == [pytest.approx(0.75)]
def test_failed_login_delay_does_not_sleep_after_minimum_duration():
sleeps: list[float] = []
authentication.enforce_failed_login_delay(
started_at=10.0,
minimum_seconds=1.0,
monotonic=lambda: 11.25,
sleep=sleeps.append,
)
assert sleeps == []
def test_authentication_correlation_id_is_noncredential_session_state():
session_state: dict[str, object] = {}
correlation_id = authentication.get_auth_correlation_id(session_state, token_hex=lambda size: "safe-correlation")
assert correlation_id == "safe-correlation"
assert session_state == {authentication.AUTH_CORRELATION_SESSION_KEY: "safe-correlation"}
def test_authentication_event_log_contains_only_event_and_correlation(monkeypatch):
calls: list[tuple[str, tuple[object, ...]]] = []
monkeypatch.setattr(authentication.logger, "info", lambda message, *args: calls.append((message, args)))
authentication.log_auth_event("login_failed", "safe-correlation")
assert calls == [("dashboard_auth event=%s correlation_id=%s", ("login_failed", "safe-correlation"))]
assert ADMIN_ID not in repr(calls)
assert ADMIN_PASSWORD not in repr(calls)