mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
315 lines
11 KiB
Python
315 lines
11 KiB
Python
import httpx
|
|
import pytest
|
|
import pytest_asyncio
|
|
from datasette.app import Datasette
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def datasette(ds_client):
|
|
await ds_client.ds.invoke_startup()
|
|
return ds_client.ds
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def datasette_with_permissions():
|
|
"""A datasette instance with permission restrictions for testing"""
|
|
ds = Datasette(config={"databases": {"test_db": {"allow": {"id": "admin"}}}})
|
|
await ds.invoke_startup()
|
|
db = ds.add_memory_database("test_datasette_with_permissions", name="test_db")
|
|
await db.execute_write(
|
|
"create table if not exists test_table (id integer primary key, name text)"
|
|
)
|
|
await db.execute_write(
|
|
"insert or ignore into test_table (id, name) values (1, 'Alice')"
|
|
)
|
|
# Trigger catalog refresh
|
|
await ds.client.get("/")
|
|
return ds
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"method,path,expected_status",
|
|
[
|
|
("get", "/", 200),
|
|
("options", "/", 200),
|
|
("head", "/", 200),
|
|
("put", "/", 405),
|
|
("patch", "/", 405),
|
|
("delete", "/", 405),
|
|
],
|
|
)
|
|
async def test_client_methods(datasette, method, path, expected_status):
|
|
client_method = getattr(datasette.client, method)
|
|
response = await client_method(path)
|
|
assert isinstance(response, httpx.Response)
|
|
assert response.status_code == expected_status
|
|
# Try that again using datasette.client.request
|
|
response2 = await datasette.client.request(method, path)
|
|
assert response2.status_code == expected_status
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("prefix", [None, "/prefix/"])
|
|
async def test_client_post(datasette, prefix):
|
|
original_base_url = datasette._settings["base_url"]
|
|
try:
|
|
if prefix is not None:
|
|
datasette._settings["base_url"] = prefix
|
|
response = await datasette.client.post(
|
|
"/-/messages",
|
|
data={
|
|
"message": "A message",
|
|
},
|
|
)
|
|
assert isinstance(response, httpx.Response)
|
|
assert response.status_code == 302
|
|
assert "ds_messages" in response.cookies
|
|
finally:
|
|
datasette._settings["base_url"] = original_base_url
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"prefix,expected_path", [(None, "/asgi-scope"), ("/prefix/", "/prefix/asgi-scope")]
|
|
)
|
|
async def test_client_path(datasette, prefix, expected_path):
|
|
original_base_url = datasette._settings["base_url"]
|
|
try:
|
|
if prefix is not None:
|
|
datasette._settings["base_url"] = prefix
|
|
response = await datasette.client.get("/asgi-scope")
|
|
path = response.json()["path"]
|
|
assert path == expected_path
|
|
finally:
|
|
datasette._settings["base_url"] = original_base_url
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skip_permission_checks_allows_forbidden_access(
|
|
datasette_with_permissions,
|
|
):
|
|
"""Test that skip_permission_checks=True bypasses permission checks"""
|
|
ds = datasette_with_permissions
|
|
|
|
# Without skip_permission_checks, anonymous user should get 403 for protected database
|
|
response = await ds.client.get("/test_db.json")
|
|
assert response.status_code == 403
|
|
|
|
# With skip_permission_checks=True, should get 200
|
|
response = await ds.client.get("/test_db.json", skip_permission_checks=True)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["database"] == "test_db"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skip_permission_checks_on_table(datasette_with_permissions):
|
|
"""Test skip_permission_checks works for table access"""
|
|
ds = datasette_with_permissions
|
|
|
|
# Without skip_permission_checks, should get 403
|
|
response = await ds.client.get("/test_db/test_table.json")
|
|
assert response.status_code == 403
|
|
|
|
# With skip_permission_checks=True, should get table data
|
|
response = await ds.client.get(
|
|
"/test_db/test_table.json", skip_permission_checks=True
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["rows"] == [{"id": 1, "name": "Alice"}]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"method", ["get", "post", "put", "patch", "delete", "options", "head"]
|
|
)
|
|
async def test_skip_permission_checks_all_methods(datasette_with_permissions, method):
|
|
"""Test that skip_permission_checks works with all HTTP methods"""
|
|
ds = datasette_with_permissions
|
|
|
|
# All methods should work with skip_permission_checks=True
|
|
client_method = getattr(ds.client, method)
|
|
response = await client_method("/test_db.json", skip_permission_checks=True)
|
|
# We don't check status code since some methods might not be allowed,
|
|
# but we verify the request doesn't fail due to permissions
|
|
assert isinstance(response, httpx.Response)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skip_permission_checks_request_method(datasette_with_permissions):
|
|
"""Test that skip_permission_checks works with client.request()"""
|
|
ds = datasette_with_permissions
|
|
|
|
# Without skip_permission_checks
|
|
response = await ds.client.request("GET", "/test_db.json")
|
|
assert response.status_code == 403
|
|
|
|
# With skip_permission_checks=True
|
|
response = await ds.client.request(
|
|
"GET", "/test_db.json", skip_permission_checks=True
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skip_permission_checks_isolated_to_request(datasette_with_permissions):
|
|
"""Test that skip_permission_checks doesn't affect other concurrent requests"""
|
|
ds = datasette_with_permissions
|
|
|
|
# First request with skip_permission_checks=True should succeed
|
|
response1 = await ds.client.get("/test_db.json", skip_permission_checks=True)
|
|
assert response1.status_code == 200
|
|
|
|
# Subsequent request without it should still get 403
|
|
response2 = await ds.client.get("/test_db.json")
|
|
assert response2.status_code == 403
|
|
|
|
# And another with skip should succeed again
|
|
response3 = await ds.client.get("/test_db.json", skip_permission_checks=True)
|
|
assert response3.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skip_permission_checks_with_admin_actor(datasette_with_permissions):
|
|
"""Test that skip_permission_checks works even when actor is provided"""
|
|
ds = datasette_with_permissions
|
|
|
|
# Admin actor should normally have access
|
|
admin_cookies = {"ds_actor": ds.client.actor_cookie({"id": "admin"})}
|
|
response = await ds.client.get("/test_db.json", cookies=admin_cookies)
|
|
assert response.status_code == 200
|
|
|
|
# Non-admin actor should get 403
|
|
user_cookies = {"ds_actor": ds.client.actor_cookie({"id": "user"})}
|
|
response = await ds.client.get("/test_db.json", cookies=user_cookies)
|
|
assert response.status_code == 403
|
|
|
|
# Non-admin actor with skip_permission_checks=True should get 200
|
|
response = await ds.client.get(
|
|
"/test_db.json", cookies=user_cookies, skip_permission_checks=True
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skip_permission_checks_shows_denied_tables():
|
|
"""Test that skip_permission_checks=True shows tables from denied databases in /-/tables.json"""
|
|
ds = Datasette(
|
|
config={
|
|
"databases": {
|
|
"fixtures": {"allow": False} # Deny all access to this database
|
|
}
|
|
}
|
|
)
|
|
await ds.invoke_startup()
|
|
db = ds.add_memory_database("fixtures")
|
|
await db.execute_write(
|
|
"CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)"
|
|
)
|
|
await db.execute_write("INSERT INTO test_table (id, name) VALUES (1, 'Alice')")
|
|
await ds._refresh_schemas()
|
|
|
|
# Without skip_permission_checks, tables from denied database should not appear in /-/tables.json
|
|
response = await ds.client.get("/-/tables.json")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
table_names = [match["name"] for match in data["matches"]]
|
|
# Should not see any fixtures tables since access is denied
|
|
fixtures_tables = [name for name in table_names if name.startswith("fixtures:")]
|
|
assert len(fixtures_tables) == 0
|
|
|
|
# With skip_permission_checks=True, tables from denied database SHOULD appear
|
|
response = await ds.client.get("/-/tables.json", skip_permission_checks=True)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
table_names = [match["name"] for match in data["matches"]]
|
|
# Should see fixtures tables when permission checks are skipped
|
|
assert "fixtures: test_table" in table_names
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_in_client_returns_false_outside_request(datasette):
|
|
"""Test that datasette.in_client() returns False outside of a client request"""
|
|
assert datasette.in_client() is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_in_client_returns_true_inside_request():
|
|
"""Test that datasette.in_client() returns True inside a client request"""
|
|
from datasette import hookimpl, Response
|
|
from datasette.plugins import pm
|
|
|
|
class TestPlugin:
|
|
__name__ = "test_in_client_plugin"
|
|
|
|
@hookimpl
|
|
def register_routes(self):
|
|
async def test_view(datasette):
|
|
# Assert in_client() returns True within the view
|
|
assert datasette.in_client() is True
|
|
return Response.json({"in_client": datasette.in_client()})
|
|
|
|
return [
|
|
(r"^/-/test-in-client$", test_view),
|
|
]
|
|
|
|
pm.register(TestPlugin(), name="test_in_client_plugin")
|
|
try:
|
|
ds = Datasette()
|
|
await ds.invoke_startup()
|
|
|
|
# Outside of a client request, should be False
|
|
assert ds.in_client() is False
|
|
|
|
# Make a request via datasette.client
|
|
response = await ds.client.get("/-/test-in-client")
|
|
assert response.status_code == 200
|
|
assert response.json()["in_client"] is True
|
|
|
|
# After the request, should be False again
|
|
assert ds.in_client() is False
|
|
finally:
|
|
pm.unregister(name="test_in_client_plugin")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_in_client_with_skip_permission_checks():
|
|
"""Test that in_client() works regardless of skip_permission_checks value"""
|
|
from datasette import hookimpl
|
|
from datasette.plugins import pm
|
|
from datasette.utils.asgi import Response
|
|
|
|
in_client_values = []
|
|
|
|
class TestPlugin:
|
|
__name__ = "test_in_client_skip_plugin"
|
|
|
|
@hookimpl
|
|
def register_routes(self):
|
|
async def test_view(datasette):
|
|
in_client_values.append(datasette.in_client())
|
|
return Response.json({"in_client": datasette.in_client()})
|
|
|
|
return [
|
|
(r"^/-/test-in-client$", test_view),
|
|
]
|
|
|
|
pm.register(TestPlugin(), name="test_in_client_skip_plugin")
|
|
try:
|
|
ds = Datasette(config={"databases": {"test_db": {"allow": {"id": "admin"}}}})
|
|
await ds.invoke_startup()
|
|
|
|
# Request without skip_permission_checks
|
|
await ds.client.get("/-/test-in-client")
|
|
# Request with skip_permission_checks=True
|
|
await ds.client.get("/-/test-in-client", skip_permission_checks=True)
|
|
|
|
# Both should have detected in_client as True
|
|
assert (
|
|
len(in_client_values) == 2
|
|
), f"Expected 2 values, got {len(in_client_values)}"
|
|
assert all(in_client_values), f"Expected all True, got {in_client_values}"
|
|
finally:
|
|
pm.unregister(name="test_in_client_skip_plugin")
|