"""test_pdfforms_fields.py - Unit tests for PDF forms manipulation""" # 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 codecs import itertools import pytest from decimal import Decimal from pdfminer.psparser import PSLiteral from conservancy_beancount.pdfforms import fields as fieldsmod def field_source( name=None, value=None, field_type=None, flags=None, parent=None, kids=None, *, literal=None, ): retval = {} if isinstance(name, str): retval['T'] = name.encode('ascii') elif name is not None: retval['T'] = name if value is not None: if literal is None: literal = field_type and field_type != 'Tx' if literal: value = PSLiteral(value) retval['V'] = value if field_type is not None: retval['FT'] = PSLiteral(field_type) if flags is not None: retval['Ff'] = flags if parent is not None: retval['Parent'] = parent if kids is not None: retval['Kids'] = list(kids) return retval def appearance_states(*names): return {key: object() for key in names if key is not None} def test_empty_field(): source = field_source() field = fieldsmod.FormField(source) assert not field.name() assert field.value() is None assert field.parent() is None assert not list(field.kids()) assert field.flags() == 0 assert field.is_terminal() with pytest.raises(ValueError): field.field_type() def test_text_field_base(): source = field_source(b's', b'string of text', 'Tx') field = fieldsmod.FormField(source) assert field.field_type() is fieldsmod.FieldType.Text assert field.name() == 's' assert field.value() == b'string of text' @pytest.mark.parametrize('value', ['Off', 'Yes', 'On']) def test_checkbox_field_base(value): source = field_source(b'cb', value, 'Btn', literal=True) field = fieldsmod.FormField(source) assert field.field_type() is fieldsmod.FieldType.Button assert field.name() == 'cb' assert field.value().name == value @pytest.mark.parametrize('flags', range(4)) def test_readonly_flag(flags): source = field_source(flags=flags) field = fieldsmod.FormField(source) assert field.flags() == flags assert field.is_readonly() == flags % 2 @pytest.mark.parametrize('kid_count', range(3)) def test_kids(kid_count): kids = [field_source(f'kid{n}', field_type='Ch') for n in range(kid_count)] source = field_source(kids=iter(kids)) field = fieldsmod.FormField(source) got_kids = list(field.kids()) assert len(got_kids) == len(kids) assert field.is_terminal() == (not kids) for actual, expected in zip(got_kids, kids): assert actual.name() == expected['T'].decode('ascii') def test_kids_by_type(): kids = [field_source(field_type='Tx'), field_source(field_type='Btn')] source = field_source('topform', kids=iter(kids)) actual = fieldsmod.FormField.by_type(source).kids() assert isinstance(next(actual), fieldsmod.TextField) assert isinstance(next(actual), fieldsmod.CheckboxField) assert next(actual, None) is None def test_inheritance(): parent_source = field_source(b'parent', 'parent value', 'Tx', 17) kid_source = field_source('kid', parent=parent_source) parent_source['Kids'] = [kid_source] field = fieldsmod.FormField(kid_source) parent = field.parent() assert parent is not None assert parent.name() == 'parent' assert not parent.is_terminal() assert field.is_terminal() assert field.name() == 'kid' assert field.field_type() is fieldsmod.FieldType.Text assert field.value() == 'parent value' assert field.flags() == 17 assert not list(field.kids()) @pytest.mark.parametrize('field_type,value', [ ('Tx', b'new value'), ('Btn', PSLiteral('Yes')), ]) def test_set_value(field_type, value): source = field_source(field_type=field_type) field = fieldsmod.FormField(source) assert field.value() is None field.set_value(value) assert field.value() == value @pytest.mark.parametrize('field_type,expected', [ ('Tx', fieldsmod.TextField), ('Btn', fieldsmod.CheckboxField), ]) def test_by_type(field_type, expected): source = field_source(field_type=field_type) field = fieldsmod.FormField.by_type(source) assert isinstance(field, expected) def test_container_by_type(): kids = [field_source(field_type='Tx'), field_source(field_type='Btn')] source = field_source('topform', kids=iter(kids)) field = fieldsmod.FormField.by_type(source) assert isinstance(field, fieldsmod.FormField) @pytest.mark.parametrize('flag', [ # If you add dedicated classes for these types of buttons, you can remove # their test cases. fieldsmod.FieldFlags.Radio, fieldsmod.FieldFlags.Pushbutton, ]) def test_unsupported_button_by_type(flag): source = field_source(field_type='Btn', flags=flag) field = fieldsmod.FormField.by_type(source) assert type(field) is fieldsmod.FormField @pytest.mark.parametrize('field_type', [ # If you add dedicated classes for these types of fields, you can remove # their test cases. 'Ch', 'Sig', ]) def test_unsupported_field_by_type(field_type): source = field_source(field_type=field_type) field = fieldsmod.FormField.by_type(source) assert type(field) is fieldsmod.FormField @pytest.mark.parametrize('value', [None, 'Off', 'Yes']) def test_checkbox_value(value): source = field_source('cb', value, 'Btn', literal=True) field = fieldsmod.CheckboxField(source) assert field.value() == (value and value == 'Yes') @pytest.mark.parametrize('value,expected', [ (None, None), (False, 'Off'), (True, 'Yes'), ]) def test_checkbox_set_value(value, expected): source = field_source('cb', field_type='Btn') field = fieldsmod.CheckboxField(source) field.set_value(value) actual = fieldsmod.FormField.value(field) if expected is None: assert actual is None else: assert actual.name == expected @pytest.mark.parametrize('on_key,off_key', itertools.product( ['1', '2', 'On', 'Yes'], ['Off', None], )) def test_checkbox_options(on_key, off_key): source = field_source('cb', field_type='Btn') source['AP'] = {'N': appearance_states(on_key, off_key)} field = fieldsmod.CheckboxField(source) assert field.options() == [on_key, 'Off'] def test_checkbox_options_yes_no(): # I'm not sure this is actually allowed under the spec, but… expected = ['Yes', 'No'] source = field_source('cb', field_type='Btn') source['AP'] = {'N': appearance_states(*expected)} field = fieldsmod.CheckboxField(source) assert field.options() == expected @pytest.mark.parametrize('on_key,off_key,set_value', itertools.product( ['1', '2', 'On', 'Yes'], ['Off', None], [True, False, None], )) def test_checkbox_set_custom_value(on_key, off_key, set_value): source = field_source('cb', field_type='Btn') source['AP'] = {'N': appearance_states(on_key, off_key)} field = fieldsmod.CheckboxField(source) field.set_value(set_value) actual = fieldsmod.FormField.value(field) if set_value is None: assert actual is None elif set_value: assert actual.name == (on_key or 'Yes') else: assert actual.name == 'Off' @pytest.mark.parametrize('encoding,prefix', [ ('ascii', b''), ('utf-16be', codecs.BOM_UTF16_BE), ]) def test_text_value(encoding, prefix): expected = f'{encoding} encoding test' value = prefix + expected.encode(encoding) source = field_source('t', value, 'Tx') field = fieldsmod.TextField(source) assert field.value() == expected def test_text_value_none(): source = field_source(field_type='Tx') assert fieldsmod.TextField(source).value() is None @pytest.mark.parametrize('text,bprefix', [ ('ASCII test', b''), ('UTF—16 test', codecs.BOM_UTF16_BE), ]) def test_text_set_value(text, bprefix): source = field_source(field_type='Tx') field = fieldsmod.TextField(source) field.set_value(text) assert field.value() == text actual = fieldsmod.FormField.value(field) assert actual == bprefix + text.encode('utf-16be' if bprefix else 'ascii') @pytest.mark.parametrize('expected', [ '0', '32', '32.45', '32,768', '32,768.95', ]) def test_text_set_value_numeric(expected): num_s = expected.replace(',', '') field = fieldsmod.TextField({}) num_types = [Decimal, float] if '.' not in expected: num_types.append(int) for num_type in num_types: field.set_value(num_type(num_s)) assert field.value() == expected field.set_value(None) def test_text_set_value_none(): source = field_source('t', b'set None test', 'Tx') field = fieldsmod.TextField(source) field.set_value(None) assert fieldsmod.FormField.value(field) is None def test_empty_as_filled_fdf(): source = field_source() field = fieldsmod.FormField(source) assert field.as_filled_fdf() == {} @pytest.mark.parametrize('field_type,field_class,set_value', [ ('Btn', fieldsmod.CheckboxField, True), ('Btn', fieldsmod.CheckboxField, False), ('Ch', fieldsmod.FormField, None), ('Tx', fieldsmod.TextField, 'export test'), ('Tx', fieldsmod.TextField, 'UTF—16 export'), ]) def test_as_filled_fdf_after_set_value(field_type, field_class, set_value): source = field_source(field_type, field_type=field_type) field = field_class(source) field.set_value(set_value) actual = field.as_filled_fdf() assert actual['T'] == field_type expect_len = 2 if set_value is None: assert 'V' not in actual expect_len = 1 elif field_class is fieldsmod.CheckboxField: assert actual['V'].name == ('Yes' if set_value else 'Off') else: assert actual['V'] == set_value assert len(actual) == expect_len @pytest.mark.parametrize('field_type,expected', [ ('Btn', None), ('Tx', ''), ]) def test_as_filled_fdf_default_value(field_type, expected): source = field_source(field_type=field_type) field = fieldsmod.FormField.by_type(source) actual = field.as_filled_fdf() assert actual.get('V') == expected def test_as_filled_fdf_recursion(): buttons = [field_source(f'bt{n}', field_type='Btn') for n in range(1, 3)] pair = field_source('Buttons', kids=iter(buttons)) text = field_source('tx', field_type='Tx') source = field_source('topform', kids=[text, pair]) field = fieldsmod.FormField(source) actual = field.as_filled_fdf() assert actual['T'] == 'topform' assert 'V' not in actual actual = iter(actual['Kids']) assert next(actual)['T'] == 'tx' actual = next(actual) assert actual['T'] == 'Buttons' assert 'V' not in actual actual = iter(actual['Kids']) assert next(actual)['T'] == 'bt1' assert next(actual)['T'] == 'bt2' assert next(actual, None) is None @pytest.mark.parametrize('name,value,field_type', [ (None, None, None), ('mt', 'mapping text', 'Tx'), ('mb', 'Yes', 'Btn'), ]) def test_simple_as_mapping(name, value, field_type): source = field_source(name, value, field_type) field = fieldsmod.FormField(source) actual = field.as_mapping() key, mapped = next(actual) assert key == (name or '') assert mapped is field assert next(actual, None) is None def test_recursive_as_mapping(): btn_kids = [field_source(f'btn{n}', field_type='Btn') for n in range(1, 3)] buttons = field_source('buttons', kids=iter(btn_kids)) text_kids = [field_source(f'tx{n}', field_type='Tx') for n in range(1, 3)] texts = field_source('texts', kids=iter(text_kids)) source = field_source('root', kids=[texts, buttons]) root_field = fieldsmod.FormField(source) actual = root_field.as_mapping() for expected_key in [ 'root', 'root.texts', 'root.texts.tx1', 'root.texts.tx2', 'root.buttons', 'root.buttons.btn1', 'root.buttons.btn2', ]: key, field = next(actual) assert key == expected_key _, _, expected_name = expected_key.rpartition('.') assert field.name() == expected_name assert next(actual, None) is None def test_add_kid(): parent = fieldsmod.FormField(field_source('parent')) kid = fieldsmod.FormField(field_source('kid')) parent.add_kid(kid) actual, = parent.kids() assert actual.name() == 'kid' assert actual.parent().name() == 'parent'