From 1115f63879d0d0b4cab6105642a68934b57da579 Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Mon, 27 Jul 2026 20:23:16 +0200 Subject: [PATCH] test(v2): define repository safety contracts --- tests/integration/test_repository_safety.py | 85 +++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/integration/test_repository_safety.py diff --git a/tests/integration/test_repository_safety.py b/tests/integration/test_repository_safety.py new file mode 100644 index 0000000..d90c94b --- /dev/null +++ b/tests/integration/test_repository_safety.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +import httpx +import pytest +from backup_tool.api.app import create_app +from backup_tool.config import Settings +from backup_tool.repository import RepositoryError, initialize, inspect_repository + + +def settings_for(tmp_path: Path) -> Settings: + key = tmp_path / "key" + key.write_bytes(b"x" * 32) + key.chmod(0o600) + root = tmp_path / "repos" + root.mkdir() + return Settings( + repository_roots=(root,), + local_source_roots=(tmp_path,), + restore_roots=(tmp_path,), + master_key_file=key, + data_dir=tmp_path, + database_url=f"sqlite+aiosqlite:///{tmp_path / 'db.sqlite'}", + min_free_bytes=1, + ) + + +def test_partial_initialization_is_removed_on_publish_failure(tmp_path: Path) -> None: + settings = settings_for(tmp_path) + with ( + patch("backup_tool.repository.os.replace", side_effect=OSError("disk failure")), + pytest.raises(OSError), + ): + initialize(settings, "main", "none", "none") + assert not (settings.repository_roots[0] / "main").exists() + assert not list(settings.repository_roots[0].glob(".main.staging-*")) + + +def test_repository_inspection_rejects_noncanonical_metadata(tmp_path: Path) -> None: + settings = settings_for(tmp_path) + result = initialize(settings, "main", "none", "none") + (result.root / "repository.json").write_text( + '{"encryption":"none","compression":"none","format_version":1}\n' + ) + with pytest.raises(RepositoryError, match="canonical"): + inspect_repository(result.root) + + +@pytest.mark.asyncio +async def test_admin_can_list_and_inspect_repositories(tmp_path: Path) -> None: + settings = settings_for(tmp_path) + app = create_app(settings) + from backup_tool.db.models import Base + + async with app.state.engine.begin() as connection: + await connection.run_sync(Base.metadata.create_all) + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=app), base_url="https://test" + ) as client: + assert ( + await client.post( + "/api/v2/setup", json={"username": "admin", "password": "a secure password"} + ) + ).status_code == 201 + csrf = client.cookies["backup_tool_csrf"] + created = await client.post( + "/api/v2/repositories", + json={ + "name": "main", + "relative_path": "main", + "compression": "none", + "encryption": "none", + }, + headers={"X-CSRF-Token": csrf}, + ) + assert created.status_code == 201 + listed = await client.get("/api/v2/repositories") + assert listed.status_code == 200 + assert listed.json()["items"][0]["id"] == created.json()["id"] + inspection = await client.get(f"/api/v2/repositories/{created.json()['id']}/inspection") + assert inspection.status_code == 200 + assert inspection.json()["format_version"] == 1 + await app.state.engine.dispose()