dclient/tests/test_env.py
Simon Willison 3621f13263 v2 config system, CLI rewrite, and updated core commands
Replace aliases.json with config.json storing instances with
default_database. Instance resolution: -i flag → config default →
DATASETTE_URL. Database resolution: -d flag → instance default_database →
DATASETTE_DATABASE. Token resolution: --token → auth.json by alias →
auth.json by URL → DATASETTE_TOKEN.

Add click-default-group dependency for bare SQL shortcut support.
Add DCLIENT_CONFIG_DIR env var to override config directory.

Rewrite query, insert, get, and actor commands for v2 API where
instance is always -i flag. Add upsert command sharing insert
implementation. Add databases, tables, schema, plugins commands.
Add default_query hidden command for bare SQL shortcut.

Add alias default, alias default-db subcommands.
Add auth status subcommand.

refs #29

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 15:21:52 -08:00

221 lines
8.3 KiB
Python

from click.testing import CliRunner
from dclient.cli import cli
import json
import pathlib
QUERY_RESPONSE = {
"ok": True,
"database": "data",
"query_name": None,
"rows": [{"id": 1}],
"truncated": False,
"columns": ["id"],
"query": {"sql": "select 1", "params": {}},
"error": None,
"private": False,
"allow_execute_sql": True,
}
# -- DATASETTE_TOKEN tests --
def test_datasette_token_used_as_fallback(httpx_mock, mocker, tmpdir):
"""DATASETTE_TOKEN is used when no --token flag and no auth.json match."""
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
httpx_mock.add_response(json=QUERY_RESPONSE, status_code=200)
runner = CliRunner(env={"DATASETTE_TOKEN": "env-token-123"})
result = runner.invoke(
cli, ["query", "data", "select 1", "-i", "https://example.com"]
)
assert result.exit_code == 0
request = httpx_mock.get_request()
assert request.headers["authorization"] == "Bearer env-token-123"
def test_token_flag_overrides_datasette_token(httpx_mock, mocker, tmpdir):
"""--token flag takes priority over DATASETTE_TOKEN."""
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
httpx_mock.add_response(json=QUERY_RESPONSE, status_code=200)
runner = CliRunner(env={"DATASETTE_TOKEN": "env-token"})
result = runner.invoke(
cli,
[
"query", "data", "select 1",
"-i", "https://example.com",
"--token", "flag-token",
],
)
assert result.exit_code == 0
request = httpx_mock.get_request()
assert request.headers["authorization"] == "Bearer flag-token"
def test_auth_json_overrides_datasette_token(httpx_mock, mocker, tmpdir):
"""Stored auth.json token takes priority over DATASETTE_TOKEN."""
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
auth_file = pathlib.Path(tmpdir) / "auth.json"
auth_file.write_text(json.dumps({"https://example.com": "stored-token"}))
httpx_mock.add_response(json=QUERY_RESPONSE, status_code=200)
runner = CliRunner(env={"DATASETTE_TOKEN": "env-token"})
result = runner.invoke(
cli, ["query", "data", "select 1", "-i", "https://example.com"]
)
assert result.exit_code == 0
request = httpx_mock.get_request()
assert request.headers["authorization"] == "Bearer stored-token"
# -- DATASETTE_URL tests --
def test_datasette_url_used_as_instance(httpx_mock, mocker, tmpdir):
"""DATASETTE_URL provides the instance when no -i flag."""
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
httpx_mock.add_response(json=QUERY_RESPONSE, status_code=200)
runner = CliRunner(env={"DATASETTE_URL": "https://my-instance.datasette.cloud"})
result = runner.invoke(cli, ["query", "data", "select 1"])
assert result.exit_code == 0
request = httpx_mock.get_request()
assert request.url.host == "my-instance.datasette.cloud"
assert request.url.path == "/data.json"
def test_datasette_url_with_trailing_slash(httpx_mock, mocker, tmpdir):
"""DATASETTE_URL with trailing slash still works correctly."""
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
httpx_mock.add_response(json=QUERY_RESPONSE, status_code=200)
runner = CliRunner(env={"DATASETTE_URL": "https://my-instance.datasette.cloud/"})
result = runner.invoke(cli, ["query", "data", "select 1"])
assert result.exit_code == 0
request = httpx_mock.get_request()
assert request.url.path == "/data.json"
def test_explicit_instance_ignores_datasette_url(httpx_mock, mocker, tmpdir):
"""An explicit -i flag is used, ignoring DATASETTE_URL."""
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
httpx_mock.add_response(json=QUERY_RESPONSE, status_code=200)
runner = CliRunner(env={"DATASETTE_URL": "https://should-be-ignored.com"})
result = runner.invoke(
cli, ["query", "db", "select 1", "-i", "https://other.example.com"]
)
assert result.exit_code == 0
request = httpx_mock.get_request()
assert request.url.host == "other.example.com"
assert request.url.path == "/db.json"
def test_alias_takes_priority_over_datasette_url(httpx_mock, mocker, tmpdir):
"""Alias match via -i takes priority over DATASETTE_URL."""
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
config_file = pathlib.Path(tmpdir) / "config.json"
config_file.write_text(
json.dumps(
{
"default_instance": None,
"instances": {
"myalias": {
"url": "https://aliased.example.com",
"default_database": None,
}
},
}
)
)
httpx_mock.add_response(json=QUERY_RESPONSE, status_code=200)
runner = CliRunner(env={"DATASETTE_URL": "https://should-be-ignored.com"})
result = runner.invoke(cli, ["query", "db", "select 1", "-i", "myalias"])
assert result.exit_code == 0
request = httpx_mock.get_request()
assert request.url.host == "aliased.example.com"
assert request.url.path == "/db.json"
# -- DATASETTE_URL with other commands --
def test_datasette_url_with_insert(httpx_mock, mocker, tmpdir):
"""DATASETTE_URL works with the insert command."""
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
httpx_mock.add_response(json={"ok": True}, status_code=200)
csv_path = pathlib.Path(tmpdir) / "data.csv"
csv_path.write_text("id,name\n1,hello\n")
runner = CliRunner(
env={
"DATASETTE_URL": "https://my-instance.datasette.cloud",
"DATASETTE_TOKEN": "env-token",
}
)
result = runner.invoke(
cli,
["insert", "data", "my_table", str(csv_path), "--csv", "--create"],
catch_exceptions=False,
)
assert result.exit_code == 0
request = httpx_mock.get_request()
assert request.url.host == "my-instance.datasette.cloud"
assert "/-/create" in str(request.url.path)
assert request.headers["authorization"] == "Bearer env-token"
def test_datasette_url_with_actor(httpx_mock, mocker, tmpdir):
"""DATASETTE_URL works with the actor command."""
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
httpx_mock.add_response(
json={"actor": {"id": "root"}},
status_code=200,
)
runner = CliRunner(
env={
"DATASETTE_URL": "https://my-instance.datasette.cloud",
"DATASETTE_TOKEN": "env-token",
}
)
result = runner.invoke(cli, ["actor"])
assert result.exit_code == 0
request = httpx_mock.get_request()
assert request.url.host == "my-instance.datasette.cloud"
assert request.headers["authorization"] == "Bearer env-token"
# -- Both together --
def test_datasette_url_and_token_together(httpx_mock, mocker, tmpdir):
"""DATASETTE_URL and DATASETTE_TOKEN work together for a complete config."""
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
httpx_mock.add_response(json=QUERY_RESPONSE, status_code=200)
runner = CliRunner(
env={
"DATASETTE_URL": "https://my-instance.datasette.cloud",
"DATASETTE_TOKEN": "env-token-456",
}
)
result = runner.invoke(cli, ["query", "mydb", "select 1"])
assert result.exit_code == 0
request = httpx_mock.get_request()
assert request.url.host == "my-instance.datasette.cloud"
assert request.url.path == "/mydb.json"
assert request.headers["authorization"] == "Bearer env-token-456"
# -- DATASETTE_DATABASE tests --
def test_datasette_database_with_default_query(httpx_mock, mocker, tmpdir):
"""DATASETTE_DATABASE is used by default_query shortcut."""
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
httpx_mock.add_response(json=QUERY_RESPONSE, status_code=200)
runner = CliRunner(
env={
"DATASETTE_URL": "https://my-instance.datasette.cloud",
"DATASETTE_DATABASE": "mydb",
}
)
result = runner.invoke(cli, ["select 1"])
assert result.exit_code == 0
request = httpx_mock.get_request()
assert request.url.host == "my-instance.datasette.cloud"
assert request.url.path == "/mydb.json"