From 6115d2eccf3212c8ea7f647af4cff3dd3b3d26a6 Mon Sep 17 00:00:00 2001 From: autonomic-bot Date: Thu, 28 May 2026 16:05:51 +0100 Subject: [PATCH] =?UTF-8?q?feat(2):=20Q4.3=20=E2=80=94=20bluesky-pds=20Pha?= =?UTF-8?q?se-2=20enrollment=20+=203=20tests=20cold=20green?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tests/bluesky-pds/recipe_meta.py: HEALTH_PATH=/xrpc/_health, 600s timeouts. - tests/bluesky-pds/install_steps.sh: recipe needs pds_plc_rotation_key (32-byte secp256k1 hex, marked generate=false). Hook generates via cc-ci-run python (secrets.token_bytes(32); random 32-byte value is almost-always a valid secp256k1 private key, ~2^-128 fail rate). Inserted via 'abra app secret insert' under TTY-wrap. Per-run class-B; destroyed at teardown. - tests/bluesky-pds/PARITY.md: no health_check.py in the recipe-maintainer corpus -> Phase-2 health_check aligned with parity convention. goat_account.py parity deferred (needs goat CLI in container; operational complexity). - 3 functional tests: - test_health_check.py: GET /xrpc/_health -> 200, {version: ...}. - test_describe_server.py: GET /xrpc/com.atproto.server.describeServer -> 200, JSON with atproto config keys (availableUserDomains/inviteCodeRequired/links/did). - test_session_auth.py: GET /xrpc/com.atproto.server.getSession (no auth) -> 401 + JSON XRPC error envelope. (Replaced test_well_known_did — /.well-known/atproto-did isn't auto-published by the recipe.) Cold-verifiable: ssh cc-ci 'RECIPE=bluesky-pds STAGES=install,custom cc-ci-run runner/run_recipe_ci.py' install + 3 custom tests all PASS, deploy-count=1. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/bluesky-pds/PARITY.md | 33 +++++++++++++++++ .../functional/test_describe_server.py | 31 ++++++++++++++++ .../functional/test_health_check.py | 22 ++++++++++++ .../functional/test_session_auth.py | 35 +++++++++++++++++++ tests/bluesky-pds/install_steps.sh | 33 +++++++++++++++++ tests/bluesky-pds/recipe_meta.py | 8 +++++ 6 files changed, 162 insertions(+) create mode 100644 tests/bluesky-pds/PARITY.md create mode 100644 tests/bluesky-pds/functional/test_describe_server.py create mode 100644 tests/bluesky-pds/functional/test_health_check.py create mode 100644 tests/bluesky-pds/functional/test_session_auth.py create mode 100755 tests/bluesky-pds/install_steps.sh create mode 100644 tests/bluesky-pds/recipe_meta.py diff --git a/tests/bluesky-pds/PARITY.md b/tests/bluesky-pds/PARITY.md new file mode 100644 index 0000000..d45ac2e --- /dev/null +++ b/tests/bluesky-pds/PARITY.md @@ -0,0 +1,33 @@ +# Parity — bluesky-pds + +Phase-2 P2 mapping table. + +| recipe-maintainer file | cc-ci file | what's verified | status | +|---|---|---|---| +| (no health_check.py in the recipe-maintainer corpus) | `tests/bluesky-pds/functional/test_health_check.py` | GETs `/xrpc/_health` (the PDS health endpoint); asserts 200 + JSON with `version` field. Phase-2 health_check aligned with the parity-port convention. | **Phase-2 health_check** | +| `recipe-info/bluesky-pds/tests/goat_account.py` | (deferred to Q4.3 follow-up — needs `goat` CLI in container) | The original creates a test admin account via `abra app run app -- goat pds admin account create ...`. Doable in cc-ci via `lifecycle.exec_in_app` calling the `goat` CLI inside the PDS container, but adds operational complexity (account state cleanup across runs). Deferred to follow-up. | **deferred** | + +## Recipe-specific tests (Phase-2 P3, ≥2 beyond parity) + +bluesky-pds is an **atproto Personal Data Server** — its characteristic behavior is the +public XRPC API + the well-known `atproto-did` server identifier. Two new functional tests: + +| cc-ci file | what's verified | rationale | +|---|---|---| +| `tests/bluesky-pds/functional/test_describe_server.py` | GETs `/xrpc/com.atproto.server.describeServer` (the public atproto endpoint that advertises the PDS's available account creation policy); asserts 200 + JSON envelope with at least `availableUserDomains` (array; the PDS's hosting domains) or `inviteCodeRequired` (bool). | Proves the atproto XRPC API is alive AND the PDS-specific configuration is being served (not just a generic 200). Non-vacuous: a PDS that boots but can't serve its server description is broken. | +| `tests/bluesky-pds/functional/test_session_auth.py` | GETs `/xrpc/com.atproto.server.getSession` (no auth); asserts **401** + a JSON XRPC error envelope with an `error` field. | Proves the PDS's atproto auth contract is enforced. Non-vacuous: 200 = anonymous leak (security bug); 404 = route missing; 5xx = backend broken — only 401 + a proper XRPC error envelope indicates a correctly-wired PDS. (An earlier draft tried `/.well-known/atproto-did` but that endpoint is only published when the bare DOMAIN is registered as a server-DID, which the recipe doesn't auto-configure.) | + +Two specific tests + parity health_check = ≥2 floor met. Backup data-integrity is N/A unless the +recipe declares `backupbot.backup=true` labels (Phase-1d auto-detect handles the skip). + +## Playwright (P6) + +bluesky-pds has **no first-party browser UI** — the recipe is an atproto PDS that other apps +(bsky.app, atproto clients) consume. So P6 is N/A; HTTP/XRPC functional tests are the canonical +surface. + +## Non-ports + +`goat_account.py` is deferred (operational complexity; needs goat CLI + account state cleanup +across runs). Logged here per §7.1; will lift if/when the goat-CLI path is wrapped into the +harness. diff --git a/tests/bluesky-pds/functional/test_describe_server.py b/tests/bluesky-pds/functional/test_describe_server.py new file mode 100644 index 0000000..c993ffe --- /dev/null +++ b/tests/bluesky-pds/functional/test_describe_server.py @@ -0,0 +1,31 @@ +"""bluesky-pds — recipe-specific functional test (Phase 2 P3). + +GETs `/xrpc/com.atproto.server.describeServer` — the public atproto XRPC endpoint that advertises +the PDS's configuration. Asserts the response is JSON with at least one of the documented PDS +config fields (`availableUserDomains` array of hosting domains, OR `inviteCodeRequired` bool). + +Non-vacuous: distinguishes a working atproto PDS from a generic HTTP 200 (a misconfigured server +that returns 200 from /xrpc/* but with a non-atproto shape would fail). +""" + +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner")) +from harness import http as harness_http # noqa: E402 + + +def test_describe_server_returns_atproto_envelope(live_app): + """GET /xrpc/com.atproto.server.describeServer → 200 + atproto config JSON.""" + url = f"https://{live_app}/xrpc/com.atproto.server.describeServer" + status, body = harness_http.retry_http_get(url, expect_status=200, max_wait=60, interval=3) + assert status == 200, f"GET {url} HTTP {status} (expected 200)" + assert isinstance(body, dict), f"describe-server returned non-dict: {type(body).__name__}" + # At least one of these atproto-spec fields must be present + expected_any = ("availableUserDomains", "inviteCodeRequired", "links", "did") + present = [k for k in expected_any if k in body] + assert present, ( + f"describe-server missing all of {expected_any}; got keys: {sorted(body.keys())[:20]}" + ) diff --git a/tests/bluesky-pds/functional/test_health_check.py b/tests/bluesky-pds/functional/test_health_check.py new file mode 100644 index 0000000..0fb19a3 --- /dev/null +++ b/tests/bluesky-pds/functional/test_health_check.py @@ -0,0 +1,22 @@ +"""bluesky-pds — Phase-2 health_check (recipe-maintainer corpus has no health_check.py). + +Tests the PDS's `/xrpc/_health` endpoint; asserts 200 + JSON with a `version` field. +""" + +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner")) +from harness import http as harness_http # noqa: E402 + + +def test_pds_health_returns_version(live_app): + """GET /xrpc/_health → 200, JSON {"version": "..."}.""" + url = f"https://{live_app}/xrpc/_health" + status, body = harness_http.retry_http_get(url, expect_status=200, max_wait=60, interval=3) + assert status == 200, f"GET {url} HTTP {status} (expected 200)" + assert isinstance(body, dict) and isinstance(body.get("version"), str) and body["version"], ( + f"GET {url} response is not the expected health envelope: {body!r}" + ) diff --git a/tests/bluesky-pds/functional/test_session_auth.py b/tests/bluesky-pds/functional/test_session_auth.py new file mode 100644 index 0000000..da4c474 --- /dev/null +++ b/tests/bluesky-pds/functional/test_session_auth.py @@ -0,0 +1,35 @@ +"""bluesky-pds — recipe-specific functional test (Phase 2 P3). + +GETs the atproto session endpoint `/xrpc/com.atproto.server.getSession` WITHOUT an auth header. +Asserts the PDS responds with 401 Unauthorized — proves the auth subsystem is wired correctly: +- 200 = anonymous access leaked (would be a security bug). +- 401 = correctly enforced. +- 404 = route missing (PDS misconfigured). +- 5xx = backend broken. + +Distinguishes "the atproto XRPC server is alive AND its auth contract is enforced" from generic +HTTP 200 health. Non-vacuous: each non-401 status indicates a different class of defect. +""" + +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner")) +from harness import http as harness_http # noqa: E402 + + +def test_get_session_requires_auth(live_app): + """GET /xrpc/com.atproto.server.getSession (no token) → 401.""" + url = f"https://{live_app}/xrpc/com.atproto.server.getSession" + status, body = harness_http.retry_http_get(url, expect_status=401, max_wait=60, interval=3) + assert status == 401, ( + f"GET {url} returned {status}, expected 401 (auth required). " + f"200 = anonymous leak; 404 = route missing; 5xx = backend broken. " + f"body: {body!r}" + ) + # The XRPC error envelope is JSON with an `error` field per the atproto spec. + assert isinstance(body, dict) and body.get("error"), ( + f"expected XRPC JSON error envelope; got: {body!r}" + ) diff --git a/tests/bluesky-pds/install_steps.sh b/tests/bluesky-pds/install_steps.sh new file mode 100755 index 0000000..f5ab73e --- /dev/null +++ b/tests/bluesky-pds/install_steps.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# bluesky-pds — install-steps hook (Phase 1d DG5). +# +# bluesky-pds's `pds_plc_rotation_key` secret is marked `generate=false` in the recipe (the PLC +# rotation key is a secp256k1 private key that the deploy WILL reject if not pre-inserted). Run +# this hook AFTER `abra app secret generate` (which handles the recipe's auto-gen secrets) and +# BEFORE `abra app deploy` — generate a fresh secp256k1 key + insert it. +# +# The key is per-run class-B (each per-run domain gets its own); destroyed with the app at run end. +# +# Environment supplied by the orchestrator: +# CCCI_APP_DOMAIN — the per-run domain +# CCCI_RECIPE — "bluesky-pds" +# CCCI_APP_ENV — path to the app's .env file +set -euo pipefail +: "${CCCI_APP_DOMAIN:?CCCI_APP_DOMAIN must be set by the harness}" + +echo " bluesky-pds install_steps: generating secp256k1 PLC rotation key..." +# The recipe README's recipe uses openssl+xxd; cc-ci's PATH only has python3 (the nix +# cc-ci-run env). A random 32-byte value is overwhelmingly always a valid secp256k1 private key +# (P(invalid) ~= 2^-128); Python's secrets.token_bytes(32) is cryptographically random + the +# same shape the PDS expects (32-byte hex). Equivalent for atproto PDS bootstrap. +KEY_HEX=$(cc-ci-run -c 'import secrets; print(secrets.token_bytes(32).hex())') +if [ -z "${KEY_HEX}" ] || [ "${#KEY_HEX}" != "64" ]; then + echo " install_steps: failed to generate PLC rotation key (KEY_HEX length=${#KEY_HEX})" >&2 + exit 1 +fi + +# Insert via abra under TTY-wrap (`abra app secret insert` requires a TTY on this version). +# We DON'T log the key value — abra also doesn't print it. +script -qec "abra app secret insert ${CCCI_APP_DOMAIN} pds_plc_rotation_key v1 ${KEY_HEX} --no-input" /dev/null \ + >/dev/null 2>&1 +echo " bluesky-pds install_steps: PLC rotation key inserted (v1)." diff --git a/tests/bluesky-pds/recipe_meta.py b/tests/bluesky-pds/recipe_meta.py new file mode 100644 index 0000000..ac9f7ca --- /dev/null +++ b/tests/bluesky-pds/recipe_meta.py @@ -0,0 +1,8 @@ +# Per-recipe harness config for bluesky-pds (Phase 2 Q4.3 — TLS-passthrough atproto PDS). +# The recipe routes via Traefik with TLS termination at cc-ci (the wildcard cert covers the +# bare DOMAIN; the gateway TLS-passthroughs the wildcard zone). atproto PDS exposes XRPC +# endpoints under /xrpc/* for the public protocol. +HEALTH_PATH = "/xrpc/_health" # PDS health endpoint; returns {"version": ...} on success +HEALTH_OK = (200,) +DEPLOY_TIMEOUT = 600 +HTTP_TIMEOUT = 600