2018-05-28 20:41:53 +00:00
from bs4 import BeautifulSoup as Soup
2020-04-05 18:28:20 +00:00
from . fixtures import (
2022-12-16 17:25:37 +00:00
app_client ,
2020-04-05 18:28:20 +00:00
app_client ,
make_app_client ,
TABLES ,
TEMP_PLUGIN_SECRET_FILE ,
2022-12-13 02:05:54 +00:00
PLUGINS_DIR ,
2020-05-04 17:40:01 +00:00
TestClient as _TestClient ,
2020-04-05 18:28:20 +00:00
) # noqa
2021-08-28 01:39:42 +00:00
from click . testing import CliRunner
2020-04-05 18:28:20 +00:00
from datasette . app import Datasette
2022-12-13 02:05:54 +00:00
from datasette import cli , hookimpl , Permission
2021-12-17 19:02:14 +00:00
from datasette . filters import FilterArguments
2020-05-27 20:16:02 +00:00
from datasette . plugins import get_plugins , DEFAULT_PLUGINS , pm
2020-11-30 21:29:57 +00:00
from datasette . utils . sqlite import sqlite3
2022-12-13 02:05:54 +00:00
from datasette . utils import CustomRow , StartupError
2020-05-28 03:13:32 +00:00
from jinja2 . environment import Template
2018-08-28 10:56:57 +00:00
import base64
2021-08-28 01:39:42 +00:00
import importlib
2018-08-28 08:56:44 +00:00
import json
2019-07-04 05:36:44 +00:00
import os
2019-07-06 00:05:56 +00:00
import pathlib
2018-08-28 08:56:44 +00:00
import re
2020-04-05 18:28:20 +00:00
import textwrap
2018-05-28 20:41:53 +00:00
import pytest
2018-08-05 00:14:56 +00:00
import urllib
2018-05-28 20:41:53 +00:00
2020-05-28 02:21:41 +00:00
at_memory_re = re . compile ( r " at 0x \ w+ " )
2018-05-28 20:41:53 +00:00
2020-05-27 20:16:02 +00:00
@pytest.mark.parametrize (
" plugin_hook " , [ name for name in dir ( pm . hook ) if not name . startswith ( " _ " ) ]
)
def test_plugin_hooks_have_tests ( plugin_hook ) :
2020-12-23 17:04:32 +00:00
""" Every plugin hook should be referenced in this test module """
2020-08-16 17:49:33 +00:00
tests_in_this_module = [ t for t in globals ( ) . keys ( ) if t . startswith ( " test_hook_ " ) ]
2020-05-27 20:16:02 +00:00
ok = False
for test in tests_in_this_module :
if plugin_hook in test :
ok = True
2020-11-15 23:24:22 +00:00
assert ok , f " Plugin hook is missing tests: { plugin_hook } "
2020-05-27 20:16:02 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_plugins_dir_plugin_prepare_connection ( ds_client ) :
response = await ds_client . get (
2022-12-30 14:52:47 +00:00
" /fixtures.json?_shape=arrayfirst&sql=select+convert_units(100 % 2C+ ' m ' % 2C+ ' ft ' ) "
2018-05-28 20:41:53 +00:00
)
2022-12-30 14:52:47 +00:00
assert response . json ( ) [ 0 ] == pytest . approx ( 328.0839 )
2018-05-28 20:41:53 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_plugin_prepare_connection_arguments ( ds_client ) :
response = await ds_client . get (
2020-02-22 02:27:07 +00:00
" /fixtures.json?sql=select+prepare_connection_args()&_shape=arrayfirst "
)
assert [
" database=fixtures, datasette.plugin_config( \" name-of-plugin \" )= { ' depth ' : ' root ' } "
2022-12-16 17:25:37 +00:00
] == response . json ( )
2020-02-22 02:27:07 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
2018-08-28 10:56:57 +00:00
@pytest.mark.parametrize (
" path,expected_decoded_object " ,
[
2020-08-16 16:50:23 +00:00
(
" / " ,
{
" template " : " index.html " ,
" database " : None ,
" table " : None ,
" view_name " : " index " ,
" request_path " : " / " ,
" added " : 15 ,
2020-08-16 18:09:53 +00:00
" columns " : None ,
2020-08-16 16:50:23 +00:00
} ,
) ,
2018-08-28 10:56:57 +00:00
(
2021-10-14 18:03:44 +00:00
" /fixtures " ,
2020-08-16 16:50:23 +00:00
{
" template " : " database.html " ,
" database " : " fixtures " ,
" table " : None ,
" view_name " : " database " ,
" request_path " : " /fixtures " ,
" added " : 15 ,
2020-08-16 18:09:53 +00:00
" columns " : None ,
2020-08-16 16:50:23 +00:00
} ,
2018-08-28 10:56:57 +00:00
) ,
(
" /fixtures/sortable " ,
2020-08-16 16:50:23 +00:00
{
" template " : " table.html " ,
" database " : " fixtures " ,
" table " : " sortable " ,
" view_name " : " table " ,
" request_path " : " /fixtures/sortable " ,
" added " : 15 ,
2020-08-16 18:09:53 +00:00
" columns " : [
" pk1 " ,
" pk2 " ,
" content " ,
" sortable " ,
" sortable_with_nulls " ,
" sortable_with_nulls_2 " ,
" text " ,
] ,
2020-08-16 16:50:23 +00:00
} ,
2018-08-28 10:56:57 +00:00
) ,
] ,
)
2022-12-16 17:25:37 +00:00
async def test_hook_extra_css_urls ( ds_client , path , expected_decoded_object ) :
response = await ds_client . get ( path )
assert response . status_code == 200
links = Soup ( response . text , " html.parser " ) . findAll ( " link " )
2018-08-28 10:56:57 +00:00
special_href = [
l for l in links if l . attrs [ " href " ] . endswith ( " /extra-css-urls-demo.css " )
] [ 0 ] [ " href " ]
# This link has a base64-encoded JSON blob in it
encoded = special_href . split ( " / " ) [ 3 ]
2018-08-28 16:55:30 +00:00
assert expected_decoded_object == json . loads (
base64 . b64decode ( encoded ) . decode ( " utf8 " )
)
2018-05-28 21:23:48 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_extra_js_urls ( ds_client ) :
response = await ds_client . get ( " / " )
scripts = Soup ( response . text , " html.parser " ) . findAll ( " script " )
2021-01-14 01:50:52 +00:00
script_attrs = [ s . attrs for s in scripts ]
for attrs in [
{
2019-05-04 02:15:14 +00:00
" integrity " : " SRIHASH " ,
" crossorigin " : " anonymous " ,
2020-10-31 19:47:42 +00:00
" src " : " https://plugin-example.datasette.io/jquery.js " ,
2021-01-14 01:50:52 +00:00
} ,
{
" src " : " https://plugin-example.datasette.io/plugin.module.js " ,
" type " : " module " ,
} ,
] :
assert any ( s == attrs for s in script_attrs ) , " Expected: {} " . format ( attrs )
2018-05-28 21:23:48 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_plugins_with_duplicate_js_urls ( ds_client ) :
2018-05-28 20:41:53 +00:00
# If two plugins both require jQuery, jQuery should be loaded only once
2022-12-16 17:25:37 +00:00
response = await ds_client . get ( " /fixtures " )
2018-05-28 20:41:53 +00:00
# This test is a little tricky, as if the user has any other plugins in
# their current virtual environment those may affect what comes back too.
2020-10-31 19:47:42 +00:00
# What matters is that https://plugin-example.datasette.io/jquery.js is only there once
2018-05-28 20:41:53 +00:00
# and it comes before plugin1.js and plugin2.js which could be in either
# order
2022-12-16 17:25:37 +00:00
scripts = Soup ( response . text , " html.parser " ) . findAll ( " script " )
2019-05-04 02:15:14 +00:00
srcs = [ s [ " src " ] for s in scripts if s . get ( " src " ) ]
2018-05-28 20:41:53 +00:00
# No duplicates allowed:
assert len ( srcs ) == len ( set ( srcs ) )
# jquery.js loaded once:
2020-10-31 19:47:42 +00:00
assert 1 == srcs . count ( " https://plugin-example.datasette.io/jquery.js " )
2018-05-28 20:41:53 +00:00
# plugin1.js and plugin2.js are both there:
2020-10-31 19:47:42 +00:00
assert 1 == srcs . count ( " https://plugin-example.datasette.io/plugin1.js " )
assert 1 == srcs . count ( " https://plugin-example.datasette.io/plugin2.js " )
2018-05-28 20:41:53 +00:00
# jquery comes before them both
2020-10-31 19:47:42 +00:00
assert srcs . index ( " https://plugin-example.datasette.io/jquery.js " ) < srcs . index (
" https://plugin-example.datasette.io/plugin1.js "
2018-05-28 20:41:53 +00:00
)
2020-10-31 19:47:42 +00:00
assert srcs . index ( " https://plugin-example.datasette.io/jquery.js " ) < srcs . index (
" https://plugin-example.datasette.io/plugin2.js "
2018-05-28 20:41:53 +00:00
)
2018-08-05 00:14:56 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_render_cell_link_from_json ( ds_client ) :
2018-08-05 00:14:56 +00:00
sql = """
select ' { " href " : " http://example.com/ " , " label " : " Example " } '
""" .strip()
2019-05-04 02:15:14 +00:00
path = " /fixtures? " + urllib . parse . urlencode ( { " sql " : sql } )
2022-12-16 17:25:37 +00:00
response = await ds_client . get ( path )
td = Soup ( response . text , " html.parser " ) . find ( " table " ) . find ( " tbody " ) . find ( " td " )
2018-08-05 00:14:56 +00:00
a = td . find ( " a " )
assert a is not None , str ( a )
assert a . attrs [ " href " ] == " http://example.com/ "
2018-08-28 10:03:01 +00:00
assert a . attrs [ " data-database " ] == " fixtures "
2018-08-05 00:14:56 +00:00
assert a . text == " Example "
2018-08-28 08:35:21 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_render_cell_demo ( ds_client ) :
2023-01-28 03:34:14 +00:00
response = await ds_client . get (
" /fixtures/simple_primary_key?id=4&_render_cell_extra=1 "
)
2022-12-16 17:25:37 +00:00
soup = Soup ( response . text , " html.parser " )
2018-08-28 10:03:01 +00:00
td = soup . find ( " td " , { " class " : " col-content " } )
2022-07-07 16:30:49 +00:00
assert json . loads ( td . string ) == {
" row " : { " id " : " 4 " , " content " : " RENDER_CELL_DEMO " } ,
2018-08-28 10:03:01 +00:00
" column " : " content " ,
" table " : " simple_primary_key " ,
" database " : " fixtures " ,
2019-05-04 02:15:14 +00:00
" config " : { " depth " : " table " , " special " : " this-is-simple_primary_key " } ,
2023-01-28 03:34:14 +00:00
" render_cell_extra " : 1 ,
2022-07-07 16:30:49 +00:00
}
2018-08-28 10:03:01 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
2021-08-08 23:07:52 +00:00
@pytest.mark.parametrize (
" path " , ( " /fixtures?sql=select+ ' RENDER_CELL_ASYNC ' " , " /fixtures/simple_primary_key " )
)
2022-12-16 17:25:37 +00:00
async def test_hook_render_cell_async ( ds_client , path ) :
response = await ds_client . get ( path )
assert b " RENDER_CELL_ASYNC_RESULT " in response . content
2021-08-08 23:04:42 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_plugin_config ( ds_client ) :
assert { " depth " : " table " } == ds_client . ds . plugin_config (
2018-08-28 08:35:21 +00:00
" name-of-plugin " , database = " fixtures " , table = " sortable "
)
2022-12-16 17:25:37 +00:00
assert { " depth " : " database " } == ds_client . ds . plugin_config (
2018-08-28 08:35:21 +00:00
" name-of-plugin " , database = " fixtures " , table = " unknown_table "
)
2022-12-16 17:25:37 +00:00
assert { " depth " : " database " } == ds_client . ds . plugin_config (
2018-08-28 08:35:21 +00:00
" name-of-plugin " , database = " fixtures "
)
2022-12-16 17:25:37 +00:00
assert { " depth " : " root " } == ds_client . ds . plugin_config (
2018-08-28 08:35:21 +00:00
" name-of-plugin " , database = " unknown_database "
)
2022-12-16 17:25:37 +00:00
assert { " depth " : " root " } == ds_client . ds . plugin_config ( " name-of-plugin " )
assert None is ds_client . ds . plugin_config ( " unknown-plugin " )
2018-08-28 08:56:44 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_plugin_config_env ( ds_client ) :
2019-07-04 05:36:44 +00:00
os . environ [ " FOO_ENV " ] = " FROM_ENVIRONMENT "
2022-12-16 17:25:37 +00:00
assert { " foo " : " FROM_ENVIRONMENT " } == ds_client . ds . plugin_config ( " env-plugin " )
2019-07-04 05:47:45 +00:00
# Ensure secrets aren't visible in /-/metadata.json
2022-12-16 17:25:37 +00:00
metadata = await ds_client . get ( " /-/metadata.json " )
assert { " foo " : { " $env " : " FOO_ENV " } } == metadata . json ( ) [ " plugins " ] [ " env-plugin " ]
2019-07-04 05:36:44 +00:00
del os . environ [ " FOO_ENV " ]
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_plugin_config_env_from_list ( ds_client ) :
2020-06-12 00:21:48 +00:00
os . environ [ " FOO_ENV " ] = " FROM_ENVIRONMENT "
2022-12-16 17:25:37 +00:00
assert [ { " in_a_list " : " FROM_ENVIRONMENT " } ] == ds_client . ds . plugin_config (
2020-06-12 00:21:48 +00:00
" env-plugin-list "
)
# Ensure secrets aren't visible in /-/metadata.json
2022-12-16 17:25:37 +00:00
metadata = await ds_client . get ( " /-/metadata.json " )
assert [ { " in_a_list " : { " $env " : " FOO_ENV " } } ] == metadata . json ( ) [ " plugins " ] [
2020-06-12 00:21:48 +00:00
" env-plugin-list "
]
del os . environ [ " FOO_ENV " ]
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_plugin_config_file ( ds_client ) :
2021-03-11 16:15:49 +00:00
with open ( TEMP_PLUGIN_SECRET_FILE , " w " ) as fp :
fp . write ( " FROM_FILE " )
2022-12-16 17:25:37 +00:00
assert { " foo " : " FROM_FILE " } == ds_client . ds . plugin_config ( " file-plugin " )
2019-07-04 05:47:45 +00:00
# Ensure secrets aren't visible in /-/metadata.json
2022-12-16 17:25:37 +00:00
metadata = await ds_client . get ( " /-/metadata.json " )
assert { " foo " : { " $file " : TEMP_PLUGIN_SECRET_FILE } } == metadata . json ( ) [ " plugins " ] [
2019-07-04 05:47:45 +00:00
" file-plugin "
]
2019-07-04 05:36:44 +00:00
os . remove ( TEMP_PLUGIN_SECRET_FILE )
2018-08-28 08:56:44 +00:00
@pytest.mark.parametrize (
" path,expected_extra_body_script " ,
[
(
" / " ,
{
" template " : " index.html " ,
" database " : None ,
" table " : None ,
" config " : { " depth " : " root " } ,
2020-08-16 16:50:23 +00:00
" view_name " : " index " ,
" request_path " : " / " ,
" added " : 15 ,
2020-08-16 18:09:53 +00:00
" columns " : None ,
2018-08-28 08:56:44 +00:00
} ,
) ,
(
2021-10-14 18:03:44 +00:00
" /fixtures " ,
2018-08-28 08:56:44 +00:00
{
" template " : " database.html " ,
" database " : " fixtures " ,
" table " : None ,
" config " : { " depth " : " database " } ,
2020-08-16 16:50:23 +00:00
" view_name " : " database " ,
" request_path " : " /fixtures " ,
" added " : 15 ,
2020-08-16 18:09:53 +00:00
" columns " : None ,
2018-08-28 08:56:44 +00:00
} ,
) ,
(
" /fixtures/sortable " ,
{
" template " : " table.html " ,
" database " : " fixtures " ,
" table " : " sortable " ,
" config " : { " depth " : " table " } ,
2020-08-16 16:50:23 +00:00
" view_name " : " table " ,
" request_path " : " /fixtures/sortable " ,
" added " : 15 ,
2020-08-16 18:09:53 +00:00
" columns " : [
" pk1 " ,
" pk2 " ,
" content " ,
" sortable " ,
" sortable_with_nulls " ,
" sortable_with_nulls_2 " ,
" text " ,
] ,
2018-08-28 08:56:44 +00:00
} ,
) ,
] ,
)
2020-08-16 18:09:53 +00:00
def test_hook_extra_body_script ( app_client , path , expected_extra_body_script ) :
2021-01-14 02:14:33 +00:00
r = re . compile ( r " <script type= \" module \" >var extra_body_script = (.*?);</script> " )
2020-06-02 21:29:12 +00:00
json_data = r . search ( app_client . get ( path ) . text ) . group ( 1 )
2018-08-28 08:56:44 +00:00
actual_data = json . loads ( json_data )
assert expected_extra_body_script == actual_data
2019-07-03 03:57:28 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_asgi_wrapper ( ds_client ) :
response = await ds_client . get ( " /fixtures " )
2020-12-21 19:48:06 +00:00
assert " _internal, fixtures " == response . headers [ " x-databases " ]
2019-07-06 00:05:56 +00:00
2020-08-16 17:49:33 +00:00
def test_hook_extra_template_vars ( restore_working_directory ) :
2020-06-07 21:14:10 +00:00
with make_app_client (
2019-07-06 00:05:56 +00:00
template_dir = str ( pathlib . Path ( __file__ ) . parent / " test_templates " )
2020-06-07 21:14:10 +00:00
) as client :
2019-07-06 00:05:56 +00:00
response = client . get ( " /-/metadata " )
2022-12-16 17:25:37 +00:00
assert response . status_code == 200
2019-07-06 00:05:56 +00:00
extra_template_vars = json . loads (
2022-12-16 17:25:37 +00:00
Soup ( response . text , " html.parser " ) . select ( " pre.extra_template_vars " ) [ 0 ] . text
2019-07-06 00:05:56 +00:00
)
assert {
" template " : " show_json.html " ,
" scope_path " : " /-/metadata " ,
2020-08-16 18:09:53 +00:00
" columns " : None ,
2019-07-06 00:05:56 +00:00
} == extra_template_vars
extra_template_vars_from_awaitable = json . loads (
2022-12-16 17:25:37 +00:00
Soup ( response . text , " html.parser " )
2019-07-06 00:05:56 +00:00
. select ( " pre.extra_template_vars_from_awaitable " ) [ 0 ]
. text
)
assert {
" template " : " show_json.html " ,
" awaitable " : True ,
" scope_path " : " /-/metadata " ,
} == extra_template_vars_from_awaitable
2019-11-14 23:14:22 +00:00
def test_plugins_async_template_function ( restore_working_directory ) :
2020-06-07 21:14:10 +00:00
with make_app_client (
2019-11-14 23:14:22 +00:00
template_dir = str ( pathlib . Path ( __file__ ) . parent / " test_templates " )
2020-06-07 21:14:10 +00:00
) as client :
2019-11-14 23:14:22 +00:00
response = client . get ( " /-/metadata " )
2022-12-16 17:25:37 +00:00
assert response . status_code == 200
2019-11-14 23:14:22 +00:00
extra_from_awaitable_function = (
2022-12-16 17:25:37 +00:00
Soup ( response . text , " html.parser " )
2019-11-14 23:14:22 +00:00
. select ( " pre.extra_from_awaitable_function " ) [ 0 ]
. text
)
expected = (
sqlite3 . connect ( " :memory: " ) . execute ( " select sqlite_version() " ) . fetchone ( ) [ 0 ]
)
assert expected == extra_from_awaitable_function
2020-03-08 23:09:31 +00:00
def test_default_plugins_have_no_templates_path_or_static_path ( ) :
# The default plugins that ship with Datasette should have their static_path and
# templates_path all set to None
plugins = get_plugins ( )
for plugin in plugins :
if plugin [ " name " ] in DEFAULT_PLUGINS :
assert None is plugin [ " static_path " ]
assert None is plugin [ " templates_path " ]
2020-04-05 18:28:20 +00:00
@pytest.fixture ( scope = " session " )
def view_names_client ( tmp_path_factory ) :
tmpdir = tmp_path_factory . mktemp ( " test-view-names " )
templates = tmpdir / " templates "
templates . mkdir ( )
plugins = tmpdir / " plugins "
plugins . mkdir ( )
for template in (
" index.html " ,
" database.html " ,
" table.html " ,
" row.html " ,
" show_json.html " ,
" query.html " ,
) :
( templates / template ) . write_text ( " view_name: {{ view_name }} " , " utf-8 " )
( plugins / " extra_vars.py " ) . write_text (
textwrap . dedent (
"""
from datasette import hookimpl
@hookimpl
def extra_template_vars ( view_name ) :
return { " view_name " : view_name }
"""
) ,
" utf-8 " ,
)
db_path = str ( tmpdir / " fixtures.db " )
conn = sqlite3 . connect ( db_path )
conn . executescript ( TABLES )
2020-05-04 17:40:01 +00:00
return _TestClient (
2020-10-09 16:11:24 +00:00
Datasette ( [ db_path ] , template_dir = str ( templates ) , plugins_dir = str ( plugins ) )
2020-04-05 18:28:20 +00:00
)
@pytest.mark.parametrize (
" path,view_name " ,
(
( " / " , " index " ) ,
( " /fixtures " , " database " ) ,
( " /fixtures/units " , " table " ) ,
( " /fixtures/units/1 " , " row " ) ,
( " /-/metadata " , " json_data " ) ,
( " /fixtures?sql=select+1 " , " database " ) ,
) ,
)
def test_view_names ( view_names_client , path , view_name ) :
response = view_names_client . get ( path )
2022-12-16 17:25:37 +00:00
assert response . status_code == 200
2020-11-15 23:24:22 +00:00
assert f " view_name: { view_name } " == response . text
2020-05-28 02:21:41 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_register_output_renderer_no_parameters ( ds_client ) :
response = await ds_client . get ( " /fixtures/facetable.testnone " )
assert response . status_code == 200
assert b " Hello " == response . content
2020-05-28 02:21:41 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_register_output_renderer_all_parameters ( ds_client ) :
response = await ds_client . get ( " /fixtures/facetable.testall " )
assert response . status_code == 200
2020-05-28 02:21:41 +00:00
# Lots of 'at 0x103a4a690' in here - replace those so we can do
# an easy comparison
2020-06-02 21:29:12 +00:00
body = at_memory_re . sub ( " at 0xXXX " , response . text )
2020-12-01 00:28:02 +00:00
assert json . loads ( body ) == {
2020-05-28 02:21:41 +00:00
" datasette " : " <datasette.app.Datasette object at 0xXXX> " ,
" columns " : [
" pk " ,
" created " ,
" planet_int " ,
" on_earth " ,
" state " ,
2021-11-30 06:17:27 +00:00
" _city_id " ,
2021-11-14 04:44:54 +00:00
" _neighborhood " ,
2020-05-28 02:21:41 +00:00
" tags " ,
" complex_array " ,
" distinct_some_null " ,
2022-03-19 01:37:54 +00:00
" n " ,
2020-05-28 02:21:41 +00:00
] ,
" rows " : [
" <sqlite3.Row object at 0xXXX> " ,
" <sqlite3.Row object at 0xXXX> " ,
" <sqlite3.Row object at 0xXXX> " ,
" <sqlite3.Row object at 0xXXX> " ,
" <sqlite3.Row object at 0xXXX> " ,
" <sqlite3.Row object at 0xXXX> " ,
" <sqlite3.Row object at 0xXXX> " ,
" <sqlite3.Row object at 0xXXX> " ,
" <sqlite3.Row object at 0xXXX> " ,
" <sqlite3.Row object at 0xXXX> " ,
" <sqlite3.Row object at 0xXXX> " ,
" <sqlite3.Row object at 0xXXX> " ,
" <sqlite3.Row object at 0xXXX> " ,
" <sqlite3.Row object at 0xXXX> " ,
" <sqlite3.Row object at 0xXXX> " ,
] ,
2022-03-19 01:37:54 +00:00
" sql " : " select pk, created, planet_int, on_earth, state, _city_id, _neighborhood, tags, complex_array, distinct_some_null, n from facetable order by pk limit 51 " ,
2020-05-28 02:21:41 +00:00
" query_name " : None ,
" database " : " fixtures " ,
" table " : " facetable " ,
2021-11-19 23:13:17 +00:00
" request " : ' <asgi.Request method= " GET " url= " http://localhost/fixtures/facetable.testall " > ' ,
2020-05-28 02:21:41 +00:00
" view_name " : " table " ,
2020-12-01 00:28:02 +00:00
" 1+1 " : 2 ,
}
2020-05-28 02:21:41 +00:00
# Test that query_name is set correctly
2022-12-16 17:25:37 +00:00
query_response = await ds_client . get ( " /fixtures/pragma_cache_size.testall " )
assert query_response . json ( ) [ " query_name " ] == " pragma_cache_size "
2020-05-28 02:21:41 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_register_output_renderer_custom_status_code ( ds_client ) :
response = await ds_client . get (
" /fixtures/pragma_cache_size.testall?status_code=202 "
)
assert response . status_code == 202
2020-05-28 02:21:41 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_register_output_renderer_custom_content_type ( ds_client ) :
response = await ds_client . get (
2020-05-28 02:21:41 +00:00
" /fixtures/pragma_cache_size.testall?content_type=text/blah "
)
assert " text/blah " == response . headers [ " content-type " ]
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_register_output_renderer_custom_headers ( ds_client ) :
response = await ds_client . get (
2020-05-28 02:21:41 +00:00
" /fixtures/pragma_cache_size.testall?header=x-wow:1&header=x-gosh:2 "
)
assert " 1 " == response . headers [ " x-wow " ]
assert " 2 " == response . headers [ " x-gosh " ]
2020-05-28 03:13:32 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_register_output_renderer_returning_response ( ds_client ) :
response = await ds_client . get ( " /fixtures/facetable.testresponse " )
assert response . status_code == 200
assert response . json ( ) == { " this_is " : " json " }
2020-08-28 04:02:50 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_register_output_renderer_returning_broken_value ( ds_client ) :
response = await ds_client . get ( " /fixtures/facetable.testresponse?_broken=1 " )
assert response . status_code == 500
2020-08-28 04:02:50 +00:00
assert " this should break should be dict or Response " in response . text
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_register_output_renderer_can_render ( ds_client ) :
response = await ds_client . get ( " /fixtures/facetable?_no_can_render=1 " )
assert response . status_code == 200
2020-05-28 05:57:05 +00:00
links = (
2022-12-16 17:25:37 +00:00
Soup ( response . text , " html.parser " )
2020-05-28 05:57:05 +00:00
. find ( " p " , { " class " : " export-links " } )
. findAll ( " a " )
)
2020-10-10 03:51:56 +00:00
actual = [ l [ " href " ] for l in links ]
2020-05-28 05:57:05 +00:00
# Should not be present because we sent ?_no_can_render=1
2020-10-10 03:51:56 +00:00
assert " /fixtures/facetable.testall?_labels=on " not in actual
2020-05-28 05:57:05 +00:00
# Check that it was passed the values we expected
2022-12-16 17:25:37 +00:00
assert hasattr ( ds_client . ds , " _can_render_saw " )
2020-05-28 05:57:05 +00:00
assert {
2022-12-16 17:25:37 +00:00
" datasette " : ds_client . ds ,
2020-05-28 05:57:05 +00:00
" columns " : [
" pk " ,
" created " ,
" planet_int " ,
" on_earth " ,
" state " ,
2021-11-30 06:17:27 +00:00
" _city_id " ,
2021-11-14 04:44:54 +00:00
" _neighborhood " ,
2020-05-28 05:57:05 +00:00
" tags " ,
" complex_array " ,
" distinct_some_null " ,
2022-03-19 01:37:54 +00:00
" n " ,
2020-05-28 05:57:05 +00:00
] ,
2022-03-19 01:37:54 +00:00
" sql " : " select pk, created, planet_int, on_earth, state, _city_id, _neighborhood, tags, complex_array, distinct_some_null, n from facetable order by pk limit 51 " ,
2020-05-28 05:57:05 +00:00
" query_name " : None ,
" database " : " fixtures " ,
" table " : " facetable " ,
" view_name " : " table " ,
2022-12-16 17:25:37 +00:00
} . items ( ) < = ds_client . ds . _can_render_saw . items ( )
2020-05-28 05:57:05 +00:00
2020-05-28 03:13:32 +00:00
@pytest.mark.asyncio
2022-12-16 17:25:37 +00:00
async def test_hook_prepare_jinja2_environment ( ds_client ) :
ds_client . ds . _HELLO = " HI "
await ds_client . ds . invoke_startup ( )
template = ds_client . ds . jinja_env . from_string (
2022-09-17 03:38:15 +00:00
" Hello there, {{ a|format_numeric }}, {{ a|to_hello }}, {{ b|select_times_three }} " ,
{ " a " : 3412341 , " b " : 5 } ,
2020-05-28 03:13:32 +00:00
)
2022-12-16 17:25:37 +00:00
rendered = await ds_client . ds . render_template ( template )
2022-09-17 03:38:15 +00:00
assert " Hello there, 3,412,341, HI, 15 " == rendered
2020-05-28 03:30:32 +00:00
2020-08-16 17:49:33 +00:00
def test_hook_publish_subcommand ( ) :
2020-05-28 03:30:32 +00:00
# This is hard to test properly, because publish subcommand plugins
# cannot be loaded using the --plugins-dir mechanism - they need
# to be installed using "pip install". So I'm cheating and taking
# advantage of the fact that cloudrun/heroku use the plugin hook
# to register themselves as default plugins.
assert [ " cloudrun " , " heroku " ] == cli . publish . list_commands ( { } )
2020-05-28 04:09:16 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_register_facet_classes ( ds_client ) :
response = await ds_client . get (
2023-03-22 22:49:39 +00:00
" /fixtures/compound_three_primary_keys.json?_dummy_facet=1&_extra=suggested_facets "
2020-05-28 04:09:16 +00:00
)
2023-03-22 22:49:39 +00:00
assert response . json ( ) [ " suggested_facets " ] == [
2020-05-28 04:09:16 +00:00
{
" name " : " pk1 " ,
2023-03-22 22:49:39 +00:00
" toggle_url " : " http://localhost/fixtures/compound_three_primary_keys.json?_dummy_facet=1&_extra=suggested_facets&_facet_dummy=pk1 " ,
2020-05-28 04:09:16 +00:00
" type " : " dummy " ,
} ,
{
" name " : " pk2 " ,
2023-03-22 22:49:39 +00:00
" toggle_url " : " http://localhost/fixtures/compound_three_primary_keys.json?_dummy_facet=1&_extra=suggested_facets&_facet_dummy=pk2 " ,
2020-05-28 04:09:16 +00:00
" type " : " dummy " ,
} ,
{
" name " : " pk3 " ,
2023-03-22 22:49:39 +00:00
" toggle_url " : " http://localhost/fixtures/compound_three_primary_keys.json?_dummy_facet=1&_extra=suggested_facets&_facet_dummy=pk3 " ,
2020-05-28 04:09:16 +00:00
" type " : " dummy " ,
} ,
{
" name " : " content " ,
2023-03-22 22:49:39 +00:00
" toggle_url " : " http://localhost/fixtures/compound_three_primary_keys.json?_dummy_facet=1&_extra=suggested_facets&_facet_dummy=content " ,
2020-05-28 04:09:16 +00:00
" type " : " dummy " ,
} ,
{
" name " : " pk1 " ,
2023-03-22 22:49:39 +00:00
" toggle_url " : " http://localhost/fixtures/compound_three_primary_keys.json?_dummy_facet=1&_extra=suggested_facets&_facet=pk1 " ,
2020-05-28 04:09:16 +00:00
} ,
{
" name " : " pk2 " ,
2023-03-22 22:49:39 +00:00
" toggle_url " : " http://localhost/fixtures/compound_three_primary_keys.json?_dummy_facet=1&_extra=suggested_facets&_facet=pk2 " ,
2020-05-28 04:09:16 +00:00
} ,
{
" name " : " pk3 " ,
2023-03-22 22:49:39 +00:00
" toggle_url " : " http://localhost/fixtures/compound_three_primary_keys.json?_dummy_facet=1&_extra=suggested_facets&_facet=pk3 " ,
2020-05-28 04:09:16 +00:00
} ,
2023-03-22 22:49:39 +00:00
]
2020-05-30 22:06:33 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_actor_from_request ( ds_client ) :
await ds_client . get ( " / " )
2020-05-30 22:06:33 +00:00
# Should have no actor
2022-12-16 17:25:37 +00:00
assert ds_client . ds . _last_request . scope [ " actor " ] is None
await ds_client . get ( " /?_bot=1 " )
2020-05-30 22:06:33 +00:00
# Should have bot actor
2022-12-16 17:25:37 +00:00
assert ds_client . ds . _last_request . scope [ " actor " ] == { " id " : " bot " }
2020-05-30 22:06:33 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_actor_from_request_async ( ds_client ) :
await ds_client . get ( " / " )
2020-05-30 22:06:33 +00:00
# Should have no actor
2022-12-16 17:25:37 +00:00
assert ds_client . ds . _last_request . scope [ " actor " ] is None
await ds_client . get ( " /?_bot2=1 " )
2020-05-30 22:06:33 +00:00
# Should have bot2 actor
2022-12-16 17:25:37 +00:00
assert ds_client . ds . _last_request . scope [ " actor " ] == { " id " : " bot2 " , " 1+1 " : 2 }
2020-05-30 22:06:33 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_existing_scope_actor_respected ( ds_client ) :
await ds_client . get ( " /?_actor_in_scope=1 " )
assert ds_client . ds . _last_request . scope [ " actor " ] == { " id " : " from-scope " }
2020-06-18 18:37:28 +00:00
2020-05-30 22:24:43 +00:00
@pytest.mark.asyncio
@pytest.mark.parametrize (
" action,expected " ,
[
( " this_is_allowed " , True ) ,
( " this_is_denied " , False ) ,
( " this_is_allowed_async " , True ) ,
( " this_is_denied_async " , False ) ,
] ,
)
2022-12-13 02:05:54 +00:00
async def test_hook_permission_allowed ( action , expected ) :
class TestPlugin :
__name__ = " TestPlugin "
@hookimpl
def register_permissions ( self ) :
return [
Permission ( name , None , None , False , False , False )
for name in (
" this_is_allowed " ,
" this_is_denied " ,
" this_is_allowed_async " ,
" this_is_denied_async " ,
)
]
pm . register ( TestPlugin ( ) , name = " undo_register_extras " )
try :
ds = Datasette ( plugins_dir = PLUGINS_DIR )
await ds . invoke_startup ( )
actual = await ds . permission_allowed ( { " id " : " actor " } , action )
assert expected == actual
finally :
pm . unregister ( name = " undo_register_extras " )
2020-05-31 01:51:00 +00:00
2022-12-16 17:25:37 +00:00
@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?_bot2=1 " ) ) . json ( ) == {
" actor " : { " id " : " bot2 " , " 1+1 " : 2 }
}
2020-06-09 03:12:06 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
2020-06-09 03:12:06 +00:00
@pytest.mark.parametrize (
2020-06-27 18:30:34 +00:00
" path,body " ,
[
( " /one/ " , " 2 " ) ,
( " /two/Ray?greeting=Hail " , " Hail Ray " ) ,
( " /not-async/ " , " This was not async " ) ,
] ,
2020-06-09 03:12:06 +00:00
)
2022-12-16 17:25:37 +00:00
async def test_hook_register_routes ( ds_client , path , body ) :
response = await ds_client . get ( path )
assert response . status_code == 200
assert response . text == body
2020-06-09 03:12:06 +00:00
2021-07-26 23:16:46 +00:00
@pytest.mark.parametrize ( " configured_path " , ( " path1 " , " path2 " ) )
def test_hook_register_routes_with_datasette ( configured_path ) :
with make_app_client (
metadata = {
" plugins " : {
" register-route-demo " : {
" path " : configured_path ,
}
}
}
) as client :
response = client . get ( f " / { configured_path } / " )
2022-12-16 17:25:37 +00:00
assert response . status_code == 200
2021-07-26 23:16:46 +00:00
assert configured_path . upper ( ) == response . text
# Other one should 404
other_path = [ p for p in ( " path1 " , " path2 " ) if configured_path != p ] [ 0 ]
2022-12-16 17:25:37 +00:00
assert client . get ( f " / { other_path } / " , follow_redirects = True ) . status_code == 404
2021-07-26 23:16:46 +00:00
2021-11-19 03:07:21 +00:00
def test_hook_register_routes_override ( ) :
" Plugins can over-ride default paths such as /db/table "
with make_app_client (
metadata = {
" plugins " : {
" register-route-demo " : {
" path " : " blah " ,
}
}
}
) as client :
response = client . get ( " /db/table " )
2022-12-16 17:25:37 +00:00
assert response . status_code == 200
2021-11-19 03:07:21 +00:00
assert (
response . text
== " /db/table: [( ' db_name ' , ' db ' ), ( ' table_and_format ' , ' table ' )] "
)
2020-08-16 17:49:33 +00:00
def test_hook_register_routes_post ( app_client ) :
2020-06-18 16:21:15 +00:00
response = app_client . post ( " /post/ " , { " this is " : " post data " } , csrftoken_from = True )
2022-12-16 17:25:37 +00:00
assert response . status_code == 200
2020-06-18 16:21:15 +00:00
assert " csrftoken " in response . json
2022-12-16 17:25:37 +00:00
assert response . json [ " this is " ] == " post data "
2020-06-18 16:21:15 +00:00
2020-08-16 17:49:33 +00:00
def test_hook_register_routes_csrftoken ( restore_working_directory , tmpdir_factory ) :
2020-06-24 04:17:30 +00:00
templates = tmpdir_factory . mktemp ( " templates " )
2020-06-24 03:23:30 +00:00
( templates / " csrftoken_form.html " ) . write_text (
" CSRFTOKEN: {{ csrftoken() }} " , " utf-8 "
)
with make_app_client ( template_dir = templates ) as client :
response = client . get ( " /csrftoken-form/ " )
expected_token = client . ds . _last_request . scope [ " csrftoken " ] ( )
2020-11-15 23:24:22 +00:00
assert f " CSRFTOKEN: { expected_token } " == response . text
2020-06-24 03:23:30 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_register_routes_asgi ( ds_client ) :
response = await ds_client . get ( " /three/ " )
assert { " hello " : " world " } == response . json ( )
2020-06-09 03:12:06 +00:00
assert " 1 " == response . headers [ " x-three " ]
2020-06-13 17:55:41 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_register_routes_add_message ( ds_client ) :
response = await ds_client . get ( " /add-message/ " )
assert response . status_code == 200
assert response . text == " Added message "
decoded = ds_client . ds . unsign ( response . cookies [ " ds_messages " ] , " messages " )
assert decoded == [ [ " Hello from messages " , 1 ] ]
2020-06-29 00:25:35 +00:00
2020-08-16 17:49:33 +00:00
def test_hook_register_routes_render_message ( restore_working_directory , tmpdir_factory ) :
2020-06-29 00:50:47 +00:00
templates = tmpdir_factory . mktemp ( " templates " )
( templates / " render_message.html " ) . write_text ( ' { % e xtends " base.html " % } ' , " utf-8 " )
with make_app_client ( template_dir = templates ) as client :
response1 = client . get ( " /add-message/ " )
response2 = client . get ( " /render-message/ " , cookies = response1 . cookies )
assert 200 == response2 . status
assert " Hello from messages " in response2 . text
2020-06-13 17:55:41 +00:00
@pytest.mark.asyncio
2022-12-16 17:25:37 +00:00
async def test_hook_startup ( ds_client ) :
await ds_client . ds . invoke_startup ( )
assert ds_client . ds . _startup_hook_fired
assert 2 == ds_client . ds . _startup_hook_calculation
2020-06-18 23:22:33 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_canned_queries ( ds_client ) :
queries = ( await ds_client . get ( " /fixtures.json " ) ) . json ( ) [ " queries " ]
2020-06-18 23:22:33 +00:00
queries_by_name = { q [ " name " ] : q for q in queries }
assert {
" sql " : " select 2 " ,
" name " : " from_async_hook " ,
" private " : False ,
} == queries_by_name [ " from_async_hook " ]
assert {
" sql " : " select 1, ' null ' as actor_id " ,
" name " : " from_hook " ,
" private " : False ,
} == queries_by_name [ " from_hook " ]
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_canned_queries_non_async ( ds_client ) :
response = await ds_client . get ( " /fixtures/from_hook.json?_shape=array " )
assert [ { " 1 " : 1 , " actor_id " : " null " } ] == response . json ( )
2020-06-18 23:22:33 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_canned_queries_async ( ds_client ) :
response = await ds_client . get ( " /fixtures/from_async_hook.json?_shape=array " )
assert [ { " 2 " : 2 } ] == response . json ( )
2020-06-18 23:22:33 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_canned_queries_actor ( ds_client ) :
assert (
await ds_client . get ( " /fixtures/from_hook.json?_bot=1&_shape=array " )
) . json ( ) == [ { " 1 " : 1 , " actor_id " : " bot " } ]
2020-06-28 02:58:16 +00:00
2020-08-16 17:49:33 +00:00
def test_hook_register_magic_parameters ( restore_working_directory ) :
2020-06-28 02:58:16 +00:00
with make_app_client (
extra_databases = { " data.db " : " create table logs (line text) " } ,
metadata = {
" databases " : {
" data " : {
" queries " : {
" runme " : {
" sql " : " insert into logs (line) values (:_request_http_version) " ,
" write " : True ,
} ,
2020-09-02 22:24:55 +00:00
" get_uuid " : {
" sql " : " select :_uuid_new " ,
} ,
2020-06-28 02:58:16 +00:00
}
}
}
} ,
) as client :
response = client . post ( " /data/runme " , { } , csrftoken_from = True )
2022-12-16 17:25:37 +00:00
assert response . status_code == 302
2020-06-28 02:58:16 +00:00
actual = client . get ( " /data/logs.json?_sort_desc=rowid&_shape=array " ) . json
2020-10-09 16:11:24 +00:00
assert [ { " rowid " : 1 , " line " : " 1.1 " } ] == actual
2020-06-28 02:58:16 +00:00
# Now try the GET request against get_uuid
response_get = client . get ( " /data/get_uuid.json?_shape=array " )
assert 200 == response_get . status
new_uuid = response_get . json [ 0 ] [ " :_uuid_new " ]
assert 4 == new_uuid . count ( " - " )
2020-07-01 04:17:38 +00:00
2020-08-16 17:49:33 +00:00
def test_hook_forbidden ( restore_working_directory ) :
2020-07-01 04:17:38 +00:00
with make_app_client (
extra_databases = { " data2.db " : " create table logs (line text) " } ,
metadata = { " allow " : { } } ,
) as client :
response = client . get ( " / " )
2022-12-16 17:25:37 +00:00
assert response . status_code == 403
2021-10-14 18:03:44 +00:00
response2 = client . get ( " /data2 " )
2020-07-01 04:17:38 +00:00
assert 302 == response2 . status
2022-10-24 02:11:33 +00:00
assert (
response2 . headers [ " Location " ]
== " /login?message=You do not have permission to view this database "
)
assert (
client . ds . _last_forbidden_message
== " You do not have permission to view this database "
)
2020-10-30 03:45:15 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_handle_exception ( ds_client ) :
await ds_client . get ( " /trigger-error?x=123 " )
assert hasattr ( ds_client . ds , " _exception_hook_fired " )
request , exception = ds_client . ds . _exception_hook_fired
2022-07-17 23:24:39 +00:00
assert request . url == " http://localhost/trigger-error?x=123 "
assert isinstance ( exception , ZeroDivisionError )
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
2022-07-17 23:24:39 +00:00
@pytest.mark.parametrize ( " param " , ( " _custom_error " , " _custom_error_async " ) )
2022-12-16 17:25:37 +00:00
async def test_hook_handle_exception_custom_response ( ds_client , param ) :
response = await ds_client . get ( " /trigger-error? {} =1 " . format ( param ) )
2022-07-17 23:24:39 +00:00
assert response . text == param
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_menu_links ( ds_client ) :
2020-10-30 03:45:15 +00:00
def get_menu_links ( html ) :
soup = Soup ( html , " html.parser " )
return [
2022-10-13 21:42:52 +00:00
{ " label " : a . text , " href " : a [ " href " ] } for a in soup . select ( " .nav-menu a " )
2020-10-30 03:45:15 +00:00
]
2022-12-16 17:25:37 +00:00
response = await ds_client . get ( " / " )
2020-10-30 03:45:15 +00:00
assert get_menu_links ( response . text ) == [ ]
2022-12-16 17:25:37 +00:00
response_2 = await ds_client . get ( " /?_bot=1&_hello=BOB " )
2020-10-30 03:45:15 +00:00
assert get_menu_links ( response_2 . text ) == [
2021-06-10 04:45:24 +00:00
{ " label " : " Hello, BOB " , " href " : " / " } ,
2020-10-30 03:45:15 +00:00
{ " label " : " Hello 2 " , " href " : " / " } ,
]
2020-10-30 05:16:41 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
2020-10-31 17:40:09 +00:00
@pytest.mark.parametrize ( " table_or_view " , [ " facetable " , " simple_view " ] )
2022-12-16 17:25:37 +00:00
async def test_hook_table_actions ( ds_client , table_or_view ) :
2020-10-30 05:16:41 +00:00
def get_table_actions_links ( html ) :
soup = Soup ( html , " html.parser " )
2020-11-02 18:27:25 +00:00
details = soup . find ( " details " , { " class " : " actions-menu-links " } )
2020-10-30 05:16:41 +00:00
if details is None :
return [ ]
return [ { " label " : a . text , " href " : a [ " href " ] } for a in details . select ( " a " ) ]
2022-12-16 17:25:37 +00:00
response = await ds_client . get ( f " /fixtures/ { table_or_view } " )
2020-10-30 05:16:41 +00:00
assert get_table_actions_links ( response . text ) == [ ]
2022-12-16 17:25:37 +00:00
response_2 = await ds_client . get ( f " /fixtures/ { table_or_view } ?_bot=1&_hello=BOB " )
2020-12-03 00:49:43 +00:00
assert sorted (
get_table_actions_links ( response_2 . text ) , key = lambda l : l [ " label " ]
) == [
2020-10-30 05:16:41 +00:00
{ " label " : " Database: fixtures " , " href " : " / " } ,
2021-06-10 04:45:24 +00:00
{ " label " : " From async BOB " , " href " : " / " } ,
2020-11-15 23:24:22 +00:00
{ " label " : f " Table: { table_or_view } " , " href " : " / " } ,
2020-10-30 05:16:41 +00:00
]
2020-11-02 18:27:25 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_database_actions ( ds_client ) :
2020-11-02 18:27:25 +00:00
def get_table_actions_links ( html ) :
soup = Soup ( html , " html.parser " )
details = soup . find ( " details " , { " class " : " actions-menu-links " } )
if details is None :
return [ ]
return [ { " label " : a . text , " href " : a [ " href " ] } for a in details . select ( " a " ) ]
2022-12-16 17:25:37 +00:00
response = await ds_client . get ( " /fixtures " )
2020-11-02 18:27:25 +00:00
assert get_table_actions_links ( response . text ) == [ ]
2022-12-16 17:25:37 +00:00
response_2 = await ds_client . get ( " /fixtures?_bot=1&_hello=BOB " )
2020-11-02 18:27:25 +00:00
assert get_table_actions_links ( response_2 . text ) == [
2021-06-10 04:45:24 +00:00
{ " label " : " Database: fixtures - BOB " , " href " : " / " } ,
2020-11-02 18:27:25 +00:00
]
2021-06-23 22:39:52 +00:00
def test_hook_skip_csrf ( app_client ) :
cookie = app_client . actor_cookie ( { " id " : " test " } )
csrf_response = app_client . post (
" /post/ " ,
post_data = { " this is " : " post data " } ,
csrftoken_from = True ,
cookies = { " ds_actor " : cookie } ,
)
2022-12-16 17:25:37 +00:00
assert csrf_response . status_code == 200
2021-06-23 22:39:52 +00:00
missing_csrf_response = app_client . post (
" /post/ " , post_data = { " this is " : " post data " } , cookies = { " ds_actor " : cookie }
)
2022-12-16 17:25:37 +00:00
assert missing_csrf_response . status_code == 403
2021-06-23 22:39:52 +00:00
# But "/skip-csrf" should allow
allow_csrf_response = app_client . post (
" /skip-csrf " , post_data = { " this is " : " post data " } , cookies = { " ds_actor " : cookie }
)
2022-12-16 17:25:37 +00:00
assert allow_csrf_response . status_code == 405 # Method not allowed
2021-06-23 22:39:52 +00:00
# /skip-csrf-2 should not
second_missing_csrf_response = app_client . post (
" /skip-csrf-2 " , post_data = { " this is " : " post data " } , cookies = { " ds_actor " : cookie }
)
2022-12-16 17:25:37 +00:00
assert second_missing_csrf_response . status_code == 403
2021-06-26 22:24:54 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_get_metadata ( ds_client ) :
2022-12-17 21:40:27 +00:00
try :
orig_metadata = ds_client . ds . _metadata_local
ds_client . ds . _metadata_local = {
" title " : " Testing get_metadata hook! " ,
" databases " : { " from-local " : { " title " : " Hello from local metadata " } } ,
}
og_pm_hook_get_metadata = pm . hook . get_metadata
2021-06-26 22:25:28 +00:00
2022-12-17 21:40:27 +00:00
def get_metadata_mock ( * args , * * kwargs ) :
return [
{
" databases " : {
" from-hook " : { " title " : " Hello from the plugin hook " } ,
" from-local " : { " title " : " This will be overwritten! " } ,
}
2021-06-26 22:24:54 +00:00
}
2022-12-17 21:40:27 +00:00
]
2021-06-26 22:25:28 +00:00
2022-12-17 21:40:27 +00:00
pm . hook . get_metadata = get_metadata_mock
meta = ds_client . ds . metadata ( )
assert " Testing get_metadata hook! " == meta [ " title " ]
assert " Hello from local metadata " == meta [ " databases " ] [ " from-local " ] [ " title " ]
assert " Hello from the plugin hook " == meta [ " databases " ] [ " from-hook " ] [ " title " ]
pm . hook . get_metadata = og_pm_hook_get_metadata
finally :
ds_client . ds . _metadata_local = orig_metadata
2021-08-28 01:39:42 +00:00
def _extract_commands ( output ) :
lines = output . split ( " Commands: \n " , 1 ) [ 1 ] . split ( " \n " )
return { line . split ( ) [ 0 ] . replace ( " * " , " " ) for line in lines if line . strip ( ) }
def test_hook_register_commands ( ) :
# Without the plugin should have seven commands
runner = CliRunner ( )
result = runner . invoke ( cli . cli , " --help " )
commands = _extract_commands ( result . output )
assert commands == {
" serve " ,
" inspect " ,
" install " ,
" package " ,
" plugins " ,
" publish " ,
" uninstall " ,
2022-10-26 04:26:12 +00:00
" create-token " ,
2021-08-28 01:39:42 +00:00
}
# Now install a plugin
class VerifyPlugin :
__name__ = " VerifyPlugin "
@hookimpl
def register_commands ( self , cli ) :
@cli.command ( )
def verify ( ) :
pass
@cli.command ( )
def unverify ( ) :
pass
pm . register ( VerifyPlugin ( ) , name = " verify " )
importlib . reload ( cli )
result2 = runner . invoke ( cli . cli , " --help " )
commands2 = _extract_commands ( result2 . output )
assert commands2 == {
" serve " ,
" inspect " ,
" install " ,
" package " ,
" plugins " ,
" publish " ,
" uninstall " ,
" verify " ,
" unverify " ,
2022-10-26 04:26:12 +00:00
" create-token " ,
2021-08-28 01:39:42 +00:00
}
pm . unregister ( name = " verify " )
importlib . reload ( cli )
2021-12-17 19:02:14 +00:00
2022-12-16 17:25:37 +00:00
@pytest.mark.asyncio
async def test_hook_filters_from_request ( ds_client ) :
2021-12-17 19:02:14 +00:00
class ReturnNothingPlugin :
__name__ = " ReturnNothingPlugin "
@hookimpl
def filters_from_request ( self , request ) :
if request . args . get ( " _nothing " ) :
return FilterArguments ( [ " 1 = 0 " ] , human_descriptions = [ " NOTHING " ] )
pm . register ( ReturnNothingPlugin ( ) , name = " ReturnNothingPlugin " )
2022-12-16 17:25:37 +00:00
response = await ds_client . get ( " /fixtures/facetable?_nothing=1 " )
2021-12-17 19:02:14 +00:00
assert " 0 rows \n where NOTHING " in response . text
2022-12-16 17:25:37 +00:00
json_response = await ds_client . get ( " /fixtures/facetable.json?_nothing=1 " )
assert json_response . json ( ) [ " rows " ] == [ ]
2021-12-17 19:02:14 +00:00
pm . unregister ( name = " ReturnNothingPlugin " )
2022-12-13 02:05:54 +00:00
@pytest.mark.asyncio
@pytest.mark.parametrize ( " extra_metadata " , ( False , True ) )
async def test_hook_register_permissions ( extra_metadata ) :
ds = Datasette (
metadata = {
" plugins " : {
" datasette-register-permissions " : {
" permissions " : [
{
" name " : " extra-from-metadata " ,
" abbr " : " efm " ,
" description " : " Extra from metadata " ,
" takes_database " : False ,
" takes_resource " : False ,
" default " : True ,
}
]
}
}
}
if extra_metadata
else None ,
plugins_dir = PLUGINS_DIR ,
)
await ds . invoke_startup ( )
2022-12-13 05:21:01 +00:00
assert ds . permissions [ " permission-from-plugin " ] == Permission (
name = " permission-from-plugin " ,
2022-12-13 02:05:54 +00:00
abbr = " np " ,
2022-12-13 05:21:01 +00:00
description = " New permission added by a plugin " ,
2022-12-13 02:05:54 +00:00
takes_database = True ,
takes_resource = False ,
default = False ,
)
if extra_metadata :
assert ds . permissions [ " extra-from-metadata " ] == Permission (
name = " extra-from-metadata " ,
abbr = " efm " ,
description = " Extra from metadata " ,
takes_database = False ,
takes_resource = False ,
default = True ,
)
else :
assert " extra-from-metadata " not in ds . permissions
@pytest.mark.asyncio
@pytest.mark.parametrize ( " duplicate " , ( " name " , " abbr " ) )
async def test_hook_register_permissions_no_duplicates ( duplicate ) :
name1 , name2 = " name1 " , " name2 "
abbr1 , abbr2 = " abbr1 " , " abbr2 "
if duplicate == " name " :
name2 = " name1 "
if duplicate == " abbr " :
abbr2 = " abbr1 "
ds = Datasette (
metadata = {
" plugins " : {
" datasette-register-permissions " : {
" permissions " : [
{
" name " : name1 ,
" abbr " : abbr1 ,
" description " : None ,
" takes_database " : False ,
" takes_resource " : False ,
" default " : True ,
} ,
{
" name " : name2 ,
" abbr " : abbr2 ,
" description " : None ,
" takes_database " : False ,
" takes_resource " : False ,
" default " : True ,
} ,
]
}
}
} ,
plugins_dir = PLUGINS_DIR ,
)
# This should error:
with pytest . raises ( StartupError ) as ex :
await ds . invoke_startup ( )
assert " Duplicate permission {} " . format ( duplicate ) in str ( ex . value )
@pytest.mark.asyncio
async def test_hook_register_permissions_allows_identical_duplicates ( ) :
ds = Datasette (
metadata = {
" plugins " : {
" datasette-register-permissions " : {
" permissions " : [
{
" name " : " name1 " ,
" abbr " : " abbr1 " ,
" description " : None ,
" takes_database " : False ,
" takes_resource " : False ,
" default " : True ,
} ,
{
" name " : " name1 " ,
" abbr " : " abbr1 " ,
" description " : None ,
" takes_database " : False ,
" takes_resource " : False ,
" default " : True ,
} ,
]
}
}
} ,
plugins_dir = PLUGINS_DIR ,
)
await ds . invoke_startup ( )
# Check that ds.permissions has only one of each
assert len ( [ p for p in ds . permissions . values ( ) if p . abbr == " abbr1 " ] ) == 1