wrolpi/modules/inventory/test/test_multifile_config.py
Roland 6a46cfd17d
Add manual import + backup Restore/Merge/Overwrite for inventory configs
Inventories are a config-only MultiFileConfig (config/inventory/<slug>.yaml) and
were not reachable by the admin config Restore UI, so there was no way to re-import
a hand-edited/copied-in file or restore an inventory from its daily backup.

Backend:
- MultiFileConfig gains generic helpers: get_backup_dates(slug)/_get_backup_file
  (surface the daily config/backup/<subdir>/<slug>-DATE.yaml files) and reimport()
  (rebuild the in-memory store from disk; picks up hand-edits and copied-in files,
  drops files deleted on disk).  backup_date is constrained to YYYYMMDD so it cannot
  traverse out of the backup directory.
- InventoriesConfig.preview_restore/apply_restore: overwrite replaces the whole
  inventory; merge unions items by id (keeps current fields and existing items).
  The pre-restore state is backed up automatically, so a restore is undoable.
- API: POST /inventory/reimport, GET /inventory/<slug>/backups,
  POST /inventory/<slug>/restore/preview, POST /inventory/<slug>/restore.

Frontend:
- New InventoryImportModal (opened from a Restore button in the Inventory toolbar):
  Re-import from disk, plus a dated-backup table with per-row Merge/Overwrite that
  previews the change before applying.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 16:28:11 -06:00

81 lines
3.5 KiB
Python

"""Tests for the reusable MultiFileConfig machinery, exercised through InventoriesConfig."""
from modules.inventory import common as inventory_common
from wrolpi.dates import now
def test_discover(food_inventory_factory, test_inventory_configs):
config = test_inventory_configs
food_inventory_factory(name='Food Storage')
food_inventory_factory(name='Tools', inventory_type='tool')
assert set(config.discover()) == {'food-storage', 'tools'}
def test_save_creates_dated_backup(food_inventory_factory, test_inventory_configs, test_directory):
config = test_inventory_configs
slug = food_inventory_factory(name='Food Storage')
# First save happened on create (no prior file → no backup). A second save backs up the existing file.
config.dump_all()
backup_dir = test_directory / 'config' / 'backup' / 'inventory'
date_str = now().strftime('%Y%m%d')
assert (backup_dir / f'{slug}-{date_str}.yaml').is_file()
def test_initialize_loads_from_disk(food_inventory_factory, test_inventory_configs):
config = test_inventory_configs
food_inventory_factory(name='Food Storage')
config.dump_all()
# A new instance pointed at the same directory loads the existing files on initialize().
fresh = inventory_common.InventoriesConfig()
fresh.initialize()
assert 'food-storage' in fresh.slugs
def test_invalid_file_is_skipped(test_inventory_configs):
config = test_inventory_configs
bad = config.get_directory() / 'broken.yaml'
bad.parent.mkdir(parents=True, exist_ok=True)
bad.write_text('this: [is, not, a, valid, inventory\n') # malformed YAML
# import_all must not raise; the bad file is skipped.
config.import_all()
assert 'broken' not in config.slugs
def test_reimport_picks_up_and_drops_files(food_inventory_factory, test_inventory_configs):
from modules.inventory.defaults import default_fields
from wrolpi.common import write_config_data
config = test_inventory_configs
food_inventory_factory(name='Food Storage')
# A file copied onto disk by hand is not in memory until a reimport.
new_file = config.get_file('tools-box')
write_config_data(dict(version=1, slug='tools-box', name='Tools Box', type='tool',
fields=default_fields('tool'), items=[]), new_file)
assert config.get('tools-box') is None
config.reimport()
assert config.get('tools-box') is not None
# Deleting the file on disk and re-importing drops it from memory (memory matches disk).
new_file.unlink()
config.reimport()
assert config.get('tools-box') is None
assert 'food-storage' in config.slugs
def test_get_backup_dates(food_inventory_factory, test_inventory_configs):
from modules.inventory.defaults import default_fields
from wrolpi.common import write_config_data
config = test_inventory_configs
slug = food_inventory_factory(name='Food Storage')
assert config.get_backup_dates(slug) == []
for date in ('20260101', '20260115'):
backup = config._get_backup_file(slug, date)
backup.parent.mkdir(parents=True, exist_ok=True)
write_config_data(dict(version=1, slug=slug, name='Food Storage', type='food',
fields=default_fields('food'), items=[]), backup)
# Newest first.
assert config.get_backup_dates(slug) == ['20260115', '20260101']
# A different slug that merely shares this slug's prefix is not matched (slug 'food' vs 'food-storage-...').
assert config.get_backup_dates('food') == []