239 lines
8.6 KiB
Python
239 lines
8.6 KiB
Python
"""Test validation of receipt metadata"""
|
|
# Copyright © 2020 Brett Smith
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
import decimal
|
|
import enum
|
|
import itertools
|
|
import random
|
|
import typing
|
|
|
|
import pytest
|
|
|
|
from . import testutil
|
|
|
|
from conservancy_beancount.plugin import meta_receipt
|
|
|
|
TEST_KEY = 'receipt'
|
|
|
|
class PostType(enum.IntFlag):
|
|
NONE = 0
|
|
CREDIT = 1
|
|
DEBIT = 2
|
|
BOTH = CREDIT | DEBIT
|
|
|
|
|
|
class AccountForTesting(typing.NamedTuple):
|
|
name: str
|
|
required_types: PostType
|
|
fallback_meta: typing.Optional[str]
|
|
|
|
def missing_message(self, include_fallback=True):
|
|
if self.fallback_meta is None or not include_fallback:
|
|
rest = ""
|
|
else:
|
|
rest = f" or {self.fallback_meta}"
|
|
return f"{self.name} missing {TEST_KEY}{rest}"
|
|
|
|
def wrong_type_message(self, wrong_value, key=TEST_KEY):
|
|
return "{} has wrong type of {}: expected str but is a {}".format(
|
|
self.name,
|
|
key,
|
|
type(wrong_value).__name__,
|
|
)
|
|
|
|
|
|
ACCOUNTS = [AccountForTesting._make(t) for t in [
|
|
('Assets:Bank:CheckCard', PostType.BOTH, 'check'),
|
|
('Assets:Cash', PostType.BOTH, None),
|
|
('Assets:Checking', PostType.BOTH, 'check'),
|
|
('Assets:PayPal', PostType.CREDIT, 'paypal-id'),
|
|
('Assets:PayPal', PostType.DEBIT, None),
|
|
('Assets:Savings', PostType.BOTH, None),
|
|
('Liabilities:CreditCard', PostType.CREDIT, None),
|
|
('Liabilities:CreditCard', PostType.DEBIT, 'invoice'),
|
|
]]
|
|
|
|
ACCOUNTS_WITH_FALLBACKS = [acct for acct in ACCOUNTS if acct.fallback_meta]
|
|
ACCOUNTS_WITHOUT_FALLBACKS = [acct for acct in ACCOUNTS if not acct.fallback_meta]
|
|
KNOWN_FALLBACKS = {acct.fallback_meta for acct in ACCOUNTS_WITH_FALLBACKS}
|
|
|
|
# These are mostly fill-in values.
|
|
# We don't need to run every test on every value for these, just enough to
|
|
# convince ourselves the hook never reports errors against these accounts.
|
|
# Making this a iterator rather than a sequence means testutil.combine_values
|
|
# doesn't require the decorated test to go over every value, which in turn
|
|
# trims unnecessary test time.
|
|
NOT_REQUIRED_ACCOUNTS = itertools.cycle([
|
|
'Accrued:AccountsPayable',
|
|
'Accrued:AccountsReceivable',
|
|
'Assets:PrepaidExpenses',
|
|
'Assets:PrepaidVacation',
|
|
'Expenses:Other',
|
|
'Income:Other',
|
|
'UnearnedIncome:Donations',
|
|
])
|
|
|
|
def check(hook, test_acct, other_acct, expected, *,
|
|
txn_meta={}, post_meta={}, check_type=PostType.BOTH, min_amt=0):
|
|
check_type &= test_acct.required_types
|
|
assert check_type, "tried to test a non-applicable account"
|
|
if check_type == PostType.BOTH:
|
|
check(hook, test_acct, other_acct, expected,
|
|
txn_meta=txn_meta, post_meta=post_meta, check_type=PostType.CREDIT)
|
|
check_type = PostType.DEBIT
|
|
amount = decimal.Decimal('{:.02f}'.format(min_amt + random.random() * 100))
|
|
if check_type == PostType.DEBIT:
|
|
amount = -amount
|
|
txn = testutil.Transaction(**txn_meta, postings=[
|
|
(test_acct.name, amount, post_meta),
|
|
(other_acct, -amount),
|
|
])
|
|
actual = {error.message for error in hook.run(txn)}
|
|
if expected is None:
|
|
assert not actual
|
|
elif isinstance(expected, str):
|
|
assert expected in actual
|
|
else:
|
|
assert actual == expected
|
|
|
|
@pytest.fixture(scope='module')
|
|
def hook():
|
|
config = testutil.TestConfig()
|
|
return meta_receipt.MetaReceipt(config)
|
|
|
|
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
|
|
ACCOUNTS,
|
|
NOT_REQUIRED_ACCOUNTS,
|
|
testutil.LINK_METADATA_STRINGS,
|
|
))
|
|
def test_valid_receipt_on_post(hook, test_acct, other_acct, value):
|
|
check(hook, test_acct, other_acct, None, post_meta={TEST_KEY: value})
|
|
|
|
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
|
|
ACCOUNTS,
|
|
NOT_REQUIRED_ACCOUNTS,
|
|
testutil.NON_LINK_METADATA_STRINGS,
|
|
))
|
|
def test_invalid_receipt_on_post(hook, test_acct, other_acct, value):
|
|
check(hook, test_acct, other_acct, {test_acct.missing_message()},
|
|
post_meta={TEST_KEY: value})
|
|
|
|
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
|
|
ACCOUNTS,
|
|
NOT_REQUIRED_ACCOUNTS,
|
|
testutil.NON_STRING_METADATA_VALUES,
|
|
))
|
|
def test_bad_type_receipt_on_post(hook, test_acct, other_acct, value):
|
|
check(hook, test_acct, other_acct, test_acct.wrong_type_message(value),
|
|
post_meta={TEST_KEY: value})
|
|
|
|
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
|
|
ACCOUNTS,
|
|
NOT_REQUIRED_ACCOUNTS,
|
|
testutil.LINK_METADATA_STRINGS,
|
|
))
|
|
def test_valid_receipt_on_txn(hook, test_acct, other_acct, value):
|
|
check(hook, test_acct, other_acct, None, txn_meta={TEST_KEY: value})
|
|
|
|
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
|
|
ACCOUNTS,
|
|
NOT_REQUIRED_ACCOUNTS,
|
|
testutil.NON_LINK_METADATA_STRINGS,
|
|
))
|
|
def test_invalid_receipt_on_txn(hook, test_acct, other_acct, value):
|
|
check(hook, test_acct, other_acct, {test_acct.missing_message()},
|
|
txn_meta={TEST_KEY: value})
|
|
|
|
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
|
|
ACCOUNTS,
|
|
NOT_REQUIRED_ACCOUNTS,
|
|
testutil.NON_STRING_METADATA_VALUES,
|
|
))
|
|
def test_bad_type_receipt_on_txn(hook, test_acct, other_acct, value):
|
|
check(hook, test_acct, other_acct, test_acct.wrong_type_message(value),
|
|
txn_meta={TEST_KEY: value})
|
|
|
|
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
|
|
ACCOUNTS_WITH_FALLBACKS,
|
|
NOT_REQUIRED_ACCOUNTS,
|
|
testutil.LINK_METADATA_STRINGS,
|
|
))
|
|
def test_valid_fallback_on_post(hook, test_acct, other_acct, value):
|
|
check(hook, test_acct, other_acct, None,
|
|
post_meta={test_acct.fallback_meta: value})
|
|
|
|
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
|
|
ACCOUNTS_WITH_FALLBACKS,
|
|
NOT_REQUIRED_ACCOUNTS,
|
|
testutil.NON_LINK_METADATA_STRINGS,
|
|
))
|
|
def test_invalid_fallback_on_post(hook, test_acct, other_acct, value):
|
|
check(hook, test_acct, other_acct, {test_acct.missing_message()},
|
|
post_meta={test_acct.fallback_meta: value})
|
|
|
|
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
|
|
ACCOUNTS_WITH_FALLBACKS,
|
|
NOT_REQUIRED_ACCOUNTS,
|
|
testutil.NON_STRING_METADATA_VALUES,
|
|
))
|
|
def test_bad_type_fallback_on_post(hook, test_acct, other_acct, value):
|
|
expected = {
|
|
test_acct.missing_message(False),
|
|
test_acct.wrong_type_message(value, test_acct.fallback_meta),
|
|
}
|
|
check(hook, test_acct, other_acct, expected,
|
|
post_meta={test_acct.fallback_meta: value})
|
|
|
|
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
|
|
ACCOUNTS_WITH_FALLBACKS,
|
|
NOT_REQUIRED_ACCOUNTS,
|
|
testutil.LINK_METADATA_STRINGS,
|
|
))
|
|
def test_valid_fallback_on_txn(hook, test_acct, other_acct, value):
|
|
check(hook, test_acct, other_acct, None,
|
|
txn_meta={test_acct.fallback_meta: value})
|
|
|
|
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
|
|
ACCOUNTS_WITH_FALLBACKS,
|
|
NOT_REQUIRED_ACCOUNTS,
|
|
testutil.NON_LINK_METADATA_STRINGS,
|
|
))
|
|
def test_invalid_fallback_on_txn(hook, test_acct, other_acct, value):
|
|
check(hook, test_acct, other_acct, {test_acct.missing_message()},
|
|
txn_meta={test_acct.fallback_meta: value})
|
|
|
|
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
|
|
ACCOUNTS_WITH_FALLBACKS,
|
|
NOT_REQUIRED_ACCOUNTS,
|
|
testutil.NON_STRING_METADATA_VALUES,
|
|
))
|
|
def test_bad_type_fallback_on_txn(hook, test_acct, other_acct, value):
|
|
expected = {
|
|
test_acct.missing_message(False),
|
|
test_acct.wrong_type_message(value, test_acct.fallback_meta),
|
|
}
|
|
check(hook, test_acct, other_acct, expected,
|
|
txn_meta={test_acct.fallback_meta: value})
|
|
|
|
@pytest.mark.parametrize('test_acct,other_acct,key,value', testutil.combine_values(
|
|
ACCOUNTS_WITHOUT_FALLBACKS,
|
|
NOT_REQUIRED_ACCOUNTS,
|
|
KNOWN_FALLBACKS,
|
|
testutil.LINK_METADATA_STRINGS,
|
|
))
|
|
def test_fallback_not_accepted_on_other_accounts(hook, test_acct, other_acct, key, value):
|
|
check(hook, test_acct, other_acct, {test_acct.missing_message()},
|
|
post_meta={key: value})
|