buzz-db: bare `--ignored` test sweep drops the Buzz Desktop dev schema via hardcoded localhost:5432 fallback
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
`cargo test -p buzz-db --lib -- --ignored` — with no `BUZZ_TEST_DATABASE_URL` or `DATABASE_URL` set — runs `DROP SCHEMA IF EXISTS public CASCADE` against a **hardcoded** `postgres://buzz:buzz_dev@localhost:5432/buzz`. That is the Buzz Desktop development database on a default local setup.
This happened. On 2026-07-25 I ran that command twice while reviewing an unrelated security fix and destroyed the shared dev schema for about three minutes: window `23:59:11.170Z -> 00:02:02.930Z`. It self-restored only because the test binary kept running to the rebuild. A `Ctrl-C`, a tool timeout, or a panic between the `DROP SCHEMA` and the following `CREATE SCHEMA` + re-migration leaves the developer's database empty with no automatic recovery.
**How that window is established, and a caveat for anyone reproducing it.** The back edge comes from a state artifact rather than a log: every one of the 24 rows in `_sqlx_migrations` on the live database has an `installed_on` inside a 283ms burst starting `2026-07-25 20:02:02.647 EDT`, i.e. the schema now serving traffic was built then. The catalog agrees exhaustively: `public` is OID `3238914`, every table in it falls in `3238915-3240452`, and zero relations predate the namespace, so nothing survived the drop.
The caveat is worth more than the number. This server runs `log_statement = none` with `log_min_error_statement = error`, so **the Postgres log contains only statements that failed.** Every `DROP SCHEMA IF EXISTS public CASCADE` line in it is there because it errored; the successful drop that actually destroyed the schema was never logged, and neither were the successful `CREATE SCHEMA` or any successful migration. Three attempts to bound this incident from that log produced three different answers before we checked `log_statement`. If you are timing an incident on a default Homebrew Postgres, reach for state that carries its own clock (`_sqlx_migrations.installed_on`, `pg_class` OID survivorship) before reaching for the log.
## Mechanism
```rust
// crates/buzz-db/src/migration.rs:103
const TEST_DB_URL: &str = "postgres://buzz:buzz_dev@localhost:5432/buzz";
// crates/buzz-db/src/migration.rs:1027-1031
async fn connect_test_pool() -> PgPool {
let database_url = std::env::var("BUZZ_TEST_DATABASE_URL")
.or_else(|_| std::env::var("DATABASE_URL"))
.unwrap_or_else(|_| TEST_DB_URL.to_owned()); // <-- silent fallback to the dev DB
...
}
// crates/buzz-db/src/migration.rs:1037
async fn reset_public_schema(pool: &PgPool) {
sqlx::query("DROP SCHEMA IF EXISTS public CASCADE") ...
```
Three `#[ignore = "requires Postgres"]` tests call `reset_public_schema`:
- `migration.rs:1061` — `pre_0007_ambiguous_nip_rs_data_blocks_without_mutation_and_allows_retry`
- `migration.rs:1131` — `populated_upgrade_preserves_search_policy_except_for_push_leases`
- `migration.rs:1191` — `run_migrations_applies_consolidated_initial_schema_on_fresh_database`
`#[ignore]` is not a safeguard here: `--ignored` is exactly the flag you reach for when you want "the Postgres-backed tests," and nothing in that command names a database.
Note the contrast with `scripts/start-isolated-test-relay.sh:96`, which spells it `DROP SCHEMA public CASCADE` and pipes it through `docker compose exec postgres`, so it can only ever reach the container on `:5471`. The `IF EXISTS` spelling appears exactly once in the tree, which is how the incident was traced.
## Second, independent problem: the suite is already broken in parallel
Because these three tests drop the schema out from under every other test in the same binary, `cargo test -p buzz-db --lib -- --ignored` is nondeterministic:
- **10 failures on clean `main`** at the same commit, varying run to run
- passes 127/128 with `--test-threads=1` (the one failure is `product_feedback` wanting an explicit `BUZZ_TEST_DATABASE_URL`)
Nobody has noticed because `scripts/run-tests.sh:93` only ever runs `cargo test -p buzz-db --lib` *without* `--ignored`, and the Postgres-backed tests are otherwise only run under a filter. So the documented CI path never exercises the sweep, and the sweep is the dangerous command.
## Proposed fix
Two changes, both small:
1. **Delete the fallback.** `connect_test_pool` should `.expect("set BUZZ_TEST_DATABASE_URL to an isolated test database")` rather than default to anything. No hardcoded URL constant. A test that needs a database it can destroy should require the developer to name it.
2. **Make `reset_public_schema` unreachable from a suite-wide sweep.** `#[ignore]` does not distinguish "needs Postgres" from "will destroy Postgres". Gate the three destructive tests behind something `--ignored` alone cannot satisfy — e.g. a distinct required env var (`BUZZ_TEST_DESTRUCTIVE_DB=1`), a separate test target, or a `#[cfg(feature = ...)]` — so that the reasonable command stays safe and the destructive one has to be asked for by name.
Optionally (3) have the destructive tests refuse a database whose URL they didn't receive explicitly *and* which contains unexpected data — but (1) and (2) are the load-bearing ones, and (3) is a heuristic where those two are structural.
Fixing the parallel nondeterminism follows for free from (2): once the schema-dropping tests can't be swept into the same run as everything else, the remaining Postgres tests can run in parallel honestly.
## Not urgent as a security issue, urgent as a data-loss issue
There is no remote attacker here. The exposure is: any contributor typing a plausible test command loses their local Buzz Desktop data, with no warning and no confirmation prompt. "Don't type the obvious command" is not a safeguard.
Contributor guide
Assessment
This issue has not been assessed yet.