forked from gaugendre/ofx-processor
Improve error handling and increase coverage
This commit is contained in:
parent
936a23a30d
commit
96ea31b96c
8 changed files with 311 additions and 11 deletions
|
@ -19,4 +19,4 @@ cli.add_command(ynab.config, name="config")
|
||||||
discover_processors(cli)
|
discover_processors(cli)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
cli()
|
cli() # pragma: nocover
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import requests
|
import requests
|
||||||
|
@ -55,7 +56,15 @@ def push_transactions(transactions, account):
|
||||||
click.pause()
|
click.pause()
|
||||||
click.edit(filename=config_file)
|
click.edit(filename=config_file)
|
||||||
|
|
||||||
|
try:
|
||||||
config.read(config_file)
|
config.read(config_file)
|
||||||
|
except configparser.Error as e:
|
||||||
|
click.secho(f"Error while parsing config file: {str(e)}", fg="red", bold=True)
|
||||||
|
click.secho("Opening the file...")
|
||||||
|
click.pause()
|
||||||
|
click.edit(filename=config_file)
|
||||||
|
click.secho("Exiting...", fg="red", bold=True)
|
||||||
|
sys.exit(1)
|
||||||
section = config[account]
|
section = config[account]
|
||||||
budget_id = section["budget"]
|
budget_id = section["budget"]
|
||||||
url = f"{BASE_URL}/budgets/{budget_id}/transactions"
|
url = f"{BASE_URL}/budgets/{budget_id}/transactions"
|
||||||
|
|
140
tests/samples/bpvf_transactions.json
Normal file
140
tests/samples/bpvf_transactions.json
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
{
|
||||||
|
"transactions": [
|
||||||
|
{
|
||||||
|
"date": "2020-02-26",
|
||||||
|
"amount": -9660,
|
||||||
|
"payee_name": "PRLV SEPA Company 3",
|
||||||
|
"memo": "123456789 PAYPAL 542UHBON",
|
||||||
|
"import_id": "YNAB:-9660:2020-02-26:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-25",
|
||||||
|
"amount": -2400,
|
||||||
|
"payee_name": "H.I.K 69VILLEURBANNE",
|
||||||
|
"memo": "240220 CB****5555",
|
||||||
|
"import_id": "YNAB:-2400:2020-02-25:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-25",
|
||||||
|
"amount": -39200,
|
||||||
|
"payee_name": "DELIVEROO FR WWW",
|
||||||
|
"memo": "230220 CB****5555 39,20EUR 1 EURO = 1,000000",
|
||||||
|
"import_id": "YNAB:-39200:2020-02-25:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-25",
|
||||||
|
"amount": -9990,
|
||||||
|
"payee_name": "PRLV SEPA Company 1",
|
||||||
|
"memo": "Votre abonnement mobile: 06XXXXX 6498165189060897",
|
||||||
|
"import_id": "YNAB:-9990:2020-02-25:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-24",
|
||||||
|
"amount": -7500,
|
||||||
|
"payee_name": "COMPANY FR LYON 6EME",
|
||||||
|
"memo": "210220 CB****5555 7,50EUR 1 EURO = 1,000000",
|
||||||
|
"import_id": "YNAB:-7500:2020-02-24:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-24",
|
||||||
|
"amount": -34990,
|
||||||
|
"payee_name": "PRLV SEPA Company 2",
|
||||||
|
"memo": "24-02-2020 / 22-03-2020 56418710",
|
||||||
|
"import_id": "YNAB:-34990:2020-02-24:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-24",
|
||||||
|
"amount": -2390,
|
||||||
|
"payee_name": "VIR Person 1",
|
||||||
|
"memo": "481840871 Splitwise",
|
||||||
|
"import_id": "YNAB:-2390:2020-02-24:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-20",
|
||||||
|
"amount": 235000,
|
||||||
|
"payee_name": "VIREMENT Person 2",
|
||||||
|
"memo": "Cadeau",
|
||||||
|
"import_id": "YNAB:235000:2020-02-20:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-20",
|
||||||
|
"amount": 55000,
|
||||||
|
"payee_name": "VIREMENT Company 3",
|
||||||
|
"memo": "48716508719",
|
||||||
|
"import_id": "YNAB:55000:2020-02-20:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-19",
|
||||||
|
"amount": -55000,
|
||||||
|
"payee_name": "BDE INSA LYON 69VILLEURBANNE",
|
||||||
|
"memo": "170220 CB****5555",
|
||||||
|
"import_id": "YNAB:-55000:2020-02-19:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-19",
|
||||||
|
"amount": -900,
|
||||||
|
"payee_name": "GUY AND SONS FR LYON",
|
||||||
|
"memo": "180220 CB****5555 0,90EUR 1 EURO = 1,000000",
|
||||||
|
"import_id": "YNAB:-900:2020-02-19:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-19",
|
||||||
|
"amount": -1400,
|
||||||
|
"payee_name": "GUY AND SONS FR LYON",
|
||||||
|
"memo": "170220 CB****5555 1,40EUR 1 EURO = 1,000000",
|
||||||
|
"import_id": "YNAB:-1400:2020-02-19:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-19",
|
||||||
|
"amount": -473500,
|
||||||
|
"payee_name": "VIR Person 1",
|
||||||
|
"memo": "65187460 Acompte cuisine 2",
|
||||||
|
"import_id": "YNAB:-473500:2020-02-19:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-18",
|
||||||
|
"amount": -96960,
|
||||||
|
"payee_name": "PRLV SEPA Company 4",
|
||||||
|
"memo": "487105874 Amazon.fr 3X QC.(OJBIYN:ZOFEUBZF51871",
|
||||||
|
"import_id": "YNAB:-96960:2020-02-18:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-17",
|
||||||
|
"amount": -232000,
|
||||||
|
"payee_name": "GRAND PARC PUY 85LES EPESSES",
|
||||||
|
"memo": "150220 CB****5555",
|
||||||
|
"import_id": "YNAB:-232000:2020-02-17:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-17",
|
||||||
|
"amount": -1000,
|
||||||
|
"payee_name": "UBER BV NL HELP.UBER.CO",
|
||||||
|
"memo": "140220 CB****5555 1,00EUR 1 EURO = 1,000000",
|
||||||
|
"import_id": "YNAB:-1000:2020-02-17:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-17",
|
||||||
|
"amount": 8600,
|
||||||
|
"payee_name": "VIREMENT Person 5",
|
||||||
|
"memo": "VIREMENT DE PERSON 6",
|
||||||
|
"import_id": "YNAB:8600:2020-02-17:1",
|
||||||
|
"account_id": "<YOUR BPVF ACCOUNT ID>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
52
tests/samples/ce_transactions.json
Normal file
52
tests/samples/ce_transactions.json
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"transactions": [
|
||||||
|
{
|
||||||
|
"date": "2020-02-25",
|
||||||
|
"amount": -21000,
|
||||||
|
"payee_name": "CB DECATHLON",
|
||||||
|
"memo": "FACT 240220",
|
||||||
|
"import_id": "YNAB:-21000:2020-02-25:1",
|
||||||
|
"account_id": "<YOUR CE ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-25",
|
||||||
|
"amount": -7000,
|
||||||
|
"payee_name": "PRLV COMPANY",
|
||||||
|
"memo": "Company Ref Prlvt SEPA 99-1KIBHEF-01 45871984",
|
||||||
|
"import_id": "YNAB:-7000:2020-02-25:1",
|
||||||
|
"account_id": "<YOUR CE ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-24",
|
||||||
|
"amount": -48130,
|
||||||
|
"payee_name": "CB 3403 MONOP",
|
||||||
|
"memo": "FACT 210220",
|
||||||
|
"import_id": "YNAB:-48130:2020-02-24:1",
|
||||||
|
"account_id": "<YOUR CE ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-24",
|
||||||
|
"amount": -1200,
|
||||||
|
"payee_name": "CB MALATIER",
|
||||||
|
"memo": "FACT 210220",
|
||||||
|
"import_id": "YNAB:-1200:2020-02-24:1",
|
||||||
|
"account_id": "<YOUR CE ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-24",
|
||||||
|
"amount": 2390,
|
||||||
|
"payee_name": "VIR SEPA PERSON 1",
|
||||||
|
"memo": "_",
|
||||||
|
"import_id": "YNAB:2390:2020-02-24:1",
|
||||||
|
"account_id": "<YOUR CE ACCOUNT ID>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-02-24",
|
||||||
|
"amount": 14490,
|
||||||
|
"payee_name": "VIR SEPA PERSON 2",
|
||||||
|
"memo": "_",
|
||||||
|
"import_id": "YNAB:14490:2020-02-24:1",
|
||||||
|
"account_id": "<YOUR CE ACCOUNT ID>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
13
tests/samples/config_broken_duplicate_key.ini
Normal file
13
tests/samples/config_broken_duplicate_key.ini
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[DEFAULT]
|
||||||
|
token = <YOUR API TOKEN>
|
||||||
|
budget = <YOUR BUDGET ID>
|
||||||
|
|
||||||
|
[bpvf]
|
||||||
|
account = <YOUR BPVF ACCOUNT ID>
|
||||||
|
|
||||||
|
[revolut]
|
||||||
|
account = <YOUR REVOLUT ACCOUNT ID>
|
||||||
|
account = <YOUR REVOLUT ACCOUNT ID2>
|
||||||
|
|
||||||
|
[ce]
|
||||||
|
account = <YOUR CE ACCOUNT ID>
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from unittest.mock import call
|
from unittest.mock import call
|
||||||
|
@ -37,27 +38,72 @@ class UtilsTestCase(unittest.TestCase):
|
||||||
add_command.assert_has_calls(calls, any_order=True)
|
add_command.assert_has_calls(calls, any_order=True)
|
||||||
|
|
||||||
|
|
||||||
class ConfigEditTestCase(unittest.TestCase):
|
class ConfigTestCase(unittest.TestCase):
|
||||||
@mock.patch("click.edit")
|
@mock.patch("click.edit")
|
||||||
|
@mock.patch("ofx_processor.utils.ynab.DEFAULT_CONFIG_DIR", "tests/samples")
|
||||||
def test_config_edit(self, edit):
|
def test_config_edit(self, edit):
|
||||||
config_dir = "tests/samples"
|
|
||||||
ynab.DEFAULT_CONFIG_DIR = config_dir
|
|
||||||
expected_filename = f"{config_dir}/config.ini"
|
|
||||||
runner = CliRunner()
|
|
||||||
from ofx_processor.main import cli
|
from ofx_processor.main import cli
|
||||||
|
|
||||||
# This is run at import time and the cli module is already imported before this test
|
# This is run at import time and the cli module is already imported before this test
|
||||||
# so we need to re-run the add_command to make it available.
|
# so we need to re-run the add_command to make it available.
|
||||||
cli.add_command(ynab.config, name="config")
|
cli.add_command(ynab.config, name="config")
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
runner.invoke(cli, ["config", "edit"])
|
runner.invoke(cli, ["config", "edit"])
|
||||||
|
|
||||||
|
expected_filename = f"tests/samples/config.ini"
|
||||||
|
edit.assert_called_once_with(filename=expected_filename)
|
||||||
|
|
||||||
|
@mock.patch("click.edit")
|
||||||
|
@mock.patch(
|
||||||
|
"ofx_processor.utils.ynab.DEFAULT_CONFIG_FILENAME",
|
||||||
|
"config_broken_duplicate_key.ini",
|
||||||
|
)
|
||||||
|
@mock.patch("ofx_processor.utils.ynab.DEFAULT_CONFIG_DIR", "tests/samples")
|
||||||
|
def test_broken_config_file(self, edit):
|
||||||
|
from ofx_processor.main import cli
|
||||||
|
|
||||||
|
# This is run at import time and the cli module is already imported before this test
|
||||||
|
# so we need to re-run the add_command to make it available.
|
||||||
|
utils.discover_processors(cli)
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(cli, ["revolut", "tests/samples/revolut.csv"])
|
||||||
|
|
||||||
|
expected_filename = "tests/samples/config_broken_duplicate_key.ini"
|
||||||
|
self.assertIn("Error while parsing config file", result.output)
|
||||||
|
edit.assert_called_once_with(filename=expected_filename)
|
||||||
|
|
||||||
|
@mock.patch("ofx_processor.utils.ynab.DEFAULT_CONFIG_FILENAME", "file.ini")
|
||||||
|
@mock.patch("ofx_processor.utils.ynab.DEFAULT_CONFIG_DIR", "some/config/folder")
|
||||||
|
def test_get_config_file_name(self):
|
||||||
|
expected = "some/config/folder/file.ini"
|
||||||
|
self.assertEqual(ynab.get_config_file_name(), expected)
|
||||||
|
|
||||||
|
@mock.patch("click.edit")
|
||||||
|
@mock.patch("os.makedirs")
|
||||||
|
@mock.patch("ofx_processor.utils.ynab.DEFAULT_CONFIG_FILENAME", "notfound.ini")
|
||||||
|
@mock.patch("ofx_processor.utils.ynab.DEFAULT_CONFIG_DIR", "tests/samples")
|
||||||
|
def test_missing_config_file(self, makedirs, edit):
|
||||||
|
from ofx_processor.main import cli
|
||||||
|
|
||||||
|
# This is run at import time and the cli module is already imported before this test
|
||||||
|
# so we need to re-run the add_command to make it available.
|
||||||
|
utils.discover_processors(cli)
|
||||||
|
runner = CliRunner()
|
||||||
|
with mock.patch("ofx_processor.utils.ynab.open", mock.mock_open()) as mock_file:
|
||||||
|
result = runner.invoke(cli, ["revolut", "tests/samples/revolut.csv"])
|
||||||
|
mock_file.assert_called_once()
|
||||||
|
makedirs.assert_called_once_with(ynab.DEFAULT_CONFIG_DIR, exist_ok=True)
|
||||||
|
self.assertIn("Editing config file", result.output)
|
||||||
|
expected_filename = ynab.get_config_file_name()
|
||||||
edit.assert_called_once_with(filename=expected_filename)
|
edit.assert_called_once_with(filename=expected_filename)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("ofx_processor.utils.ynab.DEFAULT_CONFIG_DIR", "tests/samples")
|
||||||
class DataTestCase(unittest.TestCase):
|
class DataTestCase(unittest.TestCase):
|
||||||
@mock.patch("requests.post")
|
@mock.patch("requests.post")
|
||||||
def test_revolut_sends_data_only_created(self, post):
|
def test_revolut_sends_data_only_created(self, post):
|
||||||
ynab.DEFAULT_CONFIG_DIR = "tests/samples"
|
|
||||||
post.return_value.json.return_value = {
|
post.return_value.json.return_value = {
|
||||||
"data": {
|
"data": {
|
||||||
"transactions": [
|
"transactions": [
|
||||||
|
@ -104,7 +150,6 @@ class DataTestCase(unittest.TestCase):
|
||||||
|
|
||||||
@mock.patch("requests.post")
|
@mock.patch("requests.post")
|
||||||
def test_revolut_sends_data_some_created_some_duplicates(self, post):
|
def test_revolut_sends_data_some_created_some_duplicates(self, post):
|
||||||
ynab.DEFAULT_CONFIG_DIR = "tests/samples"
|
|
||||||
post.return_value.json.return_value = {
|
post.return_value.json.return_value = {
|
||||||
"data": {
|
"data": {
|
||||||
"transactions": [
|
"transactions": [
|
||||||
|
@ -152,7 +197,6 @@ class DataTestCase(unittest.TestCase):
|
||||||
|
|
||||||
@mock.patch("requests.post")
|
@mock.patch("requests.post")
|
||||||
def test_revolut_sends_data_only_duplicates(self, post):
|
def test_revolut_sends_data_only_duplicates(self, post):
|
||||||
ynab.DEFAULT_CONFIG_DIR = "tests/samples"
|
|
||||||
post.return_value.json.return_value = {
|
post.return_value.json.return_value = {
|
||||||
"data": {
|
"data": {
|
||||||
"transactions": [],
|
"transactions": [],
|
||||||
|
@ -182,3 +226,45 @@ class DataTestCase(unittest.TestCase):
|
||||||
self.assertIn("Processed 9 transactions total.", result.output)
|
self.assertIn("Processed 9 transactions total.", result.output)
|
||||||
self.assertNotIn("transactions created in YNAB.", result.output)
|
self.assertNotIn("transactions created in YNAB.", result.output)
|
||||||
self.assertIn("9 transactions ignored (duplicates).", result.output)
|
self.assertIn("9 transactions ignored (duplicates).", result.output)
|
||||||
|
|
||||||
|
@mock.patch("requests.post")
|
||||||
|
def test_bpvf_sends_to_ynab(self, post):
|
||||||
|
from ofx_processor.main import cli
|
||||||
|
|
||||||
|
# This is run at import time and the cli module is already imported before this test
|
||||||
|
# so we need to re-run the add_command to make it available.
|
||||||
|
utils.discover_processors(cli)
|
||||||
|
|
||||||
|
with open("tests/samples/bpvf_transactions.json", encoding="utf-8") as f:
|
||||||
|
expected_data = json.load(f)
|
||||||
|
|
||||||
|
expected_headers = {"Authorization": f"Bearer <YOUR API TOKEN>"}
|
||||||
|
expected_url = f"{ynab.BASE_URL}/budgets/<YOUR BUDGET ID>/transactions"
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
runner.invoke(cli, ["bpvf", "tests/samples/bpvf.ofx"])
|
||||||
|
|
||||||
|
post.assert_called_once_with(
|
||||||
|
expected_url, json=expected_data, headers=expected_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch("requests.post")
|
||||||
|
def test_ce_sends_to_ynab(self, post):
|
||||||
|
from ofx_processor.main import cli
|
||||||
|
|
||||||
|
# This is run at import time and the cli module is already imported before this test
|
||||||
|
# so we need to re-run the add_command to make it available.
|
||||||
|
utils.discover_processors(cli)
|
||||||
|
|
||||||
|
with open("tests/samples/ce_transactions.json", encoding="utf-8") as f:
|
||||||
|
expected_data = json.load(f)
|
||||||
|
|
||||||
|
expected_headers = {"Authorization": f"Bearer <YOUR API TOKEN>"}
|
||||||
|
expected_url = f"{ynab.BASE_URL}/budgets/<YOUR BUDGET ID>/transactions"
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
runner.invoke(cli, ["ce", "tests/samples/ce.ofx"])
|
||||||
|
|
||||||
|
post.assert_called_once_with(
|
||||||
|
expected_url, json=expected_data, headers=expected_headers
|
||||||
|
)
|
||||||
|
|
|
@ -12,7 +12,7 @@ class YNABIntegrationTestCase(unittest.TestCase):
|
||||||
|
|
||||||
with open("tests/samples/revolut_expected.json", encoding="utf-8") as f:
|
with open("tests/samples/revolut_expected.json", encoding="utf-8") as f:
|
||||||
transactions = json.load(f)
|
transactions = json.load(f)
|
||||||
with open("tests/samples/transactions.json", encoding="utf-8") as f:
|
with open("tests/samples/revolut_transactions.json", encoding="utf-8") as f:
|
||||||
expected_data = json.load(f)
|
expected_data = json.load(f)
|
||||||
|
|
||||||
expected_headers = {"Authorization": f"Bearer <YOUR API TOKEN>"}
|
expected_headers = {"Authorization": f"Bearer <YOUR API TOKEN>"}
|
||||||
|
|
Loading…
Reference in a new issue