mirror of
https://github.com/brain-tec/account_ebics.git
synced 2026-11-06 10:00:39 +00:00
[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:
1 parent
ed0bf5d6ae
commit
a1fc381121
50 files changed
+2075
-5
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user