diff --git a/tests/test_reports_query.py b/tests/test_reports_query.py index 8f49478a671fb6e69952b4e3dcb889582afb3ee0..fa5270b27bfa19e7a39a0d6b6c113a8b2c0f9ea2 100644 --- a/tests/test_reports_query.py +++ b/tests/test_reports_query.py @@ -21,6 +21,8 @@ import pytest from . import testutil from beancount.core import data as bc_data +from beancount.query import query_compile as bc_query_compile +from beancount.query import query_execute as bc_query_execute from beancount.query import query_parser as bc_query_parser from conservancy_beancount.books import FiscalYear from conservancy_beancount.reports import query as qmod @@ -28,6 +30,8 @@ from conservancy_beancount import rtutil from decimal import Decimal +UTC = datetime.timezone.utc + class MockRewriteRuleset: def __init__(self, multiplier=2): self.multiplier = multiplier @@ -39,6 +43,13 @@ class MockRewriteRuleset: yield post._replace(units=testutil.Amount(number, currency)) +class RowContext(bc_query_execute.RowContext): + def __init__(self, entry, posting=None): + super().__init__() + self.entry = entry + self.posting = posting + + @pytest.fixture(scope='module') def qparser(): return bc_query_parser.Parser() @@ -47,12 +58,128 @@ def qparser(): def rt(): return rtutil.RT(testutil.RTClient()) +@pytest.fixture(scope='module') +def ticket_query(): + return qmod.RTTicket.with_client(testutil.RTClient(), 'testfixture') + +def const_operands(*args): + return [bc_query_compile.EvalConstant(v) for v in args] + def pipe_main(arglist, config, stdout_type=io.StringIO): stdout = stdout_type() stderr = io.StringIO() returncode = qmod.main(arglist, stdout, stderr, config) return returncode, stdout, stderr +def test_rt_ticket_unconfigured(): + with pytest.raises(RuntimeError): + qmod.RTTicket(const_operands('id', 'rt-id')) + +@pytest.mark.parametrize('field_name', ['foo', 'bar']) +def test_rt_ticket_bad_field(ticket_query, field_name): + with pytest.raises(ValueError): + ticket_query(const_operands(field_name, 'rt-id')) + +@pytest.mark.parametrize('meta_name', ['foo', 'bar']) +def test_rt_ticket_bad_metadata(ticket_query, meta_name): + with pytest.raises(ValueError): + ticket_query(const_operands('id', meta_name)) + +@pytest.mark.parametrize('field_name,meta_name,expected', [ + ('id', 'rt-id', 1), + ('Queue', 'approval', 'general'), + ('Requestors', 'invoice', ['mx1@example.org', 'requestor2@example.org']), + ('Due', 'tax-reporting', datetime.datetime(2017, 1, 14, 12, 1, 0, tzinfo=UTC)), +]) +def test_rt_ticket_from_txn(ticket_query, field_name, meta_name, expected): + func = ticket_query(const_operands(field_name, meta_name)) + txn = testutil.Transaction(**{meta_name: 'rt:1'}, postings=[ + ('Assets:Cash', 80), + ]) + context = RowContext(txn, txn.postings[0]) + if not isinstance(expected, list): + expected = [expected] + assert func(context) == expected + +@pytest.mark.parametrize('field_name,meta_name,expected', [ + ('id', 'rt-id', 2), + ('Queue', 'approval', 'general'), + ('Requestors', 'invoice', ['mx2@example.org', 'requestor2@example.org']), + ('Due', 'tax-reporting', datetime.datetime(2017, 1, 14, 12, 2, 0, tzinfo=UTC)), +]) +def test_rt_ticket_from_post(ticket_query, field_name, meta_name, expected): + func = ticket_query(const_operands(field_name, meta_name)) + txn = testutil.Transaction(**{meta_name: 'rt:1'}, postings=[ + ('Assets:Cash', 110, {meta_name: 'rt:2/8'}), + ]) + context = RowContext(txn, txn.postings[0]) + if not isinstance(expected, list): + expected = [expected] + assert func(context) == expected + +@pytest.mark.parametrize('field_name,meta_name,expected,on_txn', [ + ('id', 'approval', [1, 2], True), + ('Queue', 'check', ['general', 'general'], False), + ('Requestors', 'invoice', [ + 'mx1@example.org', + 'mx2@example.org', + 'requestor2@example.org', + 'requestor2@example.org', + ], False), +]) +def test_rt_ticket_multi_results(ticket_query, field_name, meta_name, expected, on_txn): + func = ticket_query(const_operands(field_name, meta_name)) + txn = testutil.Transaction(**{'rt-id': 'rt:1'}, postings=[ + ('Assets:Cash', 110, {'rt-id': 'rt:2'}), + ]) + post = txn.postings[0] + meta = txn.meta if on_txn else post.meta + meta[meta_name] = 'rt:1/2 Docs/12.pdf rt:2/8' + context = RowContext(txn, post) + assert sorted(func(context)) == expected + +@pytest.mark.parametrize('meta_value,on_txn', testutil.combine_values( + ['', 'Docs/34.pdf', 'Docs/100.pdf Docs/120.pdf'], + [True, False], +)) +def test_rt_ticket_no_results(ticket_query, meta_value, on_txn): + func = ticket_query(const_operands('Queue', 'check')) + txn = testutil.Transaction(**{'rt-id': 'rt:1'}, postings=[ + ('Assets:Cash', 110, {'rt-id': 'rt:2'}), + ]) + post = txn.postings[0] + meta = txn.meta if on_txn else post.meta + meta['check'] = meta_value + context = RowContext(txn, post) + assert func(context) == [] + +def test_rt_ticket_caches_tickets(): + rt_client = testutil.RTClient() + rt_client.TICKET_DATA = testutil.RTClient.TICKET_DATA.copy() + ticket_query = qmod.RTTicket.with_client(rt_client, 'cachetestA') + func = ticket_query(const_operands('id', 'rt-id')) + txn = testutil.Transaction(postings=[ + ('Assets:Cash', 160, {'rt-id': 'rt:3'}), + ]) + context = RowContext(txn, txn.postings[0]) + assert func(context) == [3] + del rt_client.TICKET_DATA['3'] + assert func(context) == [3] + +def test_rt_ticket_caches_tickets_not_found(): + rt_client = testutil.RTClient() + rt_client.TICKET_DATA = testutil.RTClient.TICKET_DATA.copy() + rt3 = rt_client.TICKET_DATA.pop('3') + ticket_query = qmod.RTTicket.with_client(rt_client, 'cachetestB') + func = ticket_query(const_operands('id', 'rt-id')) + txn = testutil.Transaction(postings=[ + ('Assets:Cash', 160, {'rt-id': 'rt:3'}), + ]) + context = RowContext(txn, txn.postings[0]) + assert func(context) == [] + rt_client.TICKET_DATA['3'] = rt3 + assert func(context) == [] + def test_books_loader_empty(): result = qmod.BooksLoader(None)() assert not result.entries