Files
jarca-ledger-mcp/tests/test_monthly_settlement_navigation.py
T

242 lines
9.0 KiB
Python

from __future__ import annotations
import sqlite3
from jarca_ledger_mcp.db import init_db
from dashboard.page_views import monthly_settlement as view
class _FakeStreamlit:
def __init__(self, query=None):
self.query_params = dict(query or {})
self.session_state = {}
self.dataframes = []
self.errors = []
self.headers = []
self.titles = []
self.buttons = []
self.markdowns = []
def header(self, value):
self.headers.append(value)
def title(self, value):
self.titles.append(value)
def caption(self, value):
pass
def markdown(self, value, **kwargs):
self.markdowns.append(value)
def write(self, value):
self.markdowns.append(str(value))
def selectbox(self, label, options, **kwargs):
return options[0]
def info(self, value):
pass
def error(self, value):
self.errors.append(value)
def dataframe(self, value, **kwargs):
self.dataframes.append((value, kwargs))
class _Column:
def __enter__(self):
return self
def __exit__(self, *args):
return False
def button(self, label, **kwargs):
return False
def popover(self, *args, **kwargs):
return self
def columns(self, spec):
return [self._Column() for _ in spec]
def container(self, **kwargs):
return self._Column()
def button(self, label, **kwargs):
self.buttons.append((label, kwargs))
return False
def popover(self, *args, **kwargs):
return self._Column()
def link_button(self, label, url, **kwargs):
self.buttons.append((label, {"url": url, **kwargs}))
return False
def rerun(self):
return None
def _fixture_db(tmp_path):
path = tmp_path / "monthly-navigation.sqlite3"
init_db(path)
with sqlite3.connect(path) as conn:
conn.execute(
"""INSERT INTO monthly_settlement
(id, month, basis_date, status, version, created_by, created_at, updated_by, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
("settlement-2026-09", "2026-09", "2026-09-30", "draft", 1, "test", "2026-09-30T00:00:00Z", "test", "2026-09-30T00:00:00Z"),
)
conn.commit()
return path
def test_month_text_link_uses_exact_canonical_id_and_direct_detail_reads_month(tmp_path, monkeypatch):
db_path = _fixture_db(tmp_path)
fake = _FakeStreamlit()
monkeypatch.setattr(view, "st", fake)
monkeypatch.setattr(view.overview_gateway, "list_monthly_settlement_overview", lambda **kwargs: {
"success": True,
"months": [{"settlement_id": "settlement-2026-09", "month": "2026-09", "status_label": "초안", "income": 100}],
})
view._render_monthly_list(str(db_path))
month_buttons = [item for item in fake.buttons if item[0] == "2026-09"]
assert len(month_buttons) == 1
assert month_buttons[0][1]["type"] == "tertiary"
assert month_buttons[0][1]["on_click"] is view._set_monthly_detail
assert month_buttons[0][1]["args"] == ("settlement-2026-09", "전체", "전체")
assert "?page=monthly_settlement" not in "\n".join(fake.markdowns)
month_buttons[0][1]["on_click"](*month_buttons[0][1]["args"])
assert fake.query_params == {"page": "monthly_settlement", "id": "settlement-2026-09"}
assert not fake.dataframes
detail_fake = _FakeStreamlit({"page": "monthly_settlement", "id": "settlement-2026-09"})
monkeypatch.setattr(view, "st", detail_fake)
calls = []
monkeypatch.setattr(view, "_render_monthly_detail", lambda db, month: calls.append((db, month)))
view.page_monthly_settlement(str(db_path))
assert calls == [(str(db_path), "2026-09")]
assert detail_fake.titles == ["월정산 상세 · 2026-09"]
assert [label for label, _ in detail_fake.buttons] == ["← 월정산 목록", "검토승인"]
def test_unregistered_month_selection_does_not_create_none_link(monkeypatch):
fake = _FakeStreamlit()
monkeypatch.setattr(view, "st", fake)
monkeypatch.setattr(view.overview_gateway, "list_monthly_settlement_overview", lambda **kwargs: {
"success": True,
"months": [{"settlement_id": None, "month": "2026-08", "status_label": "정산 미생성·상세 선택 불가·현재집계(미확정)"}],
})
view._render_monthly_list("fixture.sqlite3")
rendered = "\n".join(fake.markdowns)
assert "2026-08" in rendered
assert "[2026-08]" not in rendered
assert "?page=monthly_settlement&id=None" not in rendered
def test_direct_reload_unknown_id_does_not_fallback_to_month_or_list(tmp_path, monkeypatch):
db_path = _fixture_db(tmp_path)
fake = _FakeStreamlit({"page": "monthly_settlement", "id": "unknown-id", "month": "2026-09"})
monkeypatch.setattr(view, "st", fake)
calls = []
monkeypatch.setattr(view, "_render_monthly_list", lambda db: calls.append(("list", db)))
view.page_monthly_settlement(str(db_path))
assert calls == []
assert fake.errors == ["월정산을 찾을 수 없습니다."]
def test_legacy_month_query_rewrites_to_canonical_id(tmp_path, monkeypatch):
db_path = _fixture_db(tmp_path)
fake = _FakeStreamlit({"page": "monthly_settlement", "month": "2026-09"})
monkeypatch.setattr(view, "st", fake)
monkeypatch.setattr(view.overview_gateway, "list_monthly_settlement_overview", lambda **kwargs: {
"success": True,
"months": [{"settlement_id": "settlement-2026-09", "month": "2026-09", "status_label": "초안"}],
})
monkeypatch.setattr(view, "_render_monthly_detail", lambda db, month: None)
view.page_monthly_settlement(str(db_path))
assert fake.query_params == {"page": "monthly_settlement", "id": "settlement-2026-09"}
def test_back_to_list_uses_canonical_page_and_preserves_filters(monkeypatch):
fake = _FakeStreamlit({"page": "monthly_settlement", "id": "settlement-2026-09", "year": "2026", "status": "미마감"})
monkeypatch.setattr(view, "st", fake)
view._clear_monthly_detail("2026", "미마감")
assert fake.query_params == {"page": "monthly_settlement", "year": "2026", "status": "미마감"}
assert fake.session_state["dashboard_page"] == "월정산"
def test_add_button_opens_dialog_and_navigates_to_created_canonical_id(monkeypatch):
class DialogFake(_FakeStreamlit):
def __init__(self):
super().__init__()
self.reruns = 0
def dialog(self, title):
assert title == "월정산 추가"
return lambda fn: fn
def text_input(self, label, **kwargs):
assert label == "대상 월"
return "2026-10"
def button(self, label, **kwargs):
self.buttons.append((label, kwargs))
return label in {"+", "추가"}
def rerun(self):
self.reruns += 1
fake = DialogFake()
monkeypatch.setattr(view, "st", fake)
monkeypatch.setattr(view.overview_gateway, "list_monthly_settlement_overview", lambda **kwargs: {"success": True, "months": []})
monkeypatch.setattr(view.settlement_gateway, "create_monthly_settlement", lambda month, **kwargs: {"success": True, "settlement_id": "settlement-2026-10"})
view._render_monthly_list("fixture.sqlite3")
assert fake.query_params == {"page": "monthly_settlement", "id": "settlement-2026-10"}
assert fake.reruns == 1
def test_month_link_target_contains_no_write_operation():
source = view._render_monthly_list.__code__.co_names
assert "create_settlement_draft" not in source
assert "finalize_settlement_draft" not in source
def test_back_button_single_click_clears_detail_query_and_reruns(monkeypatch):
class ClickFake(_FakeStreamlit):
def __init__(self):
super().__init__({"page": "monthly_settlement", "id": "settlement-2026-09", "month": "2026-09", "year": "2026", "status": "미마감"})
self.session_state.update({"dashboard_page": "월정산", "monthly_settlement_detail_id": "settlement-2026-09", "monthly_settlement_detail_month": "2026-09"})
self.reruns = 0
def rerun(self):
self.reruns += 1
fake = ClickFake()
monkeypatch.setattr(view, "st", fake)
view._clear_monthly_detail("2026", "미마감")
assert fake.reruns == 1
assert fake.query_params == {"page": "monthly_settlement", "year": "2026", "status": "미마감"}
assert "monthly_settlement_detail_id" not in fake.session_state
assert "monthly_settlement_detail_month" not in fake.session_state
def test_next_rerun_without_detail_query_does_not_restore_detail(monkeypatch):
fake = _FakeStreamlit({"page": "monthly_settlement", "year": "2026", "status": "미마감"})
fake.session_state.update({"dashboard_page": "월정산"})
monkeypatch.setattr(view, "st", fake)
rendered = []
monkeypatch.setattr(view, "_render_monthly_list", lambda db: rendered.append(db))
monkeypatch.setattr(view, "_render_monthly_detail", lambda db, month: (_ for _ in ()).throw(AssertionError("detail restored")))
view.page_monthly_settlement("fixture.sqlite3")
assert rendered == ["fixture.sqlite3"]