[IMP] account_ebics - Add unit tests

Add 97 tests:

- account_ebics: order numbers, camt and CFONB split per account,
  journal lookup, duplicate statements, download and upload wizards,
  key initialisation (INI/HIA/HPB), passphrase change, admin orders.
- account_ebics_batch: scheduled import, import log, reprocess.
- account_ebics_payment_order / account_ebics_batch_payment: upload of
  the payment file and link to the payment document.
- account_ebics_statement_import_test (new, test-only, not to install
  in production): installs the OCA camt and CFONB parsers in CI and
  runs the end-to-end statement import.

The bank is simulated by patching ebics.xfer._setup_client, or the
fintech client classes for the key initialisation; keys, certificates
and the INI letter are real, created in a temporary directory.
This commit is contained in:
Jérémy Didderen committed 2026-10-27 23:14:34 +01:00
1 parent ed0bf5d6ae
commit a1fc381121
50 files changed
+2075 -5

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
from . import test_ebics_admin_wizards
from . import test_ebics_config
from . import test_ebics_file
from . import test_ebics_file_split
from . import test_ebics_userid
from . import test_ebics_xfer
from . import test_ebics_xfer_client
+198
View File
@@ -0,0 +1,198 @@
# Copyright 2026 Noviat.
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
import base64
from contextlib import contextmanager
from unittest.mock import patch
from odoo import Command, fields
from odoo.tools import file_open
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
# Synthetic files: structures mirror real bank files, all values are invented.
SAMPLES_DIR = "account_ebics/tests/samples"
# Bank accounts used in the samples (fictitious, checksum-valid).
IBAN_CMB = "FR7615589297011000000010111"
IBAN_CA833 = [
"FR7613306000012000000020197",
"FR7613306000012000000020294",
"FR7613306000012000000020391",
]
IBAN_LCL = [
"FR7630002004563000000030102",
"FR7630002004563000000030296",
"FR7630002004563000000030393",
"FR7630002004563000000030490",
]
IBAN_CA879 = [
"FR7617906000324000000040136",
"FR7617906000324000000040233",
"FR7617906000324000000040330",
"FR7617906000324000000040427",
]
IBAN_CIC = "FR7630087331005000000050175"
IBAN_HVB = "DE54793200750010000001"
IBAN_SPK = "DE52305500000010000002"
IBAN_MULTI = ["DE69370400440010000003", "DE42370400440010000004"]
IBAN_SUFFIX = "DE15370400440010000005"
def read_sample(name):
with file_open(f"{SAMPLES_DIR}/{name}", "rb") as f:
return f.read()
class FakeEbicsClient:
"""Stand-in for fintech.ebics.EbicsClient, returned by a patched
ebics.xfer._setup_client so that no keyring and no bank are needed.
:param downloads: dict order type ("BTD", "FDL" or the EBICS 2 order
type) -> bytes (or dict of document name -> bytes) to return.
:param error: exception instance raised by every transfer call.
"""
def __init__(self, downloads=None, order_id="N001", error=None):
self.downloads = downloads or {}
self.order_id = order_id
self.error = error
self.uploads = []
self.confirmed = []
self.last_trans_id = "TRANS0001"
def _download(self, key):
if self.error:
raise self.error
return self.downloads[key]
def _upload(self, key, data, kwargs):
if self.error:
raise self.error
self.uploads.append((key, data, kwargs))
return self.order_id
def BTD(self, btf, start=None, end=None, **kwargs):
return self._download("BTD")
def FDL(self, filetype, start=None, end=None):
return self._download("FDL")
def download(self, order_type, params=None):
return self._download(order_type)
def BTU(self, btf, data, **kwargs):
return self._upload("BTU", data, kwargs)
def FUL(self, filetype, data, **kwargs):
return self._upload("FUL", data, kwargs)
def upload(self, order_type, data):
return self._upload(order_type, data, {})
def confirm_download(self, trans_id=None, success=True):
self.confirmed.append((trans_id, success))
class EbicsTestCommon(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.user.groups_id |= cls.env.ref("account_ebics.group_ebics_manager")
cls.eur = cls.env.ref("base.EUR")
cls.eur.active = True
cls.company = cls.env.company
cls.ebics_config = cls.env["ebics.config"].create(
{
"name": "Test Bank",
"ebics_host": "TESTHOST",
"ebics_url": "https://ebics.example.invalid/ebics",
"ebics_partner": "PARTNER1",
"ebics_version": "H004",
"state": "confirm",
}
)
cls.ebics_userid = cls.env["ebics.userid"].create(
{
"name": "USER1",
"ebics_config_id": cls.ebics_config.id,
"user_ids": [Command.set(cls.env.user.ids)],
"company_ids": [Command.set(cls.company.ids)],
"ebics_passphrase": "test-passphrase",
"ebics_passphrase_store": True,
"state": "active_keys",
}
)
cls.ff_camt053 = cls.env.ref("account_ebics.ebics_ff_C53")
cls.ff_cfonb120 = cls.env.ref(
"account_ebics.ebics_ff_FDL_camt_xxx_cfonb120_stm"
)
cls.ff_btd_cfonb120 = cls.env.ref("account_ebics.ebics_ff_btf_cfonb120")
@classmethod
def _create_bank_journal(cls, code, acc_number, currency=None):
# sudo: accountants may not create bank accounts (res.partner.bank)
journal = (
cls.env["account.journal"]
.sudo()
.create(
{
"name": f"Bank {code}",
"code": code,
"type": "bank",
"bank_acc_number": acc_number,
"currency_id": (currency or cls.eur).id,
"company_id": cls.company.id,
}
)
.sudo(False)
)
cls.ebics_config.journal_ids = [Command.link(journal.id)]
return journal
@classmethod
def _create_ebics_file(cls, sample, file_format, name=None):
return cls.env["ebics.file"].create(
{
"name": name or sample,
"data": base64.b64encode(read_sample(sample)),
"date": fields.Datetime.now(),
"format_id": file_format.id,
}
)
@staticmethod
def _new_res():
return {"statement_ids": [], "notifications": []}
@contextmanager
def _fake_client(self, client):
xfer_class = type(self.env["ebics.xfer"])
with patch.object(xfer_class, "_setup_client", return_value=client):
yield client
def _download_wizard(self, file_format, **vals):
return (
self.env["ebics.xfer"]
.with_context(ebics_download=True)
.create(
{
"ebics_config_id": self.ebics_config.id,
"ebics_userid_id": self.ebics_userid.id,
"format_id": file_format.id,
**vals,
}
)
)
def _upload_wizard(self, file_format, data=b"<Document/>", fname="pay.xml", **vals):
return self.env["ebics.xfer"].create(
{
"ebics_config_id": self.ebics_config.id,
"ebics_userid_id": self.ebics_userid.id,
"format_id": file_format.id,
"upload_data": base64.b64encode(data),
"upload_fname": fname,
**vals,
}
)
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"><BkToCstmrStmt><GrpHdr><MsgId>MSG-TEST-0001</MsgId><CreDtTm>2026-10-13T22:00:00</CreDtTm></GrpHdr><Stmt><Id>SUFFIX-STMT-1</Id><ElctrncSeqNb>1</ElctrncSeqNb><CreDtTm>2026-10-13T22:00:00</CreDtTm><Acct><Id><Othr><Id>0010000005EUR</Id></Othr></Id><Ccy>EUR</Ccy></Acct><Bal><Tp><CdOrPrtry><Cd>OPBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">10.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Bal><Tp><CdOrPrtry><Cd>CLBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">15.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Ntry><NtryRef>SUFFIX-0001</NtryRef><Amt Ccy="EUR">5.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Sts><Cd>BOOK</Cd></Sts><BookgDt><Dt>2026-10-13</Dt></BookgDt><ValDt><Dt>2026-10-13</Dt></ValDt><AcctSvcrRef>SUFFIX-0001</AcctSvcrRef><BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn><Prtry><Cd>NTRF+116</Cd><Issr>DK</Issr></Prtry></BkTxCd><NtryDtls><TxDtls><Refs><AcctSvcrRef>SUFFIX-0001</AcctSvcrRef></Refs><Amt Ccy="EUR">5.00</Amt><RmtInf><Ustrd>Rho refund</Ustrd></RmtInf></TxDtls></NtryDtls></Ntry></Stmt></BkToCstmrStmt></Document>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.08"><BkToCstmrStmt><GrpHdr><MsgId>MSG-TEST-0001</MsgId><CreDtTm>2026-10-13T22:00:00</CreDtTm></GrpHdr><Stmt><Id>HVB-STMT-0001</Id><ElctrncSeqNb>1</ElctrncSeqNb><CreDtTm>2026-10-13T22:00:00</CreDtTm><Acct><Id><IBAN>DE54793200750010000001</IBAN></Id><Ccy>EUR</Ccy></Acct><Bal><Tp><CdOrPrtry><Cd>OPBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">5000.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Bal><Tp><CdOrPrtry><Cd>CLBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">3992.50</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Bal><Tp><CdOrPrtry><Cd>CLAV</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">3992.50</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Ntry><NtryRef>HVB-0001</NtryRef><Amt Ccy="EUR">150.00</Amt><CdtDbtInd>DBIT</CdtDbtInd><Sts><Cd>BOOK</Cd></Sts><BookgDt><Dt>2026-10-13</Dt></BookgDt><ValDt><Dt>2026-10-13</Dt></ValDt><AcctSvcrRef>HVB-0001</AcctSvcrRef><BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn><Prtry><Cd>NTRF+116</Cd><Issr>DK</Issr></Prtry></BkTxCd><NtryDtls><TxDtls><Refs><AcctSvcrRef>HVB-0001</AcctSvcrRef><EndToEndId>E2E-LAMBDA-118</EndToEndId></Refs><Amt Ccy="EUR">150.00</Amt><RmtInf><Ustrd>Rechnung 2026-118 Lambda GmbH</Ustrd></RmtInf></TxDtls></NtryDtls></Ntry><Ntry><NtryRef>HVB-0002</NtryRef><Amt Ccy="EUR">857.50</Amt><CdtDbtInd>DBIT</CdtDbtInd><Sts><Cd>BOOK</Cd></Sts><BookgDt><Dt>2026-10-13</Dt></BookgDt><ValDt><Dt>2026-10-13</Dt></ValDt><AcctSvcrRef>HVB-0002</AcctSvcrRef><BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn><Prtry><Cd>NTRF+116</Cd><Issr>DK</Issr></Prtry></BkTxCd><NtryDtls><TxDtls><Refs><AcctSvcrRef>HVB-0002</AcctSvcrRef></Refs><Amt Ccy="EUR">857.50</Amt><AmtDtls><InstdAmt><Amt Ccy="USD">1000.00</Amt></InstdAmt></AmtDtls><Chrgs><TtlChrgsAndTaxAmt Ccy="EUR">12.50</TtlChrgsAndTaxAmt></Chrgs><RmtInf><Ustrd>Zahlung USD Mu Inc</Ustrd></RmtInf></TxDtls></NtryDtls></Ntry></Stmt></BkToCstmrStmt></Document>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"><BkToCstmrStmt><GrpHdr><MsgId>MSG-TEST-0001</MsgId><CreDtTm>2026-10-13T22:00:00</CreDtTm></GrpHdr><Stmt><Id>MULTI-STMT-1</Id><ElctrncSeqNb>1</ElctrncSeqNb><CreDtTm>2026-10-13T22:00:00</CreDtTm><Acct><Id><IBAN>DE69370400440010000003</IBAN></Id><Ccy>EUR</Ccy></Acct><Bal><Tp><CdOrPrtry><Cd>OPBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">100.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Bal><Tp><CdOrPrtry><Cd>CLBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">130.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Ntry><NtryRef>MULTI-0001</NtryRef><Amt Ccy="EUR">30.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Sts><Cd>BOOK</Cd></Sts><BookgDt><Dt>2026-10-13</Dt></BookgDt><ValDt><Dt>2026-10-13</Dt></ValDt><AcctSvcrRef>MULTI-0001</AcctSvcrRef><BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn><Prtry><Cd>NTRF+116</Cd><Issr>DK</Issr></Prtry></BkTxCd><NtryDtls><TxDtls><Refs><AcctSvcrRef>MULTI-0001</AcctSvcrRef></Refs><Amt Ccy="EUR">30.00</Amt><RmtInf><Ustrd>Omikron payment</Ustrd></RmtInf></TxDtls></NtryDtls></Ntry></Stmt><Stmt><Id>MULTI-STMT-2</Id><ElctrncSeqNb>1</ElctrncSeqNb><CreDtTm>2026-10-13T22:00:00</CreDtTm><Acct><Id><IBAN>DE42370400440010000004</IBAN></Id><Ccy>EUR</Ccy></Acct><Bal><Tp><CdOrPrtry><Cd>OPBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">200.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Bal><Tp><CdOrPrtry><Cd>CLBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">180.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Ntry><NtryRef>MULTI-0002</NtryRef><Amt Ccy="EUR">20.00</Amt><CdtDbtInd>DBIT</CdtDbtInd><Sts><Cd>BOOK</Cd></Sts><BookgDt><Dt>2026-10-13</Dt></BookgDt><ValDt><Dt>2026-10-13</Dt></ValDt><AcctSvcrRef>MULTI-0002</AcctSvcrRef><BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn><Prtry><Cd>NTRF+116</Cd><Issr>DK</Issr></Prtry></BkTxCd><NtryDtls><TxDtls><Refs><AcctSvcrRef>MULTI-0002</AcctSvcrRef></Refs><Amt Ccy="EUR">20.00</Amt><RmtInf><Ustrd>Pi fee</Ustrd></RmtInf></TxDtls></NtryDtls></Ntry></Stmt></BkToCstmrStmt></Document>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.08"><BkToCstmrStmt><GrpHdr><MsgId>MSG-TEST-0001</MsgId><CreDtTm>2026-10-13T22:00:00</CreDtTm></GrpHdr><Stmt><Id>NOACC-STMT-1</Id><ElctrncSeqNb>1</ElctrncSeqNb><CreDtTm>2026-10-13T22:00:00</CreDtTm><Acct><Ccy>EUR</Ccy></Acct><Bal><Tp><CdOrPrtry><Cd>OPBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">1.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Bal><Tp><CdOrPrtry><Cd>CLBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">2.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Ntry><NtryRef>NOACC-0001</NtryRef><Amt Ccy="EUR">1.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Sts><Cd>BOOK</Cd></Sts><BookgDt><Dt>2026-10-13</Dt></BookgDt><ValDt><Dt>2026-10-13</Dt></ValDt><AcctSvcrRef>NOACC-0001</AcctSvcrRef><BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn><Prtry><Cd>NTRF+116</Cd><Issr>DK</Issr></Prtry></BkTxCd><NtryDtls><TxDtls><Refs><AcctSvcrRef>NOACC-0001</AcctSvcrRef></Refs><Amt Ccy="EUR">1.00</Amt><RmtInf><Ustrd>Sigma</Ustrd></RmtInf></TxDtls></NtryDtls></Ntry></Stmt></BkToCstmrStmt></Document>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.08"><BkToCstmrStmt><GrpHdr><MsgId>MSG-TEST-0001</MsgId><CreDtTm>2026-10-13T22:00:00</CreDtTm></GrpHdr><Stmt><Id>EMPTY-STMT-1</Id><ElctrncSeqNb>1</ElctrncSeqNb><CreDtTm>2026-10-13T22:00:00</CreDtTm><Acct><Id><IBAN>DE52305500000010000002</IBAN></Id><Ccy>EUR</Ccy></Acct><Bal><Tp><CdOrPrtry><Cd>OPBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">720.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Bal><Tp><CdOrPrtry><Cd>CLBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">720.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal></Stmt></BkToCstmrStmt></Document>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.08"><BkToCstmrStmt><GrpHdr><MsgId>MSG-TEST-0001</MsgId><CreDtTm>2026-10-13T22:00:00</CreDtTm></GrpHdr><Stmt><Id>SPK-STMT-0001</Id><ElctrncSeqNb>1</ElctrncSeqNb><CreDtTm>2026-10-13T22:00:00</CreDtTm><Acct><Id><IBAN>DE52305500000010000002</IBAN></Id><Ccy>EUR</Ccy></Acct><Bal><Tp><CdOrPrtry><Cd>OPBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">720.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Bal><Tp><CdOrPrtry><Cd>CLBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">1065.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Ntry><NtryRef>SPK-0001</NtryRef><Amt Ccy="EUR">400.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Sts><Cd>BOOK</Cd></Sts><BookgDt><Dt>2026-10-13</Dt></BookgDt><ValDt><Dt>2026-10-13</Dt></ValDt><AcctSvcrRef>SPK-0001</AcctSvcrRef><BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn><Prtry><Cd>NTRF+116</Cd><Issr>DK</Issr></Prtry></BkTxCd><NtryDtls><TxDtls><Refs><AcctSvcrRef>SPK-0001</AcctSvcrRef></Refs><Amt Ccy="EUR">400.00</Amt><RmtInf><Ustrd>Gutschrift Nu KG</Ustrd></RmtInf></TxDtls></NtryDtls></Ntry><Ntry><NtryRef>SPK-0002</NtryRef><Amt Ccy="EUR">55.00</Amt><CdtDbtInd>DBIT</CdtDbtInd><Sts><Cd>BOOK</Cd></Sts><BookgDt><Dt>2026-10-13</Dt></BookgDt><ValDt><Dt>2026-10-13</Dt></ValDt><AcctSvcrRef>SPK-0002</AcctSvcrRef><BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn><Prtry><Cd>NTRF+116</Cd><Issr>DK</Issr></Prtry></BkTxCd><NtryDtls><TxDtls><Refs><AcctSvcrRef>SPK-0002</AcctSvcrRef></Refs><Amt Ccy="EUR">55.00</Amt><RmtInf><Ustrd>Lastschrift Xi Versicherung</Ustrd></RmtInf></TxDtls></NtryDtls></Ntry></Stmt></BkToCstmrStmt></Document>
@@ -0,0 +1 @@
0115589 29701EUR2 10000000101 131026 0000000015000{
@@ -0,0 +1,10 @@
0117906 00032EUR2 40000000401 131026** COMPTE COURANT 0000000000100{
0717906 00032EUR2 40000000401 131026--------------------------------------------------0000000000100{
0117906 00032EUR2 40000000402 131026** COMPTE EPARGNE 0000000000200{
0717906 00032EUR2 40000000402 131026--------------------------------------------------0000000000200{
0117906 00032EUR2 40000000403 131026 0000000000300{
0717906 00032EUR2 40000000403 131026 0000000000300{
0117906 00032EUR2 40000000404 131026 0000000040000{
0417906000000032EUR2 4000000040405131026 131026VIR ETA SA 0000000 0000000015000{
0417906000000032EUR2 4000000040405131026 131026PRLV THETA ASSURANCES 0000000 0000000002100}
0717906 00032EUR2 40000000404 131026 0000000052900{
@@ -0,0 +1,14 @@
0113306 00001EUR2 20000000201 131026 0000000084000{
0413306000000001EUR2 2000000020105131026 131026VIR SEPA RECU GAMMA SARL 0000000 0000000006400{
0513306000000001EUR2 2000000020105131026 NBEGAMMA SARL
0513306000000001EUR2 2000000020105131026 LCCFACT 99812
0513306000000001EUR2 2000000020105131026 REFE2E-GAMMA-1
0413306000000001EUR2 2000000020105131026 131026PRLV SEPA DELTA ENERGIE 0000000 0000000001325N
0513306000000001EUR2 2000000020105131026 NPYDELTA ENERGIE
0513306000000001EUR2 2000000020105131026 RUMRUM-DELTA-0042
0713306 00001EUR2 20000000201 131026 0000000089074E
0113306 00001EUR2 20000000202 131026 0000000012345{
0713306 00001EUR2 20000000202 131026 0000000012345{
0113306 00001EUR2 20000000203 131026 0000000000500{
0413306000000001EUR2 2000000020305131026 131026FRAIS TENUE DE COMPTE 0000000 0000000000099}
0713306 00001EUR2 20000000203 131026 0000000000401{
@@ -0,0 +1,14 @@
0130087 33100EUR2 50000000501 131026 0000000009900{
0430087000033100EUR2 5000000050105131026 131026VIR SEPA IOTA DISTRIBUTION 0000000 0000000021500{
0530087000033100EUR2 5000000050105131026 NOMIOTA DISTRIBUTION
0530087000033100EUR2 5000000050105131026 RIB30087331005000000050199
0530087000033100EUR2 5000000050105131026 IBEFR7630087331005000000050199
0530087000033100EUR2 5000000050105131026 CBECMCIFRPP
0530087000033100EUR2 5000000050105131026 LCCFACTURE 7731
0530087000033100EUR2 5000000050105131026 RCNSALE
0530087000033100EUR2 5000000050105131026 REFE2E-IOTA-7731
0430087000033100EUR2 5000000050105131026 131026PRLV SEPA KAPPA TELECOM 0000000 0000000000499R
0530087000033100EUR2 5000000050105131026 NPYKAPPA TELECOM
0530087000033100EUR2 5000000050105131026 RUMRUM-KAPPA-1
0530087000033100EUR2 5000000050105131026 LIBABONNEMENT
0730087 33100EUR2 50000000501 131026 0000000030900A
@@ -0,0 +1 @@
0115589 29701EUR2 10000000101 131026 0000000015000{ 0715589 29701EUR2 10000000101 131026 0000000015000{
@@ -0,0 +1 @@
0130002 00456EUR2 30000000301 131026 0000000001000{ 0730002 00456EUR2 30000000301 131026 0000000001000{ 0130002 00456EUR2 30000000302 131026 0000000025000{ 0430002000000456EUR2 3000000030205131026 131026VIR SEPA EPSILON SAS 0000000 0000000007800{ 0530002000000456EUR2 3000000030205131026 LIBACOMPTE CHANTIER 12 0430002000000456EUR2 3000000030205131026 131026CB STATION ZETA 0000000 0000000000612} 0730002 00456EUR2 30000000302 131026 0000000032188{ 0130002 00456EUR2 30000000303 131026 0000000000000{ 0730002 00456EUR2 30000000303 131026 0000000000000{ 0130002 00456EUR2 30000000304 131026 0000000003200{ 0430002000000456EUR2 3000000030405131026 131026REMISE CHEQUES 0000000 0000000004500{ 0730002 00456EUR2 30000000304 131026 0000000007700{
@@ -0,0 +1 @@
0115589 29701EUR2 10000000101 131026 0000000015000{ 0415589000029701EUR2 1000000010105131026 131026PRLV SEPA FOURNISSEUR ALPHA 0000000 0000000002504} 0515589000029701EUR2 1000000010105131026 LIBFACTURE 2026-0412 0515589000029701EUR2 1000000010105131026 RCNREF ALPHA 77881 0415589000029701EUR2 1000000010105131026 131026VIR SEPA CLIENT BETA 0000000 0000000012000{ 0515589000029701EUR2 1000000010105131026 LCCCOMMANDE 5521 0715589 29701EUR2 10000000101 131026 0000000024496{
+1
View File
@@ -0,0 +1 @@
this is not an xml document
@@ -0,0 +1,3 @@
Kundenprotokoll (PTK)
Auftragsart: C53 Auftragsnummer: A00T Teilnehmer: TESTUSER1
Status: Auftrag erfolgreich verarbeitet
@@ -0,0 +1,152 @@
# Copyright 2026 Noviat.
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
import pprint
from unittest.mock import patch
from fintech.ebics import EbicsFunctionalError, EbicsKeyRing, EbicsTechnicalError
from odoo.exceptions import UserError
from odoo.tests import tagged
from .test_ebics_userid import EbicsKeysCommon
# Test passphrases for throwaway keys created in a temporary directory.
INIT_PHRASE = "init-passphrase"
NEXT_PHRASE = "next-passphrase"
OTHER_PHRASE = "other-passphrase"
WRONG_PHRASE = "wrong-passphrase"
@tagged("post_install", "-at_install")
class TestChangePassphrase(EbicsKeysCommon):
def setUp(self):
super().setUp()
self.userid = self._new_userid()
self._init_1(self.userid)
def _change(self, **vals):
wizard = self.env["ebics.change.passphrase"].create(
{"ebics_userid_id": self.userid.id, **vals}
)
wizard.change_passphrase()
return wizard
def test_change_passphrase(self):
wizard = self._change(
old_pass=INIT_PHRASE,
new_pass=NEXT_PHRASE,
new_pass_check=NEXT_PHRASE,
)
self.assertEqual(self.userid.ebics_passphrase, NEXT_PHRASE)
self.assertIn("has been changed", wizard.note)
keyring = EbicsKeyRing(keys=self.userid.ebics_keys_fn, passphrase=NEXT_PHRASE)
self.assertTrue(keyring["#USER"])
def test_wrong_old_passphrase(self):
with self.assertRaisesRegex(UserError, "Incorrect old passphrase"):
self._change(
old_pass=WRONG_PHRASE,
new_pass=NEXT_PHRASE,
new_pass_check=NEXT_PHRASE,
)
def test_verification_mismatch(self):
with self.assertRaisesRegex(UserError, "verification error"):
self._change(new_pass=NEXT_PHRASE, new_pass_check=OTHER_PHRASE)
def test_new_equals_old(self):
with self.assertRaisesRegex(UserError, "equal to old"):
self._change(new_pass=INIT_PHRASE, new_pass_check=INIT_PHRASE)
def test_wrong_old_passphrase_not_stored(self):
"""Without a stored passphrase, fintech rejects the wrong one."""
self.userid.ebics_passphrase_store = False
self.userid.ebics_passphrase = False
with self.assertRaisesRegex(UserError, "(?i)passphrase"):
self._change(
old_pass=WRONG_PHRASE,
new_pass=NEXT_PHRASE,
new_pass_check=NEXT_PHRASE,
)
def test_change_passphrase_not_stored(self):
"""The new passphrase must not be written when storing is off."""
self.userid.ebics_passphrase_store = False
self.userid.ebics_passphrase = False
wizard = self._change(
old_pass=INIT_PHRASE,
new_pass=NEXT_PHRASE,
new_pass_check=NEXT_PHRASE,
)
self.assertFalse(self.userid.ebics_passphrase)
self.assertIn("has been changed", wizard.note)
keyring = EbicsKeyRing(keys=self.userid.ebics_keys_fn, passphrase=NEXT_PHRASE)
self.assertTrue(keyring["#USER"])
class FakeAdminClient:
def __init__(self, error=None):
self.error = error
def _order(self, name):
if self.error:
raise self.error
return {"order": name, "Services": [{"ServiceName": "SCT"}]}
def HAA(self, parsed=False):
return self._order("HAA")
def HPD(self, parsed=False):
return self._order("HPD")
def HKD(self, parsed=False):
return self._order("HKD")
def HTD(self, parsed=False):
return self._order("HTD")
@tagged("post_install", "-at_install")
class TestAdminOrder(EbicsKeysCommon):
def _run(self, order_type, client):
wizard = self.env["ebics.admin.order"].create(
{
"ebics_config_id": self.ebics_config.id,
"ebics_userid_id": self.ebics_userid.id,
"admin_order_type": order_type,
}
)
wizard_class = type(wizard)
with patch.object(wizard_class, "_setup_client", return_value=client):
wizard.ebics_admin_order()
return wizard
def test_admin_orders(self):
for order_type in ("HAA", "HPD", "HKD", "HTD"):
with self.subTest(order_type=order_type):
wizard = self._run(order_type, FakeAdminClient())
expected = pprint.PrettyPrinter().pformat(
FakeAdminClient()._order(order_type)
)
self.assertEqual(wizard.note, expected)
def test_admin_order_errors(self):
cases = [
(EbicsTechnicalError("061001", "EBICS_AUTHENTICATION_FAILED"), "Technical"),
(
EbicsFunctionalError("091005", "EBICS_NO_DOWNLOAD_DATA_AVAILABLE"),
"Functional",
),
]
for error, expected in cases:
with self.subTest(error=type(error).__name__):
wizard = self._run("HAA", FakeAdminClient(error=error))
self.assertIn(
f"EBICS {expected} Error during execution of order HAA", wizard.note
)
self.assertIn(f"(code: {error.code})", wizard.note)
def test_admin_order_client_setup_failure(self):
wizard = self._run("HPD", False)
self.assertIn("client setup failed", wizard.note)
+52
View File
@@ -0,0 +1,52 @@
# Copyright 2026 Noviat.
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
from odoo.exceptions import UserError
from odoo.tests import tagged
from .common import EbicsTestCommon
IBAN_FOR_COMPANY_TEST = "FR7630004000031234567890143"
@tagged("post_install", "-at_install")
class TestEbicsConfig(EbicsTestCommon):
def test_update_order_number(self):
cases = [
("A000", "A001"),
("A009", "A00A"),
("A00Z", "A010"),
("A0Z9", "A0ZA"),
("AZZZ", "B000"),
("ZZZY", "A000"),
# ZZZZ is accepted by the constraint, it must wrap as well
("ZZZZ", "A000"),
]
for current, expected in cases:
with self.subTest(current=current):
self.ebics_config._update_order_number(current)
self.assertEqual(self.ebics_config.order_number, expected)
def test_order_number_pattern(self):
for valid in ("A000", "B1Z9", "ZZZZ"):
with self.subTest(valid=valid):
self.ebics_config.order_number = valid
# No 5-character case: Char(size=4) truncates before the constraint runs.
for invalid in ("a001", "A01", "1000", "A-01"):
with self.subTest(invalid=invalid), self.assertRaises(UserError):
self.ebics_config.order_number = invalid
def test_unlink_confirmed_config_is_blocked(self):
with self.assertRaises(UserError):
self.ebics_config.unlink()
def test_unlink_draft_config(self):
self.ebics_userid.unlink()
self.ebics_config.set_to_draft()
self.ebics_config.unlink()
self.assertFalse(self.ebics_config.exists())
def test_company_ids_follow_journals(self):
journal = self._create_bank_journal("TCMP", IBAN_FOR_COMPANY_TEST)
self.assertEqual(self.ebics_config.company_ids, journal.company_id)
self.assertEqual(journal.ebics_config_id, self.ebics_config)
+170
View File
@@ -0,0 +1,170 @@
# Copyright 2026 Noviat.
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
from unittest.mock import patch
from odoo import Command
from odoo.exceptions import UserError
from odoo.tests import tagged
from .common import IBAN_SPK, EbicsTestCommon
@tagged("post_install", "-at_install")
class TestEbicsFile(EbicsTestCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.journal = cls._create_bank_journal("TSPK", IBAN_SPK)
def _statement(self, name, date="2026-10-13", import_format="camt053"):
statement = self.env["account.bank.statement"].create(
{
"name": name,
"journal_id": self.journal.id,
"line_ids": [
Command.create(
{
"journal_id": self.journal.id,
"date": date,
"payment_ref": f"line {name}",
"amount": 10.0,
}
)
],
}
)
statement.import_format = import_format
return statement
def test_duplicate_statement_is_removed(self):
self._statement("ST-1")
duplicate = self._statement("ST-1")
res = self._new_res()
res["statement_ids"] = duplicate.ids
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
kept = ebics_file._statement_duplicate_check(res, duplicate)
self.assertFalse(kept)
self.assertFalse(duplicate.exists())
self.assertEqual(res["statement_ids"], [])
self.assertEqual(res["notifications"][0]["type"], "warning")
self.assertIn("ST-1", res["notifications"][0]["message"])
def test_duplicate_check_scope(self):
"""A checked statement only collides with the same name, date and format."""
# existing statements: same name, other date / same name, other format
self._statement("ST-2", date="2026-10-14")
self._statement("ST-2", import_format="cfonb120")
new = self._statement("ST-2")
# outside DUP_CHECK_FORMATS: never checked
unchecked = self._statement("ST-2", import_format="camt052")
statements = new | unchecked
res = self._new_res()
res["statement_ids"] = statements.ids
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
kept = ebics_file._statement_duplicate_check(res, statements)
self.assertEqual(kept, statements)
self.assertFalse(res["notifications"])
def test_process_result_note(self):
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
ebics_file.note_process = ""
statement = self._statement("ST-3")
res = {
"statement_ids": statement.ids,
"notifications": [
{"type": "error", "message": "Broken entry", "details": "line 4"},
{"type": "warning", "message": "Odd balance"},
{"info": "Something to know"},
"Plain text warning",
],
}
ebics_file._process_download_result(res, file_format="camt053")
note = ebics_file.note_process
self.assertIn("Broken entry\nline 4", note)
self.assertIn("Number of errors: 1", note)
self.assertIn("Number of warnings: 3", note)
self.assertIn("1 bank statement has been imported", note)
self.assertEqual(ebics_file.bank_statement_ids, statement)
self.assertEqual(statement.import_format, "camt053")
self.assertEqual(ebics_file.company_ids, self.company)
def test_process_result_empty_file(self):
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
ebics_file.note_process = ""
ebics_file._process_download_result(self._new_res(), file_format="camt053")
self.assertIn("doesn't contain any transaction", ebics_file.note_process)
def test_unlink_done_file_is_blocked(self):
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
ebics_file.set_to_done()
with self.assertRaises(UserError):
ebics_file.unlink()
def test_unlink_removes_statements(self):
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
statement = self._statement("ST-4")
statement.ebics_file_id = ebics_file
ebics_file.unlink()
self.assertFalse(statement.exists())
def test_unlink_pain002_file(self):
"""pain.002 processing is a placeholder: its files must stay removable."""
ebics_file = self._create_ebics_file(
"ptk_protocol.txt", self.env.ref("account_ebics.ebics_ff_Z01")
)
ebics_file.unlink()
self.assertFalse(ebics_file.exists())
def test_process_format_without_method(self):
"""Modelled on a Sparkasse customer protocol (PTK): stored, not processed."""
ptk = self.env["ebics.file.format"].create(
{"name": "PTK", "type": "down", "order_type": "PTK", "suffix": "txt"}
)
ebics_file = self._create_ebics_file("ptk_protocol.txt", ptk)
with self.assertRaisesRegex(UserError, "no support"):
ebics_file.process()
def test_check_import_module(self):
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
self.assertTrue(ebics_file._check_import_module("account_ebics"))
self.assertFalse(
ebics_file._check_import_module("no_such_module", raise_if_not_found=False)
)
with self.assertRaises(UserError):
ebics_file._check_import_module("no_such_module")
def test_enterprise_import_does_not_commit_in_tests(self):
"""The Enterprise path commits after each statement in production."""
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
statement = self._statement("ST-5")
st_datas = [{"acc_number": "0010000002", "journal_id": self.journal.id}]
def create_statement(res, st_data):
res["statement_ids"].append(statement.id)
file_class = type(ebics_file)
cursor_class = type(self.env.cr)
res = self._new_res()
with (
patch.object(
file_class, "_create_bank_statement_oe", side_effect=create_statement
),
patch.object(cursor_class, "commit", side_effect=AssertionError("commit")),
):
ebics_file._process_bank_statement_oe(res, st_datas)
self.assertEqual(res["statement_ids"], statement.ids)
self.assertFalse(res["notifications"])
def test_enterprise_import_reports_errors(self):
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
st_datas = [{"acc_number": "0010000002", "journal_id": self.journal.id}]
res = self._new_res()
with patch.object(
type(ebics_file),
"_create_bank_statement_oe",
side_effect=UserError("Journal mismatch"),
):
ebics_file._process_bank_statement_oe(res, st_datas)
self.assertIn("Journal mismatch", res["notifications"][0]["message"])
self.assertIn("0010000002", res["notifications"][0]["message"])
@@ -0,0 +1,232 @@
# Copyright 2026 Noviat.
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
import base64
from lxml import etree
from odoo.tests import tagged
from .common import (
IBAN_CA833,
IBAN_CA879,
IBAN_CIC,
IBAN_CMB,
IBAN_HVB,
IBAN_LCL,
IBAN_MULTI,
IBAN_SPK,
IBAN_SUFFIX,
EbicsTestCommon,
)
@tagged("post_install", "-at_install")
class TestSplitCfonb(EbicsTestCommon):
"""EBICS files may hold several accounts and empty statements: they are
split per statement before being handed to the CFONB parser."""
def _split(self, sample):
ebics_file = self._create_ebics_file(sample, self.ff_cfonb120)
res = self._new_res()
return ebics_file._split_cfonb(res), res
def _lines(self, st_data):
return base64.b64decode(st_data["data"]).decode("latin-1").splitlines()
def test_no_line_terminator_single_account(self):
"""Modelled on Crédit Mutuel de Bretagne: no line terminator at all."""
journal = self._create_bank_journal("TCMB", IBAN_CMB)
datas, res = self._split("cfonb120_no_eol_single_account.dat")
self.assertFalse(res["notifications"])
self.assertEqual(len(datas), 1)
self.assertEqual(datas[0]["acc_number"], "10000000101")
self.assertEqual(datas[0]["journal_id"], journal.id)
self.assertEqual(datas[0]["company_id"], self.company.id)
lines = self._lines(datas[0])
self.assertEqual(
[x[:2] for x in lines], ["01", "04", "05", "05", "04", "05", "07"]
)
self.assertTrue(all(len(x) == 120 for x in lines))
def test_empty_statements_only(self):
self._create_bank_journal("TCMB", IBAN_CMB)
datas, res = self._split("cfonb120_no_eol_empty_only.dat")
self.assertEqual(datas, [])
self.assertFalse(res["notifications"])
def test_multi_account_with_complement_lines(self):
"""Modelled on Crédit Agricole: 3 accounts, the middle one empty, and a
positive-zero overpunch ('{') on its balance."""
j1 = self._create_bank_journal("TCA1", IBAN_CA833[0])
j3 = self._create_bank_journal("TCA3", IBAN_CA833[2])
datas, res = self._split("cfonb120_lf_multi_account_05.dat")
self.assertFalse(res["notifications"])
self.assertEqual([d["journal_id"] for d in datas], [j1.id, j3.id])
self.assertEqual(
[x[:2] for x in self._lines(datas[0])],
["01", "04", "05", "05", "05", "04", "05", "05", "07"],
)
def test_multi_account_missing_journal(self):
self._create_bank_journal("TCA1", IBAN_CA833[0])
datas, res = self._split("cfonb120_lf_multi_account_05.dat")
self.assertEqual(len(datas), 1)
self.assertEqual(len(res["notifications"]), 1)
self.assertEqual(res["notifications"][0]["type"], "error")
self.assertIn("20000000203", res["notifications"][0]["message"])
def test_no_line_terminator_mixed_empty_and_full(self):
"""Modelled on LCL: 4 accounts, no terminator, empty and full mixed."""
journals = [
self._create_bank_journal(f"TLC{i}", iban)
for i, iban in enumerate(IBAN_LCL)
]
datas, res = self._split("cfonb120_no_eol_multi_account_mixed.dat")
self.assertFalse(res["notifications"])
self.assertEqual(
[d["journal_id"] for d in datas], [journals[1].id, journals[3].id]
)
def test_minimal_file_without_complement_lines(self):
"""Modelled on Crédit Agricole 879: no 05 records, an account label in
the free zone of 01 and dashes in 07, three empty statements before
the only one with transactions."""
journal = self._create_bank_journal("TCA9", IBAN_CA879[3])
datas, res = self._split("cfonb120_lf_minimal_no_05.dat")
self.assertFalse(res["notifications"])
self.assertEqual(len(datas), 1)
self.assertEqual(datas[0]["journal_id"], journal.id)
self.assertEqual(
[x[:2] for x in self._lines(datas[0])], ["01", "04", "04", "07"]
)
def test_rich_complement_lines_are_kept(self):
"""Modelled on CIC: every 05 qualifier stays with its statement."""
self._create_bank_journal("TCIC", IBAN_CIC)
datas, res = self._split("cfonb120_lf_rich_05.dat")
self.assertFalse(res["notifications"])
self.assertEqual(len(datas), 1)
qualifiers = [x[45:48] for x in self._lines(datas[0]) if x[:2] == "05"]
self.assertEqual(
qualifiers,
["NOM", "RIB", "IBE", "CBE", "LCC", "RCN", "REF", "NPY", "RUM", "LIB"],
)
def test_bad_length(self):
datas, res = self._split("cfonb120_bad_length.dat")
self.assertEqual(datas, [])
self.assertEqual(res["notifications"][0]["type"], "error")
self.assertIn("120", res["notifications"][0]["message"])
def test_journal_in_other_currency(self):
usd = self.env.ref("base.USD")
self._create_bank_journal("TCMB", IBAN_CMB, currency=usd)
datas, res = self._split("cfonb120_no_eol_single_account.dat")
self.assertEqual(datas, [])
self.assertEqual(res["notifications"][0]["type"], "error")
@tagged("post_install", "-at_install")
class TestSplitCamt(EbicsTestCommon):
def _split(self, sample):
ebics_file = self._create_ebics_file(sample, self.ff_camt053)
res = self._new_res()
return ebics_file._split_camt(res), res
def _statements(self, st_data):
root = etree.fromstring(base64.b64decode(st_data["data"]))
ns = {"ns": root.nsmap[None]}
return root.xpath("//ns:Stmt/ns:Acct/ns:Id//text()", namespaces=ns)
def test_single_statement_clav_fx(self):
"""Modelled on UniCredit HVB: OPBD/CLBD/CLAV balances, FX entry."""
journal = self._create_bank_journal("THVB", IBAN_HVB)
datas, res = self._split("camt053_hvb_clav_fx.xml")
self.assertFalse(res["notifications"])
self.assertEqual(len(datas), 1)
self.assertEqual(datas[0]["journal_id"], journal.id)
self.assertEqual(datas[0]["acc_number"], IBAN_HVB)
def test_multi_account_is_split(self):
j1 = self._create_bank_journal("TMU1", IBAN_MULTI[0])
j2 = self._create_bank_journal("TMU2", IBAN_MULTI[1])
datas, res = self._split("camt053_multi_account.xml")
self.assertFalse(res["notifications"])
self.assertEqual([d["journal_id"] for d in datas], [j1.id, j2.id])
self.assertEqual(self._statements(datas[0]), [IBAN_MULTI[0]])
self.assertEqual(self._statements(datas[1]), [IBAN_MULTI[1]])
def test_currency_suffix_is_stripped(self):
"""Built from the code: some banks (e.g. Commerzbank) append the
currency to the account number."""
journal = self._create_bank_journal("TSUF", IBAN_SUFFIX)
datas, res = self._split("camt053_currency_suffix.xml")
self.assertFalse(res["notifications"])
self.assertEqual(datas[0]["acc_number"], "0010000005")
self.assertEqual(datas[0]["journal_id"], journal.id)
def test_statement_without_entries_is_skipped(self):
self._create_bank_journal("TSPK", IBAN_SPK)
datas, res = self._split("camt053_no_entries.xml")
self.assertEqual(datas, [])
self.assertFalse(res["notifications"])
def test_statement_without_account(self):
datas, res = self._split("camt053_no_account.xml")
self.assertEqual(datas, [])
self.assertEqual(res["notifications"][0]["type"], "error")
self.assertIn(
"No bank account number found", res["notifications"][0]["message"]
)
def test_invalid_xml(self):
datas, res = self._split("invalid.xml")
self.assertEqual(datas, [])
self.assertEqual(res["notifications"][0]["type"], "error")
self.assertIn("Invalid XML", res["notifications"][0]["message"])
@tagged("post_install", "-at_install")
class TestLookupJournal(EbicsTestCommon):
def setUp(self):
super().setUp()
self.ebics_file = self._create_ebics_file(
"camt053_sparkasse.xml", self.ff_camt053
)
self.res = self._new_res()
def test_unknown_currency(self):
currency, journal = self.ebics_file._lookup_journal(
self.res, "0010000002", "XYZ"
)
self.assertFalse(currency)
self.assertFalse(journal)
self.assertIn("XYZ", self.res["notifications"][0]["message"])
def test_no_journal(self):
currency, journal = self.ebics_file._lookup_journal(
self.res, "0010000002", "EUR"
)
self.assertEqual(currency, self.eur)
self.assertFalse(journal)
self.assertEqual(self.res["notifications"][0]["type"], "error")
def test_journal_currency_match(self):
expected = self._create_bank_journal("TSPK", IBAN_SPK)
currency, journal = self.ebics_file._lookup_journal(
self.res, "0010000002", "eur"
)
self.assertEqual(currency, self.eur)
self.assertEqual(journal, expected)
self.assertFalse(self.res["notifications"])
def test_journal_without_currency_uses_company_currency(self):
expected = self._create_bank_journal("TSPK", IBAN_SPK)
expected.sudo().currency_id = False
code = self.company.currency_id.name
currency, journal = self.ebics_file._lookup_journal(
self.res, "0010000002", code
)
self.assertEqual(currency, self.company.currency_id)
self.assertEqual(journal, expected)
+284
View File
@@ -0,0 +1,284 @@
# Copyright 2026 Noviat.
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
import base64
import gc
import itertools
import os
import shutil
import tempfile
from unittest.mock import patch
from urllib.error import URLError
from fintech.ebics import EbicsFunctionalError, EbicsKeyRing, EbicsTechnicalError
from odoo import Command, fields
from odoo.exceptions import UserError
from odoo.tests import tagged
from odoo.tools import mute_logger
from odoo.addons.account_ebics.models import ebics_userid as ebics_userid_module
from .common import EbicsTestCommon
# Throwaway passphrase for keys created in a temporary directory.
WRONG_PHRASE = "wrong-passphrase"
PUBLIC_BANK_KEYS = (
"Public bank keys (simulated)\nX002 hash: 00 11 22\nE002 hash: 33 44 55\n"
)
def fake_init_client(hev=None, ini_error=None, hpb_error=None):
"""Return a stand-in for fintech's EbicsClient covering the bank side of
the key ceremony. Keys, certificates and the INI letter stay real."""
class FakeInitClient:
calls = []
def __init__(self, bank, user, version=None):
self.bank = bank
def HEV(self):
self.calls.append("HEV")
return hev if hev is not None else {"H004": "2.5", "H005": "3.0"}
def _order(self, name):
self.calls.append(name)
return self.bank._next_order_id("PARTNER1")
def INI(self):
if ini_error:
raise ini_error
return self._order("INI")
def HIA(self):
return self._order("HIA")
def HPB(self):
if hpb_error:
raise hpb_error
self.calls.append("HPB")
return PUBLIC_BANK_KEYS
return FakeInitClient
class EbicsKeysCommon(EbicsTestCommon):
"""Real keys in a temporary directory, simulated bank."""
_userid_seq = itertools.count(1)
@classmethod
def setUpClass(cls):
super().setUpClass()
keys_dir = tempfile.mkdtemp(prefix="ebics_keys_test_")
cls.addClassCleanup(shutil.rmtree, keys_dir, ignore_errors=True)
cls.ebics_config.write({"ebics_keys": keys_dir, "ebics_key_bitlength": 2048})
cls.bank_fr = cls.env["res.bank"].create(
{"name": "Banque Test", "country": cls.env.ref("base.fr").id}
)
cls.journal = cls._create_bank_journal("TKEY", "FR7630004000031234567890143")
cls.journal.bank_account_id.sudo().bank_id = cls.bank_fr
def _new_userid(self, **vals):
# A new name per test: fintech keeps open keyrings per file in-process.
return self.env["ebics.userid"].create(
{
"name": f"KEYUSER{next(self._userid_seq)}",
"ebics_config_id": self.ebics_config.id,
"user_ids": [Command.set(self.env.user.ids)],
"company_ids": [Command.set(self.company.ids)],
"ebics_passphrase": "init-passphrase",
**vals,
}
)
def _init_1(self, userid, client_class=None):
client_class = client_class or fake_init_client()
with patch.object(ebics_userid_module, "EbicsClient", client_class):
userid.ebics_init_1()
return client_class
@tagged("post_install", "-at_install")
class TestKeyCeremony(EbicsKeysCommon):
def test_full_ceremony(self):
userid = self._new_userid(ebics_key_x509=True, ebics_key_x509_dn_cn="Test")
client_class = self._init_1(userid)
self.assertEqual(client_class.calls, ["HEV", "INI", "HIA"])
self.assertEqual(userid.state, "init")
self.assertTrue(os.path.isfile(userid.ebics_keys_fn))
self.assertTrue(userid.ebics_keys_found)
self.assertTrue(base64.b64decode(userid.ebics_ini_letter).startswith(b"%PDF"))
today = fields.Date.today().isoformat()
self.assertEqual(userid.ebics_ini_letter_fn, f"TESTHOST_ini_letter_{today}.pdf")
userid.ebics_init_2()
self.assertEqual(userid.state, "get_bank_keys")
with patch.object(ebics_userid_module, "EbicsClient", client_class):
userid.ebics_init_3()
self.assertEqual(userid.state, "to_verify")
self.assertEqual(
base64.b64decode(userid.ebics_public_bank_keys).decode(), PUBLIC_BANK_KEYS
)
self.assertEqual(
userid.ebics_public_bank_keys_fn, f"TESTHOST_public_bank_keys_{today}.txt"
)
bank_class = ebics_userid_module.EbicsBank
with patch.object(bank_class, "activate_keys") as activate_keys:
userid.ebics_init_4()
activate_keys.assert_called_once()
self.assertEqual(userid.state, "active_keys")
# stored passphrase is kept
self.assertEqual(userid.ebics_passphrase, "init-passphrase")
def test_passphrase_not_stored_is_cleared(self):
userid = self._new_userid(ebics_passphrase_store=False)
self._init_1(userid)
self.assertFalse(userid.ebics_passphrase)
def test_existing_keys_are_kept(self):
"""A second init reuses the key file: reset tools must delete it."""
userid = self._new_userid()
self._init_1(userid)
with open(userid.ebics_keys_fn, "rb") as f:
keys = f.read()
userid.set_to_draft()
self._init_1(userid)
with open(userid.ebics_keys_fn, "rb") as f:
self.assertEqual(f.read(), keys)
def test_step_guards(self):
userid = self._new_userid()
userid.state = "init"
with self.assertRaisesRegex(UserError, "draft"):
userid.ebics_init_1()
userid.state = "draft"
for method in ("ebics_init_2", "ebics_init_3", "ebics_init_4"):
with self.subTest(method=method), self.assertRaises(UserError):
getattr(userid, method)()
def test_init_requires_passphrase(self):
userid = self._new_userid(ebics_passphrase=False)
with self.assertRaisesRegex(UserError, "passphrase"):
userid.ebics_init_1()
def test_3skey_requires_certificate(self):
userid = self._new_userid(swift_3skey=True)
with self.assertRaisesRegex(UserError, "3SKey certificate"):
userid.ebics_init_1()
def test_init_requires_a_journal_with_bank(self):
self.ebics_config.journal_ids = False
userid = self._new_userid()
with self.assertRaisesRegex(UserError, "No bank defined"):
self._init_1(userid)
def test_version_not_supported_by_bank(self):
userid = self._new_userid()
with self.assertRaisesRegex(UserError, "version mismatch"):
self._init_1(userid, fake_init_client(hev={"H003": "2.4"}))
self.assertEqual(userid.state, "draft")
def test_init_errors(self):
cases = [
(URLError("connection refused"), "urlopen error"),
(
EbicsFunctionalError("091002", "EBICS_INVALID_USER_OR_USER_STATE"),
"091002",
),
(EbicsTechnicalError("061001", "EBICS_AUTHENTICATION_FAILED"), "061001"),
]
for error, expected in cases:
with self.subTest(error=type(error).__name__):
userid = self._new_userid()
with (
mute_logger(ebics_userid_module.__name__),
self.assertRaisesRegex(UserError, expected),
):
self._init_1(userid, fake_init_client(ini_error=error))
def test_get_bank_keys_error(self):
userid = self._new_userid()
self._init_1(userid)
userid.ebics_init_2()
error = EbicsFunctionalError("091002", "EBICS_INVALID_USER_OR_USER_STATE")
client_class = fake_init_client(hpb_error=error)
with (
patch.object(ebics_userid_module, "EbicsClient", client_class),
self.assertRaisesRegex(UserError, "EBICS Functional Error"),
):
userid.ebics_init_3()
def test_h003_order_numbers(self):
self.ebics_config.write({"ebics_version": "H003", "order_number": "A000"})
userid = self._new_userid()
self._init_1(userid, fake_init_client(hev={"H003": "2.4"}))
# INI uses A000, HIA uses A001
self.assertEqual(self.ebics_config.order_number, "A002")
def test_ini_letter_language(self):
calls = []
class SpyUser(ebics_userid_module.EbicsUser):
def create_ini_letter(self, bankname, lang=None, **kwargs):
calls.append((bankname, lang))
return b"%PDF-1.4 spy"
cases = [
("base.fr", "fr_FR", "fr"),
("base.de", "fr_FR", "de"),
("base.be", "fr_FR", "fr"),
("base.be", "en_US", "en"),
]
for country, user_lang, expected in cases:
with self.subTest(country=country, user_lang=user_lang):
lang = (
self.env["res.lang"]
.sudo()
.with_context(active_test=False)
.search([("code", "=", user_lang)])
)
lang.active = True
self.bank_fr.country = self.env.ref(country)
self.env.user.lang = user_lang
userid = self._new_userid()
with patch.object(ebics_userid_module, "EbicsUser", SpyUser):
self._init_1(userid)
self.assertEqual(calls[-1], ("Banque Test", expected))
def test_key_renewal_requires_draft_config(self):
userid = self._new_userid()
with self.assertRaisesRegex(UserError, "Key Renewal"):
userid.set_to_get_bank_keys()
self.ebics_config.set_to_draft()
userid.set_to_get_bank_keys()
self.assertEqual(userid.state, "get_bank_keys")
def test_h005_requires_x509(self):
self.ebics_config.ebics_version = "H005"
with self.assertRaisesRegex(UserError, "X.509"):
self._new_userid(ebics_key_x509=False)
def test_stored_passphrase_is_checked(self):
userid = self._new_userid()
self._init_1(userid)
gc.collect() # no other keyring instance: fintech raises ValueError
userid.ebics_passphrase = WRONG_PHRASE
with self.assertRaisesRegex(UserError, "Passphrase mismatch"):
userid._onchange_ebics_passphrase_store()
def test_stored_passphrase_is_checked_with_open_keyring(self):
"""In a worker the keyring may already be open with the right
passphrase: fintech then raises RuntimeError."""
userid = self._new_userid()
self._init_1(userid)
open_keyring = EbicsKeyRing(
keys=userid.ebics_keys_fn, passphrase=userid.ebics_passphrase
)
userid.ebics_passphrase = WRONG_PHRASE
with self.assertRaisesRegex(UserError, "Passphrase mismatch"):
userid._onchange_ebics_passphrase_store()
del open_keyring
+245
View File
@@ -0,0 +1,245 @@
# Copyright 2026 Noviat.
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
import base64
from datetime import date
from fintech.ebics import (
EbicsFunctionalError,
EbicsTechnicalError,
EbicsVerificationError,
)
from odoo.tests import tagged
from .common import EbicsTestCommon, FakeEbicsClient, read_sample
@tagged("post_install", "-at_install")
class TestEbicsXferDownload(EbicsTestCommon):
def test_line_terminator(self):
xfer = self.env["ebics.xfer"]
line_a, line_b = b"A" * 120, b"B" * 120
self.assertEqual(
xfer._handle_cfonb120(line_a + line_b), line_a + b"\n" + line_b + b"\n"
)
self.assertEqual(
xfer._handle_cfonb120(line_a + b"\r\n" + line_b + b"\r\n"),
line_a + b"\n" + line_b + b"\n",
)
self.assertEqual(
xfer._handle_cfonb240(line_a + line_b), line_a + line_b + b"\n"
)
# Current behaviour: an incomplete trailing line is dropped.
self.assertEqual(xfer._handle_cfonb120(line_a + b"C" * 10), line_a + b"\n")
def test_download_fdl_cfonb(self):
data = read_sample("cfonb120_no_eol_single_account.dat")
wizard = self._download_wizard(self.ff_cfonb120, date_to=date(2026, 10, 13))
with self._fake_client(FakeEbicsClient(downloads={"FDL": data})) as client:
action = wizard.ebics_download()
self.assertEqual(action["context"]["err_cnt"], 0)
ebics_file = self.env["ebics.file"].browse(action["context"]["ebics_file_ids"])
self.assertEqual(ebics_file.name, "TESTHOST_PARTNER1_2026-10-13.cfonb120.dat")
self.assertEqual(ebics_file.state, "draft")
self.assertEqual(ebics_file.ebics_userid_id, self.ebics_userid)
self.assertEqual(ebics_file.company_ids, self.ebics_config.company_ids)
# FDL CFONB downloads get a line terminator every 120 characters
stored = base64.b64decode(ebics_file.data)
self.assertEqual(stored.count(b"\n"), 7)
self.assertEqual(client.confirmed, [("TRANS0001", True)])
self.assertIn("is available for further processing", wizard.note)
def test_download_btd_cfonb(self):
data = read_sample("cfonb120_no_eol_single_account.dat")
wizard = self._download_wizard(self.ff_btd_cfonb120, date_to=date(2026, 10, 13))
with self._fake_client(FakeEbicsClient(downloads={"BTD": data})):
action = wizard.ebics_download()
ebics_file = self.env["ebics.file"].browse(action["context"]["ebics_file_ids"])
self.assertEqual(len(ebics_file), 1)
# BTD formats have no name, hence no line terminator handler.
self.assertEqual(base64.b64decode(ebics_file.data), data)
def test_same_name_in_another_format(self):
"""FDL and BTD CFONB formats share the file suffix: same file name,
but a different format is not a duplicate."""
data = read_sample("cfonb120_no_eol_single_account.dat")
names = []
for file_format, order_type in (
(self.ff_cfonb120, "FDL"),
(self.ff_btd_cfonb120, "BTD"),
):
wizard = self._download_wizard(file_format, date_to=date(2026, 10, 13))
client = FakeEbicsClient(downloads={order_type: data})
with self._fake_client(client):
action = wizard.ebics_download()
self.assertEqual(action["context"]["err_cnt"], 0)
ebics_file = self.env["ebics.file"].browse(
action["context"]["ebics_file_ids"]
)
names.append(ebics_file.name)
self.assertEqual(names[0], names[1])
def test_download_same_file_twice(self):
data = read_sample("cfonb120_no_eol_single_account.dat")
for expected_errors in (0, 1):
wizard = self._download_wizard(self.ff_cfonb120, date_to=date(2026, 10, 13))
with self._fake_client(FakeEbicsClient(downloads={"FDL": data})) as client:
action = wizard.ebics_download()
self.assertEqual(action["context"]["err_cnt"], expected_errors)
self.assertIn("has already been downloaded", wizard.note)
self.assertEqual(client.confirmed, [("TRANS0001", False)])
def test_download_several_documents(self):
ptk = self.env["ebics.file.format"].create(
{"name": "PTK", "type": "down", "order_type": "PTK", "suffix": "txt"}
)
data = read_sample("ptk_protocol.txt")
wizard = self._download_wizard(ptk)
client = FakeEbicsClient(downloads={"PTK": {"doc1": data, "doc2": data}})
with self._fake_client(client):
action = wizard.ebics_download()
ebics_files = self.env["ebics.file"].browse(action["context"]["ebics_file_ids"])
self.assertEqual(
sorted(ebics_files.mapped("name")),
["TESTHOST_PARTNER1_doc1.txt", "TESTHOST_PARTNER1_doc2.txt"],
)
# no processing method: the files are done right away
self.assertEqual(set(ebics_files.mapped("state")), {"done"})
def test_mark_as_downloaded(self):
data = read_sample("cfonb120_no_eol_single_account.dat")
wizard = self._download_wizard(self.ff_cfonb120).with_context(
ebics_mark_as_downloaded=True
)
with self._fake_client(FakeEbicsClient(downloads={"FDL": data})) as client:
action = wizard.ebics_download()
self.assertFalse(action["context"]["ebics_file_ids"])
self.assertEqual(client.confirmed, [("TRANS0001", True)])
def test_download_errors(self):
cases = [
(
EbicsFunctionalError("091005", "EBICS_NO_DOWNLOAD_DATA_AVAILABLE"),
"EBICS Functional Error",
"EBICS_NO_DOWNLOAD_DATA_AVAILABLE (code: 091005)",
),
(
EbicsTechnicalError("061002", "EBICS_INVALID_REQUEST"),
"EBICS Technical Error",
"EBICS_INVALID_REQUEST (code: 061002)",
),
(
EbicsVerificationError(),
"EBICS Verification Error",
"could not be verified",
),
(RuntimeError("socket closed"), "Unknown Error", "socket closed"),
]
for error, title, detail in cases:
with self.subTest(error=type(error).__name__):
wizard = self._download_wizard(self.ff_cfonb120)
with self._fake_client(FakeEbicsClient(error=error)) as client:
action = wizard.ebics_download()
self.assertEqual(action["context"]["err_cnt"], 1)
self.assertFalse(action["context"]["ebics_file_ids"])
self.assertIn(title, wizard.note)
self.assertIn(detail, wizard.note)
self.assertEqual(client.confirmed, [("TRANS0001", False)])
def test_client_setup_failure(self):
wizard = self._download_wizard(self.ff_cfonb120)
with self._fake_client(False):
action = wizard.ebics_download()
self.assertEqual(action["context"]["err_cnt"], 1)
self.assertIn("client setup failed", wizard.note)
@tagged("post_install", "-at_install")
class TestEbicsXferUpload(EbicsTestCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ff_btu = cls.env["ebics.file.format"].create(
{
"type": "up",
"order_type": "BTU",
"btf_service": "SCT",
"btf_message": "pain.001",
"suffix": "xml",
}
)
cls.ff_ful = cls.env.ref("account_ebics.ebics_ff_LCR")
bank = cls.env["res.bank"].create(
{"name": "Test Bank FR", "country": cls.env.ref("base.fr").id}
)
journal = cls._create_bank_journal("TUPL", "FR7630004000031234567890143")
journal.bank_account_id.sudo().bank_id = bank
def test_upload_btu(self):
wizard = self._upload_wizard(self.ff_btu, test_mode=True)
with self._fake_client(FakeEbicsClient(order_id="N042")) as client:
ebics_file = wizard._ebics_upload()
self.assertEqual(ebics_file.name, "pay.xml")
self.assertEqual(ebics_file.state, "done")
self.assertEqual(ebics_file.pain_state, "uploaded")
self.assertEqual(ebics_file.ebics_order_number, "N042")
self.assertEqual(client.uploads, [("BTU", b"<Document/>", {"TEST": "TRUE"})])
self.assertIn("OrderID N042", wizard.note)
# H004: the order number is managed by the bank
self.assertFalse(self.ebics_config.order_number)
def test_upload_same_name_twice(self):
names = []
for _i in range(3):
wizard = self._upload_wizard(self.ff_btu)
with self._fake_client(FakeEbicsClient()):
names.append(wizard._ebics_upload().name)
# Pinned, probably unintended: _update_ef_vals appends each counter to
# the previous candidate (pay.xml_1_2) instead of pay.xml_2.
self.assertEqual(names, ["pay.xml", "pay.xml_1", "pay.xml_1_2"])
def test_upload_ful_h003_order_number(self):
self.ebics_config.write({"ebics_version": "H003", "order_number": "A00Z"})
wizard = self._upload_wizard(self.ff_ful, fname="lcr.txt")
with self._fake_client(FakeEbicsClient(order_id="A00Z")) as client:
ebics_file = wizard._ebics_upload()
self.assertEqual(ebics_file.ebics_order_number, "A00Z")
self.assertEqual(client.uploads[0][0], "FUL")
self.assertEqual(client.uploads[0][2], {"country": "FR"})
self.assertEqual(self.ebics_config.order_number, "A010")
def test_upload_h003_failure_still_increments(self):
"""Pinned behaviour: the order number moves on even if the upload failed."""
self.ebics_config.write({"ebics_version": "H003", "order_number": "B000"})
wizard = self._upload_wizard(self.ff_ful, fname="lcr.txt")
error = EbicsFunctionalError("091112", "EBICS_INVALID_ORDER_PARAMS")
with self._fake_client(FakeEbicsClient(error=error)):
ebics_file = wizard._ebics_upload()
self.assertFalse(ebics_file)
self.assertEqual(self.ebics_config.order_number, "B001")
self.assertIn("EBICS Functional Error", wizard.note)
self.assertIn("EBICS_INVALID_ORDER_PARAMS (code: 091112)", wizard.note)
def test_upload_errors(self):
cases = [
(EbicsTechnicalError("061001", "EBICS_AUTHENTICATION_FAILED"), "Technical"),
(EbicsVerificationError(), "Verification"),
(RuntimeError("socket closed"), "Unknown Error"),
]
for error, expected in cases:
with self.subTest(error=type(error).__name__):
wizard = self._upload_wizard(self.ff_btu)
with self._fake_client(FakeEbicsClient(error=error)):
self.assertFalse(wizard._ebics_upload())
self.assertIn(expected, wizard.note)
def test_upload_onchange_selects_format(self):
"""A single upload format on the config is selected automatically."""
self.ebics_config.ebics_file_format_ids = self.ff_btu
wizard = self.env["ebics.xfer"].new(
{"ebics_config_id": self.ebics_config.id, "upload_fname": "pay.xml"}
)
wizard._onchange_ebics_config_id()
self.assertEqual(wizard.format_id, self.ff_btu)
self.assertEqual(wizard.ebics_userid_id, self.ebics_userid)
@@ -0,0 +1,115 @@
# Copyright 2026 Noviat.
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
import base64
from unittest.mock import patch
from odoo.exceptions import UserError
from odoo.tests import tagged
from odoo.addons.account_ebics.wizards import ebics_xfer as ebics_xfer_module
from .test_ebics_userid import EbicsKeysCommon
# Throwaway passphrase for keys created in a temporary directory.
WRONG_PHRASE = "wrong-passphrase"
def capturing_client(captured, init_error=None):
"""Stand-in for fintech's EbicsClient that records the real EbicsBank and
EbicsUser built by ebics.xfer._setup_client."""
class CapturingClient:
def __init__(self, bank, user, version=None):
if init_error:
raise init_error
captured.update(bank=bank, user=user, version=version)
self.bank = bank
def FUL(self, filetype, data, **kwargs):
# As fintech does on H003: the OrderID comes from the bank object.
return self.bank._next_order_id(captured["user"].partnerid)
return CapturingClient
@tagged("post_install", "-at_install")
class TestSetupClient(EbicsKeysCommon):
"""ebics.xfer._setup_client with real keys; only the transport is faked."""
def setUp(self):
super().setUp()
self.userid = self._new_userid()
self._init_1(self.userid)
self.userid.state = "active_keys"
self.ff_ful = self.env.ref("account_ebics.ebics_ff_LCR")
def _wizard(self, file_format=None, **vals):
return self.env["ebics.xfer"].create(
{
"ebics_config_id": self.ebics_config.id,
"ebics_userid_id": self.userid.id,
"format_id": (file_format or self.ff_ful).id,
"upload_data": base64.b64encode(b"LCR DATA"),
"upload_fname": "lcr.txt",
"note": "",
**vals,
}
)
def _setup(self, wizard, init_error=None):
captured = {}
client_class = capturing_client(captured, init_error=init_error)
with patch.object(ebics_xfer_module, "EbicsClient", client_class):
client = wizard._setup_client()
return client, captured
def test_client_from_config_and_keys(self):
client, captured = self._setup(self._wizard())
self.assertTrue(client)
self.assertEqual(captured["version"], "H004")
self.assertEqual(captured["bank"].hostid, "TESTHOST")
self.assertEqual(captured["bank"].url, "https://ebics.example.invalid/ebics")
self.assertEqual(captured["user"].partnerid, "PARTNER1")
self.assertEqual(captured["user"].userid, self.userid.name)
def test_signature_class(self):
cases = [
# (userid default, file format override, expected transport_only)
("T", False, True),
("E", False, False),
("T", "E", False),
("E", "T", True),
]
for user_class, format_class, expected in cases:
with self.subTest(user=user_class, file_format=format_class):
self.userid.signature_class = user_class
self.ff_ful.signature_class = format_class
_client, captured = self._setup(self._wizard())
self.assertEqual(captured["user"].transport_only, expected)
def test_h003_order_number_is_given_to_the_bank(self):
self.ebics_config.write({"ebics_version": "H003", "order_number": "B00Z"})
wizard = self._wizard()
captured = {}
with patch.object(ebics_xfer_module, "EbicsClient", capturing_client(captured)):
ebics_file = wizard._ebics_upload()
self.assertEqual(ebics_file.ebics_order_number, "B00Z")
self.assertEqual(self.ebics_config.order_number, "B010")
def test_wrong_passphrase(self):
wizard = self._wizard(ebics_passphrase=WRONG_PHRASE)
with self.assertRaisesRegex(UserError, "Error while accessing the EBICS Keys"):
self._setup(wizard)
def test_missing_keys_directory(self):
self.ebics_config.ebics_keys = "/nonexistent/ebics_keys"
with self.assertRaisesRegex(UserError, "is not available"):
self._setup(self._wizard())
def test_client_creation_error(self):
wizard = self._wizard()
client, _captured = self._setup(wizard, init_error=RuntimeError("bad url"))
self.assertFalse(client)
self.assertIn("Unknown Error", wizard.note)
self.assertIn("bad url", wizard.note)