diff --git a/tests/test_meta_payroll_type.py b/tests/test_meta_payroll_type.py new file mode 100644 index 0000000..6c3b91e --- /dev/null +++ b/tests/test_meta_payroll_type.py @@ -0,0 +1,152 @@ +"""test_meta_payroll_type.py - Unit tests for payroll-type validation hook""" +# Copyright © 2021 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 typing + +import pytest + +from . import testutil +from conservancy_beancount.plugin import meta_payroll_type + +class HookData(typing.NamedTuple): + account: str + hook_type: typing.Type + valid_values: typing.Set[str] + invalid_values: typing.Set[str] + + @classmethod + def from_hook(cls, hook, *valid_values): + return cls(hook.ACCOUNT, hook, set(valid_values), set()) + + @classmethod + def set_invalid_values(cls, datas): + all_values = frozenset(v for d in datas for v in d.valid_values) + for data in datas: + data.invalid_values.update(all_values.difference(data.valid_values)) + + +TEST_KEY = 'payroll-type' +HOOK_DATA = [ + HookData.from_hook(meta_payroll_type.HealthInsuranceHook, + 'US:HRA:Fees', 'US:HRA:Usage', 'US:Premium:Main'), + HookData.from_hook(meta_payroll_type.OtherBenefitsHook, + 'US:403b:Fees', 'US:Education', 'US:ProfessionalMembership'), + HookData.from_hook( + meta_payroll_type.SalaryHook, + 'CA:Tax:EI', + 'US:NY:Tax:NYC', + 'US:Taxes:Medicare', + 'CA:General', + 'US:403b:Match', + 'US:PTO', + ), + HookData.from_hook(meta_payroll_type.TaxHook, + 'CA:PP', 'US:IL:Unemployment', 'US:SocialSecurity'), +] +HookData.set_invalid_values(HOOK_DATA) + +@pytest.fixture(scope='module') +def config(): + return testutil.TestConfig() + +def norm_value(value): + return value.replace(':Taxes:', ':Tax:', 1) + +@pytest.mark.parametrize('hook_type,account,value', ( + (data.hook_type, data.account, value) + for data in HOOK_DATA + for value in data.valid_values +)) +def test_valid_value_on_post(config, hook_type, account, value): + txn = testutil.Transaction(postings=[ + (account, 55, {TEST_KEY: value}), + ('Assets:Checking', -55), + ]) + errors = list(hook_type(config).run(txn)) + assert not errors + testutil.check_post_meta(txn, {TEST_KEY: norm_value(value)}, None) + +@pytest.mark.parametrize('hook_type,account,value', ( + (data.hook_type, data.account, value) + for data in HOOK_DATA + for value in data.valid_values +)) +def test_valid_value_on_txn(config, hook_type, account, value): + txn = testutil.Transaction(**{TEST_KEY: value}, postings=[ + (account, 80, {TEST_KEY: value}), + ('Assets:Checking', -80), + ]) + errors = list(hook_type(config).run(txn)) + assert not errors + testutil.check_post_meta(txn, {TEST_KEY: norm_value(value)}, None) + +@pytest.mark.parametrize('hook_type,account,value', ( + (data.hook_type, data.account, value) + for data in HOOK_DATA + for value in data.invalid_values +)) +def test_invalid_value_on_post(config, hook_type, account, value): + txn = testutil.Transaction(flag='!', postings=[ + (account, 90, {TEST_KEY: value}), + ('Assets:Checking', -90), + ]) + errors = list(hook_type(config).run(txn)) + assert errors + testutil.check_post_meta(txn, {TEST_KEY: value}, None) + +@pytest.mark.parametrize('hook_type,account,value', ( + (data.hook_type, data.account, value) + for data in HOOK_DATA + for value in data.invalid_values +)) +def test_invalid_value_on_txn(config, hook_type, account, value): + txn = testutil.Transaction(flag='!', **{TEST_KEY: value}, postings=[ + (account, 105), + ('Assets:Checking', -105), + ]) + errors = list(hook_type(config).run(txn)) + assert errors + testutil.check_post_meta(txn, None, None) + +@pytest.mark.parametrize('hook_type,account', ( + (data.hook_type, data.account) + for data in HOOK_DATA +)) +def test_missing_value(config, hook_type, account): + txn = testutil.Transaction(flag='!', postings=[ + (account, 115), + ('Assets:Checking', -115), + ]) + errors = list(hook_type(config).run(txn)) + assert errors + +@pytest.mark.parametrize('hook_type,account', ( + (hook_data.hook_type, other_data.account) + for hook_data in HOOK_DATA + for other_data in HOOK_DATA + if other_data is not hook_data +)) +def test_no_overlapping_account_checks(config, hook_type, account): + txn = testutil.Transaction(postings=[ + (account, 120, {TEST_KEY: 'Test:Overlap'}), + ('Assets:Checking', -120), + ]) + errors = list(hook_type(config).run(txn)) + assert not errors + +@pytest.mark.parametrize('hook_data,value', testutil.combine_values( + HOOK_DATA, + testutil.FIXME_VALUES, +)) +def test_flagged_fixme_ok(config, hook_data, value): + txn = testutil.Transaction(flag='!', postings=[ + (hook_data.account, 120, {TEST_KEY: value}), + ('Assets:Checking', -120), + ]) + errors = list(hook_data.hook_type(config).run(txn)) + assert not errors + testutil.check_post_meta(txn, {TEST_KEY: value}, None)