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