184 lines
6.4 KiB
Python
184 lines
6.4 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
from streamlit.testing.v1 import AppTest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
from dashboard import authentication
|
|
|
|
|
|
ADMIN_ID = "test-admin"
|
|
ADMIN_PASSWORD = "test-password"
|
|
SIGNING_SECRET = "s" * 64
|
|
APP_PATH = Path(__file__).resolve().parents[1] / "dashboard" / "app.py"
|
|
|
|
|
|
def _body_text(at: AppTest) -> str:
|
|
elements = (
|
|
list(at.title)
|
|
+ list(at.header)
|
|
+ list(at.subheader)
|
|
+ list(at.text)
|
|
+ list(at.markdown)
|
|
+ list(at.caption)
|
|
+ list(at.error)
|
|
)
|
|
return "\n".join(str(item.value) for item in elements)
|
|
|
|
|
|
def _configure(monkeypatch) -> authentication.AuthSettings:
|
|
monkeypatch.setenv(authentication.ADMIN_ID_ENV, ADMIN_ID)
|
|
monkeypatch.setenv(authentication.ADMIN_PASSWORD_ENV, ADMIN_PASSWORD)
|
|
monkeypatch.setenv(authentication.SIGNING_SECRET_ENV, SIGNING_SECRET)
|
|
settings = authentication.load_auth_settings()
|
|
assert settings is not None
|
|
return settings
|
|
|
|
|
|
def _app_with_cookie(monkeypatch, token: str | None, *, page: str = "요약") -> AppTest:
|
|
_configure(monkeypatch)
|
|
monkeypatch.setattr(authentication, "_request_cookie", lambda: token)
|
|
at = AppTest.from_file(str(APP_PATH), default_timeout=30)
|
|
at.query_params["page"] = page
|
|
return at.run()
|
|
|
|
|
|
def test_missing_authentication_configuration_fails_closed(monkeypatch, tmp_path):
|
|
from scripts.ci_fixture_db import create_ci_fixture_db
|
|
|
|
fixture = tmp_path / "fixture.sqlite3"
|
|
create_ci_fixture_db(fixture)
|
|
monkeypatch.delenv(authentication.ADMIN_ID_ENV, raising=False)
|
|
monkeypatch.delenv(authentication.ADMIN_PASSWORD_ENV, raising=False)
|
|
monkeypatch.delenv(authentication.SIGNING_SECRET_ENV, raising=False)
|
|
monkeypatch.setenv("JARCA_LEDGER_DB_PATH", str(fixture))
|
|
|
|
at = AppTest.from_file(str(APP_PATH), default_timeout=30).run()
|
|
|
|
assert not at.exception
|
|
assert "관리자 로그인" in _body_text(at)
|
|
assert authentication.CONFIGURATION_ERROR_MESSAGE in _body_text(at)
|
|
assert "이번 달 수입/지출 현황" not in _body_text(at)
|
|
|
|
|
|
def test_main_does_not_resolve_database_or_dispatch_when_authentication_rejects(
|
|
monkeypatch,
|
|
):
|
|
from dashboard import app
|
|
|
|
monkeypatch.setattr(app, "require_dashboard_authentication", lambda: False)
|
|
|
|
def forbidden_database_access():
|
|
raise AssertionError("get_db_path must not run before authentication")
|
|
|
|
monkeypatch.setattr(app, "get_db_path", forbidden_database_access)
|
|
|
|
app.main()
|
|
|
|
|
|
def test_query_parameter_cannot_bypass_login_or_reach_import_page(monkeypatch):
|
|
at = _app_with_cookie(monkeypatch, None, page="Import")
|
|
|
|
assert not at.exception
|
|
assert "관리자 로그인" in _body_text(at)
|
|
assert "PDF Import" not in _body_text(at)
|
|
assert "Import 이력" not in _body_text(at)
|
|
|
|
|
|
def test_login_form_posts_credentials_to_server_side_adapter(monkeypatch):
|
|
at = _app_with_cookie(monkeypatch, None)
|
|
body = _body_text(at)
|
|
|
|
assert 'action="/api/auth/login"' in body
|
|
assert 'method="post"' in body
|
|
assert 'name="admin_id"' in body
|
|
assert 'name="admin_password"' in body
|
|
assert 'name="remember_me"' in body
|
|
assert ADMIN_ID not in repr(at.session_state)
|
|
assert ADMIN_PASSWORD not in repr(at.session_state)
|
|
assert SIGNING_SECRET not in repr(at.session_state)
|
|
|
|
|
|
def test_invalid_cookie_blocks_database_and_page_dispatch(monkeypatch):
|
|
at = _app_with_cookie(monkeypatch, "malformed-token")
|
|
|
|
assert not at.exception
|
|
assert "관리자 로그인" in _body_text(at)
|
|
assert "이번 달 수입/지출 현황" not in _body_text(at)
|
|
assert "/api/auth/cleanup" in _body_text(at)
|
|
|
|
|
|
def test_no_cookie_does_not_render_cookie_cleanup_navigation(monkeypatch):
|
|
at = _app_with_cookie(monkeypatch, None)
|
|
|
|
assert "/api/auth/cleanup" not in _body_text(at)
|
|
|
|
|
|
def test_valid_cookie_allows_dashboard_and_is_not_stored_in_session_state(
|
|
monkeypatch, tmp_path
|
|
):
|
|
from scripts.ci_fixture_db import create_ci_fixture_db
|
|
|
|
fixture = tmp_path / "fixture.sqlite3"
|
|
create_ci_fixture_db(fixture)
|
|
monkeypatch.setenv("JARCA_LEDGER_DB_PATH", str(fixture))
|
|
settings = _configure(monkeypatch)
|
|
token = authentication.issue_persistent_token(
|
|
settings, remember_me=False, now=1_700_000_000
|
|
).value
|
|
monkeypatch.setattr(authentication.time, "time", lambda: 1_700_000_001)
|
|
monkeypatch.setattr(authentication, "_request_cookie", lambda: token)
|
|
|
|
at = AppTest.from_file(str(APP_PATH), default_timeout=30).run()
|
|
|
|
assert not at.exception
|
|
assert "이번 달 수입/지출 현황" in _body_text(at)
|
|
assert 'action="/api/auth/logout"' in _body_text(at)
|
|
assert token not in repr(at.session_state)
|
|
assert ADMIN_PASSWORD not in repr(at.session_state)
|
|
assert SIGNING_SECRET not in repr(at.session_state)
|
|
|
|
|
|
def test_password_rotation_invalidates_existing_cookie(monkeypatch, tmp_path):
|
|
from scripts.ci_fixture_db import create_ci_fixture_db
|
|
|
|
fixture = tmp_path / "fixture.sqlite3"
|
|
create_ci_fixture_db(fixture)
|
|
monkeypatch.setenv("JARCA_LEDGER_DB_PATH", str(fixture))
|
|
settings = _configure(monkeypatch)
|
|
token = authentication.issue_persistent_token(settings, remember_me=True).value
|
|
monkeypatch.setattr(authentication, "_request_cookie", lambda: token)
|
|
|
|
at = AppTest.from_file(str(APP_PATH), default_timeout=30).run()
|
|
assert "이번 달 수입/지출 현황" in _body_text(at)
|
|
|
|
monkeypatch.setenv(authentication.ADMIN_PASSWORD_ENV, "rotated-password")
|
|
at = at.run()
|
|
|
|
assert "관리자 로그인" in _body_text(at)
|
|
assert "이번 달 수입/지출 현황" not in _body_text(at)
|
|
|
|
|
|
def test_signing_secret_rotation_invalidates_existing_cookie(monkeypatch, tmp_path):
|
|
from scripts.ci_fixture_db import create_ci_fixture_db
|
|
|
|
fixture = tmp_path / "fixture.sqlite3"
|
|
create_ci_fixture_db(fixture)
|
|
monkeypatch.setenv("JARCA_LEDGER_DB_PATH", str(fixture))
|
|
settings = _configure(monkeypatch)
|
|
token = authentication.issue_persistent_token(settings, remember_me=True).value
|
|
monkeypatch.setattr(authentication, "_request_cookie", lambda: token)
|
|
|
|
at = AppTest.from_file(str(APP_PATH), default_timeout=30).run()
|
|
assert "이번 달 수입/지출 현황" in _body_text(at)
|
|
|
|
monkeypatch.setenv(authentication.SIGNING_SECRET_ENV, "r" * 64)
|
|
at = at.run()
|
|
|
|
assert "관리자 로그인" in _body_text(at)
|
|
assert "/api/auth/cleanup" in _body_text(at)
|
|
assert "이번 달 수입/지출 현황" not in _body_text(at)
|