75 lines
3.1 KiB
Python
75 lines
3.1 KiB
Python
import base64
|
|
import copy
|
|
import hashlib
|
|
from datetime import UTC, datetime
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from k1link.device_plugins.vesc.archive import Archive
|
|
|
|
|
|
def backup():
|
|
uid = "01" * 12
|
|
configs = {}
|
|
for name, command in (("motor", 14), ("application", 17)):
|
|
raw = bytes([command]) + b"test-synthetic-config"
|
|
configs[name] = {"encoding": "base64", "payload": base64.b64encode(raw).decode(),
|
|
"bytes": len(raw), "sha256": hashlib.sha256(raw).hexdigest(),
|
|
"signature_hex": raw[1:5].hex()}
|
|
return {"schema": "missioncore.vesc.config-backup/v1", "operation_id": "op_" + uuid4().hex,
|
|
"device_id": "vesc_" + hashlib.sha256(("uuid:" + uid).encode()).hexdigest()[:32],
|
|
"identity": {"uuid": uid, "version": "6.06"}, "configs": configs,
|
|
"observed_at": datetime.now(UTC).isoformat()}
|
|
|
|
|
|
def test_immutable_versions_survive_restart_and_do_not_cross_node_scope(tmp_path):
|
|
archive = Archive(tmp_path / "archive")
|
|
value = backup()
|
|
archive.add("node-a", value)
|
|
archive.add("node-a", copy.deepcopy(value))
|
|
reopened = Archive(tmp_path / "archive")
|
|
assert len(reopened.listing("node-a", value["device_id"])["items"]) == 1
|
|
assert reopened.read("node-a", value["device_id"], value["operation_id"]) == value
|
|
assert reopened.listing("node-b", value["device_id"])["items"] == []
|
|
with pytest.raises(ValueError):
|
|
reopened.read("node-b", value["device_id"], value["operation_id"])
|
|
changed = copy.deepcopy(value)
|
|
changed["observed_at"] = datetime.now(UTC).isoformat()
|
|
with pytest.raises(ValueError, match="immutable"):
|
|
reopened.add("node-a", changed)
|
|
|
|
|
|
def test_corrupt_or_misattributed_payload_is_never_archived(tmp_path):
|
|
archive = Archive(tmp_path / "archive")
|
|
value = backup()
|
|
value["configs"]["motor"]["sha256"] = "0" * 64
|
|
with pytest.raises(ValueError, match="integrity"):
|
|
archive.add("a", value)
|
|
value = backup()
|
|
value["device_id"] = "vesc_" + "0" * 32
|
|
with pytest.raises(ValueError, match="mismatch"):
|
|
archive.add("a", value)
|
|
assert archive.export("a", 0)["items"] == []
|
|
|
|
|
|
def test_bounded_replication_replays_without_losing_versions_and_paginates(tmp_path):
|
|
board, core = Archive(tmp_path / "board"), Archive(tmp_path / "core")
|
|
values = [backup() for _ in range(53)]
|
|
for value in values:
|
|
board.add("local", value)
|
|
after = 0
|
|
while True:
|
|
batch = board.export("local", after)
|
|
assert len(batch["items"]) <= 4
|
|
assert core.receive("paired-node", batch) == batch["next"]
|
|
assert core.receive("paired-node", batch) == batch["next"]
|
|
if batch["next"] == after:
|
|
break
|
|
after = batch["next"]
|
|
page = core.listing("paired-node", values[0]["device_id"])
|
|
assert len(page["items"]) == 50 and page["next"]
|
|
older = core.listing("paired-node", values[0]["device_id"], int(page["next"]))
|
|
assert len(older["items"]) == 3 and older["next"] is None
|
|
assert len({item["id"] for item in page["items"] + older["items"]}) == 53
|