mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
317 lines
13 KiB
Python
317 lines
13 KiB
Python
from decimal import Decimal
|
|
from itertools import zip_longest
|
|
from typing import List, Iterable
|
|
|
|
import pytest
|
|
import sqlalchemy
|
|
from pint import Quantity
|
|
|
|
from wrolpi.common import Base
|
|
from wrolpi.db import get_db_session
|
|
from wrolpi.test.common import PytestCase
|
|
from .. import init_inventory
|
|
from ..common import sum_by_key, get_inventory_by_category, get_inventory_by_subcategory, get_inventory_by_name, \
|
|
compact_unit, cleanup_quantity, save_inventories_config, import_inventories_config
|
|
from ..errors import NoInventories
|
|
from ..inventory import get_inventories, save_inventory, update_inventory, \
|
|
delete_inventory, get_categories, get_unit_registry
|
|
from ..models import Item, Inventory
|
|
|
|
UNIT_REGISTRY = get_unit_registry()
|
|
|
|
TEST_ITEMS_COLUMNS = (
|
|
'inventory_id',
|
|
'brand',
|
|
'name',
|
|
'item_size',
|
|
'unit',
|
|
'count',
|
|
'category',
|
|
'subcategory',
|
|
'deleted_at',
|
|
'serving',
|
|
'expiration_date',
|
|
'purchase_date',
|
|
)
|
|
TEST_ITEMS_TUPLES = [
|
|
(1, 'Wheaters', 'Red Wheat', 500, 'oz', 1, 'grains', 'wheat'),
|
|
(1, 'Wheaters', 'Red Wheat', 55, 'pounds', 2, 'grains', 'wheat'),
|
|
(1, 'Ricey', 'White Rice', 8, 'pounds', 1, 'grains', 'rice'),
|
|
(1, 'Chewy', 'Chicken Breast', 16, 'oz', 8, 'meats', 'canned'),
|
|
(1, 'Chewy', 'Beef', 16, 'oz', 12, 'meats', 'canned'),
|
|
(1, 'Vibrant', 'Peaches', 24, 'oz', 2, 'fruits', 'canned'),
|
|
(1, 'Vibrant', 'Pineapple', Decimal('22.3'), 'oz', 1, 'fruits', 'canned'),
|
|
# These are in the same category, but have conflicting unit types. (gallon vs pound)
|
|
(1, 'White', 'Vinegar', 1, 'gallon', 1, 'cooking ingredients', 'vinegar'),
|
|
(1, 'Salty', 'Salt', 1, 'pound', 1, 'cooking ingredients', 'salt'),
|
|
# This item is deleted, and should always be ignored.
|
|
(1, 'deleted', 'deleted', Decimal('1'), 'oz', 1, 'fruits', 'canned', '2020-01-01'),
|
|
]
|
|
TEST_ITEMS = [dict(zip(TEST_ITEMS_COLUMNS, i)) for i in TEST_ITEMS_TUPLES]
|
|
|
|
|
|
def extract_items(lst: List[Base], keys: Iterable[str]) -> List[Base]:
|
|
extracted = []
|
|
for i in lst:
|
|
extracted.append({j: getattr(i, j) for j in keys})
|
|
|
|
return extracted
|
|
|
|
|
|
class TestInventory(PytestCase):
|
|
|
|
@staticmethod
|
|
def prepare() -> None:
|
|
"""
|
|
This exists rather than setUp because the DB is wrapped.
|
|
"""
|
|
init_inventory(force=True)
|
|
|
|
with get_db_session(commit=True) as session:
|
|
for item in TEST_ITEMS:
|
|
item = Item(**item)
|
|
session.add(item)
|
|
|
|
def test_get_categories(self, test_session):
|
|
self.prepare()
|
|
|
|
categories = get_categories()
|
|
self.assertGreater(len(categories), 0)
|
|
|
|
def test_get_inventories(self, test_session):
|
|
self.prepare()
|
|
|
|
inventories = get_inventories()
|
|
dates = extract_items(inventories, {'created_at', 'deleted_at', 'viewed_at'})
|
|
self.assertItemsTruthyOrFalsey(dates, [{'created_at': True, 'deleted_at': False, 'viewed_at': False}])
|
|
self.assertEqual(inventories[0].id, 1)
|
|
self.assertEqual(inventories[0].name, 'Food Storage')
|
|
|
|
def test_inventory1(self, test_session):
|
|
self.prepare()
|
|
|
|
# Insert a new Inventory.
|
|
inventory = {
|
|
'name': 'New Inventory',
|
|
'viewed_at': 'asdf', # This should be ignored.
|
|
}
|
|
save_inventory(inventory)
|
|
|
|
with get_db_session() as session:
|
|
i1, i2 = session.query(Inventory).order_by(Inventory.name).all()
|
|
self.assertDictContains(i2, {'name': 'New Inventory', 'viewed_at': None})
|
|
|
|
# Inventories cannot share a name.
|
|
self.assertRaises(sqlalchemy.exc.IntegrityError, save_inventory, inventory)
|
|
|
|
# Insert a second inventory.
|
|
inventory['name'] = 'Super Inventory'
|
|
save_inventory(inventory)
|
|
|
|
# Cannot update the name to a name that is already used.
|
|
with get_db_session() as session:
|
|
i = session.query(Inventory).filter_by(name='Super Inventory').one()
|
|
inventory['name'] = 'New Inventory'
|
|
self.assertRaises(sqlalchemy.exc.IntegrityError, update_inventory, i.id, inventory)
|
|
|
|
# Add some items to "New Inventory"
|
|
with get_db_session(commit=True) as session:
|
|
before_item_count = session.query(Item).count()
|
|
session.add(Item(inventory_id=2, brand='a', name='b', item_size=45, unit='pounds', count=1))
|
|
session.add(Item(inventory_id=2, brand='a', name='b', item_size=45, unit='pounds', count=1))
|
|
session.add(Item(inventory_id=2, brand='a', name='b', item_size=45, unit='pounds', count=1))
|
|
session.add(Item(inventory_id=2, brand='a', name='b', item_size=45, unit='pounds', count=1))
|
|
|
|
# You can rename a inventory to a conflicting name, if the other inventory is marked as deleted.
|
|
with get_db_session(commit=True) as session:
|
|
delete_inventory(2)
|
|
# Check that the items from the deleted Inventory were not deleted, YET.
|
|
self.assertEqual(before_item_count + 4, session.query(Item).count())
|
|
|
|
i = session.query(Inventory).filter_by(name='Super Inventory').one()
|
|
inventory['name'] = 'New Inventory'
|
|
update_inventory(i.id, inventory)
|
|
|
|
# Check that the items from the deleted Inventory were really deleted.
|
|
self.assertEqual(before_item_count, session.query(Item).count())
|
|
|
|
def test_get_inventory_by_category(self, test_session):
|
|
self.prepare()
|
|
|
|
expected = [
|
|
dict(category='cooking ingredients', total_size=Decimal('1'), unit='gallon'),
|
|
dict(category='cooking ingredients', total_size=Decimal('1'), unit='pound'),
|
|
dict(category='fruits', total_size=Decimal('4.39375'), unit='pound'),
|
|
dict(category='grains', total_size=Decimal('149.25'), unit='pound'),
|
|
dict(category='meats', total_size=Decimal('20'), unit='pound'),
|
|
]
|
|
|
|
inventory = get_inventory_by_category(1)
|
|
|
|
for idx, (i, j) in enumerate(zip_longest(inventory, expected)):
|
|
self.assertEqual(i, j, f'category inventory {idx} is not equal')
|
|
|
|
def test_get_inventory_by_subcategory(self, test_session):
|
|
self.prepare()
|
|
|
|
expected = [
|
|
dict(category='cooking ingredients', subcategory='salt', total_size=Decimal('1'), unit='pound'),
|
|
dict(category='cooking ingredients', subcategory='vinegar', total_size=Decimal('1'), unit='gallon'),
|
|
dict(category='fruits', subcategory='canned', total_size=Decimal('4.39375'), unit='pound'),
|
|
dict(category='grains', subcategory='rice', total_size=Decimal('8'), unit='pound'),
|
|
dict(category='grains', subcategory='wheat', total_size=Decimal('141.25'), unit='pound'),
|
|
dict(category='meats', subcategory='canned', total_size=Decimal('20'), unit='pound'),
|
|
]
|
|
|
|
inventory = get_inventory_by_subcategory(1)
|
|
|
|
for idx, (i, j) in enumerate(zip_longest(inventory, expected)):
|
|
self.assertEqual(i, j, f'subcategory inventory {idx} is not equal')
|
|
|
|
def test_get_inventory_by_name(self, test_session):
|
|
self.prepare()
|
|
|
|
expected = [
|
|
dict(brand='Chewy', name='Beef', total_size=Decimal('12'), unit='pound'),
|
|
dict(brand='Chewy', name='Chicken Breast', total_size=Decimal('8'), unit='pound'),
|
|
dict(brand='Ricey', name='White Rice', total_size=Decimal('8'), unit='pound'),
|
|
dict(brand='Salty', name='Salt', total_size=Decimal('1'), unit='pound'),
|
|
dict(brand='Vibrant', name='Peaches', total_size=Decimal('3'), unit='pound'),
|
|
dict(brand='Vibrant', name='Pineapple', total_size=Decimal('1.39375'), unit='pound'),
|
|
dict(brand='Wheaters', name='Red Wheat', total_size=Decimal('141.25'), unit='pound'),
|
|
dict(brand='White', name='Vinegar', total_size=Decimal('1'), unit='gallon'),
|
|
]
|
|
|
|
inventory = get_inventory_by_name(1)
|
|
|
|
for idx, (i, j) in enumerate(zip_longest(inventory, expected)):
|
|
self.assertEqual(i, j, f'named inventory {idx} is not equal')
|
|
|
|
|
|
def quantity_to_string(quantity: Quantity) -> str:
|
|
quantity = cleanup_quantity(quantity)
|
|
num, (units,) = quantity.to_tuple()
|
|
(unit, _) = units
|
|
return f'{num} {unit}'
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'quantity,expected',
|
|
[
|
|
(Decimal(5) * UNIT_REGISTRY.ounce, '5 ounce'),
|
|
(Decimal(16) * UNIT_REGISTRY.ounce, '1 pound'),
|
|
(Decimal(500) * UNIT_REGISTRY.pound, '500 pound'),
|
|
(Decimal(2000) * UNIT_REGISTRY.pound, '1 ton'),
|
|
(Decimal(128000) * UNIT_REGISTRY.ounce, '4 ton'),
|
|
]
|
|
)
|
|
def test_compact_unit(quantity, expected):
|
|
# Round the result, so we don't have to specify all those zeros for the test definition.
|
|
assert quantity_to_string(compact_unit(quantity)) == expected
|
|
|
|
|
|
pound, oz, gram, gallon = UNIT_REGISTRY('pound'), UNIT_REGISTRY('oz'), UNIT_REGISTRY('gram'), UNIT_REGISTRY('gallon')
|
|
pound, oz, gram, gallon = pound.units, oz.units, gram.units, gallon.units
|
|
mass = pound.dimensionality
|
|
length = gallon.dimensionality
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'items,expected',
|
|
[
|
|
(
|
|
# No conversion is necessary.
|
|
[{'category': 'grains', 'count': Decimal('1'), 'item_size': Decimal('1'), 'unit': 'oz'}],
|
|
{(('grains',), oz.dimensionality): Decimal('1') * oz},
|
|
),
|
|
(
|
|
# The largest of the units is what is returned.
|
|
[
|
|
{'category': 'grains', 'count': Decimal('1'), 'item_size': Decimal('1'), 'unit': 'oz'},
|
|
{'category': 'grains', 'count': Decimal('1'), 'item_size': Decimal('1'), 'unit': 'lbs'},
|
|
],
|
|
{(('grains',), pound.dimensionality): Decimal('1.0625') * pound},
|
|
),
|
|
(
|
|
# Items are summed by category.
|
|
[
|
|
{'category': 'grains', 'count': Decimal('8'), 'item_size': Decimal('24'), 'unit': 'oz'},
|
|
{'category': 'grains', 'count': Decimal('2'), 'item_size': Decimal('45'), 'unit': 'lbs'},
|
|
{'category': 'fruits', 'count': Decimal('4'), 'item_size': Decimal('1'), 'unit': 'gram'},
|
|
],
|
|
{
|
|
(('grains',), pound.dimensionality): Decimal('102') * pound,
|
|
(('fruits',), gram.dimensionality): Decimal('4') * gram,
|
|
},
|
|
),
|
|
(
|
|
[
|
|
{'category': 'cooking ingredients', 'count': Decimal('1'), 'item_size': Decimal('1'),
|
|
'unit': 'gallon'},
|
|
{'category': 'cooking ingredients', 'count': Decimal('1'), 'item_size': Decimal('1'),
|
|
'unit': 'quart'},
|
|
{'category': 'cooking ingredients', 'count': Decimal('1'), 'item_size': Decimal('1'), 'unit': 'oz'},
|
|
],
|
|
{
|
|
(('cooking ingredients',), gallon.dimensionality): Decimal('1.25') * gallon,
|
|
(('cooking ingredients',), oz.dimensionality): Decimal('1') * oz,
|
|
},
|
|
),
|
|
]
|
|
)
|
|
def test_sum_by_key(items, expected):
|
|
items = [Item(**i) for i in items]
|
|
assert sum_by_key(items, lambda i: (i.category,)) == expected
|
|
|
|
|
|
def test_no_inventories(async_client, test_session, test_directory):
|
|
"""Can't save empty inventories."""
|
|
try:
|
|
save_inventories_config()
|
|
except NoInventories:
|
|
pass
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_inventories_config(await_switches, test_session, test_directory, init_test_inventory):
|
|
for item in TEST_ITEMS:
|
|
item = Item(**item)
|
|
test_session.add(item)
|
|
test_session.commit()
|
|
|
|
# Save the Inventories/Items that were created above.
|
|
save_inventories_config()
|
|
await await_switches()
|
|
|
|
# Clear out the DB so the import will be tested
|
|
test_session.query(Item).delete()
|
|
test_session.query(Inventory).delete()
|
|
test_session.commit()
|
|
|
|
import_inventories_config()
|
|
|
|
inventories = get_inventories()
|
|
assert len(inventories) == 1
|
|
# ID has increased because we did not reset the sequence when deleting from the table.
|
|
assert inventories[0].id == 2
|
|
assert inventories[0].name == 'Food Storage'
|
|
|
|
# All items in the DB match those in the test list, except for the "deleted" item.
|
|
non_deleted_items = [i for i in inventories[0].items if i.deleted_at is None]
|
|
assert len(non_deleted_items) == len(TEST_ITEMS) - 1
|
|
test_items = {(i['name'], i['brand'], i['count']) for i in TEST_ITEMS[:-1]}
|
|
db_items = {(i.name, i.brand, i.count) for i in non_deleted_items}
|
|
assert test_items == db_items
|
|
|
|
# An extra Inventory with items should be deleted from the DB.
|
|
inventory = Inventory(name='delete me')
|
|
test_session.add(inventory)
|
|
inventory.flush()
|
|
test_session.add(Item(inventory_id=inventory.id))
|
|
test_session.commit()
|
|
assert test_session.query(Inventory).count() == 2
|
|
assert {i for i, in test_session.query(Inventory.name)} == {'Food Storage', 'delete me'}
|
|
|
|
import_inventories_config()
|
|
assert test_session.query(Inventory).count() == 1
|
|
assert test_session.query(Inventory).one().name == 'Food Storage'
|