feat(tenant): add operator provisioning for tenants, API keys, and domains
Why: - nothing over HTTP could create the first tenant: every /v1 route needs an API key, and a key cannot exist before its tenant. The service was unusable by a human without hand-written SQL. Changes: - add `provision_tenant`, owning tenant reuse-or-create, key generation and hashing, and domain registration in one transaction - expose it as `python -m src.cli.provision_tenant`, alongside `alembic upgrade head` and `qdrant_bootstrap` - add `tenants.get_by_slug`/`create` and `api_keys.create` - log `tenant.provisioned` / `api_key.provisioned` with the key prefix only Impact: - a third deployment step; the plaintext key is printed once and never logged or stored (ADR-0011, ADR-0009) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
0
tests/unit/cli/__init__.py
Normal file
0
tests/unit/cli/__init__.py
Normal file
34
tests/unit/cli/test_provision_tenant.py
Normal file
34
tests/unit/cli/test_provision_tenant.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Argument parsing for `python -m src.cli.provision_tenant`.
|
||||
|
||||
Only the argv -> arguments mapping is covered here; the provisioning behaviour
|
||||
itself needs a real database and lives in
|
||||
`tests/integration/postgres/test_provisioning.py`.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from src.application.tenants import DEFAULT_SCOPES
|
||||
from src.cli.provision_tenant import _parse_args
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def test_parse_args_defaults_scopes_and_domains() -> None:
|
||||
args = _parse_args(["--slug", "acme"])
|
||||
|
||||
assert args.slug == "acme"
|
||||
assert args.name is None
|
||||
assert args.key_name == "bootstrap"
|
||||
assert tuple(args.scopes.split(",")) == DEFAULT_SCOPES
|
||||
assert args.domains == []
|
||||
|
||||
|
||||
def test_parse_args_collects_repeated_domain_flags() -> None:
|
||||
args = _parse_args(["--slug", "acme", "--domain", "fire", "--domain", "life"])
|
||||
|
||||
assert args.domains == ["fire", "life"]
|
||||
|
||||
|
||||
def test_parse_args_requires_a_slug() -> None:
|
||||
with pytest.raises(SystemExit):
|
||||
_parse_args(["--domain", "fire"])
|
||||
Reference in New Issue
Block a user