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)