diff --git a/tests/test_cliutil_searchterm.py b/tests/test_cliutil_searchterm.py new file mode 100644 index 0000000000000000000000000000000000000000..c0c81b0fa05d46bd52ec0f90e4426976b1f6fcf3 --- /dev/null +++ b/tests/test_cliutil_searchterm.py @@ -0,0 +1,164 @@ +"""test_cliutil_searchterm - Unit tests for cliutil.SearchTerm""" +# 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 . + +import re + +import pytest + +from . import testutil + +from conservancy_beancount import cliutil +from conservancy_beancount import data + +TICKET_LINKS = [ + '123', + 'rt:123', + 'rt://ticket/123', +] + +ATTACHMENT_LINKS = [ + '123/456', + 'rt:123/456', + 'rt://ticket/123/attachments/456', +] + +REPOSITORY_LINKS = [ + '789.pdf', + 'Documents/789.pdf', +] + +RT_LINKS = TICKET_LINKS + ATTACHMENT_LINKS +ALL_LINKS = RT_LINKS + REPOSITORY_LINKS + +INVALID_METADATA_KEYS = [ + # Must start with a-z + ';key', + ' key', + 'ákey', + 'Key', + # Must only contain alphanumerics, -, and _ + 'key;', + 'key ', + 'a.key', +] + +@pytest.fixture(scope='module') +def defaults_parser(): + return cliutil.SearchTerm.arg_parser('document', 'rt-id') + +@pytest.fixture(scope='module') +def one_default_parser(): + return cliutil.SearchTerm.arg_parser('default') + +@pytest.fixture(scope='module') +def no_default_parser(): + return cliutil.SearchTerm.arg_parser() + +def check_link_regexp(regexp, match_s, first_link_only=False): + assert regexp + assert re.search(regexp, match_s) + assert re.search(regexp, match_s + ' postlink') + assert re.search(regexp, match_s + '0') is None + assert re.search(regexp, '1' + match_s) is None + end_match = re.search(regexp, 'prelink ' + match_s) + if first_link_only: + assert end_match is None + else: + assert end_match + +@pytest.mark.parametrize('arg', TICKET_LINKS) +def test_search_term_parse_ticket(defaults_parser, arg): + key, regexp = defaults_parser(arg) + assert key == 'rt-id' + check_link_regexp(regexp, 'rt:123', first_link_only=True) + check_link_regexp(regexp, 'rt://ticket/123', first_link_only=True) + +@pytest.mark.parametrize('arg', ATTACHMENT_LINKS) +def test_search_term_parse_attachment(defaults_parser, arg): + key, regexp = defaults_parser(arg) + assert key == 'document' + check_link_regexp(regexp, 'rt:123/456') + check_link_regexp(regexp, 'rt://ticket/123/attachments/456') + +@pytest.mark.parametrize('key,query', testutil.combine_values( + ['approval', 'contract', 'invoice'], + RT_LINKS, +)) +def test_search_term_parse_metadata_rt_shortcut(defaults_parser, key, query): + actual_key, regexp = defaults_parser(f'{key}={query}') + assert actual_key == key + if query.endswith('/456'): + check_link_regexp(regexp, 'rt:123/456') + check_link_regexp(regexp, 'rt://ticket/123/attachments/456') + else: + check_link_regexp(regexp, 'rt:123') + check_link_regexp(regexp, 'rt://ticket/123') + +@pytest.mark.parametrize('search_prefix', [ + '', + 'approval=', + 'contract=', + 'invoice=', +]) +def test_search_term_parse_repo_link(defaults_parser, search_prefix): + document = '1234.pdf' + actual_key, regexp = defaults_parser(f'{search_prefix}{document}') + if search_prefix: + expect_key = search_prefix.rstrip('=') + else: + expect_key = 'document' + assert actual_key == expect_key + check_link_regexp(regexp, document) + +@pytest.mark.parametrize('search,unmatched', [ + ('1234.pdf', '1234_pdf'), +]) +def test_search_term_parse_regexp_escaping(defaults_parser, search, unmatched): + _, regexp = defaults_parser(search) + assert re.search(regexp, unmatched) is None + +@pytest.mark.parametrize('key', INVALID_METADATA_KEYS) +def test_non_metadata_key(one_default_parser, key): + document = f'{key}=890' + key, pattern = one_default_parser(document) + assert key == 'default' + check_link_regexp(pattern, document) + +@pytest.mark.parametrize('arg', ALL_LINKS) +def test_default_parser(one_default_parser, arg): + key, _ = one_default_parser(arg) + assert key == 'default' + +@pytest.mark.parametrize('arg', ALL_LINKS + [ + f'{nonkey}={link}' for nonkey, link in testutil.combine_values( + INVALID_METADATA_KEYS, ALL_LINKS, + ) +]) +def test_no_key(no_default_parser, arg): + with pytest.raises(ValueError): + key, pattern = no_default_parser(arg) + +@pytest.mark.parametrize('key', ['zero', 'one', 'two']) +def test_filter_postings(key): + txn = testutil.Transaction(postings=[ + ('Income:Other', 3, {'one': '1', 'two': '2'}), + ('Income:Other', 2, {'two': '2'}), + ('Income:Other', 1, {'one': '1'}), + ]) + search = cliutil.SearchTerm(key, '.') + actual = list(search.filter_postings(data.Posting.from_txn(txn))) + assert len(actual) == 0 if key == 'zero' else 2 + assert all(post.meta.get(key) for post in actual)