feat(v2): complete v2 reimplementation

This commit is contained in:
2026-07-31 13:33:39 +02:00
parent 396219e776
commit bd107d6a30
137 changed files with 20737 additions and 155 deletions
+22 -5
View File
@@ -35,16 +35,17 @@ async def test_readyz_rejects_unmigrated_database(tmp_path) -> None:
response = await client.get("/readyz")
await app.state.engine.dispose()
assert response.status_code == 503
assert response.json()["code"] == "schema_not_current"
assert response.json()["code"] == "dependency_unavailable"
@pytest.mark.asyncio
async def test_readyz_is_truthful_before_and_after_setup(app_client) -> None:
async def test_readyz_reports_runtime_dependencies_before_and_after_setup(
app_client,
) -> None:
client, _ = app_client
before = await client.get("/readyz")
assert before.status_code == 503
assert before.headers["content-type"].startswith("application/problem+json")
assert before.json()["code"] == "setup_required"
assert before.status_code == 200
assert before.json()["status"] == "ready"
assert (await setup_admin(client)).status_code == 201
after = await client.get("/readyz")
@@ -52,6 +53,22 @@ async def test_readyz_is_truthful_before_and_after_setup(app_client) -> None:
assert after.json()["status"] == "ready"
@pytest.mark.asyncio
async def test_metrics_are_prometheus_text_without_sensitive_request_data(
app_client,
) -> None:
client, _ = app_client
assert (await client.get("/livez")).status_code == 200
response = await client.get("/metrics")
assert response.status_code == 200
assert response.headers["content-type"].startswith("text/plain; version=0.0.4")
assert "backup_tool_http_requests_total" in response.text
assert "backup_tool_active_executions" in response.text
assert "backup_tool_filesystem_free_bytes" in response.text
@pytest.mark.asyncio
async def test_protected_endpoint_uses_rfc9457_problem(app_client) -> None:
client, _ = app_client
@@ -0,0 +1,92 @@
from __future__ import annotations
import pytest
from backup_tool.db.models import IdempotencyRecord
from sqlalchemy import select
PASSWORD = "correct horse battery staple"
@pytest.mark.asyncio
async def test_catalog_subscription_and_write_only_webhook_secret(app_client) -> None:
client, _ = app_client
setup = await client.post("/api/v2/setup", json={"username": "admin", "password": PASSWORD})
assert setup.status_code == 201
csrf = client.cookies["backup_tool_csrf"]
catalog = await client.get("/api/v2/notifications/event-catalog")
assert catalog.status_code == 200
assert catalog.json()["event_schema_version"] == 1
assert "execution.queued" in catalog.json()["events"]
created = await client.post(
"/api/v2/notifications/subscriptions",
json={
"channel": "webhook",
"event_filters": ["execution.*"],
"destination": {"url": "https://hooks.example.test/backup"},
"signing_secret": "not-returned-webhook-secret",
},
headers={"X-CSRF-Token": csrf},
)
assert created.status_code == 201
assert "signing_secret" not in created.text
assert "not-returned-webhook-secret" not in created.text
assert created.headers["ETag"]
@pytest.mark.asyncio
async def test_rotation_idempotency_never_persists_secret_verifier(app_client) -> None:
client, _ = app_client
setup = await client.post("/api/v2/setup", json={"username": "admin", "password": PASSWORD})
assert setup.status_code == 201
csrf = client.cookies["backup_tool_csrf"]
created = await client.post(
"/api/v2/notifications/subscriptions",
json={
"channel": "webhook",
"event_filters": ["execution.*"],
"destination": {"url": "https://hooks.example.test/backup"},
"signing_secret": "first-signing-secret",
},
headers={"X-CSRF-Token": csrf},
)
assert created.status_code == 201
route = f"/api/v2/notifications/subscriptions/{created.json()['id']}/signing-keys/rotate"
first = await client.post(
route,
json={"secret": "rotation-secret-one", "overlap_seconds": 60},
headers={"X-CSRF-Token": csrf, "Idempotency-Key": "rotation-one"},
)
replay = await client.post(
route,
json={"secret": "rotation-secret-two", "overlap_seconds": 60},
headers={"X-CSRF-Token": csrf, "Idempotency-Key": "rotation-one"},
)
assert first.status_code == replay.status_code == 200
assert first.json() == replay.json()
app = client._transport.app
async with app.state.sessions() as db:
record = await db.scalar(
select(IdempotencyRecord).where(
IdempotencyRecord.operation == "rotate_notification_signing_key"
)
)
assert record is not None
assert "rotation-secret" not in record.request_digest
@pytest.mark.asyncio
async def test_notification_rejects_empty_or_unknown_filters(app_client) -> None:
client, _ = app_client
setup = await client.post("/api/v2/setup", json={"username": "admin", "password": PASSWORD})
assert setup.status_code == 201
response = await client.post(
"/api/v2/notifications/subscriptions",
json={
"channel": "email",
"event_filters": ["unknown.event"],
"destination": {"recipients": ["operator@example.test"]},
},
headers={"X-CSRF-Token": client.cookies["backup_tool_csrf"]},
)
assert response.status_code == 422
assert response.json()["code"] == "validation_failed"
+8
View File
@@ -144,6 +144,14 @@ def test_state_errors_capabilities_and_fault_points_are_frozen() -> None:
capabilities = load(CONTRACT / "capabilities-v2.0.json")
assert capabilities["sources"] == ["local", "ssh"]
manifest_schema = load(CONTRACT / "manifest.schema.json")
adapter_kinds = manifest_schema["properties"]["source_consistency"]["properties"]["adapter"][
"enum"
]
assert adapter_kinds == ["local", "postgresql", "mysql"]
assert "ssh" not in adapter_kinds
assert not capabilities["features"]["tar_download"]
assert not capabilities["features"]["postgresql"]
assert not capabilities["features"]["mysql"]