mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-13 12:50:59 +01:00
Some checks are pending
crdt_sync_dart tests / test (push) Waiting to run
crdt_sync_flutter tests / test (push) Waiting to run
crdt-sync pre-commit / pre-commit (push) Waiting to run
crdt-sync tests / test (push) Waiting to run
dependency freshness / dependencies on newest stable (push) Waiting to run
design_system tests / test (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
sync_settings_ui tests / test (push) Waiting to run
Byte-compatible with crdt_sync_dart 0.14.0, proven by golden vectors in both directions. EncryptingStore, -e2e layout, one-time copy, and open_encrypted_client for the Python daemons. Dev pins bumped. Todo: fda71ed1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
166 lines
5.2 KiB
Python
166 lines
5.2 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki
|
|
"""EncryptingStore passthroughs, bulk reads and copy/open edge cases."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pytest
|
|
|
|
from crdt_sync import (
|
|
Argon2Params,
|
|
E2eLayout,
|
|
E2eLockedError,
|
|
EncryptingStore,
|
|
copy_to_encrypted,
|
|
load_log,
|
|
open_encrypted_client,
|
|
reset_e2e_copy_memo,
|
|
unlock_e2e,
|
|
)
|
|
from crdt_sync._e2e_store import _blobs, _fingerprint
|
|
from crdt_sync.tests.conftest import FakeStore, FakeStoreWithoutBulkRead
|
|
|
|
if TYPE_CHECKING:
|
|
from crdt_sync import E2eKey
|
|
|
|
PHRASE = "pw"
|
|
CHEAP = Argon2Params(8, 1, 1)
|
|
OLD = "app/devices"
|
|
LAYOUT = E2eLayout.of(OLD)
|
|
CLOCK = "2026-01-01T00:00:00.000Z-0000-pc"
|
|
|
|
|
|
def _log(title: str = "x", rid: str = "a") -> str:
|
|
return json.dumps(
|
|
{
|
|
rid: {
|
|
"id": rid,
|
|
"fields": {"title": [title, CLOCK]},
|
|
"deleted": False,
|
|
"deleted_hlc": None,
|
|
}
|
|
}
|
|
)
|
|
|
|
|
|
def _key(remote: FakeStore) -> E2eKey:
|
|
return unlock_e2e(
|
|
remote,
|
|
path_prefix=LAYOUT.prefix,
|
|
passphrase=PHRASE,
|
|
params=CHEAP,
|
|
e2e_path=LAYOUT.e2e_path,
|
|
)
|
|
|
|
|
|
class HidingStore(FakeStore):
|
|
"""Lists every file but reads ``hidden`` paths as absent."""
|
|
|
|
def __init__(self, files: dict[str, str], hidden: set[str]) -> None:
|
|
super().__init__(files)
|
|
self._hidden = hidden
|
|
|
|
def get_file_text(self, path: str) -> str | None:
|
|
if path in self._hidden:
|
|
return None
|
|
return super().get_file_text(path)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _memo() -> None:
|
|
reset_e2e_copy_memo()
|
|
|
|
|
|
def test_passthroughs_and_keyed_revisions() -> None:
|
|
remote = FakeStore({"other/file": "raw"})
|
|
store = EncryptingStore(remote, _key(remote), LAYOUT)
|
|
assert store.list_directory("other") == ["file"]
|
|
assert store.get_file_text("other/file") == "raw"
|
|
assert store.get_file_text(f"{LAYOUT.prefix}/pc/log.json") is None
|
|
assert store.can_access_remote()
|
|
store.put_file_text(f"{LAYOUT.revs_path}/pc", "text", message="m")
|
|
assert remote.files[f"{LAYOUT.revs_path}/pc"] == store.key.revision("text")
|
|
store.put_file_text("other/file", "plain", message="m")
|
|
assert remote.files["other/file"] == "plain"
|
|
store.delete_file("other/file")
|
|
assert "other/file" not in remote.files
|
|
|
|
|
|
def test_bulk_read_opens_sealed_and_drops_bad(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
remote = FakeStore()
|
|
store = EncryptingStore(remote, _key(remote), LAYOUT)
|
|
store.put_file_text(f"{LAYOUT.prefix}/pc", _log(), message="m")
|
|
remote.files[f"{LAYOUT.prefix}/bad"] = '{"v": 2, "r": {}}'
|
|
remote.files[f"{LAYOUT.revs_path}/pc"] = "rev"
|
|
with caplog.at_level(logging.WARNING):
|
|
out = store.get_string_map(LAYOUT.prefix)
|
|
assert set(out) == {"pc"}
|
|
assert set(load_log(out["pc"])) == {"a"}
|
|
assert "Undecryptable entry" in caplog.text
|
|
assert store.get_string_map(LAYOUT.revs_path) == {"pc": "rev"}
|
|
|
|
|
|
def test_bulk_read_is_empty_without_inner_support() -> None:
|
|
remote = FakeStoreWithoutBulkRead()
|
|
store = EncryptingStore(remote, _key(FakeStore()), LAYOUT)
|
|
assert store.get_string_map(LAYOUT.prefix) == {}
|
|
assert _fingerprint(remote, OLD) == ""
|
|
|
|
|
|
def test_blobs_cover_flat_nested_and_skipped_paths() -> None:
|
|
remote = HidingStore(
|
|
{
|
|
f"{OLD}/flat": "f",
|
|
f"{OLD}/gone": "g",
|
|
f"{OLD}/pc/log.json": "n",
|
|
f"{OLD}/pc/hidden.json": "h",
|
|
"app/revs/pc": "r",
|
|
f"{OLD}/e2e": "h",
|
|
f"{OLD}/revs": "r",
|
|
},
|
|
{f"{OLD}/gone", f"{OLD}/pc/hidden.json"},
|
|
)
|
|
assert _blobs(remote, OLD) == {"flat": "f", "pc/log.json": "n"}
|
|
|
|
|
|
def test_copy_skips_unreadable_and_flat_logs(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
remote = FakeStore({f"{OLD}/flat": _log(), f"{OLD}/pc/log.json": "not json"})
|
|
with caplog.at_level(logging.WARNING):
|
|
copied = copy_to_encrypted(remote, _key(remote), OLD, LAYOUT)
|
|
assert copied == 1
|
|
assert "Unreadable old log" in caplog.text
|
|
assert f"{LAYOUT.prefix}/flat" in remote.files
|
|
assert f"{LAYOUT.prefix}/pc/log.json" not in remote.files
|
|
|
|
|
|
def test_changed_old_prefix_is_copied_again() -> None:
|
|
remote = FakeStore({f"{OLD}/pc/log.json": _log("one")})
|
|
open_encrypted_client(remote, OLD, passphrase=PHRASE, params=CHEAP)
|
|
remote.files[f"{OLD}/pc/log.json"] = _log("two", rid="b")
|
|
remote.files["app/revs/pc"] = "newer"
|
|
store, layout = open_encrypted_client(remote, OLD, passphrase=PHRASE, params=CHEAP)
|
|
text = store.get_file_text(f"{layout.prefix}/pc/log.json")
|
|
assert text is not None
|
|
assert set(load_log(text)) == {"a", "b"}
|
|
|
|
|
|
def test_unchanged_prefix_is_not_copied_twice() -> None:
|
|
remote = FakeStore({f"{OLD}/pc/log.json": _log()})
|
|
open_encrypted_client(remote, OLD, passphrase=PHRASE, params=CHEAP)
|
|
remote.writes.clear()
|
|
open_encrypted_client(remote, OLD, passphrase=PHRASE, params=CHEAP)
|
|
assert remote.writes == []
|
|
|
|
|
|
def test_unreadable_header_locks_the_client() -> None:
|
|
remote = FakeStore({LAYOUT.e2e_path: "not json"})
|
|
with pytest.raises(E2eLockedError, match="not JSON"):
|
|
open_encrypted_client(remote, OLD, passphrase=PHRASE, params=CHEAP)
|