food-records/tests/test_crypto.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

79 lines
2.6 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 pytest
from app import crypto
def test_round_trip():
data = '{"привет": "мир"}'.encode("utf-8")
blob = crypto.encrypt(data, "пароль123")
assert crypto.decrypt(blob, "пароль123") == data
def test_wrong_password():
blob = crypto.encrypt(b"secret", "правильный")
with pytest.raises(crypto.WrongPassword):
crypto.decrypt(blob, "неправильный")
def test_empty_password_still_works():
blob = crypto.encrypt(b"secret", "")
assert crypto.decrypt(blob, "") == b"secret"
def test_same_data_gives_different_bytes():
"""Соль и nonce случайны, поэтому шифртекст не детерминирован.
Это причина, по которой storage сравнивает хеш открытого JSON, а не байты
файла — иначе git получал бы новый коммит на каждое сохранение.
"""
a = crypto.encrypt(b"same", "pw")
b = crypto.encrypt(b"same", "pw")
assert a != b
assert crypto.decrypt(a, "pw") == crypto.decrypt(b, "pw")
def test_header_is_authenticated():
"""Подмена соли в заголовке ловится как ошибка, а не молча меняет ключ."""
blob = bytearray(crypto.encrypt(b"secret", "pw"))
blob[9] ^= 0xFF # первый байт соли
with pytest.raises(crypto.WrongPassword):
crypto.decrypt(bytes(blob), "pw")
def test_tampered_ciphertext_rejected():
blob = bytearray(crypto.encrypt(b"secret", "pw"))
blob[-1] ^= 0xFF
with pytest.raises(crypto.WrongPassword):
crypto.decrypt(bytes(blob), "pw")
def test_not_a_vault_file():
with pytest.raises(crypto.UnsupportedFormat):
crypto.decrypt(b"\x00" * 100, "pw")
def test_truncated_file():
with pytest.raises(crypto.UnsupportedFormat):
crypto.decrypt(b"FMDB\x01", "pw")
def test_future_format_version_reports_clearly():
blob = bytearray(crypto.encrypt(b"secret", "pw"))
blob[4] = 99
with pytest.raises(crypto.UnsupportedFormat, match="новой версией"):
crypto.decrypt(bytes(blob), "pw")
def test_absurd_scrypt_params_rejected_without_allocating():
"""Чужой файл не должен заставить нас выделить гигабайты под scrypt."""
blob = bytearray(crypto.encrypt(b"secret", "pw"))
blob[6] = 40 # log2(n) = 40
with pytest.raises(crypto.UnsupportedFormat):
crypto.decrypt(bytes(blob), "pw")
def test_compression_actually_helps():
data = b'{"bun": "poppy"}' * 500
blob = crypto.encrypt(data, "pw")
assert len(blob) < len(data) // 4