import gzip
from decimal import Decimal
import pytest
from ccma.services.gnucash_import import GnuCashImportError, list_accounts, list_transactions
SAMPLE_GNUCASH_XML = """
book-1
Root Account
root-guid
ROOT
Aktiva
assets-guid
ASSET
root-guid
Girokonto
bank-guid
BANK
assets-guid
Beitraege
income-guid
INCOME
root-guid
tx-1
2026-06-21 10:59:00 +0200
Mitgliedsbeitrag Max Mustermann
split-1a
Beitrag Juni
15000/100
15000/100
bank-guid
split-1b
-15000/100
-15000/100
income-guid
tx-2
2026-07-01 08:30:00 +0200
Bankgebuehr
split-2a
-500/100
-500/100
bank-guid
split-2b
500/100
500/100
income-guid
"""
def _write_sample(tmp_path, *, gzipped: bool = False, suffix: str = ".gnucash"):
path = tmp_path / f"test{suffix}"
data = SAMPLE_GNUCASH_XML.encode("utf-8")
if gzipped:
data = gzip.compress(data)
path.write_bytes(data)
return path
def test_list_accounts_builds_hierarchical_full_names(tmp_path) -> None:
path = _write_sample(tmp_path)
accounts = list_accounts(path)
by_guid = {account.guid: account for account in accounts}
assert "root-guid" not in by_guid
assert by_guid["bank-guid"].full_name == "Aktiva:Girokonto"
assert by_guid["bank-guid"].account_type == "BANK"
assert by_guid["assets-guid"].full_name == "Aktiva"
assert by_guid["income-guid"].full_name == "Beitraege"
def test_list_transactions_returns_splits_for_selected_account(tmp_path) -> None:
path = _write_sample(tmp_path)
transactions = list_transactions(path, "bank-guid")
assert [item.guid for item in transactions] == ["tx-1", "tx-2"]
first = transactions[0]
assert first.date.isoformat() == "2026-06-21"
assert first.description == "Mitgliedsbeitrag Max Mustermann"
assert first.memo == "Beitrag Juni"
assert first.amount == Decimal("150.00")
second = transactions[1]
assert second.amount == Decimal("-5.00")
def test_list_transactions_only_includes_splits_on_the_requested_account(tmp_path) -> None:
path = _write_sample(tmp_path)
transactions = list_transactions(path, "income-guid")
assert [item.guid for item in transactions] == ["tx-1", "tx-2"]
assert transactions[0].amount == Decimal("-150.00")
assert transactions[1].amount == Decimal("5.00")
def test_gzip_compressed_gnucash_file_is_read_transparently(tmp_path) -> None:
path = _write_sample(tmp_path, gzipped=True, suffix=".gnucash")
accounts = list_accounts(path)
assert any(account.guid == "bank-guid" for account in accounts)
transactions = list_transactions(path, "bank-guid")
assert len(transactions) == 2
def test_invalid_xml_raises_gnucash_import_error(tmp_path) -> None:
path = tmp_path / "broken.gnucash"
path.write_text("not xml at all <<<", encoding="utf-8")
with pytest.raises(GnuCashImportError):
list_accounts(path)
def test_missing_file_raises_gnucash_import_error(tmp_path) -> None:
with pytest.raises(GnuCashImportError):
list_accounts(tmp_path / "does-not-exist.gnucash")
def test_file_without_book_raises_gnucash_import_error(tmp_path) -> None:
path = tmp_path / "empty.gnucash"
path.write_text('', encoding="utf-8")
with pytest.raises(GnuCashImportError, match="gnc:book"):
list_accounts(path)