Due to the use of the dataclasses mixin on the SQLAlchemy types, a back_populates creates a RecursiveError when comparing two types. This occurs because the dataclass will overwrite the __eq__ operator, and it doesn't know when to stop comparing relationships. Removing the dataclasses isn't the best approach, since then __init__, __eq__ and __repr__ methods would have to be added to all types. Thus the solution was to remove the relationship on the child (on a one-to-one relationship) from the __eq__ operation, with the use of the compare parameter. Took the opportunity to define more logical __init__ methods on the `Rule` and child classes. Also revised the parameter options on some DB types.
100 lines
3.1 KiB
Python
100 lines
3.1 KiB
Python
import datetime as dt
|
|
from decimal import Decimal
|
|
from typing import Any, Optional
|
|
import pytest
|
|
import requests
|
|
|
|
import mocks.nordigen as mock
|
|
|
|
from pfbudget.db.model import AccountType, Bank, BankTransaction, Nordigen
|
|
from pfbudget.extract.exceptions import BankError, CredentialsError
|
|
from pfbudget.extract.extract import Extractor
|
|
from pfbudget.extract.nordigen import NordigenClient, NordigenCredentials
|
|
from pfbudget.extract.psd2 import PSD2Extractor
|
|
|
|
|
|
class MockGet:
|
|
def __init__(self, mock_exception: Optional[Exception] = None):
|
|
self._status_code = 200
|
|
self._mock_exception = mock_exception
|
|
|
|
def __call__(self, *args: Any, **kwargs: Any):
|
|
if self._mock_exception:
|
|
raise self._mock_exception
|
|
|
|
self._headers: dict[str, str] = kwargs["headers"]
|
|
if "Authorization" not in self._headers or not self._headers["Authorization"]:
|
|
self._status_code = 401
|
|
|
|
self.url: str = kwargs["url"]
|
|
return self
|
|
|
|
@property
|
|
def ok(self):
|
|
return True if self._status_code < 400 else False
|
|
|
|
@property
|
|
def status_code(self):
|
|
return self._status_code
|
|
|
|
def json(self):
|
|
if self.url.endswith("accounts/" + mock.id + "/"):
|
|
return mock.accounts_id
|
|
elif self.url.endswith("accounts/" + mock.id + "/transactions/"):
|
|
return mock.accounts_id_transactions
|
|
elif self.url.endswith("requisitions/"):
|
|
return mock.requisitions
|
|
elif self.url.endswith("requisitions/" + mock.id + "/"):
|
|
return mock.requisitions_id
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_requests(monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setattr("requests.get", MockGet())
|
|
monkeypatch.delattr("requests.post")
|
|
monkeypatch.delattr("requests.put")
|
|
monkeypatch.delattr("requests.delete")
|
|
|
|
|
|
@pytest.fixture
|
|
def extractor() -> Extractor:
|
|
credentials = NordigenCredentials("ID", "KEY", "TOKEN")
|
|
return PSD2Extractor(NordigenClient(credentials))
|
|
|
|
|
|
@pytest.fixture
|
|
def bank() -> Bank:
|
|
bank = Bank("Bank#1", "", AccountType.checking)
|
|
bank.nordigen = Nordigen("", "", mock.id, False)
|
|
return bank
|
|
|
|
|
|
class TestExtractPSD2:
|
|
def test_empty_credentials(self):
|
|
cred = NordigenCredentials("", "")
|
|
with pytest.raises(CredentialsError):
|
|
NordigenClient(cred)
|
|
|
|
def test_no_psd2_bank(self, extractor: Extractor):
|
|
with pytest.raises(BankError):
|
|
extractor.extract(Bank("", "", AccountType.checking))
|
|
|
|
def test_timeout(
|
|
self, monkeypatch: pytest.MonkeyPatch, extractor: Extractor, bank: Bank
|
|
):
|
|
monkeypatch.setattr(
|
|
"requests.get", MockGet(mock_exception=requests.ReadTimeout())
|
|
)
|
|
with pytest.raises(requests.Timeout):
|
|
extractor.extract(bank)
|
|
|
|
def test_extract(self, extractor: Extractor, bank: Bank):
|
|
assert extractor.extract(bank) == [
|
|
BankTransaction(
|
|
dt.date(2023, 1, 14), "string", Decimal("328.18"), bank="Bank#1"
|
|
),
|
|
BankTransaction(
|
|
dt.date(2023, 2, 14), "string", Decimal("947.26"), bank="Bank#1"
|
|
),
|
|
]
|