mirror of
https://github.com/simonw/dclient.git
synced 2026-09-15 21:14:11 +02:00
Tests and docs for v2 introspection commands and alias features
Add test_commands_v2.py with 28 tests covering databases, tables, plugins, schema, default_query, upsert, auth status, actor, and get commands. Add test_alias_v2.py with 5 tests for alias default, alias default-db, and removing the default alias. Update aliases.md and authentication.md docs for v2 API. refs #29 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3621f13263
commit
92c0d2a5ba
4 changed files with 812 additions and 36 deletions
111
docs/aliases.md
111
docs/aliases.md
|
|
@ -1,17 +1,40 @@
|
|||
# Aliases
|
||||
|
||||
You can assign an alias to a Datasette database using the `dclient alias` command:
|
||||
You can assign an alias to a Datasette instance using the `dclient alias add` command:
|
||||
|
||||
dclient alias add content https://datasette.io/content
|
||||
dclient alias add latest https://latest.datasette.io
|
||||
|
||||
You can list aliases with `dclient alias list`:
|
||||
|
||||
$ dclient alias list
|
||||
content = https://datasette.io/content
|
||||
latest = https://latest.datasette.io
|
||||
|
||||
Once registered, you can pass an alias to commands such as `dclient query`:
|
||||
Once registered, you can pass an alias to commands using the `-i` flag:
|
||||
|
||||
dclient query content "select * from news limit 1"
|
||||
dclient query fixtures "select * from news limit 1" -i latest
|
||||
|
||||
## Default instance
|
||||
|
||||
Set a default instance so you don't need `-i` every time:
|
||||
|
||||
dclient alias default latest
|
||||
|
||||
Now commands will use `latest` automatically:
|
||||
|
||||
dclient databases
|
||||
dclient tables -d fixtures
|
||||
|
||||
## Default database
|
||||
|
||||
Set a default database for an alias:
|
||||
|
||||
dclient alias default-db latest fixtures
|
||||
|
||||
Now you can run bare SQL queries directly:
|
||||
|
||||
dclient "select * from facetable limit 5"
|
||||
|
||||
This uses the default instance and default database.
|
||||
|
||||
## dclient alias --help
|
||||
<!-- [[[cog
|
||||
|
|
@ -34,9 +57,11 @@ Options:
|
|||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
add Add an alias
|
||||
list List aliases
|
||||
remove Remove an alias
|
||||
add Add an alias for a Datasette instance
|
||||
default Set or show the default instance
|
||||
default-db Set or show the default database for an alias
|
||||
list List aliases
|
||||
remove Remove an alias
|
||||
|
||||
```
|
||||
<!-- [[[end]]] -->
|
||||
|
|
@ -56,10 +81,6 @@ Usage: dclient alias list [OPTIONS]
|
|||
|
||||
List aliases
|
||||
|
||||
Example usage:
|
||||
|
||||
dclient aliases list
|
||||
|
||||
Options:
|
||||
--json Output raw JSON
|
||||
--help Show this message and exit.
|
||||
|
|
@ -80,15 +101,11 @@ cog.out(
|
|||
```
|
||||
Usage: dclient alias add [OPTIONS] NAME URL
|
||||
|
||||
Add an alias
|
||||
Add an alias for a Datasette instance
|
||||
|
||||
Example usage:
|
||||
|
||||
dclient alias add content https://datasette.io/content
|
||||
|
||||
Then:
|
||||
|
||||
dclient query content 'select * from news limit 3'
|
||||
dclient alias add prod https://myapp.datasette.cloud
|
||||
|
||||
Options:
|
||||
--help Show this message and exit.
|
||||
|
|
@ -113,10 +130,66 @@ Usage: dclient alias remove [OPTIONS] NAME
|
|||
|
||||
Example usage:
|
||||
|
||||
dclient alias remove content
|
||||
dclient alias remove prod
|
||||
|
||||
Options:
|
||||
--help Show this message and exit.
|
||||
|
||||
```
|
||||
<!-- [[[end]]] -->
|
||||
|
||||
## dclient alias default --help
|
||||
|
||||
<!-- [[[cog
|
||||
import cog
|
||||
result = runner.invoke(cli.cli, ["alias", "default", "--help"])
|
||||
help = result.output.replace("Usage: cli", "Usage: dclient")
|
||||
cog.out(
|
||||
"```\n{}\n```".format(help)
|
||||
)
|
||||
]]] -->
|
||||
```
|
||||
Usage: dclient alias default [OPTIONS] [NAME]
|
||||
|
||||
Set or show the default instance
|
||||
|
||||
Example usage:
|
||||
|
||||
dclient alias default prod
|
||||
dclient alias default
|
||||
dclient alias default --clear
|
||||
|
||||
Options:
|
||||
--clear Clear default instance
|
||||
--help Show this message and exit.
|
||||
|
||||
```
|
||||
<!-- [[[end]]] -->
|
||||
|
||||
## dclient alias default-db --help
|
||||
|
||||
<!-- [[[cog
|
||||
import cog
|
||||
result = runner.invoke(cli.cli, ["alias", "default-db", "--help"])
|
||||
help = result.output.replace("Usage: cli", "Usage: dclient")
|
||||
cog.out(
|
||||
"```\n{}\n```".format(help)
|
||||
)
|
||||
]]] -->
|
||||
```
|
||||
Usage: dclient alias default-db [OPTIONS] ALIAS_NAME [DB]
|
||||
|
||||
Set or show the default database for an alias
|
||||
|
||||
Example usage:
|
||||
|
||||
dclient alias default-db prod main
|
||||
dclient alias default-db prod
|
||||
dclient alias default-db prod --clear
|
||||
|
||||
Options:
|
||||
--clear Clear default database for this alias
|
||||
--help Show this message and exit.
|
||||
|
||||
```
|
||||
<!-- [[[end]]] -->
|
||||
|
|
|
|||
|
|
@ -4,35 +4,57 @@
|
|||
|
||||
`dclient` can handle API tokens for Datasette instances that require authentication.
|
||||
|
||||
You can pass an API token to `query` using `-t/--token` like this:
|
||||
You can pass an API token to any command using `--token` like this:
|
||||
|
||||
```bash
|
||||
dclient query https://latest.datasette.io/fixtures "select * from facetable" -t dstok_mytoken
|
||||
dclient query fixtures "select * from facetable" --token dstok_mytoken -i latest
|
||||
```
|
||||
|
||||
A more convenient way to handle this is to store tokens to be used with different URL prefixes.
|
||||
A more convenient way to handle this is to store tokens for your aliases.
|
||||
|
||||
## Using stored tokens
|
||||
|
||||
To always use `dstok_mytoken` for any URL on the `https://latest.datasette.io/` instance you can run this:
|
||||
To store a token for an alias:
|
||||
```bash
|
||||
dclient auth add https://latest.datasette.io/
|
||||
dclient auth add latest
|
||||
```
|
||||
Then paste in the token and hit enter when prompted to do so.
|
||||
|
||||
To list which URLs you have set tokens for, run the `auth list` command:
|
||||
Tokens can also be stored for direct URLs:
|
||||
```bash
|
||||
dclient auth add https://latest.datasette.io
|
||||
```
|
||||
|
||||
To list which aliases/URLs you have set tokens for, run the `auth list` command:
|
||||
```bash
|
||||
dclient auth list
|
||||
```
|
||||
To delete the token for a specific URL, run `auth remove`:
|
||||
To delete the token for a specific alias or URL, run `auth remove`:
|
||||
```bash
|
||||
dclient auth remove https://latest.datasette.io/
|
||||
dclient auth remove latest
|
||||
```
|
||||
|
||||
## Token resolution order
|
||||
|
||||
When making a request, dclient resolves the token in this order:
|
||||
|
||||
1. `--token` CLI flag (highest priority)
|
||||
2. Token stored by alias name in `auth.json`
|
||||
3. Token stored by URL prefix in `auth.json`
|
||||
4. `DATASETTE_TOKEN` environment variable (lowest priority)
|
||||
|
||||
## Testing a token
|
||||
|
||||
The `dclient actor` command can be used to test a token, retrieving the actor that the token represents.
|
||||
The `dclient auth status` command can be used to verify authentication by calling `/-/actor.json`:
|
||||
```bash
|
||||
dclient actor https://latest.datasette.io/content
|
||||
dclient auth status
|
||||
dclient auth status -i prod
|
||||
```
|
||||
|
||||
The `dclient actor` command also shows the actor:
|
||||
```bash
|
||||
dclient actor
|
||||
dclient actor -i prod
|
||||
```
|
||||
The output looks like this:
|
||||
```json
|
||||
|
|
@ -68,6 +90,7 @@ Commands:
|
|||
add Add an authentication token for an alias or URL
|
||||
list List stored API tokens
|
||||
remove Remove the API token for an alias or URL
|
||||
status Verify authentication by calling /-/actor.json
|
||||
|
||||
```
|
||||
<!-- [[[end]]] -->
|
||||
|
|
@ -89,7 +112,8 @@ Usage: dclient auth add [OPTIONS] ALIAS_OR_URL
|
|||
|
||||
Example usage:
|
||||
|
||||
dclient auth add https://datasette.io/content
|
||||
dclient auth add prod
|
||||
dclient auth add https://datasette.io
|
||||
|
||||
Paste in the token when prompted.
|
||||
|
||||
|
|
@ -142,7 +166,7 @@ Usage: dclient auth remove [OPTIONS] ALIAS_OR_URL
|
|||
|
||||
Example usage:
|
||||
|
||||
dclient auth remove https://datasette.io/content
|
||||
dclient auth remove prod
|
||||
|
||||
Options:
|
||||
--help Show this message and exit.
|
||||
|
|
@ -150,6 +174,34 @@ Options:
|
|||
```
|
||||
<!-- [[[end]]] -->
|
||||
|
||||
## dclient auth status --help
|
||||
|
||||
<!-- [[[cog
|
||||
import cog
|
||||
result = runner.invoke(cli.cli, ["auth", "status", "--help"])
|
||||
help = result.output.replace("Usage: cli", "Usage: dclient")
|
||||
cog.out(
|
||||
"```\n{}\n```".format(help)
|
||||
)
|
||||
]]] -->
|
||||
```
|
||||
Usage: dclient auth status [OPTIONS]
|
||||
|
||||
Verify authentication by calling /-/actor.json
|
||||
|
||||
Example usage:
|
||||
|
||||
dclient auth status
|
||||
dclient auth status -i prod
|
||||
|
||||
Options:
|
||||
-i, --instance TEXT Datasette instance URL or alias
|
||||
--token TEXT API token
|
||||
--help Show this message and exit.
|
||||
|
||||
```
|
||||
<!-- [[[end]]] -->
|
||||
|
||||
## dclient actor --help
|
||||
|
||||
<!-- [[[cog
|
||||
|
|
@ -161,17 +213,19 @@ cog.out(
|
|||
)
|
||||
]]] -->
|
||||
```
|
||||
Usage: dclient actor [OPTIONS] [URL_OR_ALIAS]
|
||||
Usage: dclient actor [OPTIONS]
|
||||
|
||||
Show the actor represented by an API token
|
||||
|
||||
Example usage:
|
||||
|
||||
dclient actor https://latest.datasette.io/fixtures
|
||||
dclient actor
|
||||
dclient actor -i prod
|
||||
|
||||
Options:
|
||||
--token TEXT API token
|
||||
--help Show this message and exit.
|
||||
-i, --instance TEXT Datasette instance URL or alias
|
||||
--token TEXT API token
|
||||
--help Show this message and exit.
|
||||
|
||||
```
|
||||
<!-- [[[end]]] -->
|
||||
<!-- [[[end]]] -->
|
||||
|
|
|
|||
111
tests/test_alias_v2.py
Normal file
111
tests/test_alias_v2.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
"""Tests for v2 alias command: default, default-db subcommands."""
|
||||
|
||||
from click.testing import CliRunner
|
||||
from dclient.cli import cli
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
|
||||
def test_alias_default_workflow(mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
runner = CliRunner()
|
||||
|
||||
# Add an alias
|
||||
result = runner.invoke(cli, ["alias", "add", "prod", "https://prod.example.com"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# No default yet
|
||||
result = runner.invoke(cli, ["alias", "default"])
|
||||
assert result.exit_code == 0
|
||||
assert "No default instance set" in result.output
|
||||
|
||||
# Set default
|
||||
result = runner.invoke(cli, ["alias", "default", "prod"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Show default
|
||||
result = runner.invoke(cli, ["alias", "default"])
|
||||
assert result.exit_code == 0
|
||||
assert result.output.strip() == "prod"
|
||||
|
||||
# List should show * marker
|
||||
result = runner.invoke(cli, ["alias", "list"])
|
||||
assert result.exit_code == 0
|
||||
assert "* prod" in result.output
|
||||
|
||||
# Clear default
|
||||
result = runner.invoke(cli, ["alias", "default", "--clear"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
result = runner.invoke(cli, ["alias", "default"])
|
||||
assert "No default instance set" in result.output
|
||||
|
||||
|
||||
def test_alias_default_unknown_alias(mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["alias", "default", "nonexistent"])
|
||||
assert result.exit_code == 1
|
||||
assert "No such alias" in result.output
|
||||
|
||||
|
||||
def test_alias_default_db_workflow(mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
runner = CliRunner()
|
||||
|
||||
# Add an alias
|
||||
result = runner.invoke(cli, ["alias", "add", "prod", "https://prod.example.com"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# No default database yet
|
||||
result = runner.invoke(cli, ["alias", "default-db", "prod"])
|
||||
assert result.exit_code == 0
|
||||
assert "No default database set" in result.output
|
||||
|
||||
# Set default database
|
||||
result = runner.invoke(cli, ["alias", "default-db", "prod", "main"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Show default database
|
||||
result = runner.invoke(cli, ["alias", "default-db", "prod"])
|
||||
assert result.exit_code == 0
|
||||
assert result.output.strip() == "main"
|
||||
|
||||
# List should show db info
|
||||
result = runner.invoke(cli, ["alias", "list"])
|
||||
assert "(db: main)" in result.output
|
||||
|
||||
# Clear default database
|
||||
result = runner.invoke(cli, ["alias", "default-db", "prod", "--clear"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
result = runner.invoke(cli, ["alias", "default-db", "prod"])
|
||||
assert "No default database set" in result.output
|
||||
|
||||
|
||||
def test_alias_default_db_unknown_alias(mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["alias", "default-db", "nonexistent", "main"])
|
||||
assert result.exit_code == 1
|
||||
assert "No such alias" in result.output
|
||||
|
||||
|
||||
def test_alias_remove_clears_default(mocker, tmpdir):
|
||||
"""Removing the default alias also clears default_instance."""
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
runner = CliRunner()
|
||||
|
||||
runner.invoke(cli, ["alias", "add", "prod", "https://prod.example.com"])
|
||||
runner.invoke(cli, ["alias", "default", "prod"])
|
||||
|
||||
# Verify it's set
|
||||
config = json.loads((pathlib.Path(tmpdir) / "config.json").read_text())
|
||||
assert config["default_instance"] == "prod"
|
||||
|
||||
# Remove alias
|
||||
runner.invoke(cli, ["alias", "remove", "prod"])
|
||||
|
||||
config = json.loads((pathlib.Path(tmpdir) / "config.json").read_text())
|
||||
assert config["default_instance"] is None
|
||||
assert "prod" not in config["instances"]
|
||||
538
tests/test_commands_v2.py
Normal file
538
tests/test_commands_v2.py
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
"""Tests for v2 commands: databases, tables, plugins, schema, default_query, upsert."""
|
||||
|
||||
from click.testing import CliRunner
|
||||
from dclient.cli import cli
|
||||
import json
|
||||
import pathlib
|
||||
import pytest
|
||||
|
||||
|
||||
# -- databases command --
|
||||
|
||||
|
||||
def test_databases_json(httpx_mock, mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
httpx_mock.add_response(
|
||||
json={
|
||||
"databases": [
|
||||
{"name": "main", "tables_count": 12},
|
||||
{"name": "extra", "tables_count": 3},
|
||||
]
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli, ["databases", "-i", "https://example.com", "--json"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert len(data) == 2
|
||||
assert data[0]["name"] == "main"
|
||||
|
||||
|
||||
def test_databases_plain(httpx_mock, mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
httpx_mock.add_response(
|
||||
json={
|
||||
"databases": [
|
||||
{"name": "main", "tables_count": 12},
|
||||
{"name": "extra", "tables_count": 3},
|
||||
]
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["databases", "-i", "https://example.com"])
|
||||
assert result.exit_code == 0
|
||||
assert "main\n" in result.output
|
||||
assert "extra\n" in result.output
|
||||
|
||||
|
||||
def test_databases_url(httpx_mock, mocker, tmpdir):
|
||||
"""databases command hits /.json on the instance."""
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
httpx_mock.add_response(
|
||||
json={"databases": [{"name": "db1"}]},
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["databases", "-i", "https://example.com"])
|
||||
assert result.exit_code == 0
|
||||
request = httpx_mock.get_request()
|
||||
assert request.url.path == "/.json"
|
||||
|
||||
|
||||
# -- tables command --
|
||||
|
||||
|
||||
def test_tables_json(httpx_mock, mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
httpx_mock.add_response(
|
||||
json={
|
||||
"tables": [
|
||||
{"name": "facetable", "count": 15, "hidden": False},
|
||||
{"name": "facet_cities", "count": 4, "hidden": False},
|
||||
],
|
||||
"views": [],
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
["tables", "-i", "https://example.com", "-d", "fixtures", "--json"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert len(data) == 2
|
||||
|
||||
|
||||
def test_tables_plain(httpx_mock, mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
httpx_mock.add_response(
|
||||
json={
|
||||
"tables": [
|
||||
{"name": "facetable", "count": 15, "hidden": False},
|
||||
],
|
||||
"views": [],
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli, ["tables", "-i", "https://example.com", "-d", "fixtures"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "facetable" in result.output
|
||||
assert "15 rows" in result.output
|
||||
|
||||
|
||||
def test_tables_url(httpx_mock, mocker, tmpdir):
|
||||
"""tables command hits /<database>.json."""
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
httpx_mock.add_response(
|
||||
json={"tables": [], "views": []},
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli, ["tables", "-i", "https://example.com", "-d", "fixtures"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
request = httpx_mock.get_request()
|
||||
assert request.url.path == "/fixtures.json"
|
||||
|
||||
|
||||
def test_tables_with_views(httpx_mock, mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
httpx_mock.add_response(
|
||||
json={
|
||||
"tables": [{"name": "t1", "count": 5, "hidden": False}],
|
||||
"views": [{"name": "v1"}],
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli, ["tables", "-i", "https://example.com", "-d", "db", "--views"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "t1" in result.output
|
||||
assert "v1" in result.output
|
||||
|
||||
|
||||
def test_tables_views_only(httpx_mock, mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
httpx_mock.add_response(
|
||||
json={
|
||||
"tables": [{"name": "t1", "count": 5, "hidden": False}],
|
||||
"views": [{"name": "v1"}],
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
["tables", "-i", "https://example.com", "-d", "db", "--views-only"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "t1" not in result.output
|
||||
assert "v1" in result.output
|
||||
|
||||
|
||||
def test_tables_hidden(httpx_mock, mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
httpx_mock.add_response(
|
||||
json={
|
||||
"tables": [
|
||||
{"name": "visible", "count": 5, "hidden": False},
|
||||
{"name": "hidden_t", "count": 2, "hidden": True},
|
||||
],
|
||||
"views": [],
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
# Without --hidden
|
||||
result = runner.invoke(
|
||||
cli, ["tables", "-i", "https://example.com", "-d", "db"]
|
||||
)
|
||||
assert "visible" in result.output
|
||||
assert "hidden_t" not in result.output
|
||||
|
||||
# With --hidden
|
||||
httpx_mock.add_response(
|
||||
json={
|
||||
"tables": [
|
||||
{"name": "visible", "count": 5, "hidden": False},
|
||||
{"name": "hidden_t", "count": 2, "hidden": True},
|
||||
],
|
||||
"views": [],
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
["tables", "-i", "https://example.com", "-d", "db", "--hidden"],
|
||||
)
|
||||
assert "visible" in result.output
|
||||
assert "hidden_t" in result.output
|
||||
|
||||
|
||||
# -- plugins command --
|
||||
|
||||
|
||||
def test_plugins_json(httpx_mock, mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
httpx_mock.add_response(
|
||||
json=[
|
||||
{"name": "datasette-files", "version": "0.3.1"},
|
||||
{"name": "datasette-auth-tokens", "version": "0.4"},
|
||||
],
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli, ["plugins", "-i", "https://example.com", "--json"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert len(data) == 2
|
||||
|
||||
|
||||
def test_plugins_plain(httpx_mock, mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
httpx_mock.add_response(
|
||||
json=[
|
||||
{"name": "datasette-files", "version": "0.3.1"},
|
||||
{"name": "datasette-auth-tokens", "version": "0.4"},
|
||||
],
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["plugins", "-i", "https://example.com"])
|
||||
assert result.exit_code == 0
|
||||
assert "datasette-files\n" in result.output
|
||||
assert "datasette-auth-tokens\n" in result.output
|
||||
|
||||
|
||||
def test_plugins_url(httpx_mock, mocker, tmpdir):
|
||||
"""plugins command hits /-/plugins.json."""
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
httpx_mock.add_response(json=[], status_code=200)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["plugins", "-i", "https://example.com"])
|
||||
assert result.exit_code == 0
|
||||
request = httpx_mock.get_request()
|
||||
assert request.url.path == "/-/plugins.json"
|
||||
|
||||
|
||||
# -- schema command --
|
||||
|
||||
|
||||
def test_schema_all_tables(httpx_mock, mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
full_schema = (
|
||||
"CREATE TABLE users (id integer primary key, name text);\n"
|
||||
"CREATE VIEW user_count AS SELECT count(*) FROM users;"
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
json={"database": "main", "schema": full_schema},
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli, ["schema", "-i", "https://example.com", "-d", "main"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "CREATE TABLE users" in result.output
|
||||
assert "CREATE VIEW user_count" in result.output
|
||||
request = httpx_mock.get_request()
|
||||
assert request.url.path == "/main/-/schema.json"
|
||||
|
||||
|
||||
def test_schema_specific_table(httpx_mock, mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
httpx_mock.add_response(
|
||||
json={
|
||||
"database": "main",
|
||||
"table": "users",
|
||||
"schema": "CREATE TABLE users (id integer primary key, name text)",
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli, ["schema", "users", "-i", "https://example.com", "-d", "main"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "CREATE TABLE users" in result.output
|
||||
request = httpx_mock.get_request()
|
||||
assert request.url.path == "/main/users/-/schema.json"
|
||||
|
||||
|
||||
def test_schema_json(httpx_mock, mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
full_schema = "CREATE TABLE users (id integer primary key);"
|
||||
httpx_mock.add_response(
|
||||
json={"database": "main", "schema": full_schema},
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
["schema", "-i", "https://example.com", "-d", "main", "--json"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert "schema" in data
|
||||
|
||||
|
||||
# -- default_query (bare SQL shortcut) --
|
||||
|
||||
|
||||
def test_default_query_with_defaults(httpx_mock, mocker, tmpdir):
|
||||
"""Bare SQL uses default instance + default database."""
|
||||
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": "prod",
|
||||
"instances": {
|
||||
"prod": {
|
||||
"url": "https://prod.example.com",
|
||||
"default_database": "main",
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
json={
|
||||
"ok": True,
|
||||
"rows": [{"count(*)": 42}],
|
||||
"columns": ["count(*)"],
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["select count(*) from users"])
|
||||
assert result.exit_code == 0
|
||||
assert json.loads(result.output) == [{"count(*)": 42}]
|
||||
request = httpx_mock.get_request()
|
||||
assert request.url.host == "prod.example.com"
|
||||
assert request.url.path == "/main.json"
|
||||
|
||||
|
||||
def test_default_query_with_database_override(httpx_mock, mocker, tmpdir):
|
||||
"""Bare SQL with -d flag overrides the database."""
|
||||
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": "prod",
|
||||
"instances": {
|
||||
"prod": {
|
||||
"url": "https://prod.example.com",
|
||||
"default_database": "main",
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
json={
|
||||
"ok": True,
|
||||
"rows": [{"count(*)": 100}],
|
||||
"columns": ["count(*)"],
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli, ["select count(*) from events", "-d", "analytics"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
request = httpx_mock.get_request()
|
||||
assert request.url.path == "/analytics.json"
|
||||
|
||||
|
||||
def test_default_query_with_instance_override(httpx_mock, mocker, tmpdir):
|
||||
"""Bare SQL with -i flag overrides the instance."""
|
||||
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": "prod",
|
||||
"instances": {
|
||||
"prod": {
|
||||
"url": "https://prod.example.com",
|
||||
"default_database": "main",
|
||||
},
|
||||
"staging": {
|
||||
"url": "https://staging.example.com",
|
||||
"default_database": "main",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
json={
|
||||
"ok": True,
|
||||
"rows": [{"count(*)": 5}],
|
||||
"columns": ["count(*)"],
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli, ["select count(*) from users", "-i", "staging"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
request = httpx_mock.get_request()
|
||||
assert request.url.host == "staging.example.com"
|
||||
|
||||
|
||||
def test_default_query_no_database_error(mocker, tmpdir):
|
||||
"""Bare SQL without a default database gives an error."""
|
||||
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": "prod",
|
||||
"instances": {
|
||||
"prod": {
|
||||
"url": "https://prod.example.com",
|
||||
"default_database": None,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["select 1"])
|
||||
assert result.exit_code == 1
|
||||
assert "No database specified" in result.output
|
||||
|
||||
|
||||
# -- upsert command --
|
||||
|
||||
|
||||
def test_upsert_mocked(httpx_mock, tmpdir, mocker):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
httpx_mock.add_response(
|
||||
json={
|
||||
"ok": True,
|
||||
}
|
||||
)
|
||||
path = pathlib.Path(tmpdir) / "data.csv"
|
||||
path.write_text("a,b,c\n1,2,3\n")
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"upsert",
|
||||
"data",
|
||||
"table1",
|
||||
str(path),
|
||||
"--csv",
|
||||
"--token",
|
||||
"x",
|
||||
"-i",
|
||||
"https://datasette.example.com",
|
||||
],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
request = httpx_mock.get_request()
|
||||
assert request.headers["authorization"] == "Bearer x"
|
||||
# Should hit /-/upsert endpoint
|
||||
assert "/table1/-/upsert" in str(request.url)
|
||||
assert json.loads(request.read()) == {"rows": [{"a": 1, "b": 2, "c": 3}]}
|
||||
|
||||
|
||||
# -- auth status command --
|
||||
|
||||
|
||||
def test_auth_status(httpx_mock, mocker, tmpdir):
|
||||
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()
|
||||
result = runner.invoke(
|
||||
cli, ["auth", "status", "-i", "https://example.com", "--token", "tok"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data["actor"]["id"] == "root"
|
||||
request = httpx_mock.get_request()
|
||||
assert request.url.path == "/-/actor.json"
|
||||
|
||||
|
||||
# -- actor command --
|
||||
|
||||
|
||||
def test_actor_with_instance_flag(httpx_mock, mocker, tmpdir):
|
||||
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()
|
||||
result = runner.invoke(
|
||||
cli, ["actor", "-i", "https://example.com", "--token", "tok"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data["actor"]["id"] == "root"
|
||||
request = httpx_mock.get_request()
|
||||
assert request.url.path == "/-/actor.json"
|
||||
assert request.headers["authorization"] == "Bearer tok"
|
||||
|
||||
|
||||
# -- get command --
|
||||
|
||||
|
||||
def test_get_command(httpx_mock, mocker, tmpdir):
|
||||
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
||||
httpx_mock.add_response(
|
||||
json={"hello": "world"},
|
||||
status_code=200,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli, ["get", "/-/plugins.json", "-i", "https://example.com"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data == {"hello": "world"}
|
||||
request = httpx_mock.get_request()
|
||||
assert request.url.path == "/-/plugins.json"
|
||||
Loading…
Add table
Add a link
Reference in a new issue