Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/_ci-relay.yml
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,8 @@ jobs:
ON CONFLICT (lower(host)) DO NOTHING
;"
- name: Start relay
env:
BUZZ_V1_ENABLED: "true"
run: |
chmod +x ./target/ci/buzz-relay
nohup env \
Expand Down Expand Up @@ -545,6 +547,8 @@ jobs:
--archive-file target/ci/backend-integration-tests.tar.zst \
-E 'package(buzz-relay) and test(/workflow_sink/)'
- name: Start relay
env:
BUZZ_V1_ENABLED: "true"
run: |
chmod +x ./target/ci/buzz-relay
nohup env \
Expand Down Expand Up @@ -614,17 +618,25 @@ jobs:
merge-multiple: true
path: target/ci
- name: Start relay
env:
BUZZ_V1_ENABLED: "true"
run: |
chmod +x ./target/ci/buzz-relay ./target/ci/git-credential-nostr
./scripts/start-relay-for-tests.sh --no-build
- name: Relay E2E tests
run: |
cargo test -p buzz-test-client --test e2e_persona --test e2e_team_catalog --test e2e_nostr_interop --test e2e_thread_roots --test e2e_project -- --ignored --nocapture
cargo test -p buzz-test-client --test community_ban_routes -- --ignored --nocapture
cargo test -p buzz-test-client --test regression_relay_admin_ban_gate -- --ignored --nocapture
cargo test -p buzz-test-client --test e2e_relay invite -- --ignored --nocapture
cargo test -p buzz-test-client --test e2e_relay nip43_membership_snapshots_are_rejected -- --ignored --nocapture
cargo test -p buzz-test-client --test e2e_relay nip29_departure_wire -- --ignored --nocapture
env:
RELAY_URL: ws://localhost:3000
RELAY_HTTP_URL: http://localhost:3000
REPRO_RELAY_HTTP: http://localhost:3000
REPRO_HOST: localhost:3000
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
GIT_CREDENTIAL_NOSTR_BIN: ${{ github.workspace }}/target/ci/git-credential-nostr
- name: Media read-auth e2e
# Reads require kind:24242 `t=get` auth, so these binaries are the only
Expand Down
3 changes: 3 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ test-unit:
#!/usr/bin/env bash
set -euo pipefail
./scripts/test-ensure-local-relay-key.sh
python3 scripts/check-community-ban-route-inventory.py
if command -v cargo-nextest &>/dev/null; then
cargo nextest run -p buzz-core -p buzz-auth --lib
cargo nextest run -p buzz-audit --lib
Expand Down Expand Up @@ -591,6 +592,8 @@ test-unit:
+ test(=state::tests::disconnect_community_wins_reason_losing_nip_fi_does_not_enqueue_frame)
+ test(=state::tests::manager_disconnect_sets_reason_enqueues_frame_then_cancels)
+ test(/^api::nip_fi::/)'
# Retain partially consumed mesh receives across housekeeping ticks.
cargo nextest run -p buzz-relay --lib -E 'test(=mesh_boot::tests::demo_echo_retains_pending_receive_across_housekeeping_ticks)'
# boot_lifecycle spawns the real relay binary and asserts its startup
# lifecycle, including that buzz_startup_phase_* reaches /metrics. Its
# non-ignored tests need no Postgres or Redis; the Postgres cases are
Expand Down
85 changes: 83 additions & 2 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,87 @@ combination space (PR #6807). Scope Playwright locators — unscoped
`getByText` in a required smoke test is a strict-mode flake (PR #6980).
(PRs #6807, #6980, #6996, #7013)

### Community-ban route coverage

`crates/buzz-test-client/tests/fixtures/community-ban-route-inventory.tsv`
classifies the relay's production Axum routes by listener, method, path,
authentication plane, membership behavior, and community-restriction behavior.
The inventory guard finds direct `.route(...)` calls in production functions
named `router` or `*_router`, resolves literal or declared `&str` paths, and
applies direct literal `.nest(...)` prefixes from the relay router. It fails
when one of those registrations is missing from the table or a stale row
remains. Run it directly with:

```bash
python3 scripts/check-community-ban-route-inventory.py
```

`just test-unit` and `just test` run the guard; CI also runs the ignored HTTP
and root-WebSocket behavior matrix in the Relay E2E lane:

```bash
cargo test -p buzz-test-client --test community_ban_routes -- --ignored --nocapture
```

Use a fresh disposable local relay, PostgreSQL, Redis, and MinIO stack for that
command, and set `DATABASE_URL` or `BUZZ_TEST_DATABASE_URL` to the disposable
PostgreSQL database. Start the relay with `BUZZ_V1_ENABLED=true` so the
accessory-route cases exercise their production handlers. The matrix creates
unique tenant hosts and identities; GIF
requests stop at malformed local input, and Git/workflow requests use absent resources.
Do not point these tests at a shared, development, or hosted database or relay.

The inventory keeps membership separate from restriction policy. Public NIP-11,
NIP-05, policy documents, and health probes have no authenticated community
principal. Anonymous policy-receipt acceptance is pre-membership and has no
principal to restrict. Invite claim is also pre-membership, but it carries a
verified NIP-98 principal and remains restriction-enforced: its coverage remains
pending in the inventory until BUZZ-268 qualifies the updated
v1/v2 lifecycle:
retain valid unexpired v1 codes and mint only v2, with a 72-hour default and
30-day maximum lifetime. Both formats reject banned claimants and owners
transactionally, and issuer bans permanently invalidate v2. For v1, an unset
operator `invalid_after` uses natural expiry; when set, the code rejects at or
after that absolute boundary, while an earlier code expiry still takes effect.
That operator boundary does not affect v2. Legacy v1 codes have no issuer
identity, so issuer-specific revocation remains limited until a verified fleet
drain or security approval. Relay-operator APIs, deployment-admin APIs,
NIP-FI commands, the localhost Git hook, and the gated mesh test endpoint use
separate auth planes; their exemption reason is recorded on each route row.
Workflow webhooks use a secret-authenticated caller with no user principal, but
run admission must also check the saved workflow owner's community ban. This
coverage remains explicitly pending BUZZ-272. It is not an exemption for banned
owners. BUZZ-269 currently has two pending sibling cases: invite claim and
workflow-owner webhook admission; final integration requires zero pending rows.

Root and huddle admission, final-admission races, fail-closed reads, owner-to-
agent restriction, timeout behavior, and tenant-scoped eviction point to their
existing regression tests in `community-ban-regressions.tsv`. The local socket
eviction tests establish pod-local closure. Delivery to sessions on other pods
depends on the Redis pub/sub path and is not inferred from those local tests.
The moderation-read matrix also checks a moderator-privileged agent against a
fresh signed owner proof and a stored owner link; the bridge harness verifies
that the ban denial uses the NIP-98 signed creation time, with a clear-owner
control that reaches the queue.

This scanner is a source-level guard, not a Rust router interpreter. It reads
direct `.route(...)` calls inside production functions named `router` or ending
in `_router`; a `.route(...)` elsewhere is a scanner error. It resolves a
`.nest(...)` prefix only when the relay's `build_router` passes a literal
or a resolved string constant and a direct call to another recognized router function. It does not
expand macros, discover `.route_service(...)` or `.nest_service(...)`, resolve
dynamic path/prefix expressions, or interpret inline nested routers and
conditional/opaque router composition. It also does not derive auth or
restriction policy from handler code, or prove that the named regression
function contains the expected assertions or is selected by CI. If production
routes use an unrecognized registration shape, extend the scanner and bind
that shape to the inventory before treating the table as complete.

The method column records explicitly declared methods. Axum's `get(...)` helper
also serves `HEAD` through the GET handler unless an explicit `.head(...)`
handler is attached; the scanner does not add that implicit `HEAD` behavior as a
separate inventory row or exercise it separately.

---

## Live Local Relay
Expand Down Expand Up @@ -190,7 +271,7 @@ relay key for authoritative replacement:

```bash
export PATH="$PWD/target/release:$PATH"
export DATABASE_URL="postgres://buzz:buzz_dev@localhost:5432/buzz_roster_e2e"
export DATABASE_URL="${BUZZ_TEST_DATABASE_URL:?set this to the disposable roster database}"
export BUZZ_RELAY_URL="http://localhost:3030" # match the relay from step 3
export RELAY_URL="ws://localhost:3030"
export BUZZ_RELAY_PRIVATE_KEY="<same key used by buzz-relay>"
Expand Down Expand Up @@ -331,7 +412,7 @@ out of the box with `just setup` or `just relay`. Common overrides:
| `BUZZ_HEALTH_PORT` | `8080` | `/_liveness`, `/_readiness` |
| `BUZZ_METRICS_PORT` | `9102` | Prometheus `/metrics` |
| `RELAY_URL` | `ws://localhost:3000` | Advertised in NIP-11 / NIP-42 challenges. **Note: no `BUZZ_` prefix.** |
| `DATABASE_URL` | `postgres://buzz:buzz_dev@localhost:5432/buzz` | |
| `DATABASE_URL` | local PostgreSQL URL from `.env.example` | |
| `REDIS_URL` | `redis://localhost:6379` | |
| `BUZZ_REQUIRE_AUTH_TOKEN` | `false` | When true, REST requires NIP-98 (no `X-Pubkey` fallback) |
| `BUZZ_REQUIRE_RELAY_MEMBERSHIP` | `false` | When true, only pubkeys in `relay_members` can connect |
Expand Down
19 changes: 18 additions & 1 deletion crates/buzz-db/src/store/event_follow_up_postgres_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1018,13 +1018,30 @@ async fn assert_async_isolation(db: &Db, community: CommunityId, keys: &Keys) {
endpoint_grant, max_class, subscriptions, expires_at, updated_at) \
SELECT $1, author, installation_id, source_event_id, source_created_at, generation, \
active, endpoint_enabled, endpoint_hash, endpoint_grant, max_class, \
subscriptions, expires_at, clock_timestamp() FROM push_leases WHERE community_id=$2",
subscriptions, expires_at, \
(SELECT received_at + interval '1 second' FROM events WHERE community_id=$1 AND id=$3) \
FROM push_leases WHERE community_id=$2",
)
.bind(later.as_uuid())
.bind(community.as_uuid())
.bind(before_enrollment.id.as_bytes().as_slice())
.execute(&mut *activation)
.await
.unwrap();
let leases_strictly_newer: bool = sqlx::query_scalar(
"SELECT count(*) > 0 AND bool_and(l.updated_at > e.received_at) \
FROM push_leases l JOIN events e ON e.community_id=l.community_id \
WHERE l.community_id=$1 AND e.id=$2",
)
.bind(later.as_uuid())
.bind(before_enrollment.id.as_bytes().as_slice())
.fetch_one(&mut *activation)
.await
.unwrap();
assert!(
leases_strictly_newer,
"fixture leases must follow recorded receipt"
);
activation.commit().await.unwrap();
assert_eq!(match_count(&isolated, later, &before_enrollment).await, 0);
assert_eq!(
Expand Down
Loading
Loading