Files @ 5784068904e8
Branch filter:

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

bkuhn
payroll-type — US:403b:Employee:Roth — needed separate since taxable

Since Roth contributions are taxable, there are some reports that
need to include these amounts in total salary (i.e., when running a
report that seeks to show total taxable income for an employee). As
such, we need a `payroll-type` specifically for Roth 403(b)
contributions.
"""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'