feat(release): add fail-closed admission and bounded service draining
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""Contract/admission-route tests; Root file/atomicity and drain acceptance are VM-only."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from jsonschema import Draft202012Validator
|
||||
|
||||
from kf_oj.api.availability import AvailabilityMiddleware, needs_open
|
||||
from kf_oj.contracts.availability import AvailabilityRecord
|
||||
|
||||
|
||||
@pytest.mark.parametrize("state", ["open", "draining", "quiescent"])
|
||||
def test_availability_is_exactly_bound_to_a_known_version_and_state(state):
|
||||
path = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "tests/fixtures/contracts/delivery-availability.valid.json"
|
||||
)
|
||||
value = json.loads(path.read_text())
|
||||
value["state"] = state
|
||||
assert AvailabilityRecord.model_validate(value).state == state
|
||||
schema = path.parents[3] / "contracts/schemas/delivery-availability.v1.schema.json"
|
||||
Draft202012Validator(json.loads(schema.read_text())).validate(value)
|
||||
for key, replacement in (
|
||||
("schema_version", True),
|
||||
("release_id", "../release"),
|
||||
("state", "always_allow"),
|
||||
):
|
||||
with pytest.raises(ValueError):
|
||||
AvailabilityRecord.model_validate({**value, key: replacement})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"method,path,blocked",
|
||||
[
|
||||
("POST", "/api/v1/submissions", True),
|
||||
("POST", "/api/v1/admin/problem-imports", True),
|
||||
("POST", "/api/v1/admin/problems/publish", True),
|
||||
("POST", "/api/v1/auth/login", True),
|
||||
("GET", "/api/v1/internal/evaluations/artifacts", False),
|
||||
("POST", "/api/v1/auth/logout", False),
|
||||
("POST", "/api/v1/submissions/11111111-1111-4111-8111-111111111111/cancel", False),
|
||||
(
|
||||
"PUT",
|
||||
"/api/v1/admin/problem-imports/11111111-1111-4111-8111-111111111111/archive",
|
||||
False,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_drain_blocks_new_mutations_without_cutting_accepted_reads_and_cancel(
|
||||
method, path, blocked
|
||||
):
|
||||
assert needs_open(method, path) is blocked
|
||||
|
||||
|
||||
def test_missing_admission_record_rejects_before_body_or_domain_execution(monkeypatch):
|
||||
# Transport policy only: this stub does not verify Root ownership or Linux file safety.
|
||||
calls = []
|
||||
messages = []
|
||||
|
||||
def missing(*_):
|
||||
raise FileNotFoundError("record missing")
|
||||
|
||||
monkeypatch.setattr("kf_oj.api.availability.current", missing)
|
||||
|
||||
async def receive():
|
||||
calls.append("body")
|
||||
return {"type": "http.request", "body": b""}
|
||||
|
||||
async def domain(*_):
|
||||
calls.append("domain")
|
||||
|
||||
async def send(message):
|
||||
messages.append(message)
|
||||
|
||||
settings = SimpleNamespace(
|
||||
environment="production", delivery_release_id=None, delivery_configuration_id=None
|
||||
)
|
||||
asyncio.run(
|
||||
AvailabilityMiddleware(domain, settings=settings)(
|
||||
{"type": "http", "method": "POST", "path": "/api/v1/submissions"}, receive, send
|
||||
)
|
||||
)
|
||||
assert calls == []
|
||||
assert messages[0]["status"] == 503
|
||||
assert (b"cache-control", b"no-store") in messages[0]["headers"]
|
||||
Reference in New Issue
Block a user