242 lines
9.0 KiB
Python
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"]
|