specs/TrustServiceOnboarding.md defines the mechanism: a Phase Manifest file is committed to the declaring repo (durable, independently foldable forever) and separately registered with the hosted service; once registered, the Ledger's live authoritative copy is the hosted service only, not a second competing file. Licensor token bootstrapping is explicitly out of scope here (a WP-0008-T01 governance action). scripts/trf_onboard.py: a dependency-light CLI (stdlib urllib + target_revenue.validation only, no FastAPI/psycopg needed to onboard a Phase) with validate/register-phase/append-entry/status subcommands. The Licensor token is read only from a named environment variable, never accepted as a literal argument. tests/test_trf_onboard.py (4 tests, no network/Docker) proves invalid-manifest and missing-token-env cases fail before any HTTP attempt, by monkeypatching the request function to raise if called. tests/test_onboarding_hosted.py (1 Docker-gated test) runs an actual uvicorn server on a real socket and drives the full register -> append -> status round trip through the CLI as an external repo would invoke it.
173 lines
5.8 KiB
Python
173 lines
5.8 KiB
Python
"""End-to-end test: scripts/trf_onboard.py against a real, running hosted
|
|
Trust Service instance (WP-0006-T07), not just FastAPI's in-process
|
|
TestClient — this is the one test in the suite that exercises an actual
|
|
HTTP round trip over a real socket, since the onboarding CLI uses
|
|
`urllib` against a real URL rather than an ASGI transport.
|
|
|
|
Same ephemeral, disposable Postgres-via-Docker pattern as the other
|
|
hosted tests (never the shared state-hub instance).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
psycopg = pytest.importorskip("psycopg")
|
|
pytest.importorskip("fastapi")
|
|
uvicorn = pytest.importorskip("uvicorn")
|
|
|
|
from conftest import golden_manifest # noqa: E402
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
MIGRATIONS = [
|
|
REPO_ROOT / "migrations" / "0001_registries.sql",
|
|
REPO_ROOT / "migrations" / "0002_ledger.sql",
|
|
REPO_ROOT / "migrations" / "0003_attestations.sql",
|
|
]
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
shutil.which("docker") is None, reason="docker not available"
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def pg_container():
|
|
name = f"trf-test-pg-onboard-{uuid.uuid4().hex[:8]}"
|
|
subprocess.run(
|
|
[
|
|
"docker", "run", "--rm", "-d",
|
|
"--name", name,
|
|
"-e", "POSTGRES_PASSWORD=postgres",
|
|
"-e", "POSTGRES_DB=target_revenue_test",
|
|
"-p", "127.0.0.1::5432",
|
|
"postgres:16-alpine",
|
|
],
|
|
check=True, capture_output=True,
|
|
)
|
|
try:
|
|
port_out = subprocess.run(
|
|
["docker", "port", name, "5432/tcp"], check=True, capture_output=True, text=True
|
|
).stdout.strip()
|
|
host_port = port_out.split(":")[-1]
|
|
dsn = f"host=127.0.0.1 port={host_port} dbname=target_revenue_test user=postgres password=postgres"
|
|
|
|
for _ in range(60):
|
|
try:
|
|
with psycopg.connect(dsn, connect_timeout=1):
|
|
break
|
|
except psycopg.OperationalError:
|
|
time.sleep(0.5)
|
|
else:
|
|
raise RuntimeError("postgres container did not become ready in time")
|
|
|
|
with psycopg.connect(dsn) as conn:
|
|
for migration in MIGRATIONS:
|
|
conn.execute(migration.read_text(encoding="utf-8"))
|
|
conn.commit()
|
|
token = "test-token-onboard"
|
|
conn.execute(
|
|
"INSERT INTO licensors (token, licensor_id) VALUES (%s, %s)",
|
|
(token, "onboard-corp"),
|
|
)
|
|
conn.commit()
|
|
|
|
app_dsn = (
|
|
f"host=127.0.0.1 port={host_port} dbname=target_revenue_test "
|
|
f"user=trf_app password=changeme-in-deployment"
|
|
)
|
|
yield {"app_dsn": app_dsn, "token": token}
|
|
finally:
|
|
subprocess.run(["docker", "stop", name], capture_output=True)
|
|
|
|
|
|
@pytest.fixture()
|
|
def live_server(pg_container, monkeypatch):
|
|
monkeypatch.setenv("TRF_DATABASE_URL", pg_container["app_dsn"])
|
|
monkeypatch.setenv("TRF_SIGNING_KEY_HEX", "22" * 32)
|
|
from target_revenue.service import app as app_module
|
|
|
|
if hasattr(app_module.app.state, "pool"):
|
|
app_module.app.state.pool.close()
|
|
del app_module.app.state.pool
|
|
if hasattr(app_module.app.state, "signing_key"):
|
|
del app_module.app.state.signing_key
|
|
|
|
config = uvicorn.Config(app_module.app, host="127.0.0.1", port=0, log_level="warning")
|
|
server = uvicorn.Server(config)
|
|
thread = threading.Thread(target=server.run, daemon=True)
|
|
thread.start()
|
|
for _ in range(100):
|
|
if getattr(server, "started", False):
|
|
break
|
|
time.sleep(0.05)
|
|
else:
|
|
raise RuntimeError("uvicorn server did not start in time")
|
|
|
|
port = server.servers[0].sockets[0].getsockname()[1]
|
|
try:
|
|
yield f"http://127.0.0.1:{port}"
|
|
finally:
|
|
server.should_exit = True
|
|
thread.join(timeout=5)
|
|
|
|
|
|
def test_onboard_cli_registers_phase_and_appends_entry_against_live_server(
|
|
live_server, pg_container, tmp_path, monkeypatch, capsys
|
|
):
|
|
import importlib.util
|
|
|
|
spec = importlib.util.spec_from_file_location("trf_onboard", REPO_ROOT / "scripts" / "trf_onboard.py")
|
|
trf_onboard = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(trf_onboard)
|
|
|
|
manifest = golden_manifest()
|
|
manifest["phase"]["id"] = manifest["phase"]["id"] + "-onboard-" + uuid.uuid4().hex[:6]
|
|
manifest_path = tmp_path / "manifest.json"
|
|
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
monkeypatch.setenv("TRF_ONBOARD_TEST_TOKEN", pg_container["token"])
|
|
|
|
trf_onboard.main(
|
|
[
|
|
"register-phase",
|
|
"--url", live_server,
|
|
"--token-env", "TRF_ONBOARD_TEST_TOKEN",
|
|
"--manifest", str(manifest_path),
|
|
]
|
|
)
|
|
|
|
entry = {
|
|
"id": "trsl:entry:onboardcli0001",
|
|
"phase": manifest["phase"]["id"],
|
|
"type": "development-credit",
|
|
"amount": 1000,
|
|
"currency": "USD",
|
|
"recognized_at": "2026-08-01T00:00:00Z",
|
|
"evidence_reference": "confidential:evidence:onboardcli0001",
|
|
"extension": {"id": "trsl:extension:development-license", "version": "1.0"},
|
|
}
|
|
entry_path = tmp_path / "entry.json"
|
|
entry_path.write_text(json.dumps(entry), encoding="utf-8")
|
|
|
|
trf_onboard.main(
|
|
[
|
|
"append-entry",
|
|
"--url", live_server,
|
|
"--token-env", "TRF_ONBOARD_TEST_TOKEN",
|
|
"--phase-id", manifest["phase"]["id"],
|
|
"--entry", str(entry_path),
|
|
]
|
|
)
|
|
|
|
capsys.readouterr() # discard register-phase/append-entry output
|
|
trf_onboard.main(["status", "--url", live_server, "--phase-id", manifest["phase"]["id"]])
|
|
status = json.loads(capsys.readouterr().out)
|
|
assert status["facts"]["cumulative_development_credit"] == 1000
|