test(relay): run supported handler tests on SQLite

Signed-off-by: npub1z3hmzc9ryehxzedl5wzlvpyvja0d483peaja5zt6pd0209f9x2jspe2dxh <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz>
This commit is contained in:
npub1z3hmzc9ryehxzedl5wzlvpyvja0d483peaja5zt6pd0209f9x2jspe2dxh
2026-08-10 12:43:29 -04:00
committed by Brother Darryl
parent 46283f4038
commit 89a033cac0
9 changed files with 144 additions and 362 deletions
@@ -0,0 +1,41 @@
# SQLite relay handler test classification
S4.2 inventory for relay handler, API, and workflow tests that were originally
`#[ignore = "requires Postgres"]` at harness commit `60c067e`. SQLite runs the
25 implemented-only tests through `BUZZ_TEST_BACKEND=sqlite`; the nine remaining
PostgreSQL tests carry the same greppable `SQLite skip:` reason in their
`#[ignore]` attribute.
Reasons are copied from [`buzz-db`'s SQLite backend
inventory](../buzz-db/SQLITE_BACKEND_INVENTORY.md).
## Runs on both backends (25)
| Test module | Tests converted to shared harness | SQLite methods exercised |
| --- | --- | --- |
| `api::bridge` | four HTTP rejection-counter tests | `ensure_configured_community` |
| `api::git::policy` | `push_gate_denies_owner_through_broken_binding` | `ensure_configured_community`, `insert_event`, read-gate queries |
| `api::git::transport` | four read-gate tests | community/user/channel/member/event lifecycle methods |
| `api::invites` | 11 invite validation/claim/policy/document tests | relay-member and invite methods; side-effect publication is separately skipped below |
| `api::operator` | `non_allowlisted_operator_key_gets_403`, `post_operator_body_requires_payload_tag`, and two malformed transfer-request tests | no community lifecycle mutation; request/auth validation only |
| `handlers::relay_admin` | two kind-9033 admission tests | `ensure_configured_community`, relay-member and icon methods |
## SQLite skips (9)
| Test | S2 unsupported method | Inventory reason |
| --- | --- | --- |
| `api::invites::bounded_v2_claims_publish_side_effects_only_for_joined` | `publish_nip43_membership_locked` | relay membership maintenance is PostgreSQL-only |
| `api::operator::unmapped_management_host_can_check_availability` | `lookup_community_by_host_for_management` | community lifecycle management is PostgreSQL-only |
| `api::operator::unmapped_management_host_can_list_owned_communities` | `list_communities_owned_by` | community lifecycle management is PostgreSQL-only |
| `api::operator::unarchive_restores_admission_and_is_idempotent_without_changing_ownership` | `unarchive_community_owned_by` | community lifecycle management is PostgreSQL-only |
| `api::operator::archive_publish_failure_is_retryable_and_preserves_timestamp` | `archive_community_owned_by` | community lifecycle management is PostgreSQL-only |
| `api::operator::happy_path_create_returns_created_and_bootstraps_owner` | `create_community_with_owner` | community lifecycle management is PostgreSQL-only |
| `api::operator::fresh_host_at_owner_limit_returns_limit_reached_conflict` | `create_community_with_owner` | community lifecycle management is PostgreSQL-only |
| `api::operator::happy_path_transfer_swaps_owner_and_demotes_old_to_member` | `transfer_ownership` | relay membership maintenance is PostgreSQL-only |
| `workflow_sink::integration_tests::workflow_send_message_p_tags_mentioned_member` | `create_community_with_owner` | community lifecycle management is PostgreSQL-only |
## Known PostgreSQL-side failures (not changed here)
The existing PostgreSQL integration run has three known failures: the
`product_feedback`, `relay_members`, and owner-limit suites. They reproduce at
the S4.1 base and are not SQLite-harness regressions.
+16 -70
View File
@@ -3363,61 +3363,19 @@ mod tests {
}
}
const TEST_DB_URL: &str = "postgres://buzz:buzz_dev@localhost:5432/buzz"; // sadscan:disable np.postgres.1
/// Build an AppState suitable for handler-level bridge tests.
///
/// - `require_auth_token = false` → X-Pubkey dev-mode fallback active.
/// - `require_relay_membership = false` → membership check short-circuits to
/// OpenRelay without a DB lookup.
/// - `nip98_replay` replaced with an always-fresh guard → no Redis needed
/// for replay detection.
/// - Redis pool points at the local dev instance for the admission check.
///
/// Returns `None` when local Postgres is not reachable.
async fn bridge_handler_test_state() -> Option<Arc<crate::state::AppState>> {
let mut config = crate::config::Config::from_env().ok()?;
config.database_url = TEST_DB_URL.to_string();
// Use the real local Redis so enforce_http_admission can pass.
config.redis_url =
std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
config.relay_url = "wss://bridge-test.local".to_string();
config.require_auth_token = false;
config.require_relay_membership = false;
let pool = sqlx::PgPool::connect(TEST_DB_URL).await.ok()?;
let db = buzz_db::Db::from_pool(pool.clone());
let redis_pool = deadpool_redis::Config::from_url(&config.redis_url)
.create_pool(Some(deadpool_redis::Runtime::Tokio1))
.ok()?;
let pubsub = Arc::new(
buzz_pubsub::PubSubManager::new(&config.redis_url, redis_pool.clone())
.await
.ok()?,
);
let audit = buzz_audit::AuditService::new(pool.clone());
let auth = buzz_auth::AuthService::new(config.auth.clone());
let search = buzz_search::SearchService::new(pool.clone());
let workflow_engine = Arc::new(buzz_workflow::WorkflowEngine::new(
db.clone(),
buzz_workflow::WorkflowConfig::default(),
));
let media_storage = buzz_media::MediaStorage::new(&config.media).ok()?;
let (mut state, _audit_shutdown) = crate::state::AppState::new(
config,
db,
redis_pool,
audit,
pubsub,
auth,
search,
workflow_engine,
Keys::generate(),
media_storage,
);
state.nip98_replay = Arc::new(AlwaysFreshReplayGuard);
Some(Arc::new(state))
async fn bridge_handler_test_state() -> Arc<crate::state::AppState> {
crate::test_support::test_state_with_config_and_state(
|config| {
config.redis_url = std::env::var("REDIS_URL")
.unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
config.relay_url = "wss://bridge-test.local".to_string();
config.require_auth_token = false;
config.require_relay_membership = false;
},
|state| state.nip98_replay = Arc::new(AlwaysFreshReplayGuard),
)
.await
}
/// Drive a single POST /events request through the router and return the
@@ -3483,16 +3441,13 @@ mod tests {
/// Discriminating: if the `reject_with_transport` call in bridge.rs's
/// `serde_json::from_slice` map_err closure is removed, this test fails.
#[test]
#[ignore = "requires Postgres"]
fn submit_event_invalid_json_body_increments_http_transport_counter() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("current_thread runtime");
let Some(state) = rt.block_on(bridge_handler_test_state()) else {
panic!("local Postgres not reachable — start Postgres on 127.0.0.1:5432 before running ignored bridge handler tests");
};
let state = rt.block_on(bridge_handler_test_state());
// Provision a fresh community so bind_community succeeds.
let host = {
@@ -3539,16 +3494,13 @@ mod tests {
/// Discriminating: if the `reject_with_transport` call in bridge.rs's
/// IngestError::Rejected match arm is removed, this test fails.
#[test]
#[ignore = "requires Postgres"]
fn submit_event_relay_only_kind_increments_http_transport_counter() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("current_thread runtime");
let Some(state) = rt.block_on(bridge_handler_test_state()) else {
panic!("local Postgres not reachable — start Postgres on 127.0.0.1:5432 before running ignored bridge handler tests");
};
let state = rt.block_on(bridge_handler_test_state());
let host = {
let h = format!("bridge-test-{}.local", uuid::Uuid::new_v4().simple());
@@ -3671,16 +3623,13 @@ mod tests {
/// Discriminating: if the attribution log is removed from the ParseFail
/// arm in submit_event, this test fails.
#[test]
#[ignore = "requires Postgres"]
fn submit_event_invalid_json_emits_exactly_one_attribution_line() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("current_thread runtime");
let state = rt
.block_on(bridge_handler_test_state())
.expect("local Postgres not reachable — start Postgres on 127.0.0.1:5432 before running ignored bridge handler tests");
let state = rt.block_on(bridge_handler_test_state());
let host = {
let h = format!("bridge-attr-{}.local", uuid::Uuid::new_v4().simple());
@@ -3722,16 +3671,13 @@ mod tests {
/// Discriminating: if two log lines are emitted (old double-log bug), this
/// test fails.
#[test]
#[ignore = "requires Postgres"]
fn submit_event_relay_only_kind_emits_exactly_one_attribution_line() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("current_thread runtime");
let state = rt
.block_on(bridge_handler_test_state())
.expect("local Postgres not reachable — start Postgres on 127.0.0.1:5432 before running ignored bridge handler tests");
let state = rt.block_on(bridge_handler_test_state());
let host = {
let h = format!("bridge-attr-{}.local", uuid::Uuid::new_v4().simple());
+1 -42
View File
@@ -822,48 +822,8 @@ printf '%s' "$HMAC_INPUT" | openssl dgst -sha256 -hmac "{secret}" -hex 2>/dev/nu
// ── hook_policy_check binding gate (requires Postgres) ──────────────
const TEST_DB_URL: &str = "postgres://buzz:buzz_dev@localhost:5432/buzz"; // sadscan:disable np.postgres.1
async fn policy_test_state() -> Arc<AppState> {
let mut config = crate::config::Config::from_env().expect("default config loads");
config.require_relay_membership = false;
config.redis_url = "redis://127.0.0.1:1".to_string();
config.database_url = std::env::var("BUZZ_TEST_DATABASE_URL")
.or_else(|_| std::env::var("DATABASE_URL"))
.unwrap_or_else(|_| TEST_DB_URL.to_string());
let pool = sqlx::PgPool::connect(&config.database_url)
.await
.expect("connect test DB");
let db = buzz_db::Db::from_pool(pool.clone());
let redis_pool = deadpool_redis::Config::from_url(&config.redis_url)
.create_pool(Some(deadpool_redis::Runtime::Tokio1))
.expect("redis pool");
let pubsub = Arc::new(
buzz_pubsub::PubSubManager::new(&config.redis_url, redis_pool.clone())
.await
.expect("pubsub manager"),
);
let audit = buzz_audit::AuditService::new(pool.clone());
let auth = buzz_auth::AuthService::new(config.auth.clone());
let search = buzz_search::SearchService::new(pool.clone());
let workflow_engine = Arc::new(buzz_workflow::WorkflowEngine::new(
db.clone(),
buzz_workflow::WorkflowConfig::default(),
));
let media_storage = buzz_media::MediaStorage::new(&config.media).expect("media storage");
let (state, _audit_shutdown) = AppState::new(
config,
db,
redis_pool,
audit,
pubsub,
auth,
search,
workflow_engine,
nostr::Keys::generate(),
media_storage,
);
Arc::new(state)
crate::test_support::test_state().await
}
/// Announce `repo_id` with the given tags, then push to it as its own
@@ -927,7 +887,6 @@ printf '%s' "$HMAC_INPUT" | openssl dgst -sha256 -hmac "{secret}" -hex 2>/dev/nu
/// the owner a push path through a binding the read gate refuses to
/// honor. The remediation token stays reserved for genuinely NotBound.
#[tokio::test]
#[ignore = "requires Postgres"]
async fn push_gate_denies_owner_through_broken_binding() {
use nostr::{Keys, Tag};
+1 -11
View File
@@ -2799,14 +2799,8 @@ mod sec005_read_gate_tests {
// ── authorize_git_read matrix (requires Postgres) ────────────────────
const TEST_DB_URL: &str = "postgres://buzz:buzz_dev@localhost:5432/buzz"; // sadscan:disable np.postgres.1
async fn setup_db() -> buzz_db::Db {
let url = std::env::var("BUZZ_TEST_DATABASE_URL")
.or_else(|_| std::env::var("DATABASE_URL"))
.unwrap_or_else(|_| TEST_DB_URL.to_string());
let pool = sqlx::PgPool::connect(&url).await.expect("connect test DB");
buzz_db::Db::from_pool(pool)
crate::test_support::test_state().await.db.clone()
}
/// How the fixture's kind:30617 binds (or fails to bind) a channel.
@@ -2909,7 +2903,6 @@ mod sec005_read_gate_tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn read_gate_allows_current_member_denies_removed_and_owner_bypass() {
let f = setup_repo(Binding::Channel).await;
@@ -2955,7 +2948,6 @@ mod sec005_read_gate_tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn read_gate_denies_missing_or_malformed_binding_and_absent_repo() {
// Missing buzz-channel tag → deny even for a channel member, with
// the generic body: the remediation carve-out is author-only.
@@ -3035,7 +3027,6 @@ mod sec005_read_gate_tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn read_gate_gives_author_of_unbound_repo_remediation_body() {
// Issue #3527: the author of a never-bound announcement is the one
// identity that can fix it (30617 is keyed by (author, d)) and the
@@ -3082,7 +3073,6 @@ mod sec005_read_gate_tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn read_gate_follows_current_announcement_not_stale_registry() {
// Max's registry/pointer concern: a soft-deleted 30617 can leave the
// `git_repo_names` reservation and the manifest pointer alive. Reads
+30 -97
View File
@@ -565,8 +565,6 @@ mod tests {
}
}
const TEST_DB_URL: &str = "postgres://buzz:buzz_dev@localhost:5432/buzz"; // sadscan:disable np.postgres.1
fn claim_cache(
capacity: u64,
ttl: Duration,
@@ -648,53 +646,23 @@ mod tests {
}
/// Build a closed-relay (`require_relay_membership = true`) test state with
/// a fresh community on `host`; returns `None` when Postgres is unavailable.
async fn invite_test_state(host: &str) -> Option<Arc<AppState>> {
let mut config = crate::config::Config::from_env().ok()?;
let database_url = std::env::var("BUZZ_TEST_DATABASE_URL")
.or_else(|_| std::env::var("DATABASE_URL"))
.unwrap_or_else(|_| TEST_DB_URL.to_string());
config.database_url = database_url.clone();
config.redis_url = "redis://127.0.0.1:1".to_string();
config.relay_url = format!("wss://{host}");
// The claim route must work on relays where membership is enforced —
// that is the entire point of an invite.
config.require_relay_membership = true;
let pool = sqlx::PgPool::connect(&database_url).await.ok()?;
let db = buzz_db::Db::from_pool(pool.clone());
db.ensure_configured_community(host).await.ok()?;
let redis_pool = deadpool_redis::Config::from_url(&config.redis_url)
.create_pool(Some(deadpool_redis::Runtime::Tokio1))
.ok()?;
let pubsub = Arc::new(
buzz_pubsub::PubSubManager::new(&config.redis_url, redis_pool.clone())
.await
.ok()?,
);
let audit = buzz_audit::AuditService::new(pool.clone());
let auth = buzz_auth::AuthService::new(config.auth.clone());
let search = buzz_search::SearchService::new(pool.clone());
let workflow_engine = Arc::new(buzz_workflow::WorkflowEngine::new(
db.clone(),
buzz_workflow::WorkflowConfig::default(),
));
let media_storage = buzz_media::MediaStorage::new(&config.media).ok()?;
let (mut state, _audit_shutdown) = AppState::new(
config,
db,
redis_pool,
audit,
pubsub,
auth,
search,
workflow_engine,
Keys::generate(),
media_storage,
);
state.nip98_replay = Arc::new(AlwaysFreshReplayGuard);
Some(Arc::new(state))
/// a fresh community on `host`.
async fn invite_test_state(host: &str) -> Arc<AppState> {
let state = crate::test_support::test_state_with_config_and_state(
|config| {
config.relay_url = format!("wss://{host}");
// The claim route must work on relays where membership is enforced.
config.require_relay_membership = true;
},
|state| state.nip98_replay = Arc::new(AlwaysFreshReplayGuard),
)
.await;
state
.db
.ensure_configured_community(host)
.await
.expect("ensure community");
state
}
async fn post_json(
@@ -838,13 +806,10 @@ mod tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn mint_validates_max_uses_and_ttl_bounds() {
let host = format!("invites-validation-{}.example", Uuid::new_v4().simple());
let owner = Keys::generate();
let state = invite_test_state(&host)
.await
.expect("requires reachable Postgres and relay test state");
let state = invite_test_state(&host).await;
let community = state
.db
.lookup_community_by_host(&host)
@@ -896,13 +861,10 @@ mod tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn malformed_and_unknown_v2_codes_are_forbidden_without_v1_fallback() {
let host = format!("invites-v2-invalid-{}.example", Uuid::new_v4().simple());
let joiner = Keys::generate();
let state = invite_test_state(&host)
.await
.expect("requires reachable Postgres and relay test state");
let state = invite_test_state(&host).await;
let unknown = format!("v2.{}", URL_SAFE_NO_PAD.encode([9_u8; 32]));
for code in [
@@ -931,7 +893,7 @@ mod tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
#[ignore = "requires Postgres; SQLite skip: publish_nip43_membership_locked — relay membership maintenance is PostgreSQL-only"]
async fn bounded_v2_claims_publish_side_effects_only_for_joined() {
let host = format!(
"invites-v2-side-effects-{}.example",
@@ -940,9 +902,7 @@ mod tests {
let owner = Keys::generate();
let first = Keys::generate();
let second = Keys::generate();
let state = invite_test_state(&host)
.await
.expect("requires reachable Postgres and relay test state");
let state = invite_test_state(&host).await;
let community = state
.db
.lookup_community_by_host(&host)
@@ -1078,14 +1038,11 @@ mod tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn owner_mints_and_new_pubkey_claims() {
let host = format!("invites-{}.example", Uuid::new_v4().simple());
let owner = Keys::generate();
let joiner = Keys::generate();
let Some(state) = invite_test_state(&host).await else {
return;
};
let state = invite_test_state(&host).await;
let community = state
.db
.lookup_community_by_host(&host)
@@ -1148,14 +1105,11 @@ mod tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn join_policy_gate_end_to_end() {
let host = format!("invites-policy-{}.example", Uuid::new_v4().simple());
let owner = Keys::generate();
let joiner = Keys::generate();
let Some(state) = invite_test_state(&host).await else {
return;
};
let state = invite_test_state(&host).await;
// Force the join policy on regardless of env.
let mut state_inner = (*state).clone();
let mut config = state_inner.config.as_ref().clone();
@@ -1357,14 +1311,11 @@ mod tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn non_admin_cannot_mint() {
let host = format!("invites-{}.example", Uuid::new_v4().simple());
let member = Keys::generate();
let outsider = Keys::generate();
let Some(state) = invite_test_state(&host).await else {
return;
};
let state = invite_test_state(&host).await;
let community = state
.db
.lookup_community_by_host(&host)
@@ -1386,13 +1337,10 @@ mod tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn claim_rejects_invalid_code() {
let host = format!("invites-{}.example", Uuid::new_v4().simple());
let joiner = Keys::generate();
let Some(state) = invite_test_state(&host).await else {
return;
};
let state = invite_test_state(&host).await;
let body = serde_json::json!({ "code": "garbage.code" }).to_string();
let response = post_json(state.clone(), &host, "/api/invites/claim", &joiner, body).await;
@@ -1418,15 +1366,12 @@ mod tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn code_minted_for_one_community_fails_on_another() {
let host_a = format!("invites-a-{}.example", Uuid::new_v4().simple());
let host_b = format!("invites-b-{}.example", Uuid::new_v4().simple());
let owner = Keys::generate();
let joiner = Keys::generate();
let Some(state) = invite_test_state(&host_a).await else {
return;
};
let state = invite_test_state(&host_a).await;
state
.db
.ensure_configured_community(&host_b)
@@ -1498,13 +1443,10 @@ mod tests {
/// rejected by `/api/invites/claim` with the distinguishable
/// `invite_expired` body, and do not admit the caller.
#[tokio::test]
#[ignore = "requires Postgres"]
async fn claim_rejects_expired_code() {
let host = format!("invites-{}.example", Uuid::new_v4().simple());
let joiner = Keys::generate();
let state = invite_test_state(&host)
.await
.expect("requires reachable Postgres and relay test state");
let state = invite_test_state(&host).await;
let community = state
.db
.lookup_community_by_host(&host)
@@ -1569,14 +1511,11 @@ mod tests {
/// Authorization header (same signed NIP-98 event id) is rejected as
/// replay before the invite verification ever runs.
#[tokio::test]
#[ignore = "requires Postgres"]
async fn claim_rejects_replayed_nip98_auth() {
let host = format!("invites-{}.example", Uuid::new_v4().simple());
let owner = Keys::generate();
let joiner = Keys::generate();
let state_arc = invite_test_state(&host)
.await
.expect("requires reachable Postgres and relay test state");
let state_arc = invite_test_state(&host).await;
// Swap the always-fresh guard for one that fires the second time the
// same event id is presented — the code path we're pinning.
let mut state_owned =
@@ -1656,13 +1595,10 @@ mod tests {
/// `invite_invalid` (403) to `too many invite claim attempts` (429) proves
/// the limiter guard is on the request path and fires on repeat pubkey.
#[tokio::test]
#[ignore = "requires Postgres"]
async fn claim_rate_limit_fires_on_repeat_pubkey() {
let host = format!("invites-{}.example", Uuid::new_v4().simple());
let joiner = Keys::generate();
let state_arc = invite_test_state(&host)
.await
.expect("requires reachable Postgres and relay test state");
let state_arc = invite_test_state(&host).await;
// Fresh limiter with the production limit so the assertion pins the
// in-endpoint threshold, not a test-only budget.
let mut state_owned =
@@ -1725,12 +1661,9 @@ mod tests {
/// The document routes are public (no NIP-98) and 404 until configured,
/// exactly like the JSON policy endpoint they sit beside.
#[tokio::test]
#[ignore = "requires Postgres"]
async fn join_policy_document_pages_serve_configured_markdown() {
let host = format!("invites-docs-{}.example", Uuid::new_v4().simple());
let Some(state) = invite_test_state(&host).await else {
return;
};
let state = invite_test_state(&host).await;
let get_page = |state: Arc<crate::state::AppState>, path: &'static str| {
let host = host.clone();
+32 -90
View File
@@ -533,7 +533,6 @@ mod tests {
}
}
const TEST_DB_URL: &str = "postgres://buzz:buzz_dev@localhost:5432/buzz"; // sadscan:disable np.postgres.1
const INGRESS_HOST: &str = "operator-ingress.example";
fn nip98_auth_header(keys: &Keys, url: &str, method: &str, body: Option<&[u8]>) -> String {
@@ -569,51 +568,20 @@ mod tests {
format!("Nostr {encoded}")
}
async fn operator_test_state(operator_keys: &[Keys]) -> Option<Arc<AppState>> {
let mut config = crate::config::Config::from_env().ok()?;
config.database_url = TEST_DB_URL.to_string();
config.redis_url = "redis://127.0.0.1:1".to_string();
config.relay_url = "wss://tenant.example".to_string();
config.relay_operator_api_origin = Some(format!("http://{INGRESS_HOST}"));
config.relay_operator_pubkeys = operator_keys
.iter()
.map(|keys| keys.public_key().to_hex())
.collect();
config.require_relay_membership = true;
let pool = sqlx::PgPool::connect(TEST_DB_URL).await.ok()?;
let db = buzz_db::Db::from_pool(pool.clone());
let redis_pool = deadpool_redis::Config::from_url(&config.redis_url)
.create_pool(Some(deadpool_redis::Runtime::Tokio1))
.ok()?;
let pubsub = Arc::new(
buzz_pubsub::PubSubManager::new(&config.redis_url, redis_pool.clone())
.await
.ok()?,
);
let audit = buzz_audit::AuditService::new(pool.clone());
let auth = buzz_auth::AuthService::new(config.auth.clone());
let search = buzz_search::SearchService::new(pool.clone());
let workflow_engine = Arc::new(buzz_workflow::WorkflowEngine::new(
db.clone(),
buzz_workflow::WorkflowConfig::default(),
));
let media_storage = buzz_media::MediaStorage::new(&config.media).ok()?;
let (mut state, _audit_shutdown) = AppState::new(
config,
db,
redis_pool,
audit,
pubsub,
auth,
search,
workflow_engine,
Keys::generate(),
media_storage,
);
state.nip98_replay = Arc::new(AlwaysFreshReplayGuard);
Some(Arc::new(state))
async fn operator_test_state(operator_keys: &[Keys]) -> Arc<AppState> {
crate::test_support::test_state_with_config_and_state(
|config| {
config.relay_url = "wss://tenant.example".to_string();
config.relay_operator_api_origin = Some(format!("http://{INGRESS_HOST}"));
config.relay_operator_pubkeys = operator_keys
.iter()
.map(|keys| keys.public_key().to_hex())
.collect();
config.require_relay_membership = true;
},
|state| state.nip98_replay = Arc::new(AlwaysFreshReplayGuard),
)
.await
}
async fn read_json(response: axum::response::Response) -> Value {
@@ -703,13 +671,10 @@ mod tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn non_allowlisted_operator_key_gets_403() {
let operator = Keys::generate();
let outsider = Keys::generate();
let Some(state) = operator_test_state(&[operator]).await else {
return;
};
let state = operator_test_state(&[operator]).await;
let body = format!(
r#"{{"host":"community-{}.example"}}"#,
Uuid::new_v4().simple()
@@ -735,12 +700,9 @@ mod tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn post_operator_body_requires_payload_tag() {
let operator = Keys::generate();
let Some(state) = operator_test_state(std::slice::from_ref(&operator)).await else {
return;
};
let state = operator_test_state(std::slice::from_ref(&operator)).await;
let body = format!(
r#"{{"host":"community-{}.example"}}"#,
Uuid::new_v4().simple()
@@ -774,12 +736,10 @@ mod tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
#[ignore = "requires Postgres; SQLite skip: lookup_community_by_host_for_management — community lifecycle management is PostgreSQL-only"]
async fn unmapped_management_host_can_check_availability() {
let operator = Keys::generate();
let Some(state) = operator_test_state(std::slice::from_ref(&operator)).await else {
return;
};
let state = operator_test_state(std::slice::from_ref(&operator)).await;
let host = format!("community-{}.example", Uuid::new_v4().simple());
let query = format!("host={host}");
let url = format!("http://{INGRESS_HOST}/operator/communities/availability?{query}");
@@ -803,13 +763,11 @@ mod tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
#[ignore = "requires Postgres; SQLite skip: list_communities_owned_by — community lifecycle management is PostgreSQL-only"]
async fn unmapped_management_host_can_list_owned_communities() {
let operator = Keys::generate();
let owner = Keys::generate();
let Some(state) = operator_test_state(std::slice::from_ref(&operator)).await else {
return;
};
let state = operator_test_state(std::slice::from_ref(&operator)).await;
let owner_hex = owner.public_key().to_hex();
let query = format!("owner_pubkey={owner_hex}");
let url = format!("http://{INGRESS_HOST}/operator/communities?{query}");
@@ -836,14 +794,12 @@ mod tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
#[ignore = "requires Postgres; SQLite skip: unarchive_community_owned_by — community lifecycle management is PostgreSQL-only"]
async fn unarchive_restores_admission_and_is_idempotent_without_changing_ownership() {
let operator = Keys::generate();
let owner = Keys::generate();
let outsider = Keys::generate();
let Some(state) = operator_test_state(std::slice::from_ref(&operator)).await else {
return;
};
let state = operator_test_state(std::slice::from_ref(&operator)).await;
let host = format!("community-{}.example", Uuid::new_v4().simple());
assert_eq!(
provision_community(Arc::clone(&state), &operator, &host, &owner)
@@ -927,13 +883,11 @@ mod tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
#[ignore = "requires Postgres; SQLite skip: archive_community_owned_by — community lifecycle management is PostgreSQL-only"]
async fn archive_publish_failure_is_retryable_and_preserves_timestamp() {
let operator = Keys::generate();
let owner = Keys::generate();
let Some(state) = operator_test_state(std::slice::from_ref(&operator)).await else {
return;
};
let state = operator_test_state(std::slice::from_ref(&operator)).await;
let host = format!("community-{}.example", Uuid::new_v4().simple());
let owner_hex = owner.public_key().to_hex();
let create_body = serde_json::json!({
@@ -1039,13 +993,11 @@ mod tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
#[ignore = "requires Postgres; SQLite skip: create_community_with_owner — community lifecycle management is PostgreSQL-only"]
async fn happy_path_create_returns_created_and_bootstraps_owner() {
let operator = Keys::generate();
let owner = Keys::generate();
let Some(state) = operator_test_state(std::slice::from_ref(&operator)).await else {
return;
};
let state = operator_test_state(std::slice::from_ref(&operator)).await;
let host = format!("community-{}.example", Uuid::new_v4().simple());
let response = provision_community(state.clone(), &operator, &host, &owner).await;
@@ -1075,13 +1027,11 @@ mod tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
#[ignore = "requires Postgres; SQLite skip: create_community_with_owner — community lifecycle management is PostgreSQL-only"]
async fn fresh_host_at_owner_limit_returns_limit_reached_conflict() {
let operator = Keys::generate();
let owner = Keys::generate();
let Some(state) = operator_test_state(std::slice::from_ref(&operator)).await else {
return;
};
let state = operator_test_state(std::slice::from_ref(&operator)).await;
for _ in 0..buzz_db::relay_members::MAX_COMMUNITIES_PER_OWNER {
let host = format!("community-{}.example", Uuid::new_v4().simple());
@@ -1112,14 +1062,12 @@ mod tests {
/// the old owner to `member`, and publishes a NIP-43 snapshot reflecting the
/// new roles.
#[tokio::test]
#[ignore = "requires Postgres"]
#[ignore = "requires Postgres; SQLite skip: transfer_ownership — relay membership maintenance is PostgreSQL-only"]
async fn happy_path_transfer_swaps_owner_and_demotes_old_to_member() {
let operator = Keys::generate();
let initial_owner = Keys::generate();
let new_owner = Keys::generate();
let Some(state) = operator_test_state(std::slice::from_ref(&operator)).await else {
return;
};
let state = operator_test_state(std::slice::from_ref(&operator)).await;
let host = format!("community-{}.example", Uuid::new_v4().simple());
let create_response =
@@ -1199,13 +1147,10 @@ mod tests {
/// Transfer with an invalid community_id returns 400.
#[tokio::test]
#[ignore = "requires Postgres"]
async fn transfer_with_invalid_community_id_returns_400() {
let operator = Keys::generate();
let new_owner = Keys::generate();
let Some(state) = operator_test_state(std::slice::from_ref(&operator)).await else {
return;
};
let state = operator_test_state(std::slice::from_ref(&operator)).await;
let body = serde_json::json!({
"community_id": "not-a-uuid",
"new_owner_pubkey": new_owner.public_key().to_hex(),
@@ -1226,12 +1171,9 @@ mod tests {
/// Transfer with an invalid new_owner_pubkey returns 400.
#[tokio::test]
#[ignore = "requires Postgres"]
async fn transfer_with_invalid_pubkey_returns_400() {
let operator = Keys::generate();
let Some(state) = operator_test_state(std::slice::from_ref(&operator)).await else {
return;
};
let state = operator_test_state(std::slice::from_ref(&operator)).await;
let body = serde_json::json!({
"community_id": Uuid::new_v4().to_string(),
"new_owner_pubkey": "not-a-pubkey",
+9 -50
View File
@@ -696,63 +696,24 @@ mod tests {
// explicitly in CI's Backend Integration job; requires local Postgres
// (and hard-fails rather than skipping when it is unreachable).
const TEST_DB_URL: &str = "postgres://buzz:buzz_dev@localhost:5432/buzz"; // sadscan:disable np.postgres.1
/// Build a real `AppState` + tenant for a fresh community on `host`, with
/// `require_relay_membership` set as given. Mirrors
/// `api::invites::tests::invite_test_state`.
/// `require_relay_membership` set as given.
async fn workspace_profile_test_state(
host: &str,
require_relay_membership: bool,
) -> (Arc<AppState>, TenantContext) {
let mut config = crate::config::Config::from_env().expect("config from env");
let database_url = std::env::var("BUZZ_TEST_DATABASE_URL")
.or_else(|_| std::env::var("DATABASE_URL"))
.unwrap_or_else(|_| TEST_DB_URL.to_string());
config.database_url = database_url.clone();
config.redis_url = "redis://127.0.0.1:1".to_string();
config.relay_url = format!("wss://{host}");
config.require_relay_membership = require_relay_membership;
let pool = sqlx::PgPool::connect(&database_url)
.await
.expect("requires reachable Postgres");
let db = buzz_db::Db::from_pool(pool.clone());
let record = db
let state = crate::test_support::test_state_with_config(|config| {
config.relay_url = format!("wss://{host}");
config.require_relay_membership = require_relay_membership;
})
.await;
let record = state
.db
.ensure_configured_community(host)
.await
.expect("ensure community");
let tenant = TenantContext::resolved(record.id, host);
let redis_pool = deadpool_redis::Config::from_url(&config.redis_url)
.create_pool(Some(deadpool_redis::Runtime::Tokio1))
.expect("redis pool config");
let pubsub = Arc::new(
buzz_pubsub::PubSubManager::new(&config.redis_url, redis_pool.clone())
.await
.expect("pubsub manager"),
);
let audit = buzz_audit::AuditService::new(pool.clone());
let auth = buzz_auth::AuthService::new(config.auth.clone());
let search = buzz_search::SearchService::new(pool.clone());
let workflow_engine = Arc::new(buzz_workflow::WorkflowEngine::new(
db.clone(),
buzz_workflow::WorkflowConfig::default(),
));
let media_storage = buzz_media::MediaStorage::new(&config.media).expect("media storage");
let (state, _audit_shutdown) = AppState::new(
config,
db,
redis_pool,
audit,
pubsub,
auth,
search,
workflow_engine,
Keys::generate(),
media_storage,
);
(Arc::new(state), tenant)
(state, tenant)
}
/// Sign a fresh kind:9033 with `icon` and run it through the real
@@ -785,7 +746,6 @@ mod tests {
/// Discriminating: fails if the call site inverts or drops
/// `require_relay_membership`, or stops consulting `has_admin_or_owner`.
#[tokio::test]
#[ignore = "requires Postgres"]
async fn open_relay_9033_admits_roleless_only_until_a_steward_exists() {
let host = format!("icon-gate-open-{}.example", uuid::Uuid::new_v4().simple());
let (state, tenant) = workspace_profile_test_state(&host, false).await;
@@ -844,7 +804,6 @@ mod tests {
/// branch. Together with the open-relay test this kills the inverted-flag
/// mutant: no assignment of the flag satisfies both.
#[tokio::test]
#[ignore = "requires Postgres"]
async fn closed_relay_9033_still_requires_admin_or_owner() {
let host = format!("icon-gate-closed-{}.example", uuid::Uuid::new_v4().simple());
let (state, tenant) = workspace_profile_test_state(&host, true).await;
+13 -1
View File
@@ -32,6 +32,17 @@ pub(crate) async fn test_state() -> Arc<AppState> {
/// Build relay test state after applying test-specific configuration.
pub(crate) async fn test_state_with_config(configure: impl FnOnce(&mut Config)) -> Arc<AppState> {
test_state_with_config_and_state(configure, |_| {}).await
}
/// Build relay test state with test-specific configuration and state overrides.
///
/// The latter is intentionally limited to test-only seams such as the NIP-98
/// replay guard; database selection remains owned by this shared harness.
pub(crate) async fn test_state_with_config_and_state(
configure: impl FnOnce(&mut Config),
configure_state: impl FnOnce(&mut AppState),
) -> Arc<AppState> {
let mut config = Config::from_env().expect("default config loads");
config.require_relay_membership = false;
config.redis_url = "redis://127.0.0.1:1".to_string();
@@ -68,7 +79,7 @@ pub(crate) async fn test_state_with_config(configure: impl FnOnce(&mut Config))
buzz_workflow::WorkflowConfig::default(),
));
let media_storage = buzz_media::MediaStorage::new(&config.media).expect("media storage");
let (state, _audit_shutdown) = AppState::new(
let (mut state, _audit_shutdown) = AppState::new(
config,
db,
redis_pool,
@@ -80,6 +91,7 @@ pub(crate) async fn test_state_with_config(configure: impl FnOnce(&mut Config))
nostr::Keys::generate(),
media_storage,
);
configure_state(&mut state);
Arc::new(state)
}
+1 -1
View File
@@ -576,7 +576,7 @@ mod integration_tests {
}
#[tokio::test]
#[ignore = "requires Postgres"]
#[ignore = "requires Postgres; SQLite skip: create_community_with_owner — community lifecycle management is PostgreSQL-only"]
async fn workflow_send_message_p_tags_mentioned_member() {
let state = test_state().await;