feat(v2): complete v2 reimplementation
This commit is contained in:
@@ -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"
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user