mirror of
https://github.com/brain-tec/account_ebics.git
synced 2026-11-06 10:00:39 +00:00
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.
285 lines
11 KiB
Python
285 lines
11 KiB
Python
# 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
|