File diff 1b7fdf4f3b00 → 13c66e8ce296
tests/test_pdfforms_fields.py
Show inline comments
 
new file 100644
 
"""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 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')
 

	
 
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