import json from decimal import Decimal import pytest from app import crypto, journal from app.models import SCHEMA_VERSION, Document from app.storage import Vault, content_hash, migrate def test_create_and_reopen(tmp_path): path = tmp_path / "vault.fmdb" vault = Vault.create(path, "pw") journal.create_product(vault, "Булка с маком", cost_price=20, retail_price=35) vault.save() reopened = Vault.open(path, "pw") assert [p.name for p in reopened.doc.products] == ["Булка с маком"] assert reopened.doc.products[0].cost_price == Decimal("20.00") def test_open_with_wrong_password(tmp_path): path = tmp_path / "vault.fmdb" Vault.create(path, "pw") with pytest.raises(crypto.WrongPassword): Vault.open(path, "nope") def test_create_refuses_to_clobber(tmp_path): path = tmp_path / "vault.fmdb" Vault.create(path, "pw") with pytest.raises(crypto.VaultError): Vault.create(path, "other") def test_save_skips_write_when_nothing_changed(tmp_path): """Главная защита от мусорных коммитов в git. Шифртекст каждый раз новый, поэтому решение «писать или нет» принимается по хешу открытых данных, а не по байтам файла. """ path = tmp_path / "vault.fmdb" vault = Vault.create(path, "pw") journal.create_product(vault, "Булка", cost_price=10, retail_price=20) assert vault.save() is True assert vault.save() is False assert vault.save() is False def test_save_detects_real_change(tmp_path): vault = Vault.create(tmp_path / "vault.fmdb", "pw") vault.save() journal.create_product(vault, "Булка", cost_price=10, retail_price=20) assert vault.is_dirty is True assert vault.save() is True assert vault.is_dirty is False def test_freshly_opened_vault_is_not_dirty(tmp_path): """Иначе каждое открытие программы порождало бы коммит на пустом месте.""" path = tmp_path / "vault.fmdb" vault = Vault.create(path, "pw") journal.create_product(vault, "Булка", cost_price=10, retail_price=20) vault.save() reopened = Vault.open(path, "pw") assert reopened.is_dirty is False assert reopened.save() is False def test_content_hash_ignores_key_order(): assert content_hash({"a": 1, "b": 2}) == content_hash({"b": 2, "a": 1}) def test_change_password(tmp_path): path = tmp_path / "vault.fmdb" vault = Vault.create(path, "old") journal.create_product(vault, "Булка", cost_price=10, retail_price=20) vault.save() vault.change_password("new") with pytest.raises(crypto.WrongPassword): Vault.open(path, "old") assert Vault.open(path, "new").doc.products[0].name == "Булка" def test_atomic_write_leaves_backup(tmp_path): path = tmp_path / "vault.fmdb" vault = Vault.create(path, "pw") journal.create_product(vault, "Булка", cost_price=10, retail_price=20) vault.save() assert path.with_suffix(".fmdb.bak").exists() assert not path.with_suffix(".fmdb.tmp").exists() def test_decimals_survive_round_trip(tmp_path): """Деньги ходят через строки, поэтому копейки не должны разъезжаться.""" path = tmp_path / "vault.fmdb" vault = Vault.create(path, "pw") journal.create_product(vault, "Булка", cost_price="33.33", retail_price="66.67") vault.save() product = Vault.open(path, "pw").doc.products[0] assert product.cost_price == Decimal("33.33") assert product.retail_price == Decimal("66.67") def test_export_plain_json(tmp_path): vault = Vault.create(tmp_path / "vault.fmdb", "pw") journal.create_product(vault, "Булка с маком", cost_price=20, retail_price=35) out = vault.export_plain_json(tmp_path / "export.json") data = json.loads(out.read_text(encoding="utf-8")) assert data["products"][0]["name"] == "Булка с маком" def test_migration_adds_journal(): raw = migrate({"schema_version": 0, "products": []}) assert raw["schema_version"] == SCHEMA_VERSION assert raw["journal"] == [] def test_migration_adds_packaging_without_changing_behaviour(): """Старая база должна открыться так, будто всё записано в базовых единицах.""" raw = migrate( { "schema_version": 1, "products": [{"id": "p_1", "name": "Печенье", "unit": "шт", "cost_price": "20"}], "batches": [ { "id": "b_1", "number": 1, "date": "2026-08-01", "due_date": "2026-08-15", "lines": [{"product_id": "p_1", "qty": "10", "unit_cost": "20"}], } ], "sales": [ { "id": "s_1", "date": "2026-08-02", "kind": "retail", "lines": [{"product_id": "p_1", "qty": "3", "unit_price": "30"}], } ], } ) assert raw["schema_version"] == SCHEMA_VERSION assert raw["products"][0]["packs"] == [] assert raw["batches"][0]["lines"][0] == { "product_id": "p_1", "qty": "10", "unit_cost": "20", "uom": "", "uom_size": "1" } doc = Document.from_dict(raw) assert doc.batches[0].lines[0].base_qty == Decimal("10.000") assert doc.batches[0].lines[0].base_unit_cost == Decimal("20.00") assert doc.sales[0].lines[0].base_unit_price == Decimal("30.00") def test_future_schema_refuses_to_open(): with pytest.raises(crypto.UnsupportedFormat, match="новой версией"): migrate({"schema_version": 999}) def test_corrupted_json_inside_valid_encryption(tmp_path): """GCM подтвердил целостность, но внутри не JSON — сообщаем внятно.""" path = tmp_path / "vault.fmdb" path.write_bytes(crypto.encrypt(b"not json at all", "pw")) with pytest.raises(crypto.VaultError, match="повреждено"): Vault.open(path, "pw")