food-market/tests/test_storage.py
Claude c8b9f5048b Учёт закупки и перепродажи булок
Десктопное приложение на PySide6. Вся база — один JSON-документ,
зашифрованный AES-256-GCM под паролем (ключ через scrypt), лежит
в data/vault.fmdb и раз в час уезжает в этот репозиторий.

Основное:
- партии с дедлайном возврата себестоимости пекарне, FIFO-разнос продаж
  по партиям и прогресс покрытия к сроку;
- типы выбытия: розница, другу по себестоимости, съел сам, подарок,
  списание — съеденное вычитается из прибыли, за него платить всё равно;
- долги контрагентов с частичными оплатами;
- номенклатура с историей изменения цен;
- журнал изменений внутри базы: git хранит непрозрачные снимки,
  поэтому настоящая история ведётся здесь.

Синхронизация коммитит только путь базы и схлопывает часовые пуши
в один коммит на день через amend + force-with-lease.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 17:29:53 +03:00

135 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
from decimal import Decimal
import pytest
from app import crypto, journal
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"] == 1
assert raw["journal"] == []
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")