- Route Devii-driven AI gateway cost to the action/tool that triggered it instead of a blanket "internal" bucket, so per-feature AI spend is attributable. - Fix the quiz attempt review to show one previously-answered question at a time instead of all of them at once, and stop a quiz endpoint linked from the quiz flow from responding with raw JSON. - Add DB API async query result route and AI Usage Analyzer annotated source/media routes, with traversal-safe uid/path handling and matching tests. - Add Code Farm action audit logging (plant/harvest/buy-plot/upgrade/ fertilize) and related admin workspace/services/trash/gateway route and doc touch-ups. - Drop redundant docstrings from access_tokens.py per the no-comments convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VL9Xn57W5UR3HZbbuuzxdK
152 lines
4.7 KiB
Python
152 lines
4.7 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
import json
|
|
import shutil
|
|
from datetime import datetime, timezone
|
|
|
|
import pytest
|
|
from starlette.websockets import WebSocketDisconnect
|
|
|
|
from devplacepy.config import DBAPI_DIR
|
|
from devplacepy.services.manager import service_manager
|
|
|
|
|
|
def test_query_requires_auth(client):
|
|
assert client.post("/dbapi/query", json={"sql": "SELECT 1"}).status_code == 403
|
|
|
|
|
|
def test_select_runs(client, auth):
|
|
response = client.post("/dbapi/query", json={"sql": "SELECT 1 AS n"}, headers=auth)
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["valid"] is True
|
|
assert body["rows"] == [{"n": 1}]
|
|
|
|
|
|
def test_non_select_is_409(client, auth):
|
|
response = client.post(
|
|
"/dbapi/query", json={"sql": "DELETE FROM posts"}, headers=auth
|
|
)
|
|
assert response.status_code == 409
|
|
assert response.json()["statement_type"] == "delete"
|
|
|
|
|
|
def test_invalid_sql_is_400(client, auth):
|
|
response = client.post(
|
|
"/dbapi/query", json={"sql": "SELECT * FROM no_such_table_zzz"}, headers=auth
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
|
|
def test_suspicious_select_is_flagged(client, auth):
|
|
response = client.post(
|
|
"/dbapi/query", json={"sql": "SELECT * FROM users"}, headers=auth
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["suspicious"]
|
|
|
|
|
|
def test_async_enqueue_and_status(client, auth):
|
|
enqueued = client.post(
|
|
"/dbapi/query/async", json={"sql": "SELECT uid FROM users LIMIT 3"}, headers=auth
|
|
)
|
|
assert enqueued.status_code == 200
|
|
uid = enqueued.json()["uid"]
|
|
assert enqueued.json()["ws_url"].endswith("/ws")
|
|
assert client.get(f"/dbapi/query/{uid}", headers=auth).status_code == 200
|
|
|
|
|
|
def test_async_non_select_is_409(client, auth):
|
|
response = client.post(
|
|
"/dbapi/query/async", json={"sql": "UPDATE users SET role = 1"}, headers=auth
|
|
)
|
|
assert response.status_code == 409
|
|
|
|
|
|
def test_ws_unknown_job_is_closed(client, auth):
|
|
with client.websocket_connect("/dbapi/query/not-a-real-uid/ws") as ws:
|
|
with pytest.raises(WebSocketDisconnect) as info:
|
|
ws.receive_json()
|
|
assert info.value.code == 1008
|
|
|
|
|
|
def test_ws_non_owner_retries(client, auth):
|
|
service_manager.set_lock_owner(False)
|
|
try:
|
|
with client.websocket_connect("/dbapi/query/anything/ws") as ws:
|
|
with pytest.raises(WebSocketDisconnect) as info:
|
|
ws.receive_json()
|
|
assert info.value.code == 4013
|
|
finally:
|
|
service_manager.set_lock_owner(True)
|
|
|
|
|
|
def test_result_route_serves_the_persisted_rows(client, auth):
|
|
enqueued = client.post(
|
|
"/dbapi/query/async", json={"sql": "SELECT 1 AS n"}, headers=auth
|
|
)
|
|
uid = enqueued.json()["uid"]
|
|
output_dir = DBAPI_DIR / uid
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
result = {
|
|
"sql": "SELECT 1 AS n",
|
|
"row_count": 1,
|
|
"truncated": False,
|
|
"suspicious": [],
|
|
"rows": [{"n": 1}],
|
|
}
|
|
(output_dir / "result.json").write_text(json.dumps(result), encoding="utf-8")
|
|
try:
|
|
response = client.get(f"/dbapi/query/{uid}/result", headers=auth)
|
|
assert response.status_code == 200, response.text
|
|
assert response.json()["rows"] == [{"n": 1}]
|
|
finally:
|
|
shutil.rmtree(output_dir, ignore_errors=True)
|
|
|
|
|
|
def test_result_route_404s_without_a_written_result(client, auth):
|
|
enqueued = client.post(
|
|
"/dbapi/query/async", json={"sql": "SELECT 1 AS n"}, headers=auth
|
|
)
|
|
uid = enqueued.json()["uid"]
|
|
response = client.get(f"/dbapi/query/{uid}/result", headers=auth)
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_result_route_rejects_a_traversal_uid(client, auth):
|
|
from devplacepy.database import get_table
|
|
|
|
evil_uid = ".."
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
get_table("jobs").insert(
|
|
{
|
|
"uid": evil_uid,
|
|
"kind": "dbquery",
|
|
"status": "done",
|
|
"owner_kind": "user",
|
|
"owner_id": "traversal-owner",
|
|
"preferred_name": "",
|
|
"payload": "{}",
|
|
"result": "{}",
|
|
"error": "",
|
|
"retry_count": 0,
|
|
"created_at": now,
|
|
"started_at": now,
|
|
"completed_at": now,
|
|
"updated_at": now,
|
|
"duration_ms": 0,
|
|
"last_accessed_at": "",
|
|
"expires_at": "",
|
|
"bytes_in": 0,
|
|
"bytes_out": 0,
|
|
"item_count": 0,
|
|
}
|
|
)
|
|
escape_target = DBAPI_DIR.parent / "result.json"
|
|
try:
|
|
response = client.get(f"/dbapi/query/{evil_uid}/result", headers=auth)
|
|
assert response.status_code in (400, 404), response.text
|
|
assert not escape_target.exists()
|
|
finally:
|
|
get_table("jobs").delete(uid=evil_uid)
|