from datasette.app import Datasette from datasette.plugins import DEFAULT_PLUGINS from datasette.utils.sqlite import supports_table_xinfo from datasette.version import __version__ from .fixtures import ( # noqa app_client, app_client_no_files, app_client_with_dot, app_client_shorter_time_limit, app_client_two_attached_databases_one_immutable, app_client_larger_cache_size, app_client_with_cors, app_client_two_attached_databases, app_client_conflicting_database_names, app_client_immutable_and_inspect_file, make_app_client, EXPECTED_PLUGINS, METADATA, ) import pathlib import pytest import sys import urllib @pytest.mark.asyncio async def test_homepage(ds_client): response = await ds_client.get("/.json") assert response.status_code == 200 assert "application/json; charset=utf-8" == response.headers["content-type"] data = response.json() assert data.keys() == {"fixtures": 0}.keys() d = data["fixtures"] assert d["name"] == "fixtures" assert isinstance(d["tables_count"], int) assert isinstance(len(d["tables_and_views_truncated"]), int) assert d["tables_and_views_more"] is True assert isinstance(d["hidden_tables_count"], int) assert isinstance(d["hidden_table_rows_sum"], int) assert isinstance(d["views_count"], int) @pytest.mark.asyncio async def test_homepage_sort_by_relationships(ds_client): response = await ds_client.get("/.json?_sort=relationships") assert response.status_code == 200 tables = [ t["name"] for t in response.json()["fixtures"]["tables_and_views_truncated"] ] assert tables == [ "simple_primary_key", "foreign_key_references", "complex_foreign_keys", "roadside_attraction_characteristics", "searchable_tags", ] @pytest.mark.asyncio async def test_database_page(ds_client): response = await ds_client.get("/fixtures.json") assert response.status_code == 200 data = response.json() assert data["database"] == "fixtures" assert data["tables"] == [ { "name": "123_starts_with_digits", "columns": ["content"], "primary_keys": [], "count": 0, "hidden": False, "fts_table": None, "foreign_keys": {"incoming": [], "outgoing": []}, "private": False, }, { "name": "Table With Space In Name", "columns": ["pk", "content"], "primary_keys": ["pk"], "count": 0, "hidden": False, "fts_table": None, "foreign_keys": {"incoming": [], "outgoing": []}, "private": False, }, { "name": "attraction_characteristic", "columns": ["pk", "name"], "primary_keys": ["pk"], "count": 2, "hidden": False, "fts_table": None, "foreign_keys": { "incoming": [ { "other_table": "roadside_attraction_characteristics", "column": "pk", "other_column": "characteristic_id", } ], "outgoing": [], }, "private": False, }, { "name": "binary_data", "columns": ["data"], "primary_keys": [], "count": 3, "hidden": False, "fts_table": None, "foreign_keys": {"incoming": [], "outgoing": []}, "private": False, }, { "name": "complex_foreign_keys", "columns": ["pk", "f1", "f2", "f3"], "primary_keys": ["pk"], "count": 1, "hidden": False, "fts_table": None, "foreign_keys": { "incoming": [], "outgoing": [ { "other_table": "simple_primary_key", "column": "f3", "other_column": "id", }, { "other_table": "simple_primary_key", "column": "f2", "other_column": "id", }, { "other_table": "simple_primary_key", "column": "f1", "other_column": "id", }, ], }, "private": False, }, { "name": "compound_primary_key", "columns": ["pk1", "pk2", "content"], "primary_keys": ["pk1", "pk2"], "count": 2, "hidden": False, "fts_table": None, "foreign_keys": {"incoming": [], "outgoing": []}, "private": False, }, { "name": "compound_three_primary_keys", "columns": ["pk1", "pk2", "pk3", "content"], "primary_keys": ["pk1", "pk2", "pk3"], "count": 1001, "hidden": False, "fts_table": None, "foreign_keys": {"incoming": [], "outgoing": []}, "private": False, }, { "name": "custom_foreign_key_label", "columns": ["pk", "foreign_key_with_custom_label"], "primary_keys": ["pk"], "count": 1, "hidden": False, "fts_table": None, "foreign_keys": { "incoming": [], "outgoing": [ { "other_table": "primary_key_multiple_columns_explicit_label", "column": "foreign_key_with_custom_label", "other_column": "id", } ], }, "private": False, }, { "name": "facet_cities", "columns": ["id", "name"], "primary_keys": ["id"], "count": 4, "hidden": False, "fts_table": None, "foreign_keys": { "incoming": [ { "other_table": "facetable", "column": "id", "other_column": "_city_id", } ], "outgoing": [], }, "private": False, }, { "name": "facetable", "columns": [ "pk", "created", "planet_int", "on_earth", "state", "_city_id", "_neighborhood", "tags", "complex_array", "distinct_some_null", "n", ], "primary_keys": ["pk"], "count": 15, "hidden": False, "fts_table": None, "foreign_keys": { "incoming": [], "outgoing": [ { "other_table": "facet_cities", "column": "_city_id", "other_column": "id", } ], }, "private": False, }, { "name": "foreign_key_references", "columns": [ "pk", "foreign_key_with_label", "foreign_key_with_blank_label", "foreign_key_with_no_label", "foreign_key_compound_pk1", "foreign_key_compound_pk2", ], "primary_keys": ["pk"], "count": 2, "hidden": False, "fts_table": None, "foreign_keys": { "incoming": [], "outgoing": [ { "other_table": "primary_key_multiple_columns", "column": "foreign_key_with_no_label", "other_column": "id", }, { "other_table": "simple_primary_key", "column": "foreign_key_with_blank_label", "other_column": "id", }, { "other_table": "simple_primary_key", "column": "foreign_key_with_label", "other_column": "id", }, ], }, "private": False, }, ] + [ { "name": "infinity", "columns": ["value"], "primary_keys": [], "count": 3, "hidden": False, "fts_table": None, "foreign_keys": {"incoming": [], "outgoing": []}, "private": False, }, { "name": "primary_key_multiple_columns", "columns": ["id", "content", "content2"], "primary_keys": ["id"], "count": 1, "hidden": False, "fts_table": None, "foreign_keys": { "incoming": [ { "other_table": "foreign_key_references", "column": "id", "other_column": "foreign_key_with_no_label", } ], "outgoing": [], }, "private": False, }, { "name": "primary_key_multiple_columns_explicit_label", "columns": ["id", "content", "content2"], "primary_keys": ["id"], "count": 1, "hidden": False, "fts_table": None, "foreign_keys": { "incoming": [ { "other_table": "custom_foreign_key_label", "column": "id", "other_column": "foreign_key_with_custom_label", } ], "outgoing": [], }, "private": False, }, { "name": "roadside_attraction_characteristics", "columns": ["attraction_id", "characteristic_id"], "primary_keys": [], "count": 5, "hidden": False, "fts_table": None, "foreign_keys": { "incoming": [], "outgoing": [ { "other_table": "attraction_characteristic", "column": "characteristic_id", "other_column": "pk", }, { "other_table": "roadside_attractions", "column": "attraction_id", "other_column": "pk", }, ], }, "private": False, }, { "name": "roadside_attractions", "columns": ["pk", "name", "address", "url", "latitude", "longitude"], "primary_keys": ["pk"], "count": 4, "hidden": False, "fts_table": None, "foreign_keys": { "incoming": [ { "other_table": "roadside_attraction_characteristics", "column": "pk", "other_column": "attraction_id", } ], "outgoing": [], }, "private": False, }, { "name": "searchable", "columns": ["pk", "text1", "text2", "name with . and spaces"], "primary_keys": ["pk"], "count": 2, "hidden": False, "fts_table": "searchable_fts", "foreign_keys": { "incoming": [ { "other_table": "searchable_tags", "column": "pk", "other_column": "searchable_id", } ], "outgoing": [], }, "private": False, }, { "name": "searchable_tags", "columns": ["searchable_id", "tag"], "primary_keys": ["searchable_id", "tag"], "count": 2, "hidden": False, "fts_table": None, "foreign_keys": { "incoming": [], "outgoing": [ {"other_table": "tags", "column": "tag", "other_column": "tag"}, { "other_table": "searchable", "column": "searchable_id", "other_column": "pk", }, ], }, "private": False, }, { "name": "select", "columns": ["group", "having", "and", "json"], "primary_keys": [], "count": 1, "hidden": False, "fts_table": None, "foreign_keys": {"incoming": [], "outgoing": []}, "private": False, }, { "name": "simple_primary_key", "columns": ["id", "content"], "primary_keys": ["id"], "count": 5, "hidden": False, "fts_table": None, "foreign_keys": { "incoming": [ { "other_table": "foreign_key_references", "column": "id", "other_column": "foreign_key_with_blank_label", }, { "other_table": "foreign_key_references", "column": "id", "other_column": "foreign_key_with_label", }, { "other_table": "complex_foreign_keys", "column": "id", "other_column": "f3", }, { "other_table": "complex_foreign_keys", "column": "id", "other_column": "f2", }, { "other_table": "complex_foreign_keys", "column": "id", "other_column": "f1", }, ], "outgoing": [], }, "private": False, }, { "name": "sortable", "columns": [ "pk1", "pk2", "content", "sortable", "sortable_with_nulls", "sortable_with_nulls_2", "text", ], "primary_keys": ["pk1", "pk2"], "count": 201, "hidden": False, "fts_table": None, "foreign_keys": {"incoming": [], "outgoing": []}, "private": False, }, { "name": "table/with/slashes.csv", "columns": ["pk", "content"], "primary_keys": ["pk"], "count": 1, "hidden": False, "fts_table": None, "foreign_keys": {"incoming": [], "outgoing": []}, "private": False, }, { "name": "tags", "columns": ["tag"], "primary_keys": ["tag"], "count": 2, "hidden": False, "fts_table": None, "foreign_keys": { "incoming": [ { "other_table": "searchable_tags", "column": "tag", "other_column": "tag", } ], "outgoing": [], }, "private": False, }, { "name": "units", "columns": ["pk", "distance", "frequency"], "primary_keys": ["pk"], "count": 3, "hidden": False, "fts_table": None, "foreign_keys": {"incoming": [], "outgoing": []}, "private": False, }, { "name": "no_primary_key", "columns": ["content", "a", "b", "c"], "primary_keys": [], "count": 201, "hidden": True, "fts_table": None, "foreign_keys": {"incoming": [], "outgoing": []}, "private": False, }, { "name": "searchable_fts", "columns": [ "text1", "text2", "name with . and spaces", ] + ( [ "searchable_fts", "docid", "__langid", ] if supports_table_xinfo() else [] ), "primary_keys": [], "count": 2, "hidden": True, "fts_table": "searchable_fts", "foreign_keys": {"incoming": [], "outgoing": []}, "private": False, }, { "name": "searchable_fts_docsize", "columns": ["docid", "size"], "primary_keys": ["docid"], "count": 2, "hidden": True, "fts_table": None, "foreign_keys": {"incoming": [], "outgoing": []}, "private": False, }, { "name": "searchable_fts_segdir", "columns": [ "level", "idx", "start_block", "leaves_end_block", "end_block", "root", ], "primary_keys": ["level", "idx"], "count": 1, "hidden": True, "fts_table": None, "foreign_keys": {"incoming": [], "outgoing": []}, "private": False, }, { "name": "searchable_fts_segments", "columns": ["blockid", "block"], "primary_keys": ["blockid"], "count": 0, "hidden": True, "fts_table": None, "foreign_keys": {"incoming": [], "outgoing": []}, "private": False, }, { "name": "searchable_fts_stat", "columns": ["id", "value"], "primary_keys": ["id"], "count": 1, "hidden": True, "fts_table": None, "foreign_keys": {"incoming": [], "outgoing": []}, "private": False, }, ] def test_no_files_uses_memory_database(app_client_no_files): response = app_client_no_files.get("/.json") assert response.status == 200 assert { "_memory": { "name": "_memory", "hash": None, "color": "a6c7b9", "path": "/_memory", "tables_and_views_truncated": [], "tables_and_views_more": False, "tables_count": 0, "table_rows_sum": 0, "show_table_row_counts": False, "hidden_table_rows_sum": 0, "hidden_tables_count": 0, "views_count": 0, "private": False, } } == response.json # Try that SQL query response = app_client_no_files.get( "/_memory.json?sql=select+sqlite_version()&_shape=array" ) assert 1 == len(response.json) assert ["sqlite_version()"] == list(response.json[0].keys()) @pytest.mark.parametrize( "path,expected_redirect", ( ("/:memory:", "/_memory"), ("/:memory:.json", "/_memory.json"), ("/:memory:?sql=select+1", "/_memory?sql=select+1"), ("/:memory:.json?sql=select+1", "/_memory.json?sql=select+1"), ("/:memory:.csv?sql=select+1", "/_memory.csv?sql=select+1"), ), ) def test_old_memory_urls_redirect(app_client_no_files, path, expected_redirect): response = app_client_no_files.get(path) assert response.status == 301 assert response.headers["location"] == expected_redirect def test_database_page_for_database_with_dot_in_name(app_client_with_dot): response = app_client_with_dot.get("/fixtures~2Edot.json") assert response.status == 200 @pytest.mark.asyncio async def test_custom_sql(ds_client): response = await ds_client.get( "/fixtures.json?sql=select+content+from+simple_primary_key" ) data = response.json() assert data == { "rows": [ {"content": "hello"}, {"content": "world"}, {"content": ""}, {"content": "RENDER_CELL_DEMO"}, {"content": "RENDER_CELL_ASYNC"}, ], "ok": True, "truncated": False, } def test_sql_time_limit(app_client_shorter_time_limit): response = app_client_shorter_time_limit.get("/fixtures.json?sql=select+sleep(0.5)") assert 400 == response.status assert response.json == { "ok": False, "error": ( "

SQL query took too long. The time limit is controlled by the\n" 'sql_time_limit_ms\n' "configuration option.

\n" '\n' "" ), "status": 400, "title": "SQL Interrupted", } @pytest.mark.asyncio async def test_custom_sql_time_limit(ds_client): response = await ds_client.get("/fixtures.json?sql=select+sleep(0.01)") assert response.status_code == 200 response = await ds_client.get("/fixtures.json?sql=select+sleep(0.01)&_timelimit=5") assert response.status_code == 400 assert response.json()["title"] == "SQL Interrupted" @pytest.mark.asyncio async def test_invalid_custom_sql(ds_client): response = await ds_client.get("/fixtures.json?sql=.schema") assert response.status_code == 400 assert response.json()["ok"] is False assert "Statement must be a SELECT" == response.json()["error"] @pytest.mark.asyncio async def test_row(ds_client): response = await ds_client.get("/fixtures/simple_primary_key/1.json?_shape=objects") assert response.status_code == 200 assert response.json()["rows"] == [{"id": "1", "content": "hello"}] @pytest.mark.asyncio async def test_row_strange_table_name(ds_client): response = await ds_client.get( "/fixtures/table~2Fwith~2Fslashes~2Ecsv/3.json?_shape=objects" ) assert response.status_code == 200 assert response.json()["rows"] == [{"pk": "3", "content": "hey"}] @pytest.mark.asyncio async def test_row_foreign_key_tables(ds_client): response = await ds_client.get( "/fixtures/simple_primary_key/1.json?_extras=foreign_key_tables" ) assert response.status_code == 200 assert response.json()["foreign_key_tables"] == [ { "other_table": "foreign_key_references", "column": "id", "other_column": "foreign_key_with_blank_label", "count": 0, "link": "/fixtures/foreign_key_references?foreign_key_with_blank_label=1", }, { "other_table": "foreign_key_references", "column": "id", "other_column": "foreign_key_with_label", "count": 1, "link": "/fixtures/foreign_key_references?foreign_key_with_label=1", }, { "other_table": "complex_foreign_keys", "column": "id", "other_column": "f3", "count": 1, "link": "/fixtures/complex_foreign_keys?f3=1", }, { "other_table": "complex_foreign_keys", "column": "id", "other_column": "f2", "count": 0, "link": "/fixtures/complex_foreign_keys?f2=1", }, { "other_table": "complex_foreign_keys", "column": "id", "other_column": "f1", "count": 1, "link": "/fixtures/complex_foreign_keys?f1=1", }, ] def test_databases_json(app_client_two_attached_databases_one_immutable): response = app_client_two_attached_databases_one_immutable.get("/-/databases.json") databases = response.json assert 2 == len(databases) extra_database, fixtures_database = databases assert "extra database" == extra_database["name"] assert None == extra_database["hash"] assert True == extra_database["is_mutable"] assert False == extra_database["is_memory"] assert "fixtures" == fixtures_database["name"] assert fixtures_database["hash"] is not None assert False == fixtures_database["is_mutable"] assert False == fixtures_database["is_memory"] @pytest.mark.asyncio async def test_metadata_json(ds_client): response = await ds_client.get("/-/metadata.json") assert response.json() == ds_client.ds.metadata() @pytest.mark.asyncio async def test_threads_json(ds_client): response = await ds_client.get("/-/threads.json") expected_keys = {"threads", "num_threads"} if sys.version_info >= (3, 7, 0): expected_keys.update({"tasks", "num_tasks"}) data = response.json() assert set(data.keys()) == expected_keys # Should be at least one _execute_writes thread for __INTERNAL__ thread_names = [thread["name"] for thread in data["threads"]] assert "_execute_writes for database __INTERNAL__" in thread_names @pytest.mark.asyncio async def test_plugins_json(ds_client): response = await ds_client.get("/-/plugins.json") # Filter out TrackEventPlugin actual_plugins = sorted( [p for p in response.json() if p["name"] != "TrackEventPlugin"], key=lambda p: p["name"], ) assert EXPECTED_PLUGINS == actual_plugins # Try with ?all=1 response = await ds_client.get("/-/plugins.json?all=1") names = {p["name"] for p in response.json()} assert names.issuperset(p["name"] for p in EXPECTED_PLUGINS) assert names.issuperset(DEFAULT_PLUGINS) @pytest.mark.asyncio async def test_versions_json(ds_client): response = await ds_client.get("/-/versions.json") data = response.json() assert "python" in data assert "3.0" == data.get("asgi") assert "version" in data["python"] assert "full" in data["python"] assert "datasette" in data assert "version" in data["datasette"] assert data["datasette"]["version"] == __version__ assert "sqlite" in data assert "version" in data["sqlite"] assert "fts_versions" in data["sqlite"] assert "compile_options" in data["sqlite"] @pytest.mark.asyncio async def test_settings_json(ds_client): response = await ds_client.get("/-/settings.json") assert response.json() == { "default_page_size": 50, "default_facet_size": 30, "default_allow_sql": True, "facet_suggest_time_limit_ms": 50, "facet_time_limit_ms": 200, "max_returned_rows": 100, "max_insert_rows": 100, "sql_time_limit_ms": 200, "allow_download": True, "allow_signed_tokens": True, "max_signed_tokens_ttl": 0, "allow_facet": True, "suggest_facets": True, "default_cache_ttl": 5, "num_sql_threads": 1, "cache_size_kb": 0, "allow_csv_stream": True, "max_csv_mb": 100, "truncate_cells_html": 2048, "force_https_urls": False, "template_debug": False, "trace_debug": False, "base_url": "/", } test_json_columns_default_expected = [ {"intval": 1, "strval": "s", "floatval": 0.5, "jsonval": '{"foo": "bar"}'} ] @pytest.mark.asyncio @pytest.mark.parametrize( "extra_args,expected", [ ("", test_json_columns_default_expected), ("&_json=intval", test_json_columns_default_expected), ("&_json=strval", test_json_columns_default_expected), ("&_json=floatval", test_json_columns_default_expected), ( "&_json=jsonval", [{"intval": 1, "strval": "s", "floatval": 0.5, "jsonval": {"foo": "bar"}}], ), ], ) async def test_json_columns(ds_client, extra_args, expected): sql = """ select 1 as intval, "s" as strval, 0.5 as floatval, '{"foo": "bar"}' as jsonval """ path = "/fixtures.json?" + urllib.parse.urlencode({"sql": sql, "_shape": "array"}) path += extra_args response = await ds_client.get(path) assert response.json() == expected def test_config_cache_size(app_client_larger_cache_size): response = app_client_larger_cache_size.get("/fixtures/pragma_cache_size.json") assert response.json["rows"] == [{"cache_size": -2500}] def test_config_force_https_urls(): with make_app_client(settings={"force_https_urls": True}) as client: response = client.get( "/fixtures/facetable.json?_size=3&_facet=state&_extra=next_url,suggested_facets" ) assert response.json["next_url"].startswith("https://") assert response.json["facet_results"]["results"]["state"]["results"][0][ "toggle_url" ].startswith("https://") assert response.json["suggested_facets"][0]["toggle_url"].startswith("https://") # Also confirm that request.url and request.scheme are set correctly response = client.get("/") assert client.ds._last_request.url.startswith("https://") assert client.ds._last_request.scheme == "https" @pytest.mark.parametrize( "path,status_code", [ ("/fixtures.db", 200), ("/fixtures.json", 200), ("/fixtures/no_primary_key.json", 200), # A 400 invalid SQL query should still have the header: ("/fixtures.json?sql=select+blah", 400), # Write APIs ("/fixtures/-/create", 405), ("/fixtures/facetable/-/insert", 405), ("/fixtures/facetable/-/drop", 405), ], ) def test_cors( app_client_with_cors, app_client_two_attached_databases_one_immutable, path, status_code, ): response = app_client_with_cors.get(path) assert response.status == status_code assert response.headers["Access-Control-Allow-Origin"] == "*" assert ( response.headers["Access-Control-Allow-Headers"] == "Authorization, Content-Type" ) assert response.headers["Access-Control-Expose-Headers"] == "Link" assert ( response.headers["Access-Control-Allow-Methods"] == "GET, POST, HEAD, OPTIONS" ) assert response.headers["Access-Control-Max-Age"] == "3600" # Same request to app_client_two_attached_databases_one_immutable # should not have those headers - I'm using that fixture because # regular app_client doesn't have immutable fixtures.db which means # the test for /fixtures.db returns a 403 error response = app_client_two_attached_databases_one_immutable.get(path) assert response.status == status_code assert "Access-Control-Allow-Origin" not in response.headers assert "Access-Control-Allow-Headers" not in response.headers assert "Access-Control-Expose-Headers" not in response.headers assert "Access-Control-Allow-Methods" not in response.headers assert "Access-Control-Max-Age" not in response.headers @pytest.mark.parametrize( "path", ( "/", ".json", "/searchable", "/searchable.json", "/searchable_view", "/searchable_view.json", ), ) def test_database_with_space_in_name(app_client_two_attached_databases, path): response = app_client_two_attached_databases.get( "/extra~20database" + path, follow_redirects=True ) assert response.status == 200 def test_common_prefix_database_names(app_client_conflicting_database_names): # https://github.com/simonw/datasette/issues/597 assert ["foo-bar", "foo", "fixtures"] == [ d["name"] for d in app_client_conflicting_database_names.get("/-/databases.json").json ] for db_name, path in (("foo", "/foo.json"), ("foo-bar", "/foo-bar.json")): data = app_client_conflicting_database_names.get(path).json assert db_name == data["database"] def test_inspect_file_used_for_count(app_client_immutable_and_inspect_file): response = app_client_immutable_and_inspect_file.get( "/fixtures/sortable.json?_extra=count" ) assert response.json["count"] == 100 @pytest.mark.asyncio async def test_http_options_request(ds_client): response = await ds_client.options("/fixtures") assert response.status_code == 200 assert response.text == "ok" @pytest.mark.asyncio async def test_db_path(app_client): # Needs app_client because needs file based database db = app_client.ds.get_database() path = pathlib.Path(db.path) assert path.exists() datasette = Datasette([path]) # Previously this broke if path was a pathlib.Path: await datasette.refresh_schemas() @pytest.mark.asyncio async def test_hidden_sqlite_stat1_table(): ds = Datasette() db = ds.add_memory_database("db") await db.execute_write("create table normal (id integer primary key, name text)") await db.execute_write("create index idx on normal (name)") await db.execute_write("analyze") data = (await ds.client.get("/db.json?_show_hidden=1")).json() tables = [(t["name"], t["hidden"]) for t in data["tables"]] assert tables in ( [("normal", False), ("sqlite_stat1", True)], [("normal", False), ("sqlite_stat1", True), ("sqlite_stat4", True)], ) @pytest.mark.asyncio async def test_hide_tables_starting_with_underscore(): ds = Datasette() db = ds.add_memory_database("test_hide_tables_starting_with_underscore") await db.execute_write("create table normal (id integer primary key, name text)") await db.execute_write("create table _hidden (id integer primary key, name text)") data = ( await ds.client.get( "/test_hide_tables_starting_with_underscore.json?_show_hidden=1" ) ).json() tables = [(t["name"], t["hidden"]) for t in data["tables"]] assert tables == [("normal", False), ("_hidden", True)] @pytest.mark.asyncio @pytest.mark.parametrize("db_name", ("foo", r"fo%o", "f~/c.d")) async def test_tilde_encoded_database_names(db_name): ds = Datasette() ds.add_memory_database(db_name) response = await ds.client.get("/.json") assert db_name in response.json().keys() path = response.json()[db_name]["path"] # And the JSON for that database response2 = await ds.client.get(path + ".json") assert response2.status_code == 200 @pytest.mark.asyncio @pytest.mark.parametrize( "config,expected", ( ({}, {}), ({"plugins": {"datasette-foo": "bar"}}, {"plugins": {"datasette-foo": "bar"}}), # Test redaction ( { "plugins": { "datasette-auth": {"secret_key": "key"}, "datasette-foo": "bar", "datasette-auth2": {"password": "password"}, "datasette-sentry": { "dsn": "sentry:///foo", }, } }, { "plugins": { "datasette-auth": {"secret_key": "***"}, "datasette-foo": "bar", "datasette-auth2": {"password": "***"}, "datasette-sentry": {"dsn": "***"}, } }, ), ), ) async def test_config_json(config, expected): "/-/config.json should return redacted configuration" ds = Datasette(config=config) response = await ds.client.get("/-/config.json") assert response.json() == expected @pytest.mark.asyncio @pytest.mark.parametrize( "metadata,expected_config,expected_metadata", ( ({}, {}, {}), ( # Metadata input { "title": "Datasette Fixtures", "databases": { "fixtures": { "tables": { "sortable": { "sortable_columns": [ "sortable", "sortable_with_nulls", "sortable_with_nulls_2", "text", ], }, "no_primary_key": {"sortable_columns": [], "hidden": True}, "units": {"units": {"distance": "m", "frequency": "Hz"}}, "primary_key_multiple_columns_explicit_label": { "label_column": "content2" }, "simple_view": {"sortable_columns": ["content"]}, "searchable_view_configured_by_metadata": { "fts_table": "searchable_fts", "fts_pk": "pk", }, "roadside_attractions": { "columns": { "name": "The name of the attraction", "address": "The street address for the attraction", } }, "attraction_characteristic": {"sort_desc": "pk"}, "facet_cities": {"sort": "name"}, "paginated_view": {"size": 25}, }, } }, }, # Should produce a config with just the table configuration keys { "databases": { "fixtures": { "tables": { "sortable": { "sortable_columns": [ "sortable", "sortable_with_nulls", "sortable_with_nulls_2", "text", ] }, "units": {"units": {"distance": "m", "frequency": "Hz"}}, # These one get redacted: "no_primary_key": "***", "primary_key_multiple_columns_explicit_label": "***", "simple_view": {"sortable_columns": ["content"]}, "searchable_view_configured_by_metadata": { "fts_table": "searchable_fts", "fts_pk": "pk", }, "attraction_characteristic": {"sort_desc": "pk"}, "facet_cities": {"sort": "name"}, "paginated_view": {"size": 25}, } } } }, # And metadata with everything else { "title": "Datasette Fixtures", "databases": { "fixtures": { "tables": { "roadside_attractions": { "columns": { "name": "The name of the attraction", "address": "The street address for the attraction", } }, } } }, }, ), ), ) async def test_upgrade_metadata(metadata, expected_config, expected_metadata): ds = Datasette(metadata=metadata) response = await ds.client.get("/-/config.json") assert response.json() == expected_config response2 = await ds.client.get("/-/metadata.json") assert response2.json() == expected_metadata