Files
KF-OnlineJudge/backend/tests/test_delivery_backup.py
T

350 lines
13 KiB
Python

"""Trusted byte fixtures only; no privileged backup, recovery or RTO acceptance."""
import hashlib
import importlib
import os
import sys
import time
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
backup_manifest = importlib.import_module("scripts.release.backup_manifest")
backup_objects = importlib.import_module("scripts.release.backup_objects")
policy = importlib.import_module("scripts.release.policy")
def empty_drain_projection():
return {
"execution": {
"database_clock_unix_ms": 1791830000000,
**{
name: {"count_capped": 0, "truncated": False}
for name in ("queued", "in_flight", "cleanup_unconfirmed")
},
},
"outbox": {
"schema_version": 1,
"pending_capped": 0,
"locked_capped": 0,
"truncated": False,
},
"imports": {
"schema_version": 1,
"worker_ready": True,
"reserved_bytes": 0,
"uploaded_capped": 0,
"validating_capped": 0,
"truncated": False,
},
"broker": {
"schema_version": 1,
"metadata_verified": True,
"messages_ready": 0,
"messages_unacknowledged": 0,
"message_bytes": 0,
"consumers": 1,
},
"factory": {
"schema_version": 1,
"configuration_sha256": "b" * 64,
"supervisor_authorized": True,
"execution_gate": "required_per_execution",
"isolation_verified": False,
"max_concurrent_runs": 1,
"active_helpers": 0,
"cleanup_complete": True,
"remaining_requests": 4096,
"remaining_lifetime_seconds": 3600,
},
}
@pytest.fixture
def snapshot():
data = b"trusted repeated bytes\n"
digest = hashlib.sha256(data).hexdigest()
files = [
{
"path": path,
"uid": uid,
"gid": uid,
"mode": mode,
"bytes": len(data),
"sha256": digest,
}
for path, uid, mode in [
("postgres/PG_VERSION", 1203, 0o600),
("postgres/global/pg_control", 1203, 0o600),
("releases/first", 0, 0o444),
("releases/second", 0, 0o555),
]
]
boot = "12345678-1234-1234-1234-123456789abc"
selected = {
"schema_version": 1,
"release_id": "sd-" + "c" * 12,
"manifest_sha256": "b" * 64,
"configuration_id": "cfg-" + "d" * 24,
"receipt_sha256": "e" * 64,
}
return {
"schema_version": 3,
"format": backup_manifest.FORMAT,
"backup_id": "bk-" + "2" * 32,
"source_sha": "c" * 40,
"manifest_sha256": selected["manifest_sha256"],
"configuration_id": selected["configuration_id"],
"created_unix_seconds": 1793070000,
"expires_unix_seconds": 1793070000 + 7 * 86400,
"selected": selected,
"consistent_cut": {
"schema_version": 1,
"source_sha": "c" * 40,
"configuration_id": selected["configuration_id"],
"action": "stop",
"phase": "complete",
"success": True,
"events": [],
"root_cleanup_confirmed": True,
"boot_id": boot,
"drain_projection": empty_drain_projection(),
},
"artifact_references": {
"schema_version": 1,
"kind": "kf.private.artifact-backup-inventory",
"read_only": True,
"quiescence_verified": False,
"boot_id": boot,
"storage_identity": {"device": 1, "inode": 2, "uid": 1200},
"objects": [],
},
"broker_cookie_sha256": "b" * 64,
"database_bootstrap_sha256": "e" * 64,
"images": {
key: {
"toolchain_id": key,
"image_kind": key,
"runtime_version": "1",
"image_revision": 1,
"image_digest": "f" * 64,
}
for key in ("gnu", "python313", "java21")
},
"directories": [
{"path": path, "uid": 0, "gid": 0, "mode": 0o700}
for path in sorted(backup_manifest.DOMAINS | {"postgres/global"})
],
"files": files,
"payload_bytes": len(data) * len(files),
"restored": False,
"execution_verified": False,
}
def object_format(value):
return {
**value,
"schema_version": 4,
"format": backup_manifest.OBJECT_FORMAT,
"logical_bytes": value["payload_bytes"],
"payload_bytes": value["files"][0]["bytes"],
}
def test_v3_reader_and_v4_logical_permissions_survive_content_deduplication(snapshot, tmp_path):
old = backup_manifest.validate(snapshot)
new = backup_manifest.validate(object_format(snapshot))
assert len(backup_manifest.physical_files(old)) == 4
assert len(backup_manifest.physical_files(new)) == 1
assert backup_manifest.logical_bytes(old) == backup_manifest.logical_bytes(new)
assert new["payload_bytes"] * 4 == new["logical_bytes"]
assert new["files"][2]["mode"] != new["files"][3]["mode"]
for item in new["files"]:
assert backup_manifest.member_path(tmp_path, new, item) == (
tmp_path / "objects" / item["sha256"]
)
assert backup_manifest.named_member(tmp_path, old, item["path"]) == (
tmp_path / "payload" / item["path"]
)
@pytest.mark.parametrize(
"field,change",
[
("payload_bytes", 1),
("logical_bytes", 1),
("schema_version", True),
("format", backup_manifest.FORMAT),
],
)
def test_v4_cannot_confuse_stored_bytes_with_restored_bytes(snapshot, field, change):
value = object_format(snapshot)
value[field] = change
with pytest.raises(policy.DeliveryError):
backup_manifest.validate(value)
def test_v4_preserves_logical_versions_above_v3_limit_without_raising_physical_limit(snapshot):
value = object_format(snapshot)
size = backup_manifest.MAX_FILE_BYTES
value["files"] = [
{**value["files"][0], "path": "releases/version-" + str(index), "bytes": size}
for index in range(32)
] + [{**item, "bytes": size} for item in snapshot["files"][:2]]
value["logical_bytes"] = len(value["files"]) * size
value["payload_bytes"] = size
backup_manifest.validate(value)
assert value["logical_bytes"] > backup_manifest.MAX_BYTES
old = {
**value,
"schema_version": 3,
"format": backup_manifest.FORMAT,
"payload_bytes": value["logical_bytes"],
}
old.pop("logical_bytes")
with pytest.raises(policy.DeliveryError):
backup_manifest.validate(old)
# Distinct object digests still consume the unchanged physical budget.
for index, item in enumerate(value["files"]):
item["sha256"] = hashlib.sha256(str(index).encode()).hexdigest()
value["payload_bytes"] = value["logical_bytes"]
with pytest.raises(policy.DeliveryError):
backup_manifest.validate(value)
def test_store_reads_each_source_but_writes_one_single_link_object(tmp_path, monkeypatch):
source, repeated, objects = tmp_path / "source", tmp_path / "repeated", tmp_path / "objects"
objects.mkdir(mode=0o700)
data = b"trusted source\n" * 2000
source.write_bytes(data)
repeated.write_bytes(data)
repeated.chmod(0o444)
original = backup_objects.read_stream
observed = []
def read(incoming, before, deadline, outgoing=None):
observed.append((before.st_ino, outgoing is not None))
return original(incoming, before, deadline, outgoing)
monkeypatch.setattr(backup_objects, "read_stream", read)
seen = {}
first, added = backup_objects.store(
source, source.stat(), objects, seen, time.monotonic() + 10, remaining_bytes=len(data)
)
second, repeated_bytes = backup_objects.store(
repeated, repeated.stat(), objects, seen, time.monotonic() + 10, remaining_bytes=0
)
assert first == second == hashlib.sha256(data).hexdigest()
assert (added, repeated_bytes) == (len(data), 0)
assert observed == [
(source.stat().st_ino, False),
(source.stat().st_ino, True),
(repeated.stat().st_ino, False),
]
assert list(objects.iterdir()) == [objects / first]
assert (objects / first).read_bytes() == data
assert (objects / first).stat().st_nlink == 1
assert (objects / first).stat().st_mode & 0o777 == 0o600
def test_failed_second_source_read_retains_incomplete_private_copy(tmp_path, monkeypatch):
source, objects = tmp_path / "source", tmp_path / "objects"
source.write_bytes(b"trusted fixture")
objects.mkdir(mode=0o700)
original = backup_objects.read_stream
def read(incoming, before, deadline, outgoing=None):
if outgoing is not None:
raise OSError("fixture write failure")
return original(incoming, before, deadline)
monkeypatch.setattr(backup_objects, "read_stream", read)
seen = {}
with pytest.raises(OSError):
backup_objects.store(
source, source.stat(), objects, seen, time.monotonic() + 10, remaining_bytes=100
)
assert seen == {}
assert list(objects.iterdir()) == [objects / ".pending"]
assert not (tmp_path / "manifest.json").exists()
def test_restore_expands_repeated_content_into_independent_files_with_own_modes(tmp_path):
restore = importlib.import_module("scripts.release.restore")
content = tmp_path / "object"
data = b"trusted restoration fixture\n"
content.write_bytes(data)
row = {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}
first, second = tmp_path / "first", tmp_path / "second"
for path, mode in ((first, 0o600), (second, 0o444)):
witness = restore.write_file(
content, path, row, os.getuid(), os.getgid(), mode, time.monotonic() + 10
)
restore.unchanged_file(path, witness, os.getuid(), os.getgid(), mode)
assert path.read_bytes() == data
assert path.stat().st_mode & 0o777 == mode
assert path.stat().st_nlink == 1
assert len({path.stat().st_ino for path in (content, first, second)}) == 3
def test_delayed_filesystem_write_error_is_not_success(tmp_path, monkeypatch):
class SyncFailure:
def __call__(self, descriptor):
assert os.fstat(descriptor).st_ino == tmp_path.stat().st_ino
return -1
class Library:
syncfs = SyncFailure()
monkeypatch.setattr(backup_objects.ctypes, "CDLL", lambda *_, **__: Library())
with pytest.raises(policy.DeliveryError, match="filesystem synchronization failed"):
backup_objects.sync_filesystem(tmp_path, time.monotonic() + 10)
@pytest.mark.parametrize("missing", ["releases/older", "releases/older/nested"])
def test_every_directory_ancestor_is_required_even_when_file_parent_exists(snapshot, missing):
value = object_format(snapshot)
value["directories"] = value["directories"] + [
{"path": path, "uid": 0, "gid": 0, "mode": 0o700}
for path in ("releases/older", "releases/older/nested")
if path != missing
]
value["files"] = [
{**row, "path": "releases/older/nested/file"} if row["path"] == "releases/first" else row
for row in value["files"]
]
with pytest.raises(policy.DeliveryError, match="private backup parent missing"):
backup_manifest.validate(value)
def test_v4_complete_version_inventory_keeps_an_explicit_logical_entry_bound(snapshot):
value = object_format(snapshot)
count = backup_manifest.MAX_LOGICAL_ENTRIES - len(value["directories"])
template = {**value["files"][0], "bytes": 1, "sha256": hashlib.sha256(b"x").hexdigest()}
value["files"] = [{**template, "path": row["path"]} for row in value["files"][:2]] + [
{**template, "path": "releases/item-" + str(i)} for i in range(count - 2)
]
value.update(logical_bytes=count, payload_bytes=1)
backup_manifest.validate(value)
assert len(value["files"]) + len(value["directories"]) == 98304
assert backup_manifest.MAX_BYTES == 7 * 1024**3
assert backup_manifest.MAX_LOGICAL_BYTES == 12 * 1024**3
old = {**value, "schema_version": 3, "format": backup_manifest.FORMAT, "payload_bytes": count}
old.pop("logical_bytes")
with pytest.raises(policy.DeliveryError, match="inventory count exceeds bound"):
backup_manifest.validate(old)
value["files"].append({**template, "path": "releases/one-too-many"})
value["logical_bytes"] += 1
with pytest.raises(policy.DeliveryError, match="inventory count exceeds bound"):
backup_manifest.validate(value)
def test_private_metadata_rejects_a_file_above_the_explicit_byte_limit(tmp_path):
path = tmp_path / "metadata.json"
path.write_bytes(b" " * (backup_manifest.MAX_METADATA_BYTES + 1))
with pytest.raises(policy.DeliveryError, match="metadata byte limit"):
backup_manifest.read_metadata(path)