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)