164 lines
7.9 KiB
Python
164 lines
7.9 KiB
Python
"""Real PostgreSQL owner projection/pagination acceptance; no program execution."""
|
|
|
|
import asyncio
|
|
import json
|
|
import secrets
|
|
from pathlib import Path
|
|
from uuid import uuid4
|
|
|
|
import httpx
|
|
import pytest
|
|
from sqlalchemy import text
|
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
from test_submission_create import setup_app
|
|
|
|
from kf_oj.contracts.submissions import CreateSubmission
|
|
from kf_oj.core.settings import Settings
|
|
from kf_oj.modules.submissions.queries import cursor_decode
|
|
|
|
|
|
def test_cursor_and_control_configuration_fail_closed():
|
|
for value in ["garbage", "_" * 129, "", "eHx4"]:
|
|
with pytest.raises(ValueError):
|
|
cursor_decode(value)
|
|
for url in ["http://localhost:8100", "https://127.0.0.1:8100", "http://127.0.0.1:8100/path"]:
|
|
with pytest.raises(ValueError):
|
|
Settings(_env_file=None, coordinator_url=url, coordinator_token="a" * 64)
|
|
Settings(_env_file=None, coordinator_url="http://127.0.0.1:8100", coordinator_token="a" * 64)
|
|
|
|
|
|
@pytest.mark.parametrize("wire_version", [1, 2])
|
|
def test_actual_owned_history_source_result_and_cancel(
|
|
postgres_url, tmp_path, monkeypatch, wire_version
|
|
):
|
|
async def run():
|
|
app, engine, mod = await setup_app(postgres_url, tmp_path)
|
|
monkeypatch.setattr(mod, "build_engine", lambda _: engine)
|
|
async with app.router.lifespan_context(app):
|
|
passwords = [secrets.token_urlsafe(24), secrets.token_urlsafe(24)]
|
|
owners = [
|
|
await app.state.identity.create_account(name, password)
|
|
for name, password in zip(["reader_one", "reader_two"], passwords, strict=True)
|
|
]
|
|
version = await app.state.problems.import_directory(tmp_path / "import")
|
|
records = []
|
|
for i in range(4):
|
|
records.append(
|
|
await app.state.submissions.create(
|
|
owners[0],
|
|
CreateSubmission(
|
|
problem_version_id=version,
|
|
language="c17",
|
|
idempotency_key=uuid4(),
|
|
source="synthetic private source " + str(i),
|
|
),
|
|
)
|
|
)
|
|
# Synthetic contract projection only, never claimed as a real judgement.
|
|
fixture = Path(__file__).parents[2] / (
|
|
"tests/fixtures/contracts/judge-result.valid.json"
|
|
if wire_version == 1
|
|
else "tests/fixtures/contracts-v2/judge-result.valid-c17.json"
|
|
)
|
|
report = json.loads(fixture.read_text())
|
|
report["binding"]["evaluation_id"] = str(records[0].evaluation_id)
|
|
if wire_version == 2:
|
|
report["compilation"]["program"]["binding"]["evaluation_id"] = str(
|
|
records[0].evaluation_id
|
|
)
|
|
admin = create_async_engine(postgres_url)
|
|
async with admin.begin() as conn:
|
|
await conn.execute(text("SET LOCAL ROLE kf_oj_coordinator"))
|
|
await conn.execute(
|
|
text(
|
|
"UPDATE evaluations SET state='finished',verdict='accepted',"
|
|
"final_report=CAST(:report AS jsonb) WHERE id=:id"
|
|
),
|
|
{"id": records[0].evaluation_id, "report": json.dumps(report)},
|
|
)
|
|
await admin.dispose()
|
|
|
|
class CancelProbe:
|
|
accepted = True
|
|
seen = []
|
|
|
|
async def cancel(self, evaluation_id):
|
|
self.seen.append(evaluation_id)
|
|
return self.accepted
|
|
|
|
probe = CancelProbe()
|
|
app.state.submission_queries.cancellation = probe
|
|
async with httpx.AsyncClient(
|
|
transport=httpx.ASGITransport(app=app), base_url="http://localhost:8000"
|
|
) as client:
|
|
first = await client.post(
|
|
"/api/v1/auth/login",
|
|
json={"username": "reader_one", "password": passwords[0]},
|
|
headers={"Origin": "http://localhost:8000"},
|
|
)
|
|
assert first.status_code == 200
|
|
csrf = {
|
|
"Origin": "http://localhost:8000",
|
|
"X-CSRF-Token": first.json()["csrf_token"],
|
|
}
|
|
page = await client.get("/api/v1/submissions?limit=2")
|
|
assert page.status_code == 200 and page.headers["Cache-Control"] == "no-store"
|
|
after = page.json()["next_after"]
|
|
# Concurrent insertion must not shift the cursor into repeats or omit old rows.
|
|
await app.state.submissions.create(
|
|
owners[0],
|
|
CreateSubmission(
|
|
problem_version_id=version,
|
|
language="cpp20",
|
|
idempotency_key=uuid4(),
|
|
source="new synthetic private source",
|
|
),
|
|
)
|
|
tail = await client.get("/api/v1/submissions", params={"limit": 2, "after": after})
|
|
seen = [i["submission_id"] for i in page.json()["items"] + tail.json()["items"]]
|
|
assert len(set(seen)) == 4 and set(seen) == {str(r.submission_id) for r in records}
|
|
assert tail.json()["next_after"] is None
|
|
base = f"/api/v1/submissions/{records[0].submission_id}"
|
|
public = await client.get(base)
|
|
assert public.status_code == 200 and public.json()["verdict"] == "accepted"
|
|
projection = public.json()["result"]
|
|
assert projection["cpu_time_us"] == report["run_cpu_time_us"]
|
|
assert projection["memory_peak_bytes"] == report["run_memory_peak_bytes"]
|
|
assert all(
|
|
key not in public.text
|
|
for key in ["case_id", "digest", "lease", "expected", "manifest", "program"]
|
|
)
|
|
source = await client.get(base + "/source")
|
|
assert source.json()["source"] == "synthetic private source 0"
|
|
waiting = await client.get(f"/api/v1/submissions/{records[1].submission_id}")
|
|
assert waiting.json()["result"] is None and waiting.json()["verdict"] is None
|
|
cancel = f"/api/v1/submissions/{records[1].submission_id}/cancel"
|
|
assert (await client.post(cancel)).status_code == 403
|
|
assert (await client.post(cancel, headers=csrf)).status_code == 202
|
|
assert probe.seen == [records[1].evaluation_id]
|
|
assert (await client.get(cancel.removesuffix("/cancel"))).json()[
|
|
"state"
|
|
] == "queued"
|
|
probe.accepted = False
|
|
assert (await client.post(cancel, headers=csrf)).status_code == 503
|
|
assert (await client.get("/api/v1/submissions?after=invalid")).status_code == 422
|
|
second = await client.post(
|
|
"/api/v1/auth/login",
|
|
json={"username": "reader_two", "password": passwords[1]},
|
|
headers={"Origin": "http://localhost:8000"},
|
|
)
|
|
csrf["X-CSRF-Token"] = second.json()["csrf_token"]
|
|
assert (await client.get("/api/v1/submissions")).json()["items"] == []
|
|
for suffix in ["", "/source"]:
|
|
response = await client.get(base + suffix)
|
|
assert (
|
|
response.status_code == 404
|
|
and response.headers["Cache-Control"] == "no-store"
|
|
)
|
|
assert (await client.post(cancel, headers=csrf)).status_code == 404
|
|
assert len(probe.seen) == 2
|
|
assert (await client.post("/api/v1/auth/logout", headers=csrf)).status_code == 204
|
|
assert (await client.get(base)).status_code == 401
|
|
|
|
asyncio.run(run())
|