kopia lustrzana https://github.com/simonw/datasette
1595 wiersze
46 KiB
Python
1595 wiersze
46 KiB
Python
from datasette.app import Datasette
|
|
from datasette.utils import sqlite3
|
|
from .utils import last_event
|
|
import pytest
|
|
import time
|
|
|
|
|
|
@pytest.fixture
|
|
def ds_write(tmp_path_factory):
|
|
db_directory = tmp_path_factory.mktemp("dbs")
|
|
db_path = str(db_directory / "data.db")
|
|
db_path_immutable = str(db_directory / "immutable.db")
|
|
db1 = sqlite3.connect(str(db_path))
|
|
db2 = sqlite3.connect(str(db_path_immutable))
|
|
for db in (db1, db2):
|
|
db.execute("vacuum")
|
|
db.execute(
|
|
"create table docs (id integer primary key, title text, score float, age integer)"
|
|
)
|
|
ds = Datasette([db_path], immutables=[db_path_immutable])
|
|
yield ds
|
|
db.close()
|
|
|
|
|
|
def write_token(ds, actor_id="root", permissions=None):
|
|
to_sign = {"a": actor_id, "token": "dstok", "t": int(time.time())}
|
|
if permissions:
|
|
to_sign["_r"] = {"a": permissions}
|
|
return "dstok_{}".format(ds.sign(to_sign, namespace="token"))
|
|
|
|
|
|
def _headers(token):
|
|
return {
|
|
"Authorization": "Bearer {}".format(token),
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_insert_row(ds_write):
|
|
token = write_token(ds_write)
|
|
response = await ds_write.client.post(
|
|
"/data/docs/-/insert",
|
|
json={"row": {"title": "Test", "score": 1.2, "age": 5}},
|
|
headers=_headers(token),
|
|
)
|
|
expected_row = {"id": 1, "title": "Test", "score": 1.2, "age": 5}
|
|
assert response.status_code == 201
|
|
assert response.json()["ok"] is True
|
|
assert response.json()["rows"] == [expected_row]
|
|
rows = (await ds_write.get_database("data").execute("select * from docs")).rows
|
|
assert dict(rows[0]) == expected_row
|
|
# Analytics event
|
|
event = last_event(ds_write)
|
|
assert event.name == "insert-rows"
|
|
assert event.num_rows == 1
|
|
assert event.database == "data"
|
|
assert event.table == "docs"
|
|
assert not event.ignore
|
|
assert not event.replace
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_insert_row_alter(ds_write):
|
|
token = write_token(ds_write)
|
|
response = await ds_write.client.post(
|
|
"/data/docs/-/insert",
|
|
json={
|
|
"row": {"title": "Test", "score": 1.2, "age": 5, "extra": "extra"},
|
|
"alter": True,
|
|
},
|
|
headers=_headers(token),
|
|
)
|
|
assert response.status_code == 201
|
|
assert response.json()["ok"] is True
|
|
assert response.json()["rows"][0]["extra"] == "extra"
|
|
# Analytics event
|
|
event = last_event(ds_write)
|
|
assert event.name == "alter-table"
|
|
assert "extra" not in event.before_schema
|
|
assert "extra" in event.after_schema
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("return_rows", (True, False))
|
|
async def test_insert_rows(ds_write, return_rows):
|
|
token = write_token(ds_write)
|
|
data = {
|
|
"rows": [
|
|
{"title": "Test {}".format(i), "score": 1.0, "age": 5} for i in range(20)
|
|
]
|
|
}
|
|
if return_rows:
|
|
data["return"] = True
|
|
response = await ds_write.client.post(
|
|
"/data/docs/-/insert",
|
|
json=data,
|
|
headers=_headers(token),
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
# Analytics event
|
|
event = last_event(ds_write)
|
|
assert event.name == "insert-rows"
|
|
assert event.num_rows == 20
|
|
assert event.database == "data"
|
|
assert event.table == "docs"
|
|
assert not event.ignore
|
|
assert not event.replace
|
|
|
|
actual_rows = [
|
|
dict(r)
|
|
for r in (
|
|
await ds_write.get_database("data").execute("select * from docs")
|
|
).rows
|
|
]
|
|
assert len(actual_rows) == 20
|
|
assert actual_rows == [
|
|
{"id": i + 1, "title": "Test {}".format(i), "score": 1.0, "age": 5}
|
|
for i in range(20)
|
|
]
|
|
assert response.json()["ok"] is True
|
|
if return_rows:
|
|
assert response.json()["rows"] == actual_rows
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"path,input,special_case,expected_status,expected_errors",
|
|
(
|
|
(
|
|
"/data2/docs/-/insert",
|
|
{},
|
|
None,
|
|
404,
|
|
["Database not found: data2"],
|
|
),
|
|
(
|
|
"/data/docs2/-/insert",
|
|
{},
|
|
None,
|
|
404,
|
|
["Table not found: docs2"],
|
|
),
|
|
(
|
|
"/data/docs/-/insert",
|
|
{"rows": [{"title": "Test"} for i in range(10)]},
|
|
"bad_token",
|
|
403,
|
|
["Permission denied"],
|
|
),
|
|
(
|
|
"/data/docs/-/insert",
|
|
{},
|
|
"invalid_json",
|
|
400,
|
|
[
|
|
"Invalid JSON: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"
|
|
],
|
|
),
|
|
(
|
|
"/data/docs/-/insert",
|
|
{},
|
|
"invalid_content_type",
|
|
400,
|
|
["Invalid content-type, must be application/json"],
|
|
),
|
|
(
|
|
"/data/docs/-/insert",
|
|
[],
|
|
None,
|
|
400,
|
|
["JSON must be a dictionary"],
|
|
),
|
|
(
|
|
"/data/docs/-/insert",
|
|
{"row": "blah"},
|
|
None,
|
|
400,
|
|
['"row" must be a dictionary'],
|
|
),
|
|
(
|
|
"/data/docs/-/insert",
|
|
{"blah": "blah"},
|
|
None,
|
|
400,
|
|
['JSON must have one or other of "row" or "rows"'],
|
|
),
|
|
(
|
|
"/data/docs/-/insert",
|
|
{"rows": "blah"},
|
|
None,
|
|
400,
|
|
['"rows" must be a list'],
|
|
),
|
|
(
|
|
"/data/docs/-/insert",
|
|
{"rows": ["blah"]},
|
|
None,
|
|
400,
|
|
['"rows" must be a list of dictionaries'],
|
|
),
|
|
(
|
|
"/data/docs/-/insert",
|
|
{"rows": [{"title": "Test"} for i in range(101)]},
|
|
None,
|
|
400,
|
|
["Too many rows, maximum allowed is 100"],
|
|
),
|
|
(
|
|
"/data/docs/-/insert",
|
|
{"rows": [{"id": 1, "title": "Test"}, {"id": 2, "title": "Test"}]},
|
|
"duplicate_id",
|
|
400,
|
|
["UNIQUE constraint failed: docs.id"],
|
|
),
|
|
(
|
|
"/data/docs/-/insert",
|
|
{"rows": [{"title": "Test"}], "ignore": True, "replace": True},
|
|
None,
|
|
400,
|
|
['Cannot use "ignore" and "replace" at the same time'],
|
|
),
|
|
(
|
|
# Replace is not allowed if you don't have update-row
|
|
"/data/docs/-/insert",
|
|
{"rows": [{"title": "Test"}], "replace": True},
|
|
"insert-but-not-update",
|
|
403,
|
|
['Permission denied: need update-row to use "replace"'],
|
|
),
|
|
(
|
|
"/data/docs/-/insert",
|
|
{"rows": [{"title": "Test"}], "invalid_param": True},
|
|
None,
|
|
400,
|
|
['Invalid parameter: "invalid_param"'],
|
|
),
|
|
(
|
|
"/data/docs/-/insert",
|
|
{"rows": [{"title": "Test"}], "one": True, "two": True},
|
|
None,
|
|
400,
|
|
['Invalid parameter: "one", "two"'],
|
|
),
|
|
(
|
|
"/immutable/docs/-/insert",
|
|
{"rows": [{"title": "Test"}]},
|
|
None,
|
|
403,
|
|
["Database is immutable"],
|
|
),
|
|
# Validate columns of each row
|
|
(
|
|
"/data/docs/-/insert",
|
|
{"rows": [{"title": "Test", "bad": 1, "worse": 2} for i in range(2)]},
|
|
None,
|
|
400,
|
|
[
|
|
"Row 0 has invalid columns: bad, worse",
|
|
"Row 1 has invalid columns: bad, worse",
|
|
],
|
|
),
|
|
## UPSERT ERRORS:
|
|
(
|
|
"/immutable/docs/-/upsert",
|
|
{"rows": [{"title": "Test"}]},
|
|
None,
|
|
403,
|
|
["Database is immutable"],
|
|
),
|
|
(
|
|
"/data/badtable/-/upsert",
|
|
{"rows": [{"title": "Test"}]},
|
|
None,
|
|
404,
|
|
["Table not found: badtable"],
|
|
),
|
|
# missing primary key
|
|
(
|
|
"/data/docs/-/upsert",
|
|
{"rows": [{"title": "Missing PK"}]},
|
|
None,
|
|
400,
|
|
['Row 0 is missing primary key column(s): "id"'],
|
|
),
|
|
# Upsert does not support ignore or replace
|
|
(
|
|
"/data/docs/-/upsert",
|
|
{"rows": [{"id": 1, "title": "Bad"}], "ignore": True},
|
|
None,
|
|
400,
|
|
["Upsert does not support ignore or replace"],
|
|
),
|
|
# Upsert permissions
|
|
(
|
|
"/data/docs/-/upsert",
|
|
{"rows": [{"id": 1, "title": "Disallowed"}]},
|
|
"insert-but-not-update",
|
|
403,
|
|
["Permission denied: need both insert-row and update-row"],
|
|
),
|
|
(
|
|
"/data/docs/-/upsert",
|
|
{"rows": [{"id": 1, "title": "Disallowed"}]},
|
|
"update-but-not-insert",
|
|
403,
|
|
["Permission denied: need both insert-row and update-row"],
|
|
),
|
|
# Alter table forbidden without alter permission
|
|
(
|
|
"/data/docs/-/upsert",
|
|
{"rows": [{"id": 1, "title": "One", "extra": "extra"}], "alter": True},
|
|
"update-and-insert-but-no-alter",
|
|
403,
|
|
["Permission denied for alter-table"],
|
|
),
|
|
),
|
|
)
|
|
async def test_insert_or_upsert_row_errors(
|
|
ds_write, path, input, special_case, expected_status, expected_errors
|
|
):
|
|
token_permissions = []
|
|
if special_case == "insert-but-not-update":
|
|
token_permissions = ["ir", "vi"]
|
|
if special_case == "update-but-not-insert":
|
|
token_permissions = ["ur", "vi"]
|
|
if special_case == "update-and-insert-but-no-alter":
|
|
token_permissions = ["ur", "ir"]
|
|
token = write_token(ds_write, permissions=token_permissions)
|
|
if special_case == "duplicate_id":
|
|
await ds_write.get_database("data").execute_write(
|
|
"insert into docs (id) values (1)"
|
|
)
|
|
if special_case == "bad_token":
|
|
token += "bad"
|
|
kwargs = dict(
|
|
json=input,
|
|
headers={
|
|
"Authorization": "Bearer {}".format(token),
|
|
"Content-Type": (
|
|
"text/plain"
|
|
if special_case == "invalid_content_type"
|
|
else "application/json"
|
|
),
|
|
},
|
|
)
|
|
|
|
actor_response = (
|
|
await ds_write.client.get("/-/actor.json", headers=kwargs["headers"])
|
|
).json()
|
|
assert set((actor_response["actor"] or {}).get("_r", {}).get("a") or []) == set(
|
|
token_permissions
|
|
)
|
|
|
|
if special_case == "invalid_json":
|
|
del kwargs["json"]
|
|
kwargs["content"] = "{bad json"
|
|
before_count = (
|
|
await ds_write.get_database("data").execute("select count(*) from docs")
|
|
).rows[0][0] == 0
|
|
response = await ds_write.client.post(
|
|
path,
|
|
**kwargs,
|
|
)
|
|
assert response.status_code == expected_status
|
|
assert response.json()["ok"] is False
|
|
assert response.json()["errors"] == expected_errors
|
|
# Check that no rows were inserted
|
|
after_count = (
|
|
await ds_write.get_database("data").execute("select count(*) from docs")
|
|
).rows[0][0] == 0
|
|
assert before_count == after_count
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("allowed", (True, False))
|
|
async def test_upsert_permissions_per_table(ds_write, allowed):
|
|
# https://github.com/simonw/datasette/issues/2262
|
|
token = "dstok_{}".format(
|
|
ds_write.sign(
|
|
{
|
|
"a": "root",
|
|
"token": "dstok",
|
|
"t": int(time.time()),
|
|
"_r": {
|
|
"r": {
|
|
"data": {
|
|
"docs" if allowed else "other": ["ir", "ur"],
|
|
}
|
|
}
|
|
},
|
|
},
|
|
namespace="token",
|
|
)
|
|
)
|
|
response = await ds_write.client.post(
|
|
"/data/docs/-/upsert",
|
|
json={"rows": [{"id": 1, "title": "One"}]},
|
|
headers={
|
|
"Authorization": "Bearer {}".format(token),
|
|
},
|
|
)
|
|
if allowed:
|
|
assert response.status_code == 200
|
|
assert response.json()["ok"] is True
|
|
else:
|
|
assert response.status_code == 403
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"ignore,replace,expected_rows",
|
|
(
|
|
(
|
|
True,
|
|
False,
|
|
[
|
|
{"id": 1, "title": "Exists", "score": None, "age": None},
|
|
],
|
|
),
|
|
(
|
|
False,
|
|
True,
|
|
[
|
|
{"id": 1, "title": "One", "score": None, "age": None},
|
|
],
|
|
),
|
|
),
|
|
)
|
|
@pytest.mark.parametrize("should_return", (True, False))
|
|
async def test_insert_ignore_replace(
|
|
ds_write, ignore, replace, expected_rows, should_return
|
|
):
|
|
await ds_write.get_database("data").execute_write(
|
|
"insert into docs (id, title) values (1, 'Exists')"
|
|
)
|
|
token = write_token(ds_write)
|
|
data = {"rows": [{"id": 1, "title": "One"}]}
|
|
if ignore:
|
|
data["ignore"] = True
|
|
if replace:
|
|
data["replace"] = True
|
|
if should_return:
|
|
data["return"] = True
|
|
response = await ds_write.client.post(
|
|
"/data/docs/-/insert",
|
|
json=data,
|
|
headers=_headers(token),
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
# Analytics event
|
|
event = last_event(ds_write)
|
|
assert event.name == "insert-rows"
|
|
assert event.num_rows == 1
|
|
assert event.database == "data"
|
|
assert event.table == "docs"
|
|
assert event.ignore == ignore
|
|
assert event.replace == replace
|
|
|
|
actual_rows = [
|
|
dict(r)
|
|
for r in (
|
|
await ds_write.get_database("data").execute("select * from docs")
|
|
).rows
|
|
]
|
|
assert actual_rows == expected_rows
|
|
assert response.json()["ok"] is True
|
|
if should_return:
|
|
assert response.json()["rows"] == expected_rows
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"initial,input,expected_rows",
|
|
(
|
|
(
|
|
# Simple primary key update
|
|
{"rows": [{"id": 1, "title": "One"}], "pk": "id"},
|
|
{"rows": [{"id": 1, "title": "Two"}]},
|
|
[
|
|
{"id": 1, "title": "Two"},
|
|
],
|
|
),
|
|
(
|
|
# Multiple rows update one of them
|
|
{
|
|
"rows": [{"id": 1, "title": "One"}, {"id": 2, "title": "Two"}],
|
|
"pk": "id",
|
|
},
|
|
{"rows": [{"id": 1, "title": "Three"}]},
|
|
[
|
|
{"id": 1, "title": "Three"},
|
|
{"id": 2, "title": "Two"},
|
|
],
|
|
),
|
|
(
|
|
# rowid update
|
|
{"rows": [{"title": "One"}]},
|
|
{"rows": [{"rowid": 1, "title": "Two"}]},
|
|
[
|
|
{"rowid": 1, "title": "Two"},
|
|
],
|
|
),
|
|
(
|
|
# Compound primary key update
|
|
{"rows": [{"id": 1, "title": "One", "score": 1}], "pks": ["id", "score"]},
|
|
{"rows": [{"id": 1, "title": "Two", "score": 1}]},
|
|
[
|
|
{"id": 1, "title": "Two", "score": 1},
|
|
],
|
|
),
|
|
(
|
|
# Upsert with an alter
|
|
{"rows": [{"id": 1, "title": "One"}], "pk": "id"},
|
|
{"rows": [{"id": 1, "title": "Two", "extra": "extra"}], "alter": True},
|
|
[{"id": 1, "title": "Two", "extra": "extra"}],
|
|
),
|
|
),
|
|
)
|
|
@pytest.mark.parametrize("should_return", (False, True))
|
|
async def test_upsert(ds_write, initial, input, expected_rows, should_return):
|
|
token = write_token(ds_write)
|
|
# Insert initial data
|
|
initial["table"] = "upsert_test"
|
|
create_response = await ds_write.client.post(
|
|
"/data/-/create",
|
|
json=initial,
|
|
headers=_headers(token),
|
|
)
|
|
assert create_response.status_code == 201
|
|
if should_return:
|
|
input["return"] = True
|
|
response = await ds_write.client.post(
|
|
"/data/upsert_test/-/upsert",
|
|
json=input,
|
|
headers=_headers(token),
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["ok"] is True
|
|
|
|
# Analytics event
|
|
event = last_event(ds_write)
|
|
assert event.database == "data"
|
|
assert event.table == "upsert_test"
|
|
if input.get("alter"):
|
|
assert event.name == "alter-table"
|
|
assert "extra" in event.after_schema
|
|
else:
|
|
assert event.name == "upsert-rows"
|
|
assert event.num_rows == 1
|
|
|
|
if should_return:
|
|
# We only expect it to return rows corresponding to those we sent
|
|
expected_returned_rows = expected_rows[: len(input["rows"])]
|
|
assert response.json()["rows"] == expected_returned_rows
|
|
# Check the database too
|
|
actual_rows = (
|
|
await ds_write.client.get("/data/upsert_test.json?_shape=array")
|
|
).json()
|
|
assert actual_rows == expected_rows
|
|
# Drop the upsert_test table
|
|
await ds_write.get_database("data").execute_write("drop table upsert_test")
|
|
|
|
|
|
async def _insert_row(ds):
|
|
insert_response = await ds.client.post(
|
|
"/data/docs/-/insert",
|
|
json={"row": {"title": "Row one", "score": 1.2, "age": 5}, "return": True},
|
|
headers=_headers(write_token(ds)),
|
|
)
|
|
assert insert_response.status_code == 201
|
|
return insert_response.json()["rows"][0]["id"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("scenario", ("no_token", "no_perm", "bad_table"))
|
|
async def test_delete_row_errors(ds_write, scenario):
|
|
if scenario == "no_token":
|
|
token = "bad_token"
|
|
elif scenario == "no_perm":
|
|
token = write_token(ds_write, actor_id="not-root")
|
|
else:
|
|
token = write_token(ds_write)
|
|
|
|
pk = await _insert_row(ds_write)
|
|
|
|
path = "/data/{}/{}/-/delete".format(
|
|
"docs" if scenario != "bad_table" else "bad_table", pk
|
|
)
|
|
response = await ds_write.client.post(
|
|
path,
|
|
headers=_headers(token),
|
|
)
|
|
assert response.status_code == 403 if scenario in ("no_token", "bad_token") else 404
|
|
assert response.json()["ok"] is False
|
|
assert (
|
|
response.json()["errors"] == ["Permission denied"]
|
|
if scenario == "no_token"
|
|
else ["Table not found: bad_table"]
|
|
)
|
|
assert len((await ds_write.client.get("/data/docs.json?_shape=array")).json()) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"table,row_for_create,pks,delete_path",
|
|
(
|
|
("rowid_table", {"name": "rowid row"}, None, None),
|
|
("pk_table", {"id": 1, "name": "ID table"}, "id", "1"),
|
|
(
|
|
"compound_pk_table",
|
|
{"type": "article", "key": "k"},
|
|
["type", "key"],
|
|
"article,k",
|
|
),
|
|
),
|
|
)
|
|
async def test_delete_row(ds_write, table, row_for_create, pks, delete_path):
|
|
# First create the table with that example row
|
|
create_data = {
|
|
"table": table,
|
|
"row": row_for_create,
|
|
}
|
|
if pks:
|
|
if isinstance(pks, str):
|
|
create_data["pk"] = pks
|
|
else:
|
|
create_data["pks"] = pks
|
|
create_response = await ds_write.client.post(
|
|
"/data/-/create",
|
|
json=create_data,
|
|
headers=_headers(write_token(ds_write)),
|
|
)
|
|
assert create_response.status_code == 201, create_response.json()
|
|
# Should be a single row
|
|
assert (
|
|
await ds_write.client.get(
|
|
"/data.json?_shape=arrayfirst&sql=select+count(*)+from+{}".format(table)
|
|
)
|
|
).json() == [1]
|
|
# Now delete the row
|
|
if delete_path is None:
|
|
# Special case for that rowid table
|
|
delete_path = (
|
|
await ds_write.client.get(
|
|
"/data.json?_shape=arrayfirst&sql=select+rowid+from+{}".format(table)
|
|
)
|
|
).json()[0]
|
|
|
|
delete_response = await ds_write.client.post(
|
|
"/data/{}/{}/-/delete".format(table, delete_path),
|
|
headers=_headers(write_token(ds_write)),
|
|
)
|
|
assert delete_response.status_code == 200
|
|
|
|
# Analytics event
|
|
event = last_event(ds_write)
|
|
assert event.name == "delete-row"
|
|
assert event.database == "data"
|
|
assert event.table == table
|
|
assert event.pks == str(delete_path).split(",")
|
|
assert (
|
|
await ds_write.client.get(
|
|
"/data.json?_shape=arrayfirst&sql=select+count(*)+from+{}".format(table)
|
|
)
|
|
).json() == [0]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"scenario", ("no_token", "no_perm", "bad_table", "cannot_alter")
|
|
)
|
|
async def test_update_row_check_permission(ds_write, scenario):
|
|
if scenario == "no_token":
|
|
token = "bad_token"
|
|
elif scenario == "no_perm":
|
|
token = write_token(ds_write, actor_id="not-root")
|
|
elif scenario == "cannot_alter":
|
|
# update-row but no alter-table:
|
|
token = write_token(ds_write, permissions=["ur"])
|
|
else:
|
|
token = write_token(ds_write)
|
|
|
|
pk = await _insert_row(ds_write)
|
|
|
|
path = "/data/{}/{}/-/update".format(
|
|
"docs" if scenario != "bad_table" else "bad_table", pk
|
|
)
|
|
|
|
json_body = {"update": {"title": "New title"}}
|
|
if scenario == "cannot_alter":
|
|
json_body["alter"] = True
|
|
|
|
response = await ds_write.client.post(
|
|
path,
|
|
json=json_body,
|
|
headers=_headers(token),
|
|
)
|
|
assert response.status_code == 403 if scenario in ("no_token", "bad_token") else 404
|
|
assert response.json()["ok"] is False
|
|
assert (
|
|
response.json()["errors"] == ["Permission denied"]
|
|
if scenario == "no_token"
|
|
else ["Table not found: bad_table"]
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_row_invalid_key(ds_write):
|
|
token = write_token(ds_write)
|
|
|
|
pk = await _insert_row(ds_write)
|
|
|
|
path = "/data/docs/{}/-/update".format(pk)
|
|
response = await ds_write.client.post(
|
|
path,
|
|
json={"update": {"title": "New title"}, "bad_key": 1},
|
|
headers=_headers(token),
|
|
)
|
|
assert response.status_code == 400
|
|
assert response.json() == {"ok": False, "errors": ["Invalid keys: bad_key"]}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_row_alter(ds_write):
|
|
token = write_token(ds_write, permissions=["ur", "at"])
|
|
pk = await _insert_row(ds_write)
|
|
path = "/data/docs/{}/-/update".format(pk)
|
|
response = await ds_write.client.post(
|
|
path,
|
|
json={"update": {"title": "New title", "extra": "extra"}, "alter": True},
|
|
headers=_headers(token),
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json() == {"ok": True}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"input,expected_errors",
|
|
(
|
|
({"title": "New title"}, None),
|
|
({"title": None}, None),
|
|
({"score": 1.6}, None),
|
|
({"age": 10}, None),
|
|
({"title": "New title", "score": 1.6}, None),
|
|
({"title2": "New title"}, ["no such column: title2"]),
|
|
),
|
|
)
|
|
@pytest.mark.parametrize("use_return", (True, False))
|
|
async def test_update_row(ds_write, input, expected_errors, use_return):
|
|
token = write_token(ds_write)
|
|
pk = await _insert_row(ds_write)
|
|
|
|
path = "/data/docs/{}/-/update".format(pk)
|
|
|
|
data = {"update": input}
|
|
if use_return:
|
|
data["return"] = True
|
|
|
|
response = await ds_write.client.post(
|
|
path,
|
|
json=data,
|
|
headers=_headers(token),
|
|
)
|
|
if expected_errors:
|
|
assert response.status_code == 400
|
|
assert response.json()["ok"] is False
|
|
assert response.json()["errors"] == expected_errors
|
|
return
|
|
|
|
assert response.json()["ok"] is True
|
|
if not use_return:
|
|
assert "row" not in response.json()
|
|
else:
|
|
returned_row = response.json()["row"]
|
|
assert returned_row["id"] == pk
|
|
for k, v in input.items():
|
|
assert returned_row[k] == v
|
|
|
|
# Analytics event
|
|
event = last_event(ds_write)
|
|
assert event.actor == {"id": "root", "token": "dstok"}
|
|
assert event.database == "data"
|
|
assert event.table == "docs"
|
|
assert event.pks == [str(pk)]
|
|
|
|
# And fetch the row to check it's updated
|
|
response = await ds_write.client.get(
|
|
"/data/docs/{}.json?_shape=array".format(pk),
|
|
)
|
|
assert response.status_code == 200
|
|
row = response.json()[0]
|
|
assert row["id"] == pk
|
|
for k, v in input.items():
|
|
assert row[k] == v
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"scenario", ("no_token", "no_perm", "bad_table", "has_perm", "immutable")
|
|
)
|
|
async def test_drop_table(ds_write, scenario):
|
|
if scenario == "no_token":
|
|
token = "bad_token"
|
|
elif scenario == "no_perm":
|
|
token = write_token(ds_write, actor_id="not-root")
|
|
else:
|
|
token = write_token(ds_write)
|
|
should_work = scenario == "has_perm"
|
|
await ds_write.get_database("data").execute_write(
|
|
"insert into docs (id, title) values (1, 'Row 1')"
|
|
)
|
|
path = "/{database}/{table}/-/drop".format(
|
|
database="immutable" if scenario == "immutable" else "data",
|
|
table="docs" if scenario != "bad_table" else "bad_table",
|
|
)
|
|
response = await ds_write.client.post(
|
|
path,
|
|
headers=_headers(token),
|
|
)
|
|
if not should_work:
|
|
assert (
|
|
response.status_code == 403
|
|
if scenario in ("no_token", "bad_token")
|
|
else 404
|
|
)
|
|
assert response.json()["ok"] is False
|
|
expected_error = "Permission denied"
|
|
if scenario == "bad_table":
|
|
expected_error = "Table not found: bad_table"
|
|
elif scenario == "immutable":
|
|
expected_error = "Database is immutable"
|
|
assert response.json()["errors"] == [expected_error]
|
|
assert (await ds_write.client.get("/data/docs")).status_code == 200
|
|
else:
|
|
# It should show a confirmation page
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"ok": True,
|
|
"database": "data",
|
|
"table": "docs",
|
|
"row_count": 1,
|
|
"message": 'Pass "confirm": true to confirm',
|
|
}
|
|
assert (await ds_write.client.get("/data/docs")).status_code == 200
|
|
# Now send confirm: true
|
|
response2 = await ds_write.client.post(
|
|
path,
|
|
json={"confirm": True},
|
|
headers=_headers(token),
|
|
)
|
|
assert response2.json() == {"ok": True}
|
|
# Check event
|
|
event = last_event(ds_write)
|
|
assert event.name == "drop-table"
|
|
assert event.actor == {"id": "root", "token": "dstok"}
|
|
assert event.table == "docs"
|
|
assert event.database == "data"
|
|
# Table should 404
|
|
assert (await ds_write.client.get("/data/docs")).status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"input,expected_status,expected_response,expected_events",
|
|
(
|
|
# Permission error with a bad token
|
|
(
|
|
{"table": "bad", "row": {"id": 1}},
|
|
403,
|
|
{"ok": False, "errors": ["Permission denied"]},
|
|
[],
|
|
),
|
|
# Successful creation with columns:
|
|
(
|
|
{
|
|
"table": "one",
|
|
"columns": [
|
|
{
|
|
"name": "id",
|
|
"type": "integer",
|
|
},
|
|
{
|
|
"name": "title",
|
|
"type": "text",
|
|
},
|
|
{
|
|
"name": "score",
|
|
"type": "integer",
|
|
},
|
|
{
|
|
"name": "weight",
|
|
"type": "float",
|
|
},
|
|
{
|
|
"name": "thumbnail",
|
|
"type": "blob",
|
|
},
|
|
],
|
|
"pk": "id",
|
|
},
|
|
201,
|
|
{
|
|
"ok": True,
|
|
"database": "data",
|
|
"table": "one",
|
|
"table_url": "http://localhost/data/one",
|
|
"table_api_url": "http://localhost/data/one.json",
|
|
"schema": (
|
|
"CREATE TABLE [one] (\n"
|
|
" [id] INTEGER PRIMARY KEY,\n"
|
|
" [title] TEXT,\n"
|
|
" [score] INTEGER,\n"
|
|
" [weight] FLOAT,\n"
|
|
" [thumbnail] BLOB\n"
|
|
")"
|
|
),
|
|
},
|
|
["create-table"],
|
|
),
|
|
# Successful creation with rows:
|
|
(
|
|
{
|
|
"table": "two",
|
|
"rows": [
|
|
{
|
|
"id": 1,
|
|
"title": "Row 1",
|
|
"score": 1.5,
|
|
},
|
|
{
|
|
"id": 2,
|
|
"title": "Row 2",
|
|
"score": 1.5,
|
|
},
|
|
],
|
|
"pk": "id",
|
|
},
|
|
201,
|
|
{
|
|
"ok": True,
|
|
"database": "data",
|
|
"table": "two",
|
|
"table_url": "http://localhost/data/two",
|
|
"table_api_url": "http://localhost/data/two.json",
|
|
"schema": (
|
|
"CREATE TABLE [two] (\n"
|
|
" [id] INTEGER PRIMARY KEY,\n"
|
|
" [title] TEXT,\n"
|
|
" [score] FLOAT\n"
|
|
")"
|
|
),
|
|
"row_count": 2,
|
|
},
|
|
["create-table", "insert-rows"],
|
|
),
|
|
# Successful creation with row:
|
|
(
|
|
{
|
|
"table": "three",
|
|
"row": {
|
|
"id": 1,
|
|
"title": "Row 1",
|
|
"score": 1.5,
|
|
},
|
|
"pk": "id",
|
|
},
|
|
201,
|
|
{
|
|
"ok": True,
|
|
"database": "data",
|
|
"table": "three",
|
|
"table_url": "http://localhost/data/three",
|
|
"table_api_url": "http://localhost/data/three.json",
|
|
"schema": (
|
|
"CREATE TABLE [three] (\n"
|
|
" [id] INTEGER PRIMARY KEY,\n"
|
|
" [title] TEXT,\n"
|
|
" [score] FLOAT\n"
|
|
")"
|
|
),
|
|
"row_count": 1,
|
|
},
|
|
["create-table", "insert-rows"],
|
|
),
|
|
# Create with row and no primary key
|
|
(
|
|
{
|
|
"table": "four",
|
|
"row": {
|
|
"name": "Row 1",
|
|
},
|
|
},
|
|
201,
|
|
{
|
|
"ok": True,
|
|
"database": "data",
|
|
"table": "four",
|
|
"table_url": "http://localhost/data/four",
|
|
"table_api_url": "http://localhost/data/four.json",
|
|
"schema": ("CREATE TABLE [four] (\n" " [name] TEXT\n" ")"),
|
|
"row_count": 1,
|
|
},
|
|
["create-table", "insert-rows"],
|
|
),
|
|
# Create table with compound primary key
|
|
(
|
|
{
|
|
"table": "five",
|
|
"row": {"type": "article", "key": 123, "title": "Article 1"},
|
|
"pks": ["type", "key"],
|
|
},
|
|
201,
|
|
{
|
|
"ok": True,
|
|
"database": "data",
|
|
"table": "five",
|
|
"table_url": "http://localhost/data/five",
|
|
"table_api_url": "http://localhost/data/five.json",
|
|
"schema": (
|
|
"CREATE TABLE [five] (\n [type] TEXT,\n [key] INTEGER,\n"
|
|
" [title] TEXT,\n PRIMARY KEY ([type], [key])\n)"
|
|
),
|
|
"row_count": 1,
|
|
},
|
|
["create-table", "insert-rows"],
|
|
),
|
|
# Error: Table is required
|
|
(
|
|
{
|
|
"row": {"id": 1},
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["Table is required"],
|
|
},
|
|
[],
|
|
),
|
|
# Error: Invalid table name
|
|
(
|
|
{
|
|
"table": "sqlite_bad_name",
|
|
"row": {"id": 1},
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["Invalid table name"],
|
|
},
|
|
[],
|
|
),
|
|
# Error: JSON must be an object
|
|
(
|
|
[],
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["JSON must be an object"],
|
|
},
|
|
[],
|
|
),
|
|
# Error: Cannot specify columns with rows or row
|
|
(
|
|
{
|
|
"table": "bad",
|
|
"columns": [{"name": "id", "type": "integer"}],
|
|
"rows": [{"id": 1}],
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["Cannot specify columns with rows or row"],
|
|
},
|
|
[],
|
|
),
|
|
# Error: columns, rows or row is required
|
|
(
|
|
{
|
|
"table": "bad",
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["columns, rows or row is required"],
|
|
},
|
|
[],
|
|
),
|
|
# Error: columns must be a list
|
|
(
|
|
{
|
|
"table": "bad",
|
|
"columns": {"name": "id", "type": "integer"},
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["columns must be a list"],
|
|
},
|
|
[],
|
|
),
|
|
# Error: columns must be a list of objects
|
|
(
|
|
{
|
|
"table": "bad",
|
|
"columns": ["id"],
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["columns must be a list of objects"],
|
|
},
|
|
[],
|
|
),
|
|
# Error: Column name is required
|
|
(
|
|
{
|
|
"table": "bad",
|
|
"columns": [{"type": "integer"}],
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["Column name is required"],
|
|
},
|
|
[],
|
|
),
|
|
# Error: Unsupported column type
|
|
(
|
|
{
|
|
"table": "bad",
|
|
"columns": [{"name": "id", "type": "bad"}],
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["Unsupported column type: bad"],
|
|
},
|
|
[],
|
|
),
|
|
# Error: Duplicate column name
|
|
(
|
|
{
|
|
"table": "bad",
|
|
"columns": [
|
|
{"name": "id", "type": "integer"},
|
|
{"name": "id", "type": "integer"},
|
|
],
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["Duplicate column name: id"],
|
|
},
|
|
[],
|
|
),
|
|
# Error: rows must be a list
|
|
(
|
|
{
|
|
"table": "bad",
|
|
"rows": {"id": 1},
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["rows must be a list"],
|
|
},
|
|
[],
|
|
),
|
|
# Error: rows must be a list of objects
|
|
(
|
|
{
|
|
"table": "bad",
|
|
"rows": ["id"],
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["rows must be a list of objects"],
|
|
},
|
|
[],
|
|
),
|
|
# Error: pk must be a string
|
|
(
|
|
{
|
|
"table": "bad",
|
|
"row": {"id": 1},
|
|
"pk": 1,
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["pk must be a string"],
|
|
},
|
|
[],
|
|
),
|
|
# Error: Cannot specify both pk and pks
|
|
(
|
|
{
|
|
"table": "bad",
|
|
"row": {"id": 1, "name": "Row 1"},
|
|
"pk": "id",
|
|
"pks": ["id", "name"],
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["Cannot specify both pk and pks"],
|
|
},
|
|
[],
|
|
),
|
|
# Error: pks must be a list
|
|
(
|
|
{
|
|
"table": "bad",
|
|
"row": {"id": 1, "name": "Row 1"},
|
|
"pks": "id",
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["pks must be a list"],
|
|
},
|
|
[],
|
|
),
|
|
# Error: pks must be a list of strings
|
|
(
|
|
{"table": "bad", "row": {"id": 1, "name": "Row 1"}, "pks": [1, 2]},
|
|
400,
|
|
{"ok": False, "errors": ["pks must be a list of strings"]},
|
|
[],
|
|
),
|
|
# Error: ignore and replace are mutually exclusive
|
|
(
|
|
{
|
|
"table": "bad",
|
|
"row": {"id": 1, "name": "Row 1"},
|
|
"pk": "id",
|
|
"ignore": True,
|
|
"replace": True,
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["ignore and replace are mutually exclusive"],
|
|
},
|
|
[],
|
|
),
|
|
# ignore and replace require row or rows
|
|
(
|
|
{
|
|
"table": "bad",
|
|
"columns": [{"name": "id", "type": "integer"}],
|
|
"ignore": True,
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["ignore and replace require row or rows"],
|
|
},
|
|
[],
|
|
),
|
|
# ignore and replace require pk or pks
|
|
(
|
|
{
|
|
"table": "bad",
|
|
"row": {"id": 1},
|
|
"ignore": True,
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["ignore and replace require pk or pks"],
|
|
},
|
|
[],
|
|
),
|
|
(
|
|
{
|
|
"table": "bad",
|
|
"row": {"id": 1},
|
|
"replace": True,
|
|
},
|
|
400,
|
|
{
|
|
"ok": False,
|
|
"errors": ["ignore and replace require pk or pks"],
|
|
},
|
|
[],
|
|
),
|
|
),
|
|
)
|
|
async def test_create_table(
|
|
ds_write, input, expected_status, expected_response, expected_events
|
|
):
|
|
ds_write._tracked_events = []
|
|
# Special case for expected status of 403
|
|
if expected_status == 403:
|
|
token = "bad_token"
|
|
else:
|
|
token = write_token(ds_write)
|
|
response = await ds_write.client.post(
|
|
"/data/-/create",
|
|
json=input,
|
|
headers=_headers(token),
|
|
)
|
|
assert response.status_code == expected_status
|
|
data = response.json()
|
|
assert data == expected_response
|
|
# Should have tracked the expected events
|
|
events = ds_write._tracked_events
|
|
assert [e.name for e in events] == expected_events
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"permissions,body,expected_status,expected_errors",
|
|
(
|
|
(["create-table"], {"table": "t", "columns": [{"name": "c"}]}, 201, None),
|
|
# Need insert-row too if you use "rows":
|
|
(
|
|
["create-table"],
|
|
{"table": "t", "rows": [{"name": "c"}]},
|
|
403,
|
|
["Permission denied: need insert-row"],
|
|
),
|
|
# This should work:
|
|
(
|
|
["create-table", "insert-row"],
|
|
{"table": "t", "rows": [{"name": "c"}]},
|
|
201,
|
|
None,
|
|
),
|
|
# If you use replace: true you need update-row too:
|
|
(
|
|
["create-table", "insert-row"],
|
|
{"table": "t", "rows": [{"id": 1}], "pk": "id", "replace": True},
|
|
403,
|
|
["Permission denied: need update-row"],
|
|
),
|
|
),
|
|
)
|
|
async def test_create_table_permissions(
|
|
ds_write, permissions, body, expected_status, expected_errors
|
|
):
|
|
token = ds_write.create_token("root", restrict_all=["view-instance"] + permissions)
|
|
response = await ds_write.client.post(
|
|
"/data/-/create",
|
|
json=body,
|
|
headers=_headers(token),
|
|
)
|
|
assert response.status_code == expected_status
|
|
if expected_errors:
|
|
data = response.json()
|
|
assert data["ok"] is False
|
|
assert data["errors"] == expected_errors
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"input,expected_rows_after",
|
|
(
|
|
(
|
|
{
|
|
"table": "test_insert_replace",
|
|
"rows": [
|
|
{"id": 1, "name": "Row 1 new"},
|
|
{"id": 3, "name": "Row 3 new"},
|
|
],
|
|
"pk": "id",
|
|
"ignore": True,
|
|
},
|
|
[
|
|
{"id": 1, "name": "Row 1"},
|
|
{"id": 2, "name": "Row 2"},
|
|
{"id": 3, "name": "Row 3 new"},
|
|
],
|
|
),
|
|
(
|
|
{
|
|
"table": "test_insert_replace",
|
|
"rows": [
|
|
{"id": 1, "name": "Row 1 new"},
|
|
{"id": 3, "name": "Row 3 new"},
|
|
],
|
|
"pk": "id",
|
|
"replace": True,
|
|
},
|
|
[
|
|
{"id": 1, "name": "Row 1 new"},
|
|
{"id": 2, "name": "Row 2"},
|
|
{"id": 3, "name": "Row 3 new"},
|
|
],
|
|
),
|
|
),
|
|
)
|
|
async def test_create_table_ignore_replace(ds_write, input, expected_rows_after):
|
|
# Create table with two rows
|
|
token = write_token(ds_write)
|
|
first_response = await ds_write.client.post(
|
|
"/data/-/create",
|
|
json={
|
|
"rows": [{"id": 1, "name": "Row 1"}, {"id": 2, "name": "Row 2"}],
|
|
"table": "test_insert_replace",
|
|
"pk": "id",
|
|
},
|
|
headers=_headers(token),
|
|
)
|
|
assert first_response.status_code == 201
|
|
|
|
ds_write._tracked_events = []
|
|
|
|
# Try a second time
|
|
second_response = await ds_write.client.post(
|
|
"/data/-/create",
|
|
json=input,
|
|
headers=_headers(token),
|
|
)
|
|
assert second_response.status_code == 201
|
|
# Check that the rows are as expected
|
|
rows = await ds_write.client.get("/data/test_insert_replace.json?_shape=array")
|
|
assert rows.json() == expected_rows_after
|
|
|
|
# Check it fired the right events
|
|
event_names = [e.name for e in ds_write._tracked_events]
|
|
assert event_names == ["insert-rows"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_table_error_if_pk_changed(ds_write):
|
|
token = write_token(ds_write)
|
|
first_response = await ds_write.client.post(
|
|
"/data/-/create",
|
|
json={
|
|
"rows": [{"id": 1, "name": "Row 1"}, {"id": 2, "name": "Row 2"}],
|
|
"table": "test_insert_replace",
|
|
"pk": "id",
|
|
},
|
|
headers=_headers(token),
|
|
)
|
|
assert first_response.status_code == 201
|
|
# Try a second time with a different pk
|
|
second_response = await ds_write.client.post(
|
|
"/data/-/create",
|
|
json={
|
|
"rows": [{"id": 1, "name": "Row 1"}, {"id": 2, "name": "Row 2"}],
|
|
"table": "test_insert_replace",
|
|
"pk": "name",
|
|
"replace": True,
|
|
},
|
|
headers=_headers(token),
|
|
)
|
|
assert second_response.status_code == 400
|
|
assert second_response.json() == {
|
|
"ok": False,
|
|
"errors": ["pk cannot be changed for existing table"],
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_table_error_rows_twice_with_duplicates(ds_write):
|
|
# Error if you don't send ignore: True or replace: True
|
|
token = write_token(ds_write)
|
|
input = {
|
|
"rows": [{"id": 1, "name": "Row 1"}, {"id": 2, "name": "Row 2"}],
|
|
"table": "test_create_twice",
|
|
"pk": "id",
|
|
}
|
|
first_response = await ds_write.client.post(
|
|
"/data/-/create",
|
|
json=input,
|
|
headers=_headers(token),
|
|
)
|
|
assert first_response.status_code == 201
|
|
second_response = await ds_write.client.post(
|
|
"/data/-/create",
|
|
json=input,
|
|
headers=_headers(token),
|
|
)
|
|
assert second_response.status_code == 400
|
|
assert second_response.json() == {
|
|
"ok": False,
|
|
"errors": ["UNIQUE constraint failed: test_create_twice.id"],
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
(
|
|
"/data/-/create",
|
|
"/data/docs/-/drop",
|
|
"/data/docs/-/insert",
|
|
),
|
|
)
|
|
async def test_method_not_allowed(ds_write, path):
|
|
response = await ds_write.client.get(
|
|
path,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
},
|
|
)
|
|
assert response.status_code == 405
|
|
assert response.json() == {
|
|
"ok": False,
|
|
"error": "Method not allowed",
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_uses_alter_by_default_for_new_table(ds_write):
|
|
ds_write._tracked_events = []
|
|
token = write_token(ds_write)
|
|
response = await ds_write.client.post(
|
|
"/data/-/create",
|
|
json={
|
|
"table": "new_table",
|
|
"rows": [
|
|
{
|
|
"name": "Row 1",
|
|
}
|
|
]
|
|
* 100
|
|
+ [
|
|
{"name": "Row 2", "extra": "Extra"},
|
|
],
|
|
"pk": "id",
|
|
},
|
|
headers=_headers(token),
|
|
)
|
|
assert response.status_code == 201
|
|
event_names = [e.name for e in ds_write._tracked_events]
|
|
assert event_names == ["create-table", "insert-rows"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("has_alter_permission", (True, False))
|
|
async def test_create_using_alter_against_existing_table(
|
|
ds_write, has_alter_permission
|
|
):
|
|
token = write_token(
|
|
ds_write, permissions=["ir", "ct"] + (["at"] if has_alter_permission else [])
|
|
)
|
|
# First create the table
|
|
response = await ds_write.client.post(
|
|
"/data/-/create",
|
|
json={
|
|
"table": "new_table",
|
|
"rows": [
|
|
{
|
|
"name": "Row 1",
|
|
}
|
|
],
|
|
"pk": "id",
|
|
},
|
|
headers=_headers(token),
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
ds_write._tracked_events = []
|
|
# Now try to insert more rows using /-/create with alter=True
|
|
response2 = await ds_write.client.post(
|
|
"/data/-/create",
|
|
json={
|
|
"table": "new_table",
|
|
"rows": [{"name": "Row 2", "extra": "extra"}],
|
|
"pk": "id",
|
|
"alter": True,
|
|
},
|
|
headers=_headers(token),
|
|
)
|
|
if not has_alter_permission:
|
|
assert response2.status_code == 403
|
|
assert response2.json() == {
|
|
"ok": False,
|
|
"errors": ["Permission denied: need alter-table"],
|
|
}
|
|
else:
|
|
assert response2.status_code == 201
|
|
|
|
event_names = [e.name for e in ds_write._tracked_events]
|
|
assert event_names == ["alter-table", "insert-rows"]
|
|
|
|
# It should have altered the table
|
|
alter_event = ds_write._tracked_events[0]
|
|
assert alter_event.name == "alter-table"
|
|
assert "extra" not in alter_event.before_schema
|
|
assert "extra" in alter_event.after_schema
|
|
|
|
insert_rows_event = ds_write._tracked_events[1]
|
|
assert insert_rows_event.name == "insert-rows"
|
|
assert insert_rows_event.num_rows == 1
|