Files
Evil0ctal cac61daf2f feat: routes, MCP, CLI, ops, worker and containers; close three integration gaps
Six modules built in parallel, then each reviewed by an independent adversarial
pass. 40 OpenAPI paths, an MCP server, a CLI, the operational features, the task
worker and a multi-stage image with production compose.

Three gaps only visible where the modules meet, all found by running the stack
rather than by reading it:

- MCP was never mounted. register_routes now calls mount(), which also chains
  the streamable-http session manager onto the application lifespan: a mounted
  ASGI app receives no lifespan events of its own, and without that the endpoint
  fails with "Task group is not initialized".
- The worker had no definition for the logical "parse" endpoint, so both
  POST /api/v1/parse and the MCP parse_url tool - the two entry points a
  first-time user is most likely to try - failed with "unknown endpoint".
  dtk.worker.parsing expands the short link, identifies the resource and maps it
  onto a registered endpoint. Expansion is the security-sensitive half: a short
  link cannot be validated before it is followed, so the target is checked
  against the allowlist after every hop and the hop count is bounded. A test
  asserts a short link redirecting to the cloud metadata address is refused.
- The console was not served. The build output is copied to /app/web/dist and
  the image already exported DTK_CONSOLE_DIR; the code simply derived its path
  from __file__, which inside the image points into site-packages. It now reads
  the variable, with fallbacks that keep a source checkout working. The SPA
  fallback declines anything under the API prefixes, so a mistyped endpoint
  still returns a JSON envelope rather than the console's HTML with status 200.

Verified against the real stack, not a mock: docker compose up, all services
healthy, migrations producing 12 tables, 3 hypertables and 2 continuous
aggregates, the setup token read from the container log, the admin created and
the endpoint then permanently closed, login, the error envelope, three rejected
SSRF shapes, both languages, OpenAPI, the docs and the console. 25 of 25.

1316 unit and replay tests pass.
2026-09-07 06:08:08 -07:00

147 lines
5.0 KiB
Python

"""First-run initialization against real PostgreSQL and Redis.
The three properties doc 06 asks for are all exercised here: the token is
compared in constant time and single use, five failures burn it, and once an
account exists the endpoint is closed permanently.
"""
from __future__ import annotations
from typing import Any
import pytest
from dtk.api.routes import setup
from dtk.core.redis import get_redis
from tests.integration import test_api_support as support
from tests.integration.test_api_support import (
envelope,
error_code,
make_user,
)
# Fixtures are re-exported by assignment: pytest picks them up from this
# module's namespace, and a test parameter of the same name does not then
# shadow an import.
api_app = support.api_app
client = support.client
pytestmark = pytest.mark.integration
async def issue_token() -> str:
token = await setup.ensure_setup_token()
assert token is not None
return token
async def test_status_reports_uninitialized(client: Any) -> None:
response = await client.get("/api/setup/status")
assert response.status_code == 200
assert envelope(response)["data"] == {"initialized": False}
async def test_init_creates_the_first_administrator(client: Any) -> None:
token = await issue_token()
response = await client.post(
"/api/setup/init",
json={"token": token, "username": "owner", "password": "a-good-password"},
)
assert response.status_code == 201, response.text
data = envelope(response)["data"]
assert data["initialized"] is True
assert data["user"]["username"] == "owner"
assert data["user"]["role"] == "admin"
status = await client.get("/api/setup/status")
assert envelope(status)["data"] == {"initialized": True}
async def test_token_is_single_use_and_then_the_endpoint_closes(client: Any) -> None:
token = await issue_token()
first = await client.post(
"/api/setup/init",
json={"token": token, "username": "owner", "password": "a-good-password"},
)
assert first.status_code == 201
# The same token again: the account check fires before the token is even
# looked at, so this is a permanent 409 rather than a token error.
second = await client.post(
"/api/setup/init",
json={"token": token, "username": "second", "password": "a-good-password"},
)
assert second.status_code == 409
assert error_code(second) == "SETUP_ALREADY_DONE"
async def test_wrong_token_is_rejected_and_leaves_the_instance_open(client: Any) -> None:
await issue_token()
response = await client.post(
"/api/setup/init",
json={"token": "not-the-token", "username": "owner", "password": "a-good-password"},
)
assert response.status_code == 403
body = envelope(response)
assert body["error"]["code"] == "SETUP_TOKEN_INVALID"
assert body["error"]["details"]["attempts_remaining"] == setup.MAX_SETUP_ATTEMPTS - 1
status = await client.get("/api/setup/status")
assert envelope(status)["data"] == {"initialized": False}
async def test_five_failures_invalidate_the_token(client: Any) -> None:
token = await issue_token()
for _ in range(setup.MAX_SETUP_ATTEMPTS):
response = await client.post(
"/api/setup/init",
json={"token": "wrong-token-value", "username": "owner", "password": "a-good-password"},
)
assert response.status_code == 403
assert await get_redis().get(setup.SETUP_TOKEN_KEY) is None
# Even the correct token is now useless; the operator has to restart.
response = await client.post(
"/api/setup/init",
json={"token": token, "username": "owner", "password": "a-good-password"},
)
assert error_code(response) == "SETUP_TOKEN_INVALID"
async def test_missing_token_compares_against_a_decoy(client: Any) -> None:
"""No token issued at all still answers SETUP_TOKEN_INVALID, not a 500.
The comparison runs against a freshly generated value so the "no token
stored" path costs the same as a mismatch.
"""
assert await get_redis().get(setup.SETUP_TOKEN_KEY) is None
response = await client.post(
"/api/setup/init",
json={"token": "anything-at-all", "username": "owner", "password": "a-good-password"},
)
assert response.status_code == 403
assert error_code(response) == "SETUP_TOKEN_INVALID"
async def test_ensure_setup_token_is_idempotent_and_stops_once_initialized(
api_app: Any,
) -> None:
first = await issue_token()
assert await setup.ensure_setup_token() == first, "a restart must not invalidate the link"
await make_user("owner")
assert await setup.ensure_setup_token() is None
assert await get_redis().get(setup.SETUP_TOKEN_KEY) is None
async def test_init_validates_its_body(client: Any) -> None:
await issue_token()
response = await client.post(
"/api/setup/init",
json={"token": "x" * 12, "username": "ok", "password": "short"},
)
assert response.status_code == 400
assert error_code(response) == "INVALID_PARAM"