2023-05-25 17:18:43 -07:00
|
|
|
from datasette.views.base import View
|
|
|
|
|
from datasette import Request, Response
|
|
|
|
|
from datasette.app import Datasette
|
|
|
|
|
import json
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GetView(View):
|
|
|
|
|
async def get(self, request, datasette):
|
|
|
|
|
return Response.json(
|
|
|
|
|
{
|
|
|
|
|
"absolute_url": datasette.absolute_url(request, "/"),
|
|
|
|
|
"request_path": request.path,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GetAndPostView(GetView):
|
|
|
|
|
async def post(self, request, datasette):
|
|
|
|
|
return Response.json(
|
|
|
|
|
{
|
|
|
|
|
"method": request.method,
|
|
|
|
|
"absolute_url": datasette.absolute_url(request, "/"),
|
|
|
|
|
"request_path": request.path,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_get_view():
|
|
|
|
|
v = GetView()
|
|
|
|
|
datasette = Datasette()
|
|
|
|
|
response = await v(Request.fake("/foo"), datasette)
|
|
|
|
|
assert json.loads(response.body) == {
|
|
|
|
|
"absolute_url": "http://localhost/",
|
|
|
|
|
"request_path": "/foo",
|
|
|
|
|
}
|
|
|
|
|
# Try a HEAD request
|
|
|
|
|
head_response = await v(Request.fake("/foo", method="HEAD"), datasette)
|
|
|
|
|
assert head_response.body == ""
|
|
|
|
|
assert head_response.status == 200
|
|
|
|
|
# And OPTIONS
|
|
|
|
|
options_response = await v(Request.fake("/foo", method="OPTIONS"), datasette)
|
|
|
|
|
assert options_response.body == "ok"
|
|
|
|
|
assert options_response.status == 200
|
|
|
|
|
assert options_response.headers["allow"] == "HEAD, GET"
|
|
|
|
|
# And POST
|
|
|
|
|
post_response = await v(Request.fake("/foo", method="POST"), datasette)
|
|
|
|
|
assert post_response.body == "Method not allowed"
|
|
|
|
|
assert post_response.status == 405
|
|
|
|
|
# And POST with .json extension
|
|
|
|
|
post_json_response = await v(Request.fake("/foo.json", method="POST"), datasette)
|
|
|
|
|
assert json.loads(post_json_response.body) == {
|
|
|
|
|
"ok": False,
|
|
|
|
|
"error": "Method not allowed",
|
Unify JSON error responses into one canonical shape
All JSON error responses now use a single format built by the new
datasette.utils.error_body() helper:
{"ok": false, "error": "...", "errors": ["..."], "status": 400}
- error is all messages joined with '; ', errors is the full list,
status always matches the HTTP status code
- The exception handler no longer emits the legacy title key in JSON
(it is still available to the HTML error template)
- The permission debug endpoints (/-/allowed, /-/rules, /-/check,
POST /-/permissions) no longer return bare {"error": ...} objects
- JSON renderer SQL errors keep their rows/truncated context keys but
now include the canonical keys as well
- _shape=object misuse (queries or tables without primary keys) now
returns HTTP 400 instead of 200 with an error body
- Method-not-allowed 405 responses use the canonical shape
Adds tests/test_error_shape.py covering all four previous shape
producers, updates affected tests, and documents the format in a new
'Error responses' section of docs/json_api.rst.
Implements section 1 of stable-api-recommendations.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrHZSypDfMnym1tM5XJAFZ
2026-07-04 03:12:15 +00:00
|
|
|
"errors": ["Method not allowed"],
|
|
|
|
|
"status": 405,
|
2023-05-25 17:18:43 -07:00
|
|
|
}
|
|
|
|
|
assert post_json_response.status == 405
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_post_view():
|
|
|
|
|
v = GetAndPostView()
|
|
|
|
|
datasette = Datasette()
|
|
|
|
|
response = await v(Request.fake("/foo"), datasette)
|
|
|
|
|
assert json.loads(response.body) == {
|
|
|
|
|
"absolute_url": "http://localhost/",
|
|
|
|
|
"request_path": "/foo",
|
|
|
|
|
}
|
|
|
|
|
# Try a HEAD request
|
|
|
|
|
head_response = await v(Request.fake("/foo", method="HEAD"), datasette)
|
|
|
|
|
assert head_response.body == ""
|
|
|
|
|
assert head_response.status == 200
|
|
|
|
|
# And OPTIONS
|
|
|
|
|
options_response = await v(Request.fake("/foo", method="OPTIONS"), datasette)
|
|
|
|
|
assert options_response.body == "ok"
|
|
|
|
|
assert options_response.status == 200
|
|
|
|
|
assert options_response.headers["allow"] == "HEAD, GET, POST"
|
|
|
|
|
# And POST
|
|
|
|
|
post_response = await v(Request.fake("/foo", method="POST"), datasette)
|
|
|
|
|
assert json.loads(post_response.body) == {
|
|
|
|
|
"method": "POST",
|
|
|
|
|
"absolute_url": "http://localhost/",
|
|
|
|
|
"request_path": "/foo",
|
|
|
|
|
}
|