mirror of
https://github.com/simonw/datasette.git
synced 2026-07-08 16:44:34 +02:00
Add "ok": true to every JSON object success response
JsonDataView now injects "ok": true into dict responses, covering /-/versions, /-/settings, /-/config, /-/threads and /-/actor. The homepage JSON, /-/jump, the three /-/schema endpoints, /-/allowed, /-/rules, /-/check, POST /-/permissions and the table /-/autocomplete endpoint set it explicitly. The remaining top-level array endpoints (/-/plugins, /-/databases, /-/actions) will be converted to objects in separate commits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GrHZSypDfMnym1tM5XJAFZ
This commit is contained in:
parent
bc51c00724
commit
089e96a437
18 changed files with 180 additions and 47 deletions
|
|
@ -151,6 +151,7 @@ class IndexView(BaseView):
|
|||
return Response(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": True,
|
||||
"databases": {db["name"]: db for db in databases},
|
||||
"metadata": await self.ds.get_instance_metadata(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ class JsonDataView(BaseView):
|
|||
headers = {}
|
||||
if self.ds.cors:
|
||||
add_cors_headers(headers)
|
||||
if isinstance(data, dict):
|
||||
data = {"ok": True, **data}
|
||||
return Response.json(data, headers=headers)
|
||||
else:
|
||||
context = {
|
||||
|
|
@ -414,6 +416,7 @@ class AllowedResourcesView(BaseView):
|
|||
# If catalog tables don't exist yet, return empty results
|
||||
return (
|
||||
{
|
||||
"ok": True,
|
||||
"action": action,
|
||||
"actor_id": actor_id,
|
||||
"page": page,
|
||||
|
|
@ -448,6 +451,7 @@ class AllowedResourcesView(BaseView):
|
|||
return f"{request.path}?{query}"
|
||||
|
||||
response = {
|
||||
"ok": True,
|
||||
"action": action,
|
||||
"actor_id": actor_id,
|
||||
"page": page,
|
||||
|
|
@ -573,6 +577,7 @@ class PermissionRulesView(BaseView):
|
|||
return f"{request.path}?{query}"
|
||||
|
||||
response = {
|
||||
"ok": True,
|
||||
"action": action,
|
||||
"actor_id": (actor or {}).get("id") if actor else None,
|
||||
"page": page,
|
||||
|
|
@ -623,6 +628,7 @@ async def _check_permission_for_actor(ds, action, parent, child, actor):
|
|||
allowed = await ds.allowed(action=action, resource=resource_obj, actor=actor)
|
||||
|
||||
response = {
|
||||
"ok": True,
|
||||
"action": action,
|
||||
"allowed": bool(allowed),
|
||||
"resource": {
|
||||
|
|
@ -1208,7 +1214,7 @@ class JumpView(BaseView):
|
|||
match["display_name"] = row["display_name"]
|
||||
matches.append(match)
|
||||
|
||||
return Response.json({"matches": matches, "truncated": truncated})
|
||||
return Response.json({"ok": True, "matches": matches, "truncated": truncated})
|
||||
|
||||
|
||||
class SchemaBaseView(BaseView):
|
||||
|
|
@ -1230,7 +1236,7 @@ class SchemaBaseView(BaseView):
|
|||
headers = {}
|
||||
if self.ds.cors:
|
||||
add_cors_headers(headers)
|
||||
return Response.json(data, headers=headers)
|
||||
return Response.json({"ok": True, **data}, headers=headers)
|
||||
|
||||
def format_error_response(self, error_message, format_, status=404):
|
||||
"""Format error response based on requested format."""
|
||||
|
|
|
|||
|
|
@ -1525,7 +1525,7 @@ class TableAutocompleteView(BaseView):
|
|||
and value_as_boolean(initial_arg)
|
||||
)
|
||||
if not q and not initial:
|
||||
return Response.json({"rows": []})
|
||||
return Response.json({"ok": True, "rows": []})
|
||||
params = {
|
||||
"q": q,
|
||||
"like": "%{}%".format(_escape_like(q)),
|
||||
|
|
@ -1588,10 +1588,13 @@ class TableAutocompleteView(BaseView):
|
|||
custom_time_limit=AUTOCOMPLETE_TIME_LIMIT_MS,
|
||||
)
|
||||
except QueryInterrupted:
|
||||
return Response.json({"rows": []})
|
||||
return Response.json({"ok": True, "rows": []})
|
||||
|
||||
return Response.json(
|
||||
{"rows": _autocomplete_response_rows(results.rows, pks, label_column)}
|
||||
{
|
||||
"ok": True,
|
||||
"rows": _autocomplete_response_rows(results.rows, pks, label_column),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ Datasette includes some pages and JSON API endpoints for introspecting the curre
|
|||
|
||||
Each of these pages can be viewed in your browser. Add ``.json`` to the URL to get back the contents as JSON.
|
||||
|
||||
JSON responses that return an object include an ``"ok": true`` key, consistent with the rest of the :ref:`JSON API <json_api>`.
|
||||
|
||||
.. _JsonDataView_metadata:
|
||||
|
||||
/-/metadata
|
||||
|
|
@ -37,6 +39,7 @@ Shows the version of Datasette, Python and SQLite. `Versions example <https://la
|
|||
.. code-block:: json
|
||||
|
||||
{
|
||||
"ok": true,
|
||||
"datasette": {
|
||||
"version": "0.60"
|
||||
},
|
||||
|
|
@ -97,6 +100,7 @@ Shows the :ref:`settings` for this instance of Datasette. `Settings example <htt
|
|||
.. code-block:: json
|
||||
|
||||
{
|
||||
"ok": true,
|
||||
"default_facet_size": 30,
|
||||
"default_page_size": 100,
|
||||
"facet_suggest_time_limit_ms": 50,
|
||||
|
|
@ -115,6 +119,7 @@ Shows the :ref:`configuration <configuration>` for this instance of Datasette. T
|
|||
.. code-block:: json
|
||||
|
||||
{
|
||||
"ok": true,
|
||||
"settings": {
|
||||
"template_debug": true,
|
||||
"trace_debug": true,
|
||||
|
|
@ -160,6 +165,7 @@ The endpoint supports a ``?q=`` query parameter for filtering items by name.
|
|||
.. code-block:: json
|
||||
|
||||
{
|
||||
"ok": true,
|
||||
"matches": [
|
||||
{
|
||||
"name": "fixtures",
|
||||
|
|
@ -188,6 +194,7 @@ Search example with ``?q=facet`` returns only items matching ``.*facet.*``:
|
|||
.. code-block:: json
|
||||
|
||||
{
|
||||
"ok": true,
|
||||
"matches": [
|
||||
{
|
||||
"name": "fixtures: facetable",
|
||||
|
|
@ -220,6 +227,7 @@ Shows details of threads and ``asyncio`` tasks. `Threads example <https://latest
|
|||
.. code-block:: json
|
||||
|
||||
{
|
||||
"ok": true,
|
||||
"num_threads": 2,
|
||||
"threads": [
|
||||
{
|
||||
|
|
@ -251,6 +259,7 @@ Shows the currently authenticated actor. Useful for debugging Datasette authenti
|
|||
.. code-block:: json
|
||||
|
||||
{
|
||||
"ok": true,
|
||||
"actor": {
|
||||
"id": 1,
|
||||
"username": "some-user"
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ looks like this:
|
|||
"truncated": false
|
||||
}
|
||||
|
||||
``"ok"`` is always ``true`` if an error did not occur.
|
||||
``"ok"`` is always ``true`` if an error did not occur. Every Datasette JSON endpoint that returns an object includes this key on success.
|
||||
|
||||
The ``"rows"`` key is a list of objects, each one representing a row.
|
||||
|
||||
|
|
|
|||
|
|
@ -169,11 +169,13 @@ Use ``/-/schema.json`` to get the same information as JSON, which looks like thi
|
|||
.. code-block:: json
|
||||
|
||||
{
|
||||
"ok": true,
|
||||
"schemas": [
|
||||
{
|
||||
"database": "content",
|
||||
"schema": "create table posts ..."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. _DatabaseSchemaView:
|
||||
|
|
@ -181,11 +183,11 @@ Use ``/-/schema.json`` to get the same information as JSON, which looks like thi
|
|||
Database schema
|
||||
---------------
|
||||
|
||||
Use ``/database-name/-/schema`` to see the complete schema for a specific database. The ``.md`` and ``.json`` extensions work here too. The JSON returns an object with ``"database"`` and ``"schema"`` keys.
|
||||
Use ``/database-name/-/schema`` to see the complete schema for a specific database. The ``.md`` and ``.json`` extensions work here too. The JSON returns an object with ``"ok"``, ``"database"`` and ``"schema"`` keys.
|
||||
|
||||
.. _TableSchemaView:
|
||||
|
||||
Table schema
|
||||
------------
|
||||
|
||||
Use ``/database-name/table-name/-/schema`` to see the schema for a specific table. The ``.md`` and ``.json`` extensions work here too. The JSON returns an object with ``"database"``, ``"table"``, and ``"schema"`` keys.
|
||||
Use ``/database-name/table-name/-/schema`` to see the schema for a specific table. The ``.md`` and ``.json`` extensions work here too. The JSON returns an object with ``"ok"``, ``"database"``, ``"table"``, and ``"schema"`` keys.
|
||||
|
|
|
|||
|
|
@ -43,6 +43,15 @@ directory: every claim below is based on the route table in `datasette/app.py`
|
|||
- Success content type: `application/json; charset=utf-8`
|
||||
(`_shape=array&_nl=on` responses use `text/plain`).
|
||||
|
||||
### Success envelope
|
||||
|
||||
Every JSON endpoint that returns an object includes `"ok": true` on
|
||||
success. `JsonDataView` injects it automatically for dict responses
|
||||
(views/special.py); the homepage, jump, schema, permission-debug and
|
||||
autocomplete views add it explicitly. The remaining top-level-array
|
||||
endpoints (`/-/plugins`, `/-/databases`, `/-/actions`) are being converted
|
||||
to objects.
|
||||
|
||||
### Error shape (canonical)
|
||||
|
||||
Every JSON error response uses one canonical shape, built by `error_body()`
|
||||
|
|
@ -190,7 +199,7 @@ Routes: `/(\.(?P<format>jsono?))?$` and `/-/(\.(?P<format>jsono?))?$`
|
|||
further filtered by `view-database` / `view-table` for the actor.
|
||||
- **Parameters:** `_sort=relationships` sorts each database's truncated table
|
||||
list by foreign-key relationship count.
|
||||
- **JSON response** (index.py:147-161):
|
||||
- **JSON response** (index.py:147-161) — includes `ok: true` plus:
|
||||
- `databases` — an **object keyed by database name** (not a list). Each
|
||||
value: `name`, `hash` (or null), `color`, `path`,
|
||||
`tables_and_views_truncated` (up to 5 items: `name`, `columns`,
|
||||
|
|
@ -260,7 +269,7 @@ per-database `view-database` permissions**.
|
|||
app.py:2574-2579, registered with `permission=None` — **accessible to any
|
||||
request including anonymous**. No parameters.
|
||||
|
||||
Response: `{"actor": {...}}` or `{"actor": null}` (app.py:2287-2288).
|
||||
Response: `{"ok": true, "actor": {...}}` or `{"ok": true, "actor": null}` (app.py:2287-2288).
|
||||
|
||||
### GET /-/actions(.json)
|
||||
|
||||
|
|
@ -313,7 +322,7 @@ an optional `.json` suffix but the view **always returns JSON**.
|
|||
`jump_items_sql` plugin hook).
|
||||
- **Parameter:** `q` — whitespace-split terms matched as a case-insensitive
|
||||
`%term1%term2%` LIKE pattern.
|
||||
- **Response:** `{"matches": [...], "truncated": bool}`; each match:
|
||||
- **Response:** `{"ok": true, "matches": [...], "truncated": bool}`; each match:
|
||||
`name`, `url`, `type` (`database`/`table`/`view`/`query`/plugin-defined),
|
||||
`description`, optional `display_name`. Capped at 100 matches.
|
||||
|
||||
|
|
@ -324,7 +333,7 @@ an optional `.json` suffix but the view **always returns JSON**.
|
|||
- **Permission:** no explicit check; only databases the actor can
|
||||
`view-database` are included (others silently omitted).
|
||||
- **Formats:** no extension → HTML; `.json` →
|
||||
`{"schemas": [{"database": name, "schema": "..."}]}`; `.md` →
|
||||
`{"ok": true, "schemas": [{"database": name, "schema": "..."}]}`; `.md` →
|
||||
`text/markdown` rendering.
|
||||
|
||||
### GET/POST /-/logout
|
||||
|
|
@ -610,10 +619,9 @@ views/table_create_alter.py:965-1005).
|
|||
- **Unknown database** → 404; for `.json`:
|
||||
`{"ok": false, "error": "Database not found"}`. (The existence check runs
|
||||
before the permission check.)
|
||||
- **Responses:** `.json` → 200 `{"database": "<name>", "schema": "<SQL>"}`
|
||||
- **Responses:** `.json` → 200 `{"ok": true, "database": "<name>", "schema": "<SQL>"}`
|
||||
(concatenated `sqlite_master.sql` joined with `;\n`); `.md` →
|
||||
`text/markdown`; no extension → HTML. Note the JSON has **no `ok` key** on
|
||||
success.
|
||||
`text/markdown`; no extension → HTML.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -787,8 +795,8 @@ download attachment. In JSON output, binary cells appear as
|
|||
`TableSchemaView` (app.py:2751-2754; views/special.py:1332-1378).
|
||||
|
||||
- **Permission:** `view-table` via `ensure_permission` (denied → 403 HTML).
|
||||
- **Responses:** `.json` → 200 `{"database", "table", "schema"}` (no `ok`
|
||||
key); `.md` → `text/markdown`; no extension → HTML. Missing table → 404
|
||||
- **Responses:** `.json` → 200 `{"ok": true, "database", "table", "schema"}`;
|
||||
`.md` → `text/markdown`; no extension → HTML. Missing table → 404
|
||||
`{"ok": false, "error": "Table not found"}` for `.json`.
|
||||
|
||||
### GET /\<database\>/\<table\>/-/fragment
|
||||
|
|
@ -805,10 +813,10 @@ only — views get 400 `"Autocomplete is only available for tables"`.
|
|||
- **Permission:** `view-table` (denied → `Forbidden` → 403).
|
||||
- **Parameters:** `q` (matched with escaped `LIKE %q%` against pk columns and
|
||||
the label column) and `_initial` (truthy: with empty `q`, return the 10
|
||||
most recent rows). Neither → `{"rows": []}`.
|
||||
most recent rows). Neither → `{"ok": true, "rows": []}`.
|
||||
- **Response:** `{"rows": [{"pks": {pk_name: value}, "label": "..."}]}` — max
|
||||
10 items; 500 ms query budget with fallbacks, timing out to
|
||||
`{"rows": []}`.
|
||||
`{"ok": true, "rows": []}`.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -102,7 +102,15 @@ key — see §2.)
|
|||
|
||||
---
|
||||
|
||||
## 2. Success envelope: `ok` is not universal, arrays are not extensible (P1/P2)
|
||||
## 2. Success envelope: `ok` is not universal, arrays are not extensible (P1/P2) — ✅ PARTIALLY IMPLEMENTED
|
||||
|
||||
> **Status:** recommendation 2 and 3 are implemented — every JSON-object
|
||||
> success response now includes `"ok": true` (`JsonDataView` injects it for
|
||||
> dict responses; homepage, jump, schema, permission-debug and autocomplete
|
||||
> views set it explicitly; covered by `tests/test_success_envelope.py`).
|
||||
> Recommendation 1 (wrapping the `/-/plugins`, `/-/databases`, `/-/actions`
|
||||
> top-level arrays in objects) is being landed as separate per-endpoint
|
||||
> commits. §2a/2b/2c remain open.
|
||||
|
||||
Endpoints disagree about the success envelope:
|
||||
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@ def test_no_files_uses_memory_database(app_client_no_files):
|
|||
response = app_client_no_files.get("/.json")
|
||||
assert response.status == 200
|
||||
assert {
|
||||
"ok": True,
|
||||
"databases": {
|
||||
"_memory": {
|
||||
"name": "_memory",
|
||||
|
|
@ -525,7 +526,7 @@ def test_databases_json(app_client_two_attached_databases_one_immutable):
|
|||
@pytest.mark.asyncio
|
||||
async def test_threads_json(ds_client):
|
||||
response = await ds_client.get("/-/threads.json")
|
||||
expected_keys = {"threads", "num_threads"}
|
||||
expected_keys = {"ok", "threads", "num_threads"}
|
||||
if sys.version_info >= (3, 7, 0):
|
||||
expected_keys.update({"tasks", "num_tasks"})
|
||||
data = response.json()
|
||||
|
|
@ -610,6 +611,7 @@ async def test_actions_json(ds_client):
|
|||
async def test_settings_json(ds_client):
|
||||
response = await ds_client.get("/-/settings.json")
|
||||
assert response.json() == {
|
||||
"ok": True,
|
||||
"default_page_size": 50,
|
||||
"default_facet_size": 30,
|
||||
"default_allow_sql": True,
|
||||
|
|
@ -884,7 +886,7 @@ async def test_config_json(config, expected):
|
|||
"/-/config.json should return redacted configuration"
|
||||
ds = Datasette(config=config)
|
||||
response = await ds.client.get("/-/config.json")
|
||||
assert response.json() == expected
|
||||
assert response.json() == {"ok": True, **expected}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -980,7 +982,7 @@ async def test_config_json(config, expected):
|
|||
async def test_upgrade_metadata(metadata, expected_config, expected_metadata):
|
||||
ds = Datasette(metadata=metadata)
|
||||
response = await ds.client.get("/-/config.json")
|
||||
assert response.json() == expected_config
|
||||
assert response.json() == {"ok": True, **expected_config}
|
||||
response2 = await ds.client.get("/-/metadata.json")
|
||||
assert response2.json() == expected_metadata
|
||||
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ async def test_auth_with_dstok_token(ds_client, scenario, should_work):
|
|||
try:
|
||||
if should_work:
|
||||
data = response.json()
|
||||
assert data.keys() == {"actor"}
|
||||
assert data.keys() == {"ok", "actor"}
|
||||
actor = data["actor"]
|
||||
expected_keys = {"id", "token"}
|
||||
if scenario != "valid_unlimited_token":
|
||||
|
|
@ -305,7 +305,7 @@ async def test_auth_with_dstok_token(ds_client, scenario, should_work):
|
|||
if scenario != "valid_unlimited_token":
|
||||
assert isinstance(actor["token_expires"], int)
|
||||
else:
|
||||
assert response.json() == {"actor": None}
|
||||
assert response.json() == {"ok": True, "actor": None}
|
||||
finally:
|
||||
ds_client.ds._settings["allow_signed_tokens"] = True
|
||||
|
||||
|
|
@ -337,10 +337,10 @@ def test_cli_create_token(app_client, expires):
|
|||
}
|
||||
if expires and expires > 0:
|
||||
expected_actor["token_expires"] = details["t"] + expires
|
||||
assert response.json == {"actor": expected_actor}
|
||||
assert response.json == {"ok": True, "actor": expected_actor}
|
||||
else:
|
||||
expected_actor = None
|
||||
assert response.json == {"actor": expected_actor}
|
||||
assert response.json == {"ok": True, "actor": expected_actor}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -25,13 +25,14 @@ async def test_autocomplete_single_pk_exact_match_and_label_order():
|
|||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"ok": True,
|
||||
"rows": [
|
||||
{"pks": {"id": 2}, "label": "Longer non-label pk match"},
|
||||
{"pks": {"id": 20}, "label": "2"},
|
||||
{"pks": {"id": 21}, "label": "22"},
|
||||
{"pks": {"id": 3}, "label": "A label containing 2"},
|
||||
{"pks": {"id": 200}, "label": "A"},
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -52,12 +53,12 @@ async def test_autocomplete_blank_q_returns_no_results():
|
|||
response = await ds.client.get("/autocomplete_blank/people/-/autocomplete?q=")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"rows": []}
|
||||
assert response.json() == {"ok": True, "rows": []}
|
||||
|
||||
response = await ds.client.get("/autocomplete_blank/people/-/autocomplete")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"rows": []}
|
||||
assert response.json() == {"ok": True, "rows": []}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -81,11 +82,12 @@ async def test_autocomplete_initial_returns_latest_rows():
|
|||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"ok": True,
|
||||
"rows": [
|
||||
{"pks": {"id": 3}, "label": "Cleo"},
|
||||
{"pks": {"id": 2}, "label": "Bob"},
|
||||
{"pks": {"id": 1}, "label": "Alice"},
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
response = await ds.client.get(
|
||||
|
|
@ -94,11 +96,12 @@ async def test_autocomplete_initial_returns_latest_rows():
|
|||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"ok": True,
|
||||
"rows": [
|
||||
{"pks": {"id": 3}, "label": "Cleo"},
|
||||
{"pks": {"id": 2}, "label": "Bob"},
|
||||
{"pks": {"id": 1}, "label": "Alice"},
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -121,9 +124,10 @@ async def test_autocomplete_escapes_like_characters():
|
|||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"ok": True,
|
||||
"rows": [
|
||||
{"pks": {"id": 1}, "label": "100% real"},
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -149,11 +153,12 @@ async def test_autocomplete_compound_pk_searches_all_pk_columns():
|
|||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"ok": True,
|
||||
"rows": [
|
||||
{"pks": {"country": "mx", "code": "ca"}, "label": "Campeche"},
|
||||
{"pks": {"country": "us", "code": "ca"}, "label": "California"},
|
||||
{"pks": {"country": "ca", "code": "bc"}, "label": "British Columbia"},
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -184,9 +189,10 @@ async def test_autocomplete_primary_key_called_label():
|
|||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"ok": True,
|
||||
"rows": [
|
||||
{"pks": {"label": "abc"}, "label": "Display value"},
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -246,8 +252,9 @@ async def test_autocomplete_timeout_uses_prefix_fallback(monkeypatch):
|
|||
assert timeout_was_simulated
|
||||
data = response.json()
|
||||
assert data == {
|
||||
"ok": True,
|
||||
"rows": [
|
||||
{"pks": {"id": f"item-1999{i:02d}"}, "label": f"name 1999{i:02d}"}
|
||||
for i in range(10)
|
||||
]
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,10 @@ def test_serve_with_get_and_token():
|
|||
],
|
||||
)
|
||||
assert 0 == result2.exit_code, result2.output
|
||||
assert json.loads(result2.output) == {"actor": {"id": "root", "token": "dstok"}}
|
||||
assert json.loads(result2.output) == {
|
||||
"ok": True,
|
||||
"actor": {"id": "root", "token": "dstok"},
|
||||
}
|
||||
|
||||
|
||||
def test_serve_with_get_exit_code_for_error():
|
||||
|
|
@ -130,8 +133,9 @@ def test_serve_get_actor():
|
|||
)
|
||||
assert result.exit_code == 0
|
||||
assert json.loads(result.output) == {
|
||||
"ok": True,
|
||||
"actor": {
|
||||
"id": "root",
|
||||
"extra": "x",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@ async def test_homepage():
|
|||
async def test_actor_is_null():
|
||||
ds = Datasette(memory=True)
|
||||
response = await ds.client.get("/-/actor.json")
|
||||
assert response.json() == {"actor": None}
|
||||
assert response.json() == {"ok": True, "actor": None}
|
||||
# -- end test_actor_is_null --
|
||||
|
||||
|
||||
|
|
@ -258,5 +258,5 @@ async def test_signed_cookie_actor():
|
|||
ds = Datasette(memory=True)
|
||||
cookies = {"ds_actor": ds.client.actor_cookie({"id": "root"})}
|
||||
response = await ds.client.get("/-/actor.json", cookies=cookies)
|
||||
assert response.json() == {"actor": {"id": "root"}}
|
||||
assert response.json() == {"ok": True, "actor": {"id": "root"}}
|
||||
# -- end test_signed_cookie_actor --
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ async def test_num_sql_threads_zero():
|
|||
await db.execute_write("create table t(id integer primary key)")
|
||||
await db.execute_write("insert into t (id) values (1)")
|
||||
response = await ds.client.get("/-/threads.json")
|
||||
assert response.json() == {"num_threads": 0, "threads": []}
|
||||
assert response.json() == {"ok": True, "num_threads": 0, "threads": []}
|
||||
response2 = await ds.client.get("/test_num_sql_threads_zero/t.json?_shape=array")
|
||||
assert response2.json() == [{"id": 1}]
|
||||
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ async def test_actor_parameter_sets_cookie(datasette):
|
|||
"""Passing actor= should sign a ds_actor cookie and authenticate the request."""
|
||||
response = await datasette.client.get("/-/actor.json", actor={"id": "root"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"actor": {"id": "root"}}
|
||||
assert response.json() == {"ok": True, "actor": {"id": "root"}}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -327,7 +327,7 @@ async def test_actor_parameter_works_with_request_method(datasette):
|
|||
"GET", "/-/actor.json", actor={"id": "root"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"actor": {"id": "root"}}
|
||||
assert response.json() == {"ok": True, "actor": {"id": "root"}}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -362,7 +362,7 @@ async def test_actor_parameter_merges_with_other_cookies(datasette):
|
|||
cookies={"unrelated": "value"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"actor": {"id": "root"}}
|
||||
assert response.json() == {"ok": True, "actor": {"id": "root"}}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -739,6 +739,7 @@ async def test_actor_restricted_permissions(
|
|||
"path": expected_path,
|
||||
}
|
||||
expected = {
|
||||
"ok": True,
|
||||
"action": permission,
|
||||
"allowed": expected_result,
|
||||
"resource": expected_resource,
|
||||
|
|
@ -1115,7 +1116,7 @@ def test_cli_create_token(options, expected):
|
|||
],
|
||||
)
|
||||
assert 0 == result2.exit_code, result2.output
|
||||
assert json.loads(result2.output) == {"actor": expected}
|
||||
assert json.loads(result2.output) == {"ok": True, "actor": expected}
|
||||
|
||||
|
||||
_visible_tables_re = re.compile(r">\/((\w+)\/(\w+))\.json<\/a> - Get rows for")
|
||||
|
|
|
|||
|
|
@ -783,9 +783,13 @@ async def test_hook_permission_resources_sql():
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_actor_json(ds_client):
|
||||
assert (await ds_client.get("/-/actor.json")).json() == {"actor": None}
|
||||
assert (await ds_client.get("/-/actor.json")).json() == {
|
||||
"ok": True,
|
||||
"actor": None,
|
||||
}
|
||||
assert (await ds_client.get("/-/actor.json?_bot2=1")).json() == {
|
||||
"actor": {"id": "bot2", "1+1": 2}
|
||||
"ok": True,
|
||||
"actor": {"id": "bot2", "1+1": 2},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
78
tests/test_success_envelope.py
Normal file
78
tests/test_success_envelope.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""
|
||||
Tests for the canonical JSON success envelope.
|
||||
|
||||
Every JSON object returned by a Datasette endpoint on success should include
|
||||
"ok": true. (Endpoints that return a top-level array are being converted to
|
||||
objects separately - see /-/plugins, /-/databases, /-/actions.)
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datasette.app import Datasette
|
||||
from datasette.utils import sqlite3
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ds_envelope(tmp_path_factory):
|
||||
db_directory = tmp_path_factory.mktemp("dbs")
|
||||
db_path = str(db_directory / "data.db")
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute("vacuum")
|
||||
conn.execute("create table docs (id integer primary key, title text)")
|
||||
conn.close()
|
||||
ds = Datasette([db_path])
|
||||
ds.root_enabled = True
|
||||
yield ds
|
||||
ds.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
(
|
||||
"/.json",
|
||||
"/-/.json",
|
||||
"/-/versions.json",
|
||||
"/-/settings.json",
|
||||
"/-/config.json",
|
||||
"/-/threads.json",
|
||||
"/-/actor.json",
|
||||
"/-/jump.json",
|
||||
"/-/schema.json",
|
||||
"/fixtures/-/schema.json",
|
||||
"/fixtures/facetable/-/schema.json",
|
||||
"/-/allowed.json?action=view-instance",
|
||||
"/fixtures/facet_cities/-/autocomplete?_initial=1",
|
||||
),
|
||||
)
|
||||
async def test_success_object_has_ok_true(ds_client, path):
|
||||
response = await ds_client.get(path)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, dict)
|
||||
assert data["ok"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
(
|
||||
"/-/rules.json?action=view-instance",
|
||||
"/-/check.json?action=view-instance",
|
||||
),
|
||||
)
|
||||
async def test_permission_debug_success_has_ok_true(ds_envelope, path):
|
||||
response = await ds_envelope.client.get(path, actor={"id": "root"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["ok"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_permissions_post_success_has_ok_true(ds_envelope):
|
||||
response = await ds_envelope.client.post(
|
||||
"/-/permissions",
|
||||
data={"actor": '{"id": "root"}', "permission": "view-instance"},
|
||||
actor={"id": "root"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["ok"] is True
|
||||
Loading…
Add table
Add a link
Reference in a new issue