datasette/tests/test_load_extensions.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

107 lines
3.9 KiB
Python
Raw Normal View History

from pathlib import Path
from unittest import mock
import pytest
from datasette.app import Datasette
# not necessarily a full path - the full compiled path looks like "ext.dylib"
# or another suffix, but sqlite will, under the hood, decide which file
# extension to use based on the operating system (apple=dylib, windows=dll etc)
# this resolves to "./ext", which is enough for SQLite to calculate the rest
COMPILED_EXTENSION_PATH = str(Path(__file__).parent / "ext")
# See if ext.c has been compiled, based off the different possible suffixes.
def has_compiled_ext():
for ext in ["dylib", "so", "dll"]:
path = Path(__file__).parent / f"ext.{ext}"
if path.is_file():
return True
return False
@pytest.mark.parametrize("load_fails", (False, True))
def test_load_extension_is_disabled(load_fails):
ds = Datasette(sqlite_extensions=[COMPILED_EXTENSION_PATH])
connection = mock.Mock()
if load_fails:
connection.load_extension.side_effect = RuntimeError
if load_fails:
with pytest.raises(RuntimeError):
ds._prepare_connection(connection, "data")
else:
ds._prepare_connection(connection, "data")
# Extensions are loaded using the Python API, never via SQL
assert connection.load_extension.mock_calls == [
mock.call(COMPILED_EXTENSION_PATH),
]
assert connection.enable_load_extension.mock_calls == [
mock.call(True),
mock.call(False),
]
@pytest.mark.asyncio
@pytest.mark.skipif(not has_compiled_ext(), reason="Requires compiled ext.c")
async def test_load_extension_default_entrypoint():
# The default entrypoint only loads a() and NOT b() or c(), so those
# should fail.
ds = Datasette(sqlite_extensions=[COMPILED_EXTENSION_PATH])
response = await ds.client.get("/_memory/-/query.json?_shape=arrays&sql=select+a()")
assert response.status_code == 200
assert response.json()["rows"][0][0] == "a"
response = await ds.client.get("/_memory/-/query.json?_shape=arrays&sql=select+b()")
assert response.status_code == 400
assert response.json()["error"] == "no such function: b"
response = await ds.client.get("/_memory/-/query.json?_shape=arrays&sql=select+c()")
assert response.status_code == 400
assert response.json()["error"] == "no such function: c"
@pytest.mark.asyncio
@pytest.mark.skipif(not has_compiled_ext(), reason="Requires compiled ext.c")
async def test_load_extension_multiple_entrypoints():
# Load in the default entrypoint and the other 2 custom entrypoints, now
# all a(), b(), and c() should run successfully.
ds = Datasette(
sqlite_extensions=[
COMPILED_EXTENSION_PATH,
(COMPILED_EXTENSION_PATH, "sqlite3_ext_b_init"),
(COMPILED_EXTENSION_PATH, "sqlite3_ext_c_init"),
]
)
response = await ds.client.get("/_memory/-/query.json?_shape=arrays&sql=select+a()")
assert response.status_code == 200
assert response.json()["rows"][0][0] == "a"
response = await ds.client.get("/_memory/-/query.json?_shape=arrays&sql=select+b()")
assert response.status_code == 200
assert response.json()["rows"][0][0] == "b"
response = await ds.client.get("/_memory/-/query.json?_shape=arrays&sql=select+c()")
assert response.status_code == 200
assert response.json()["rows"][0][0] == "c"
@pytest.mark.asyncio
@pytest.mark.skipif(not has_compiled_ext(), reason="Requires compiled ext.c")
async def test_sql_cannot_load_additional_extension():
ds = Datasette(sqlite_extensions=[COMPILED_EXTENSION_PATH])
response = await ds.client.get(
"/_memory/-/query.json",
params={
"sql": "select load_extension(:path, :entrypoint)",
"path": COMPILED_EXTENSION_PATH,
"entrypoint": "sqlite3_ext_b_init",
},
)
assert response.status_code == 400
assert response.json()["error"] == "not authorized"