"""Test validation of paypal-id metadata""" # Copyright © 2020 Brett Smith # License: AGPLv3-or-later WITH Beancount-Plugin-Additional-Permission-1.0 # # Full copyright and licensing details can be found at toplevel file # LICENSE.txt in the repository. import itertools import pytest from . import testutil from conservancy_beancount.plugin import meta_paypal_id VALID_TXN_IDS = { # Basically normal ids. 'A1234567890BCDEFG', '12345HIJKLNMO6780', # All numeric '05678901234567890', # All alphabetic 'CDEFHSNOECRUHOECR', } VALID_INVOICE_IDS = { 'INV2-ABCD-1234-EFGH-6789', # All numeric 'INV2-1010-2020-3030-4567', # All alphabetic 'INV2-ABCD-EFGH-IJKL-MNOP', } VALID_VALUES = VALID_TXN_IDS | VALID_INVOICE_IDS INVALID_VALUES = { # Empty '', ' ', # Punctuation and whitespace 'Z12345-67890QRSTU', 'Y12345.67890QRSTU', 'X12345_67890QRSTU', 'W12345 67890QRSTU', 'INV2-ABCD.1234-EFGH-7890', 'INV2-ABCD-1234_EFGH-7890', 'INV2-ABCD-1234-EFGH 7890', # Too short 'Q1234567890RSTUV', 'INV2-ABCD-1234-EFGH-789', # Too long 'V123456789012345WX', 'INV2-ABCD-1234-EFGH-78900', 'INV2-ABCD-1234-EFGH-7890-IJKL', # Bad cadence 'INV2-ABCD-1234-EFG-H7890', 'INV2ABCD-123-EFG-456-789', 'INV2ABCDEFGHIJKLMNOPQRST', } ACCOUNTS = itertools.cycle([ 'Assets:PayPal', 'Assets:Receivable:Accounts', ]) TEST_KEY = 'paypal-id' INVALID_MSG = f"{{}} has invalid {TEST_KEY}: {{}}".format BAD_TYPE_MSG = f"{{}} has wrong type of {TEST_KEY}: expected str but is a {{}}".format @pytest.fixture(scope='module') def hook(): config = testutil.TestConfig() return meta_paypal_id.MetaPayPalID(config) def paypal_account_for_id(paypal_id): if paypal_id.startswith('INV'): return 'Assets:Receivable:Accounts' else: return 'Assets:PayPal' @pytest.mark.parametrize('src_value', VALID_VALUES) def test_valid_values_on_postings(hook, src_value): txn = testutil.Transaction(postings=[ ('Income:Donations', -25), (paypal_account_for_id(src_value), 25, {TEST_KEY: src_value}), ]) assert not list(hook.run(txn)) @pytest.mark.parametrize('src_value', INVALID_VALUES) def test_invalid_values_on_postings(hook, src_value): acct = paypal_account_for_id(src_value) txn = testutil.Transaction(postings=[ ('Income:Donations', -25), (acct, 25, {TEST_KEY: src_value}), ]) actual = {error.message for error in hook.run(txn)} assert actual == {INVALID_MSG(acct, src_value)} @pytest.mark.parametrize('src_value,acct', testutil.combine_values( testutil.NON_STRING_METADATA_VALUES, ACCOUNTS, )) def test_bad_type_values_on_postings(hook, src_value, acct): txn = testutil.Transaction(postings=[ ('Income:Donations', -25), (acct, 25, {TEST_KEY: src_value}), ]) actual = {error.message for error in hook.run(txn)} assert actual == {BAD_TYPE_MSG(acct, type(src_value).__name__)} @pytest.mark.parametrize('src_value', VALID_VALUES) def test_valid_values_on_transactions(hook, src_value): txn = testutil.Transaction(**{TEST_KEY: src_value}, postings=[ ('Income:Donations', -25), (paypal_account_for_id(src_value), 25), ]) assert not list(hook.run(txn)) @pytest.mark.parametrize('src_value', INVALID_VALUES) def test_invalid_values_on_transactions(hook, src_value): acct = paypal_account_for_id(src_value) txn = testutil.Transaction(**{TEST_KEY: src_value}, postings=[ ('Income:Donations', -25), (acct, 25), ]) actual = {error.message for error in hook.run(txn)} assert actual == {INVALID_MSG(acct, src_value)} @pytest.mark.parametrize('src_value,acct', testutil.combine_values( testutil.NON_STRING_METADATA_VALUES, ACCOUNTS, )) def test_bad_type_values_on_transactions(hook, src_value, acct): txn = testutil.Transaction(**{TEST_KEY: src_value}, postings=[ ('Income:Donations', -25), (acct, 25), ]) actual = {error.message for error in hook.run(txn)} assert actual == {BAD_TYPE_MSG(acct, type(src_value).__name__)} @pytest.mark.parametrize('src_value', VALID_INVOICE_IDS) def test_invoice_ids_not_accepted_for_non_accruals(hook, src_value): acct = 'Assets:PayPal' txn = testutil.Transaction(**{TEST_KEY: src_value}, postings=[ ('Income:Donations', -25), (acct, 25), ]) actual = {error.message for error in hook.run(txn)} assert actual == {INVALID_MSG(acct, src_value)} @pytest.mark.parametrize('src_value', VALID_TXN_IDS) def test_transaction_ids_not_accepted_for_accruals(hook, src_value): acct = 'Assets:Receivable:Accounts' txn = testutil.Transaction(**{TEST_KEY: src_value}, postings=[ ('Income:Donations', -25), (acct, 25), ]) actual = {error.message for error in hook.run(txn)} assert actual == {INVALID_MSG(acct, src_value)} def test_required_for_assets_paypal(hook): acct = 'Assets:PayPal' txn = testutil.Transaction(postings=[ ('Income:Donations', -35), (acct, 35), ]) actual = {error.message for error in hook.run(txn)} assert actual == {f"{acct} missing {TEST_KEY}"} @pytest.mark.parametrize('txn_id,inv_id', testutil.combine_values( VALID_TXN_IDS, VALID_INVOICE_IDS, )) def test_invoice_payment_transaction_ok(hook, txn_id, inv_id): txn = testutil.Transaction(**{TEST_KEY: txn_id}, postings=[ ('Assets:Receivable:Accounts', -100, {TEST_KEY: inv_id}), ('Assets:PayPal', 97), ('Expenses:BankingFees', 3), ]) assert not list(hook.run(txn)) def test_not_required_on_opening(hook): txn = testutil.Transaction(postings=[ ('Assets:PayPal', 1000), (next(testutil.OPENING_EQUITY_ACCOUNTS), -1000), ]) assert not list(hook.run(txn)) def test_still_required_on_flagged_txn(hook): txn = testutil.Transaction(flag='!', postings=[ ('Assets:PayPal', 1000), ('Income:Donations', -1000), ]) assert list(hook.run(txn))