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