Десктопное приложение на PySide6. Вся база — один JSON-документ, зашифрованный AES-256-GCM под паролем (ключ через scrypt), лежит в data/vault.fmdb и раз в час уезжает в этот репозиторий. Основное: - партии с дедлайном возврата себестоимости пекарне, FIFO-разнос продаж по партиям и прогресс покрытия к сроку; - типы выбытия: розница, другу по себестоимости, съел сам, подарок, списание — съеденное вычитается из прибыли, за него платить всё равно; - долги контрагентов с частичными оплатами; - номенклатура с историей изменения цен; - журнал изменений внутри базы: git хранит непрозрачные снимки, поэтому настоящая история ведётся здесь. Синхронизация коммитит только путь базы и схлопывает часовые пуши в один коммит на день через amend + force-with-lease. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
79 lines
2.6 KiB
Python
79 lines
2.6 KiB
Python
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
|