From ce031cdf195b82107cf2e6a382e1520503506607 Mon Sep 17 00:00:00 2001 From: Derek Date: Wed, 11 Nov 2026 10:07:17 +1100 Subject: [PATCH] fix(demo): complete the engine's first-run setup make demo created its sources but left first-run setup unfinished. Every console page opened on the setup wizard. Setup asks for one organisation besides default and one real user. The demo creates organisation demo and account demo-analyst right after login. It leaves an existing account as it is. Nobody logs in as demo-analyst, so its password is random and never shown. --- scripts/demo.py | 28 +++++++++++++++++++++++++ scripts/tests/test_demo.py | 42 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/scripts/demo.py b/scripts/demo.py index ce12c4d..1a1f7e8 100644 --- a/scripts/demo.py +++ b/scripts/demo.py @@ -29,6 +29,7 @@ import json import os import re +import secrets import shutil import subprocess import sys @@ -69,6 +70,7 @@ _api_post_json, _decoded, _engine_base, + _ensure_org, _exec_request, api_put_json, ) @@ -87,6 +89,9 @@ TRANSFORM_APP = "dfe-transform-elastic" TRANSFORM_REPO = "dfe-transform-elastic" TRANSFORM_VERSION_KEY = "DFE_TRANSFORM_ELASTIC_VERSION" +# The organisation and account that complete the engine's first-run setup +DEMO_ORG = "demo" +DEMO_ANALYST = "demo-analyst" RECEIVER_SERVICE = "dfe-receiver" # dfe-engine main reads the catalogue from its dfe-schemas wheel and older images from the seeded /app/schemas, so this tries both in that order, at any depth because newer wheels file each entry under its tag folder CATALOGUE_READ = "\n".join( @@ -505,6 +510,28 @@ def _counts(*, tables: list[str]) -> dict[str, int | None]: return {table: _row_count(source=table) for table in tables} +def _complete_setup(*, base: str, token: str) -> None: + """Make the organisation and the real user the engine's first-run setup asks for. + + Without them every console page opens on the setup wizard. Nobody logs in as the analyst, so its password is random and never shown. + """ + org = _ensure_org(base=base, token=token, name=DEMO_ORG) + if org.fault: + raise DemoError(f"the demo organisation: {org.fault}") + account = { + "username": DEMO_ANALYST, + "password": secrets.token_urlsafe(32), + "name": "Demo analyst", + "org": DEMO_ORG, + } + status, answer = _api_post_json(f"{base}/auth/accounts", account, token=token) + if status not in (201, 409): + raise DemoError( + f"creating account {DEMO_ANALYST!r} returned HTTP {status}: {answer}" + ) + _print(msg=f"setup complete: organisation {DEMO_ORG!r}, account {DEMO_ANALYST!r}") + + def _create(*, base: str, body: dict, token: str) -> dict: """Create one source from its file, then return the reply.""" name = body["source"] @@ -906,6 +933,7 @@ def _install(*, args: argparse.Namespace, manifest: dict) -> int: base = _engine_base() token = _token(base=base) + _complete_setup(base=base, token=token) replies = [] for body in [*demo.vendors, demo.intake]: replies += _ensure_source(base=base, body=body, token=token) diff --git a/scripts/tests/test_demo.py b/scripts/tests/test_demo.py index c1cf584..9ad6e2e 100644 --- a/scripts/tests/test_demo.py +++ b/scripts/tests/test_demo.py @@ -17,6 +17,7 @@ import demo from _common import SOURCE_NAME +from post import HuntOrg pytestmark = pytest.mark.usefixtures("engine_python_here") @@ -652,6 +653,47 @@ def no_container(*, service: str) -> str: ENGINE_PYTHON(code="", stdin="") +class TestCompleteSetup: + @pytest.mark.parametrize( + ("org_fault", "status", "refused"), + [ + ("", 201, None), + ("", 409, None), + ("", 500, "returned HTTP 500"), + ("POST /orgs returned HTTP 403: no", 201, "the demo organisation"), + ], + ids=["created", "already-there", "account-refused", "org-refused"], + ) + def test_makes_the_org_then_the_account( + self, + monkeypatch: pytest.MonkeyPatch, + org_fault: str, + status: int, + refused: str | None, + ) -> None: + posted: list[dict] = [] + + def ensure_org(*, base: str, token: str, name: str) -> HuntOrg: + return HuntOrg(name, name, name, True, org_fault) + + def post(url: str, payload: dict, token: str = "") -> tuple[int, dict]: + posted.append(payload) + return status, {} + + monkeypatch.setattr(demo, "_ensure_org", ensure_org) + monkeypatch.setattr(demo, "_api_post_json", post) + + if refused is not None: + with pytest.raises(demo.DemoError, match=refused): + demo._complete_setup(base="http://engine", token="t") + return + demo._complete_setup(base="http://engine", token="t") + + assert [p["username"] for p in posted] == [demo.DEMO_ANALYST] + assert posted[0]["org"] == demo.DEMO_ORG + assert len(posted[0]["password"]) >= 32 + + class TestEventsDir: def test_the_cases_cover_every_committed_vendor(self) -> None: sources = sorted(case["source"] for case in EVENTS_DIR_SOURCE_CASES)