Files @ 4a28596db267
Branch filter:

Location: NPO-Accounting/conservancy_beancount/tests/test_meta_rt_links.py

Brett Smith
data: bank-statement and tax-statement are link metadata.

Not including them earlier was an oversight.
"""Test link checker for RT links"""
# 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 itertools

import pytest

from . import testutil

from conservancy_beancount import errors as errormod
from conservancy_beancount.plugin import meta_rt_links

METADATA_KEYS = [
    'approval',
    'bank-statement',
    'check',
    'contract',
    'invoice',
    'purchase-order',
    'receipt',
    'rt-id',
    'statement',
    'tax-statement',
]

GOOD_LINKS = [
    'rt:1',
    'rt:1/5',
    'rt://ticket/2',
    'rt://ticket/3/attachments/15',
]

MALFORMED_LINKS = [
    'rt:one',
    'rt:two/three',
    'rt://4',
    'rt://ticket/5/attach/6',
]

NOT_FOUND_LINKS = [
    'rt:1/10',
    'rt:10',
    'rt://ticket/9',
    'rt://ticket/3/attachments/99',
]

MALFORMED_MSG = '{} link is malformed: {}'.format
NOT_FOUND_MSG = '{} not found in RT: {}'.format

def build_meta(keys=None, *sources):
    if keys is None:
        keys = iter(METADATA_KEYS)
    sources = (itertools.cycle(src) for src in sources)
    return {key: ' '.join(str(x) for x in rest)
            for key, *rest in zip(keys, *sources)}

@pytest.fixture(scope='module')
def hook():
    config = testutil.TestConfig(rt_client=testutil.RTClient())
    return meta_rt_links.MetaRTLinks(config)

def test_error_with_no_rt():
    config = testutil.TestConfig()
    with pytest.raises(errormod.ConfigurationError):
        meta_rt_links.MetaRTLinks(config)

def test_good_txn_links(hook):
    meta = build_meta(None, GOOD_LINKS)
    txn = testutil.Transaction(**meta, postings=[
        ('Income:Donations', -5),
        ('Assets:Cash', 5),
    ])
    assert not list(hook.run(txn))

def test_good_post_links(hook):
    meta = build_meta(None, GOOD_LINKS)
    txn = testutil.Transaction(postings=[
        ('Income:Donations', -5, meta),
        ('Assets:Cash', 5),
    ])
    assert not list(hook.run(txn))

@pytest.mark.parametrize('link_source,format_error', [
    (MALFORMED_LINKS, MALFORMED_MSG),
    (NOT_FOUND_LINKS, NOT_FOUND_MSG),
])
def test_bad_txn_links(hook, link_source, format_error):
    meta = build_meta(None, link_source)
    txn = testutil.Transaction(**meta, postings=[
        ('Income:Donations', -5),
        ('Assets:Cash', 5),
    ])
    expected = {format_error(key, value) for key, value in meta.items()}
    actual = {error.message for error in hook.run(txn)}
    assert expected == actual

@pytest.mark.parametrize('link_source,format_error', [
    (MALFORMED_LINKS, MALFORMED_MSG),
    (NOT_FOUND_LINKS, NOT_FOUND_MSG),
])
def test_bad_post_links(hook, link_source, format_error):
    meta = build_meta(None, link_source)
    txn = testutil.Transaction(postings=[
        ('Income:Donations', -5, meta.copy()),
        ('Assets:Cash', 5),
    ])
    expected = {format_error(key, value) for key, value in meta.items()}
    actual = {error.message for error in hook.run(txn)}
    assert expected == actual

@pytest.mark.parametrize('value', testutil.NON_STRING_METADATA_VALUES)
def test_bad_metadata_type(hook, value):
    txn = testutil.Transaction(**{'rt-id': value}, postings=[
        ('Income:Donations', -5),
        ('Assets:Cash', 5),
    ])
    expected = {'transaction has wrong type of rt-id: expected str but is a {}'.format(
        type(value).__name__,
    )}
    actual = {error.message for error in hook.run(txn)}
    assert expected == actual

@pytest.mark.parametrize('ext_doc', [
    'statement.txt',
    'https://example.org/',
])
def test_docs_outside_rt_not_checked(hook, ext_doc):
    txn = testutil.Transaction(
        receipt='{} {} {}'.format(GOOD_LINKS[0], ext_doc, MALFORMED_LINKS[1]),
        postings=[
            ('Income:Donations', -5),
            ('Assets:Cash', 5),
        ])
    expected = {MALFORMED_MSG('receipt', MALFORMED_LINKS[1])}
    actual = {error.message for error in hook.run(txn)}
    assert expected == actual

def test_flagged_txn_not_checked(hook):
    txn_meta = build_meta(None, MALFORMED_LINKS)
    txn_meta['flag'] = '!'
    keys = iter(METADATA_KEYS)
    txn = testutil.Transaction(**txn_meta, postings=[
        ('Income:Donations', -5, build_meta(keys, MALFORMED_LINKS)),
        ('Assets:Checking', 5, build_meta(keys, NOT_FOUND_LINKS)),
    ])
    assert not list(hook.run(txn))

def test_mixed_results(hook):
    txn = testutil.Transaction(
        approval='{} {}'.format(*GOOD_LINKS),
        contract='{} {}'.format(MALFORMED_LINKS[0], GOOD_LINKS[1]),
        postings=[
            ('Income:Donations', -5, {'invoice': '{} {}'.format(*NOT_FOUND_LINKS)}),
            ('Assets:Cash', 5, {'statement': '{} {}'.format(GOOD_LINKS[0], MALFORMED_LINKS[1])}),
        ])
    expected = {
        MALFORMED_MSG('contract', MALFORMED_LINKS[0]),
        NOT_FOUND_MSG('invoice', NOT_FOUND_LINKS[0]),
        NOT_FOUND_MSG('invoice', NOT_FOUND_LINKS[1]),
        MALFORMED_MSG('statement', MALFORMED_LINKS[1]),
    }
    actual = {error.message for error in hook.run(txn)}
    assert expected == actual