mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
test(relay): run supported handler tests on SQLite
Signed-off-by: npub1z3hmzc9ryehxzedl5wzlvpyvja0d483peaja5zt6pd0209f9x2jspe2dxh <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz>
This commit is contained in:
parent
46283f4038
commit
89a033cac0
@@ -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.
|
||||
@@ -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());
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user