From 9d1e4b257657f382d3111ce748f3da8d063b7671 Mon Sep 17 00:00:00 2001 From: Luke Tornquist Date: Tue, 25 Aug 2026 10:05:32 -0400 Subject: [PATCH] Extract community persistence (#6668) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why Community persistence is the next incremental `buzz-db` store extraction, keeping tenant lifecycle SQL, records, tests, and instrumentation out of the database runtime module without changing behavior. ## What - Move community records and the existing `impl Db` operations into `community.rs` while preserving crate-root re-exports. - Move focused PostgreSQL tests with the implementation and enforce single ownership for each method and datastore span. ## Risk Assessment Low — this is a structural move of the existing records, SQL, method bodies, and focused tests; database runtime concerns, schema, and behavior remain unchanged. ## References - Architecture guardrail: https://github.com/TheSentinel454/buzz/issues/34 - Incremental tracker: https://github.com/TheSentinel454/buzz/issues/2 - Primary task: https://github.com/TheSentinel454/buzz/issues/5 - Stacked on: https://github.com/block/buzz/pull/6660 Generated with Codex Signed-off-by: tornquist --- crates/buzz-db/src/community.rs | 989 ++++++++++++++++++++++++++++++++ crates/buzz-db/src/lib.rs | 879 +--------------------------- 2 files changed, 996 insertions(+), 872 deletions(-) create mode 100644 crates/buzz-db/src/community.rs diff --git a/crates/buzz-db/src/community.rs b/crates/buzz-db/src/community.rs new file mode 100644 index 0000000000..64c116a520 --- /dev/null +++ b/crates/buzz-db/src/community.rs @@ -0,0 +1,989 @@ +//! Community lifecycle and host-map persistence. + +use buzz_core::CommunityId; +use buzz_datastore_tracing::datastore_span; +use chrono::{DateTime, Utc}; +use sqlx::Row; +use uuid::Uuid; + +use crate::{relay_members, Db, DbError, Result}; + +/// Community host-map row returned by [`Db::lookup_community_by_host`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CommunityRecord { + /// Stable server-resolved community id. + pub id: CommunityId, + /// Normalized host that maps to this community. + pub host: String, +} + +/// Community row returned by idempotent community ensure/create operations. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EnsuredCommunityRecord { + /// Stable server-resolved community id. + pub id: CommunityId, + /// Normalized host that maps to this community. + pub host: String, + /// True only when this call inserted the `communities` row. + pub created: bool, +} + +/// Community row returned by an atomic create-with-owner operation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CreatedCommunityRecord { + /// Stable server-resolved community id. + pub id: CommunityId, + /// Normalized host stored for the community. + pub host: String, +} + +/// Result of atomically creating a community with its initial owner. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CreateCommunityWithOwnerResult { + /// The community was created, or an identical retried create found it. + Created(CreatedCommunityRecord), + /// The host already belongs to another owner. + HostExists, + /// The intended owner already owns the maximum number of communities. + LimitReached, +} + +/// Community row returned by operator-plane ownership reads. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OwnedCommunityRecord { + /// Stable server-resolved community id. + pub id: CommunityId, + /// Normalized host that maps to this community. + pub host: String, + /// When the community row was created. + pub created_at: DateTime, + /// When the community was archived; absent while active. + pub archived_at: Option>, +} + +/// Community row returned by an owner-authorized archive operation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ArchivedCommunityRecord { + /// Stable server-resolved community id. + pub id: CommunityId, + /// Reserved canonical host. + pub host: String, + /// Durable first-archive timestamp. + pub archived_at: DateTime, +} + +/// Community row returned by an owner-authorized unarchive operation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnarchivedCommunityRecord { + /// Stable server-resolved community id. + pub id: CommunityId, + /// Reserved canonical host restored to active admission. + pub host: String, +} + +impl Db { + /// Returns the community mapped to a normalized request host, if one exists. + /// + /// The caller owns host normalization and turns `None` into the fail-closed + /// request/connection error. buzz-db only reads the durable host map. + #[datastore_span(name = "lookup_community_by_host", system = "postgresql")] + pub async fn lookup_community_by_host( + &self, + normalized_host: &str, + ) -> Result> { + let row = sqlx::query( + r#" + SELECT id, host + FROM communities + WHERE lower(host) = lower($1) + AND archived_at IS NULL + AND deleted_at IS NULL + AND deletion_state = 'active' + "#, + ) + .bind(normalized_host) + .fetch_optional(&self.pool) + .await?; + + row.map(|row| { + let id: Uuid = row.try_get("id")?; + let host: String = row.try_get("host")?; + + Ok(CommunityRecord { + id: CommunityId::from_uuid(id), + host, + }) + }) + .transpose() + } + + /// Returns whether a community id still exists in the active lifecycle state. + #[datastore_span(name = "is_community_active", system = "postgresql")] + pub async fn is_community_active(&self, community_id: CommunityId) -> Result { + let active = sqlx::query_scalar::<_, bool>( + "SELECT EXISTS(SELECT 1 FROM communities WHERE id = $1 AND archived_at IS NULL AND deleted_at IS NULL AND deletion_state = 'active')", + ) + .bind(community_id.as_uuid()) + .fetch_one(&self.pool) + .await?; + Ok(active) + } + + /// Returns a community by host regardless of lifecycle state. Operator-plane only. + #[datastore_span( + name = "lookup_community_by_host_for_management", + system = "postgresql" + )] + pub async fn lookup_community_by_host_for_management( + &self, + normalized_host: &str, + ) -> Result> { + let row = sqlx::query("SELECT id, host FROM communities WHERE lower(host) = lower($1)") + .bind(normalized_host) + .fetch_optional(&self.pool) + .await?; + row.map(|row| { + Ok(CommunityRecord { + id: CommunityId::from_uuid(row.try_get("id")?), + host: row.try_get("host")?, + }) + }) + .transpose() + } + + /// Lists communities where `owner_pubkey` currently holds the `owner` role. + /// + /// This is an operator-plane helper, not a tenant-scoped data-plane read: + /// callers must gate it on deployment-level operator auth before exposing it. + #[datastore_span(name = "list_communities_owned_by", system = "postgresql")] + pub async fn list_communities_owned_by( + &self, + owner_pubkey: &str, + ) -> Result> { + let owner_pubkey = owner_pubkey.to_ascii_lowercase(); + let rows = sqlx::query( + r#" + SELECT c.id, c.host, c.created_at, c.archived_at + FROM communities c + JOIN relay_members rm ON rm.community_id = c.id + WHERE rm.pubkey = $1 + AND rm.role = 'owner' + ORDER BY c.created_at ASC, c.host ASC + "#, + ) + .bind(owner_pubkey) + .fetch_all(&self.pool) + .await?; + + rows.into_iter() + .map(|row| { + let id: Uuid = row.try_get("id")?; + let host: String = row.try_get("host")?; + let created_at: DateTime = row.try_get("created_at")?; + let archived_at: Option> = row.try_get("archived_at")?; + Ok(OwnedCommunityRecord { + id: CommunityId::from_uuid(id), + host, + created_at, + archived_at, + }) + }) + .collect() + } + + /// Returns the normalized host mapped to a community id, if the community + /// exists. + /// + /// The reverse of [`lookup_community_by_host`]: used by side-effect + /// producers that already hold a server-resolved `CommunityId` (e.g. the + /// workflow action sink running a run owned by some community) and need a + /// fully-formed [`buzz_core::tenant::TenantContext`] — host included — to + /// fan out under *that* community rather than the deployment default. The + /// community is authoritative; the host is read back for labelling only and + /// is never used to re-derive the community. + #[datastore_span(name = "lookup_community_host", system = "postgresql")] + pub async fn lookup_community_host(&self, community_id: CommunityId) -> Result> { + let row = sqlx::query( + r#" + SELECT host + FROM communities + WHERE id = $1 + AND archived_at IS NULL + AND deleted_at IS NULL + AND deletion_state = 'active' + "#, + ) + .bind(community_id.as_uuid()) + .fetch_optional(&self.pool) + .await?; + + row.map(|row| { + let host: String = row.try_get("host")?; + Ok(host) + }) + .transpose() + } + + /// Returns the community's workspace icon (NIP-11 `icon`), if set. + /// + /// Set by relay admins/owners via the kind:9033 command; the value is + /// validated and size-capped at that write path. + #[datastore_span(name = "get_community_icon", system = "postgresql")] + pub async fn get_community_icon(&self, community_id: CommunityId) -> Result> { + let row = sqlx::query( + r#" + SELECT icon + FROM communities + WHERE id = $1 + "#, + ) + .bind(community_id.as_uuid()) + .fetch_optional(&self.pool) + .await?; + + Ok(row + .map(|row| row.try_get::, _>("icon")) + .transpose()? + .flatten() + .filter(|icon| !icon.is_empty())) + } + + /// Sets or clears (`None`) the community's workspace icon. + #[datastore_span(name = "set_community_icon", system = "postgresql")] + pub async fn set_community_icon( + &self, + community_id: CommunityId, + icon: Option<&str>, + ) -> Result<()> { + sqlx::query( + r#" + UPDATE communities + SET icon = $2 + WHERE id = $1 + "#, + ) + .bind(community_id.as_uuid()) + .bind(icon) + .execute(&self.pool) + .await?; + Ok(()) + } + + /// Ensure a configured community host exists and return its row. + /// + /// This is the startup/config seeding path for N=1 deployments. Migrations + /// create the schema only; deployment-specific hosts are not hardcoded into + /// schema history. + #[datastore_span(name = "ensure_configured_community", system = "postgresql")] + pub async fn ensure_configured_community( + &self, + normalized_host: &str, + ) -> Result { + let row = sqlx::query( + r#" + INSERT INTO communities (host) + VALUES ($1) + ON CONFLICT (lower(host)) DO UPDATE SET host = communities.host + WHERE communities.deletion_state = 'active' + AND communities.deleted_at IS NULL + RETURNING id, host, (xmax = 0) AS created + "#, + ) + .bind(normalized_host) + .fetch_optional(&self.pool) + .await? + .ok_or_else(|| { + DbError::AccessDenied(format!( + "community host {normalized_host:?} is permanently tombstoned" + )) + })?; + + let id: Uuid = row.try_get("id")?; + let host: String = row.try_get("host")?; + let created: bool = row.try_get("created")?; + + Ok(EnsuredCommunityRecord { + id: CommunityId::from_uuid(id), + host, + created, + }) + } + + /// Atomically creates a community and its initial owner. + /// + /// Holds a per-owner advisory lock while enforcing the ownership limit. + /// Identical create retries return the original record; host collisions and + /// limit failures remain distinguishable to the operator API. + #[datastore_span(name = "create_community_with_owner", system = "postgresql")] + pub async fn create_community_with_owner( + &self, + normalized_host: &str, + owner_pubkey: &str, + ) -> Result { + let owner_pubkey = owner_pubkey.to_ascii_lowercase(); + let mut tx = self.pool.begin().await?; + + // Serialize on the owner pubkey so concurrent creates to the same + // owner cannot both pass the ownership count check. + sqlx::query("SELECT pg_advisory_xact_lock($1)") + .bind(relay_members::owner_count_advisory_lock_key(&owner_pubkey)) + .execute(&mut *tx) + .await?; + + let row = sqlx::query( + r#" + INSERT INTO communities (host) + VALUES ($1) + ON CONFLICT (lower(host)) DO NOTHING + RETURNING id, host + "#, + ) + .bind(normalized_host) + .fetch_optional(&mut *tx) + .await?; + + let (id, host) = if let Some(row) = row { + let id: Uuid = row.try_get("id")?; + let host: String = row.try_get("host")?; + + // Enforce the limit before inserting the new owner row. + let owned_count: i64 = sqlx::query_scalar( + "SELECT count(*) FROM relay_members WHERE pubkey = $1 AND role = 'owner'", + ) + .bind(&owner_pubkey) + .fetch_one(&mut *tx) + .await?; + + if owned_count >= relay_members::max_communities_per_owner() { + tx.rollback().await?; + return Ok(CreateCommunityWithOwnerResult::LimitReached); + } + + sqlx::query( + "INSERT INTO relay_members (community_id, pubkey, role, added_by) VALUES ($1, $2, 'owner', NULL)", + ) + .bind(id) + .bind(&owner_pubkey) + .execute(&mut *tx) + .await?; + (id, host) + } else { + let existing = sqlx::query( + r#" + SELECT c.id, c.host + FROM communities c + JOIN relay_members rm ON rm.community_id = c.id + WHERE lower(c.host) = lower($1) + AND lower(rm.pubkey) = lower($2) + AND rm.role = 'owner' + AND c.archived_at IS NULL + AND c.deletion_state = 'active' + AND c.deleted_at IS NULL + "#, + ) + .bind(normalized_host) + .bind(&owner_pubkey) + .fetch_optional(&mut *tx) + .await?; + let Some(existing) = existing else { + tx.rollback().await?; + return Ok(CreateCommunityWithOwnerResult::HostExists); + }; + (existing.try_get("id")?, existing.try_get("host")?) + }; + + tx.commit().await?; + Ok(CreateCommunityWithOwnerResult::Created( + CreatedCommunityRecord { + id: CommunityId::from_uuid(id), + host, + }, + )) + } + + /// Idempotently archives a community when the asserted pubkey is its current owner. + #[datastore_span(name = "archive_community_owned_by", system = "postgresql")] + pub async fn archive_community_owned_by( + &self, + normalized_host: &str, + owner_pubkey: &str, + protected_deployment_host: &str, + ) -> Result> { + let row = sqlx::query( + r#"UPDATE communities c + SET archived_at = COALESCE(c.archived_at, now()) + FROM relay_members rm + WHERE lower(c.host) = lower($1) + AND rm.community_id = c.id + AND lower(rm.pubkey) = lower($2) + AND rm.role = 'owner' + AND lower(c.host) <> lower($3) + AND c.deletion_state = 'active' + AND c.deleted_at IS NULL + RETURNING c.id, c.host, c.archived_at"#, + ) + .bind(normalized_host) + .bind(owner_pubkey) + .bind(protected_deployment_host) + .fetch_optional(&self.pool) + .await?; + row.map(|row| { + Ok(ArchivedCommunityRecord { + id: CommunityId::from_uuid(row.try_get("id")?), + host: row.try_get("host")?, + archived_at: row.try_get("archived_at")?, + }) + }) + .transpose() + } + + /// Idempotently restores a community when the asserted pubkey is its current owner. + #[datastore_span(name = "unarchive_community_owned_by", system = "postgresql")] + pub async fn unarchive_community_owned_by( + &self, + normalized_host: &str, + owner_pubkey: &str, + ) -> Result> { + let row = sqlx::query( + r#"UPDATE communities c + SET archived_at = NULL + FROM relay_members rm + WHERE lower(c.host) = lower($1) + AND rm.community_id = c.id + AND lower(rm.pubkey) = lower($2) + AND rm.role = 'owner' + AND c.deletion_state = 'active' + AND c.deleted_at IS NULL + RETURNING c.id, c.host"#, + ) + .bind(normalized_host) + .bind(owner_pubkey) + .fetch_optional(&self.pool) + .await?; + row.map(|row| { + Ok(UnarchivedCommunityRecord { + id: CommunityId::from_uuid(row.try_get("id")?), + host: row.try_get("host")?, + }) + }) + .transpose() + } + + /// Returns the community that owns a channel, if the channel exists. + /// + /// Internal relay producers use this to derive tenant context from the row + /// they are acting on, rather than falling back to an implicit default. + #[datastore_span(name = "community_of_channel", system = "postgresql")] + pub async fn community_of_channel(&self, channel_id: Uuid) -> Result> { + let row = sqlx::query( + r#" + SELECT community_id + FROM channels + WHERE id = $1 + AND deleted_at IS NULL + "#, + ) + .bind(channel_id) + .fetch_optional(&self.pool) + .await?; + + row.map(|row| { + let id: Uuid = row.try_get("community_id")?; + Ok(CommunityId::from_uuid(id)) + }) + .transpose() + } + + /// Batched version of [`Self::community_of_channel`]: given a list of + /// channel UUIDs, returns a map from channel id → owning community + /// for every channel that exists (soft-deletes excluded). + /// + /// Used by the runtime conformance read-seam emitters in `buzz-relay`: + /// after a `query_events`/`get_events_by_ids` returns N rows, the + /// emitter collects distinct `channel_id`s, calls this once, then + /// projects each row's true community label independently of the + /// fetch query's WHERE clause. That independence is what makes the + /// `Inv_NonInterference` / `Inv_ReadConfinement` gate non-vacuous — + /// a mutation that dropped `community_id = $X` from the fetch query + /// would still let this helper return the row's true label, and the + /// checker would see the mismatch. + /// + /// Channels missing from the result map (deleted or never existed) + /// are intentionally not present rather than mapped to a default — + /// callers MUST treat "channel-id not in map" as a coverage breach, + /// never as "use the resolved community". + #[datastore_span(name = "communities_of_channels", system = "postgresql")] + pub async fn communities_of_channels( + &self, + channel_ids: &[Uuid], + ) -> Result> { + if channel_ids.is_empty() { + return Ok(std::collections::HashMap::new()); + } + let rows = sqlx::query( + r#" + SELECT id, community_id + FROM channels + WHERE id = ANY($1) + AND deleted_at IS NULL + "#, + ) + .bind(channel_ids) + .fetch_all(&self.pool) + .await?; + + let mut out = std::collections::HashMap::with_capacity(rows.len()); + for row in rows { + let ch: Uuid = row.try_get("id")?; + let cm: Uuid = row.try_get("community_id")?; + out.insert(ch, CommunityId::from_uuid(cm)); + } + Ok(out) + } +} + +#[cfg(test)] +mod tests { + //! Pin the load-bearing contract for `Db::communities_of_channels`: + //! a channel id that does NOT exist MUST be absent from the result + //! map, never mapped to a default. The relay-side read-row emitter + //! relies on this — a missing entry triggers `MissingLookup → + //! ImplBug{row_community_lookup_missing} → CoverageBreach`. If this + //! helper ever started returning a default/zero entry for unknown + //! channels, that fail-closed chain would go blind. + use super::*; + use sqlx::PgPool; + + const TEST_DB_URL: &str = "postgres://buzz:buzz_dev@localhost:5432/buzz"; // sadscan:disable np.postgres.1 + + async fn setup_db() -> Db { + let database_url = + std::env::var("TEST_DATABASE_URL").unwrap_or_else(|_| TEST_DB_URL.into()); + let pool = PgPool::connect(&database_url) + .await + .expect("connect to test DB"); + Db::from_pool(pool) + } + + async fn make_community(pool: &PgPool) -> Uuid { + let id = Uuid::new_v4(); + let host = format!("communities-of-channels-{}.example", id.simple()); + sqlx::query("INSERT INTO communities (id, host) VALUES ($1, $2)") + .bind(id) + .bind(host) + .execute(pool) + .await + .expect("insert community"); + id + } + + async fn insert_channel(pool: &PgPool, community_id: Uuid, channel_id: Uuid) { + let creator: Vec = vec![0u8; 32]; + sqlx::query( + r#" + INSERT INTO channels + (id, community_id, name, channel_type, visibility, created_by) + VALUES + ($1, $2, $3, 'stream'::channel_type, 'open'::channel_visibility, $4) + "#, + ) + .bind(channel_id) + .bind(community_id) + .bind(format!("ch-{}", channel_id.simple())) + .bind(&creator) + .execute(pool) + .await + .expect("insert channel"); + } + + #[test] + fn community_implementation_tests_and_spans_have_single_owners() { + let community_source = include_str!("community.rs"); + let lib_source = include_str!("lib.rs"); + let operations = [ + "lookup_community_by_host", + "is_community_active", + "lookup_community_by_host_for_management", + "list_communities_owned_by", + "lookup_community_host", + "get_community_icon", + "set_community_icon", + "ensure_configured_community", + "create_community_with_owner", + "archive_community_owned_by", + "unarchive_community_owned_by", + "community_of_channel", + "communities_of_channels", + ]; + for operation in operations { + let method = format!("pub async fn {operation}("); + assert_eq!( + community_source.matches(&method).count(), + 1, + "{operation} implementation must live exactly once in community.rs", + ); + assert!( + !lib_source.contains(&method), + "{operation} implementation must not remain in lib.rs", + ); + + let span = format!("name = \"{operation}\""); + assert_eq!( + community_source.matches(&span).count(), + 1, + "{operation} must have exactly one datastore span", + ); + assert!( + !lib_source.contains(&span), + "{operation} datastore span must not remain in lib.rs", + ); + } + + let records = [ + "CommunityRecord", + "EnsuredCommunityRecord", + "CreatedCommunityRecord", + "OwnedCommunityRecord", + "ArchivedCommunityRecord", + "UnarchivedCommunityRecord", + ]; + for record in records { + let declaration = format!("pub struct {record}"); + assert_eq!(community_source.matches(&declaration).count(), 1); + assert!(!lib_source.contains(&declaration)); + } + let result_declaration = format!("pub {} {}", "enum", "CreateCommunityWithOwnerResult"); + assert_eq!(community_source.matches(&result_declaration).count(), 1); + assert!(!lib_source.contains(&result_declaration)); + + let moved_tests = [ + "lookup_community_by_host_matches_case_insensitive_host_index", + "create_community_with_owner_is_atomic_and_create_only", + "unarchive_community_owned_by_restores_admission_idempotently", + "create_community_with_owner_enforces_per_owner_limit", + "concurrent_same_owner_create_returns_the_winning_row_to_both_callers", + "ensure_configured_community_reports_insert_winner", + "list_communities_owned_by_returns_only_owner_rows", + "communities_of_channels_present_for_existing_absent_for_missing", + ]; + for test in moved_tests { + let declaration = format!("async fn {test}"); + assert_eq!(community_source.matches(&declaration).count(), 1); + assert!(!lib_source.contains(&declaration)); + } + } + + #[tokio::test] + #[ignore = "requires Postgres"] + async fn lookup_community_by_host_matches_case_insensitive_host_index() { + let db = setup_db().await; + let id = Uuid::new_v4(); + let lower_host = format!("lookup-community-{}.example", id.simple()); + let stored_host = lower_host.to_uppercase(); + + sqlx::query("INSERT INTO communities (id, host) VALUES ($1, $2)") + .bind(id) + .bind(&stored_host) + .execute(&db.pool) + .await + .expect("insert mixed-case community host"); + + let found = db + .lookup_community_by_host(&lower_host) + .await + .expect("lookup lower-case host") + .expect("community found by lower-case host"); + assert_eq!(found.id, CommunityId::from_uuid(id)); + assert_eq!(found.host, stored_host); + + let found = db + .lookup_community_by_host(&stored_host) + .await + .expect("lookup stored-case host") + .expect("community found by stored-case host"); + assert_eq!(found.id, CommunityId::from_uuid(id)); + } + + #[tokio::test] + #[ignore = "requires Postgres"] + async fn create_community_with_owner_is_atomic_and_create_only() { + let db = setup_db().await; + let host = format!("create-only-{}.example", Uuid::new_v4().simple()); + let owner = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let other = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + + let created = db + .create_community_with_owner(&host, owner) + .await + .expect("create community"); + let CreateCommunityWithOwnerResult::Created(created) = created else { + panic!("expected new community"); + }; + assert_eq!(created.host, host); + let owner_role: Option = sqlx::query_scalar( + "SELECT role FROM relay_members WHERE community_id = $1 AND pubkey = $2", + ) + .bind(created.id.as_uuid()) + .bind(owner) + .fetch_optional(&db.pool) + .await + .expect("owner role"); + assert_eq!(owner_role.as_deref(), Some("owner")); + + let retry = db + .create_community_with_owner(&host.to_ascii_uppercase(), owner) + .await + .expect("same-owner retry"); + assert_eq!( + retry, + CreateCommunityWithOwnerResult::Created(created.clone()), + "retry returns the original row" + ); + + let collision = db + .create_community_with_owner(&host, other) + .await + .expect("collision result"); + assert_eq!(collision, CreateCommunityWithOwnerResult::HostExists); + let roles: Vec<(String, String)> = sqlx::query_as( + "SELECT pubkey, role FROM relay_members WHERE community_id = $1 ORDER BY pubkey", + ) + .bind(created.id.as_uuid()) + .fetch_all(&db.pool) + .await + .expect("community roles"); + assert_eq!(roles, vec![(owner.to_string(), "owner".to_string())]); + + db.bootstrap_owner(created.id, other) + .await + .expect("rotate owner"); + let post_rotation_retry = db + .create_community_with_owner(&host, owner) + .await + .expect("post-rotation retry"); + assert_eq!( + post_rotation_retry, + CreateCommunityWithOwnerResult::HostExists + ); + } + + #[tokio::test] + #[ignore = "requires Postgres"] + async fn unarchive_community_owned_by_restores_admission_idempotently() { + let db = setup_db().await; + let host = format!("unarchive-{}.example", Uuid::new_v4().simple()); + let owner = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple()); + let outsider = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple()); + let created = db + .create_community_with_owner(&host, &owner) + .await + .expect("create community"); + let CreateCommunityWithOwnerResult::Created(created) = created else { + panic!("expected new community"); + }; + + let archived = db + .archive_community_owned_by(&host, &owner, "protected.example") + .await + .expect("archive community") + .expect("owned community"); + assert_eq!(archived.id, created.id); + assert!( + db.lookup_community_by_host(&host) + .await + .expect("active lookup") + .is_none(), + "archived communities must fail admission" + ); + assert!(db + .unarchive_community_owned_by(&host, &outsider) + .await + .expect("wrong-owner unarchive") + .is_none()); + assert!(db + .unarchive_community_owned_by("missing.example", &owner) + .await + .expect("unknown-host unarchive") + .is_none()); + + let restored = db + .unarchive_community_owned_by(&host.to_ascii_uppercase(), &owner) + .await + .expect("unarchive community") + .expect("owned community"); + assert_eq!(restored.id, created.id); + assert_eq!(restored.host, host); + assert_eq!( + db.lookup_community_by_host(&host) + .await + .expect("restored lookup") + .expect("active community") + .id, + created.id + ); + assert_eq!( + db.get_relay_member(created.id, &owner) + .await + .expect("owner lookup") + .expect("owner remains") + .role, + "owner" + ); + + let retry = db + .unarchive_community_owned_by(&host, &owner) + .await + .expect("idempotent retry") + .expect("owned community"); + assert_eq!(retry, restored); + } + + #[tokio::test] + #[ignore = "requires Postgres"] + async fn create_community_with_owner_enforces_per_owner_limit() { + let db = setup_db().await; + let owner = format!("{:064x}", Uuid::new_v4().as_u128()); + + // Create 3 communities for this owner (the max). + for i in 0..3 { + let host = format!("limit-test-{}-{}.example", i, Uuid::new_v4().simple()); + assert!(matches!( + db.create_community_with_owner(&host, &owner) + .await + .expect("create community"), + CreateCommunityWithOwnerResult::Created(_) + )); + } + + let host = format!("limit-test-3-{}.example", Uuid::new_v4().simple()); + assert_eq!( + db.create_community_with_owner(&host, &owner) + .await + .expect("create community call"), + CreateCommunityWithOwnerResult::LimitReached + ); + assert!( + db.lookup_community_by_host(&host) + .await + .expect("look up rolled-back fresh host") + .is_none(), + "limit rejection must roll back the fresh community row" + ); + } + + #[tokio::test] + #[ignore = "requires Postgres"] + async fn concurrent_same_owner_create_returns_the_winning_row_to_both_callers() { + let db = setup_db().await; + let host = format!("concurrent-create-{}.example", Uuid::new_v4().simple()); + let owner = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + let (first, second) = tokio::join!( + db.create_community_with_owner(&host, owner), + db.create_community_with_owner(&host, owner), + ); + let first = first.expect("first concurrent create"); + let second = second.expect("second concurrent create"); + + assert!(matches!(first, CreateCommunityWithOwnerResult::Created(_))); + assert_eq!(first, second, "conflict loser re-reads the winning row"); + } + + #[tokio::test] + #[ignore = "requires Postgres"] + async fn ensure_configured_community_reports_insert_winner() { + let db = setup_db().await; + let host = format!("ensure-community-{}.example", Uuid::new_v4().simple()); + + let first = db + .ensure_configured_community(&host) + .await + .expect("first ensure"); + assert!(first.created, "first ensure should report created"); + assert_eq!(first.host, host); + + let second = db + .ensure_configured_community(&host) + .await + .expect("second ensure"); + assert!(!second.created, "second ensure should report existed"); + assert_eq!(second.id, first.id); + assert_eq!(second.host, host); + } + + #[tokio::test] + #[ignore = "requires Postgres"] + async fn list_communities_owned_by_returns_only_owner_rows() { + let db = setup_db().await; + let community_a = CommunityId::from_uuid(make_community(&db.pool).await); + let community_b = CommunityId::from_uuid(make_community(&db.pool).await); + let community_c = CommunityId::from_uuid(make_community(&db.pool).await); + // Unique per run: `list_communities_owned_by` is keyed only by pubkey, + // so a shared fixed pubkey picks up communities leaked by sibling + // ignored tests running against the same database. + let owner = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple()); + let owner = owner.as_str(); + let other = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple()); + let other = other.as_str(); + + db.bootstrap_owner(community_a, owner) + .await + .expect("owner A"); + db.bootstrap_owner(community_b, other) + .await + .expect("other owner B"); + db.add_relay_member(community_c, owner, "admin", None) + .await + .expect("admin C"); + + let owned = db + .list_communities_owned_by(owner) + .await + .expect("list owned communities"); + + assert_eq!(owned.len(), 1); + assert_eq!(owned[0].id, community_a); + } + + #[tokio::test] + #[ignore = "requires Postgres"] + async fn communities_of_channels_present_for_existing_absent_for_missing() { + let db = setup_db().await; + let community = make_community(&db.pool).await; + let existing = Uuid::new_v4(); + insert_channel(&db.pool, community, existing).await; + + // Channel that is NOT inserted — the load-bearing case. + let missing = Uuid::new_v4(); + + let result = db + .communities_of_channels(&[existing, missing]) + .await + .expect("communities_of_channels"); + + // (1) Existing channel → present with its true community. + assert_eq!( + result.get(&existing).copied(), + Some(CommunityId::from_uuid(community)), + "existing channel must map to its true community", + ); + + // (2) Missing channel → ABSENT from the map (never defaulted). + // This is the contract the relay-side `MissingLookup → ImplBug` + // fail-closed guard-rail depends on. If this assertion ever + // weakens to `result.get(&missing) != Some(community)`, the + // mutate-bite below stops biting. + assert!( + !result.contains_key(&missing), + "missing channel must be absent from the result map, got {:?}", + result.get(&missing), + ); + + // (3) Map size matches: exactly one entry, the existing one. + assert_eq!( + result.len(), + 1, + "result map must contain only existing channels" + ); + } +} diff --git a/crates/buzz-db/src/lib.rs b/crates/buzz-db/src/lib.rs index cbce6d3753..a5c03a256e 100644 --- a/crates/buzz-db/src/lib.rs +++ b/crates/buzz-db/src/lib.rs @@ -32,6 +32,8 @@ pub mod api_token; pub mod archived_identities; /// Channel and membership persistence. pub mod channel; +/// Community lifecycle and host-map persistence. +pub mod community; /// Durable whole-community deletion lifecycle and PostgreSQL adapter. pub mod deletion; /// Direct message channel persistence. @@ -73,6 +75,11 @@ pub mod user; /// Workflow, run, and approval persistence. pub mod workflow; +pub use community::{ + ArchivedCommunityRecord, CommunityRecord, CreateCommunityWithOwnerResult, + CreatedCommunityRecord, EnsuredCommunityRecord, OwnedCommunityRecord, + UnarchivedCommunityRecord, +}; pub use error::{DbError, Result}; pub use event::{EventQuery, ReactionEventInsertOutcome, DEFAULT_MAX_PAGE_LIMIT}; @@ -566,79 +573,6 @@ impl Default for DbConfig { } } -/// Community host-map row returned by [`Db::lookup_community_by_host`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CommunityRecord { - /// Stable server-resolved community id. - pub id: CommunityId, - /// Normalized host that maps to this community. - pub host: String, -} - -/// Community row returned by idempotent community ensure/create operations. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EnsuredCommunityRecord { - /// Stable server-resolved community id. - pub id: CommunityId, - /// Normalized host that maps to this community. - pub host: String, - /// True only when this call inserted the `communities` row. - pub created: bool, -} - -/// Community row returned by an atomic create-with-owner operation. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CreatedCommunityRecord { - /// Stable server-resolved community id. - pub id: CommunityId, - /// Normalized host stored for the community. - pub host: String, -} - -/// Result of atomically creating a community with its initial owner. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CreateCommunityWithOwnerResult { - /// The community was created, or an identical retried create found it. - Created(CreatedCommunityRecord), - /// The host already belongs to another owner. - HostExists, - /// The intended owner already owns the maximum number of communities. - LimitReached, -} - -/// Community row returned by operator-plane ownership reads. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct OwnedCommunityRecord { - /// Stable server-resolved community id. - pub id: CommunityId, - /// Normalized host that maps to this community. - pub host: String, - /// When the community row was created. - pub created_at: DateTime, - /// When the community was archived; absent while active. - pub archived_at: Option>, -} - -/// Community row returned by an owner-authorized archive operation. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ArchivedCommunityRecord { - /// Stable server-resolved community id. - pub id: CommunityId, - /// Reserved canonical host. - pub host: String, - /// Durable first-archive timestamp. - pub archived_at: DateTime, -} - -/// Community row returned by an owner-authorized unarchive operation. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct UnarchivedCommunityRecord { - /// Stable server-resolved community id. - pub id: CommunityId, - /// Reserved canonical host restored to active admission. - pub host: String, -} - /// Token summary returned by [`Db::list_active_tokens`]. #[derive(Debug, Clone)] pub struct TokenSummary { @@ -1246,465 +1180,6 @@ impl Db { self.pool.begin().await.map_err(Into::into) } - /// Returns the community mapped to a normalized request host, if one exists. - /// - /// The caller owns host normalization and turns `None` into the fail-closed - /// request/connection error. buzz-db only reads the durable host map. - #[datastore_span(name = "lookup_community_by_host", system = "postgresql")] - pub async fn lookup_community_by_host( - &self, - normalized_host: &str, - ) -> Result> { - let row = sqlx::query( - r#" - SELECT id, host - FROM communities - WHERE lower(host) = lower($1) - AND archived_at IS NULL - AND deleted_at IS NULL - AND deletion_state = 'active' - "#, - ) - .bind(normalized_host) - .fetch_optional(&self.pool) - .await?; - - row.map(|row| { - let id: Uuid = row.try_get("id")?; - let host: String = row.try_get("host")?; - - Ok(CommunityRecord { - id: CommunityId::from_uuid(id), - host, - }) - }) - .transpose() - } - - /// Returns whether a community id still exists in the active lifecycle state. - #[datastore_span(name = "is_community_active", system = "postgresql")] - pub async fn is_community_active(&self, community_id: CommunityId) -> Result { - let active = sqlx::query_scalar::<_, bool>( - "SELECT EXISTS(SELECT 1 FROM communities WHERE id = $1 AND archived_at IS NULL AND deleted_at IS NULL AND deletion_state = 'active')", - ) - .bind(community_id.as_uuid()) - .fetch_one(&self.pool) - .await?; - Ok(active) - } - - /// Returns a community by host regardless of lifecycle state. Operator-plane only. - #[datastore_span( - name = "lookup_community_by_host_for_management", - system = "postgresql" - )] - pub async fn lookup_community_by_host_for_management( - &self, - normalized_host: &str, - ) -> Result> { - let row = sqlx::query("SELECT id, host FROM communities WHERE lower(host) = lower($1)") - .bind(normalized_host) - .fetch_optional(&self.pool) - .await?; - row.map(|row| { - Ok(CommunityRecord { - id: CommunityId::from_uuid(row.try_get("id")?), - host: row.try_get("host")?, - }) - }) - .transpose() - } - - /// Lists communities where `owner_pubkey` currently holds the `owner` role. - /// - /// This is an operator-plane helper, not a tenant-scoped data-plane read: - /// callers must gate it on deployment-level operator auth before exposing it. - #[datastore_span(name = "list_communities_owned_by", system = "postgresql")] - pub async fn list_communities_owned_by( - &self, - owner_pubkey: &str, - ) -> Result> { - let owner_pubkey = owner_pubkey.to_ascii_lowercase(); - let rows = sqlx::query( - r#" - SELECT c.id, c.host, c.created_at, c.archived_at - FROM communities c - JOIN relay_members rm ON rm.community_id = c.id - WHERE rm.pubkey = $1 - AND rm.role = 'owner' - ORDER BY c.created_at ASC, c.host ASC - "#, - ) - .bind(owner_pubkey) - .fetch_all(&self.pool) - .await?; - - rows.into_iter() - .map(|row| { - let id: Uuid = row.try_get("id")?; - let host: String = row.try_get("host")?; - let created_at: DateTime = row.try_get("created_at")?; - let archived_at: Option> = row.try_get("archived_at")?; - Ok(OwnedCommunityRecord { - id: CommunityId::from_uuid(id), - host, - created_at, - archived_at, - }) - }) - .collect() - } - - /// Returns the normalized host mapped to a community id, if the community - /// exists. - /// - /// The reverse of [`lookup_community_by_host`]: used by side-effect - /// producers that already hold a server-resolved `CommunityId` (e.g. the - /// workflow action sink running a run owned by some community) and need a - /// fully-formed [`buzz_core::tenant::TenantContext`] — host included — to - /// fan out under *that* community rather than the deployment default. The - /// community is authoritative; the host is read back for labelling only and - /// is never used to re-derive the community. - #[datastore_span(name = "lookup_community_host", system = "postgresql")] - pub async fn lookup_community_host(&self, community_id: CommunityId) -> Result> { - let row = sqlx::query( - r#" - SELECT host - FROM communities - WHERE id = $1 - AND archived_at IS NULL - AND deleted_at IS NULL - AND deletion_state = 'active' - "#, - ) - .bind(community_id.as_uuid()) - .fetch_optional(&self.pool) - .await?; - - row.map(|row| { - let host: String = row.try_get("host")?; - Ok(host) - }) - .transpose() - } - - /// Returns the community's workspace icon (NIP-11 `icon`), if set. - /// - /// Set by relay admins/owners via the kind:9033 command; the value is - /// validated and size-capped at that write path. - #[datastore_span(name = "get_community_icon", system = "postgresql")] - pub async fn get_community_icon(&self, community_id: CommunityId) -> Result> { - let row = sqlx::query( - r#" - SELECT icon - FROM communities - WHERE id = $1 - "#, - ) - .bind(community_id.as_uuid()) - .fetch_optional(&self.pool) - .await?; - - Ok(row - .map(|row| row.try_get::, _>("icon")) - .transpose()? - .flatten() - .filter(|icon| !icon.is_empty())) - } - - /// Sets or clears (`None`) the community's workspace icon. - #[datastore_span(name = "set_community_icon", system = "postgresql")] - pub async fn set_community_icon( - &self, - community_id: CommunityId, - icon: Option<&str>, - ) -> Result<()> { - sqlx::query( - r#" - UPDATE communities - SET icon = $2 - WHERE id = $1 - "#, - ) - .bind(community_id.as_uuid()) - .bind(icon) - .execute(&self.pool) - .await?; - Ok(()) - } - - /// Ensure a configured community host exists and return its row. - /// - /// This is the startup/config seeding path for N=1 deployments. Migrations - /// create the schema only; deployment-specific hosts are not hardcoded into - /// schema history. - #[datastore_span(name = "ensure_configured_community", system = "postgresql")] - pub async fn ensure_configured_community( - &self, - normalized_host: &str, - ) -> Result { - let row = sqlx::query( - r#" - INSERT INTO communities (host) - VALUES ($1) - ON CONFLICT (lower(host)) DO UPDATE SET host = communities.host - WHERE communities.deletion_state = 'active' - AND communities.deleted_at IS NULL - RETURNING id, host, (xmax = 0) AS created - "#, - ) - .bind(normalized_host) - .fetch_optional(&self.pool) - .await? - .ok_or_else(|| { - DbError::AccessDenied(format!( - "community host {normalized_host:?} is permanently tombstoned" - )) - })?; - - let id: Uuid = row.try_get("id")?; - let host: String = row.try_get("host")?; - let created: bool = row.try_get("created")?; - - Ok(EnsuredCommunityRecord { - id: CommunityId::from_uuid(id), - host, - created, - }) - } - - /// Atomically creates a community and its initial owner. - /// - /// Holds a per-owner advisory lock while enforcing the ownership limit. - /// Identical create retries return the original record; host collisions and - /// limit failures remain distinguishable to the operator API. - #[datastore_span(name = "create_community_with_owner", system = "postgresql")] - pub async fn create_community_with_owner( - &self, - normalized_host: &str, - owner_pubkey: &str, - ) -> Result { - let owner_pubkey = owner_pubkey.to_ascii_lowercase(); - let mut tx = self.pool.begin().await?; - - // Serialize on the owner pubkey so concurrent creates to the same - // owner cannot both pass the ownership count check. - sqlx::query("SELECT pg_advisory_xact_lock($1)") - .bind(relay_members::owner_count_advisory_lock_key(&owner_pubkey)) - .execute(&mut *tx) - .await?; - - let row = sqlx::query( - r#" - INSERT INTO communities (host) - VALUES ($1) - ON CONFLICT (lower(host)) DO NOTHING - RETURNING id, host - "#, - ) - .bind(normalized_host) - .fetch_optional(&mut *tx) - .await?; - - let (id, host) = if let Some(row) = row { - let id: Uuid = row.try_get("id")?; - let host: String = row.try_get("host")?; - - // Enforce the limit before inserting the new owner row. - let owned_count: i64 = sqlx::query_scalar( - "SELECT count(*) FROM relay_members WHERE pubkey = $1 AND role = 'owner'", - ) - .bind(&owner_pubkey) - .fetch_one(&mut *tx) - .await?; - - if owned_count >= relay_members::max_communities_per_owner() { - tx.rollback().await?; - return Ok(CreateCommunityWithOwnerResult::LimitReached); - } - - sqlx::query( - "INSERT INTO relay_members (community_id, pubkey, role, added_by) VALUES ($1, $2, 'owner', NULL)", - ) - .bind(id) - .bind(&owner_pubkey) - .execute(&mut *tx) - .await?; - (id, host) - } else { - let existing = sqlx::query( - r#" - SELECT c.id, c.host - FROM communities c - JOIN relay_members rm ON rm.community_id = c.id - WHERE lower(c.host) = lower($1) - AND lower(rm.pubkey) = lower($2) - AND rm.role = 'owner' - AND c.archived_at IS NULL - AND c.deletion_state = 'active' - AND c.deleted_at IS NULL - "#, - ) - .bind(normalized_host) - .bind(&owner_pubkey) - .fetch_optional(&mut *tx) - .await?; - let Some(existing) = existing else { - tx.rollback().await?; - return Ok(CreateCommunityWithOwnerResult::HostExists); - }; - (existing.try_get("id")?, existing.try_get("host")?) - }; - - tx.commit().await?; - Ok(CreateCommunityWithOwnerResult::Created( - CreatedCommunityRecord { - id: CommunityId::from_uuid(id), - host, - }, - )) - } - - /// Idempotently archives a community when the asserted pubkey is its current owner. - #[datastore_span(name = "archive_community_owned_by", system = "postgresql")] - pub async fn archive_community_owned_by( - &self, - normalized_host: &str, - owner_pubkey: &str, - protected_deployment_host: &str, - ) -> Result> { - let row = sqlx::query( - r#"UPDATE communities c - SET archived_at = COALESCE(c.archived_at, now()) - FROM relay_members rm - WHERE lower(c.host) = lower($1) - AND rm.community_id = c.id - AND lower(rm.pubkey) = lower($2) - AND rm.role = 'owner' - AND lower(c.host) <> lower($3) - AND c.deletion_state = 'active' - AND c.deleted_at IS NULL - RETURNING c.id, c.host, c.archived_at"#, - ) - .bind(normalized_host) - .bind(owner_pubkey) - .bind(protected_deployment_host) - .fetch_optional(&self.pool) - .await?; - row.map(|row| { - Ok(ArchivedCommunityRecord { - id: CommunityId::from_uuid(row.try_get("id")?), - host: row.try_get("host")?, - archived_at: row.try_get("archived_at")?, - }) - }) - .transpose() - } - - /// Idempotently restores a community when the asserted pubkey is its current owner. - #[datastore_span(name = "unarchive_community_owned_by", system = "postgresql")] - pub async fn unarchive_community_owned_by( - &self, - normalized_host: &str, - owner_pubkey: &str, - ) -> Result> { - let row = sqlx::query( - r#"UPDATE communities c - SET archived_at = NULL - FROM relay_members rm - WHERE lower(c.host) = lower($1) - AND rm.community_id = c.id - AND lower(rm.pubkey) = lower($2) - AND rm.role = 'owner' - AND c.deletion_state = 'active' - AND c.deleted_at IS NULL - RETURNING c.id, c.host"#, - ) - .bind(normalized_host) - .bind(owner_pubkey) - .fetch_optional(&self.pool) - .await?; - row.map(|row| { - Ok(UnarchivedCommunityRecord { - id: CommunityId::from_uuid(row.try_get("id")?), - host: row.try_get("host")?, - }) - }) - .transpose() - } - - /// Returns the community that owns a channel, if the channel exists. - /// - /// Internal relay producers use this to derive tenant context from the row - /// they are acting on, rather than falling back to an implicit default. - #[datastore_span(name = "community_of_channel", system = "postgresql")] - pub async fn community_of_channel(&self, channel_id: Uuid) -> Result> { - let row = sqlx::query( - r#" - SELECT community_id - FROM channels - WHERE id = $1 - AND deleted_at IS NULL - "#, - ) - .bind(channel_id) - .fetch_optional(&self.pool) - .await?; - - row.map(|row| { - let id: Uuid = row.try_get("community_id")?; - Ok(CommunityId::from_uuid(id)) - }) - .transpose() - } - - /// Batched version of [`Self::community_of_channel`]: given a list of - /// channel UUIDs, returns a map from channel id → owning community - /// for every channel that exists (soft-deletes excluded). - /// - /// Used by the runtime conformance read-seam emitters in `buzz-relay`: - /// after a `query_events`/`get_events_by_ids` returns N rows, the - /// emitter collects distinct `channel_id`s, calls this once, then - /// projects each row's true community label independently of the - /// fetch query's WHERE clause. That independence is what makes the - /// `Inv_NonInterference` / `Inv_ReadConfinement` gate non-vacuous — - /// a mutation that dropped `community_id = $X` from the fetch query - /// would still let this helper return the row's true label, and the - /// checker would see the mismatch. - /// - /// Channels missing from the result map (deleted or never existed) - /// are intentionally not present rather than mapped to a default — - /// callers MUST treat "channel-id not in map" as a coverage breach, - /// never as "use the resolved community". - #[datastore_span(name = "communities_of_channels", system = "postgresql")] - pub async fn communities_of_channels( - &self, - channel_ids: &[Uuid], - ) -> Result> { - if channel_ids.is_empty() { - return Ok(std::collections::HashMap::new()); - } - let rows = sqlx::query( - r#" - SELECT id, community_id - FROM channels - WHERE id = ANY($1) - AND deleted_at IS NULL - "#, - ) - .bind(channel_ids) - .fetch_all(&self.pool) - .await?; - - let mut out = std::collections::HashMap::with_capacity(rows.len()); - for row in rows { - let ch: Uuid = row.try_get("id")?; - let cm: Uuid = row.try_get("community_id")?; - out.insert(ch, CommunityId::from_uuid(cm)); - } - Ok(out) - } - /// Inserts an event. Returns `(StoredEvent, was_inserted)` — `false` on duplicate. #[datastore_span(name = "insert_event", system = "postgresql")] pub async fn insert_event( @@ -5239,13 +4714,6 @@ fn parse_api_token_row(row: sqlx::postgres::PgRow) -> Result { #[cfg(test)] mod tests { - //! Pin the load-bearing contract for `Db::communities_of_channels`: - //! a channel id that does NOT exist MUST be absent from the result - //! map, never mapped to a default. The relay-side read-row emitter - //! relies on this — a missing entry triggers `MissingLookup → - //! ImplBug{row_community_lookup_missing} → CoverageBreach`. If this - //! helper ever started returning a default/zero entry for unknown - //! channels, that fail-closed chain would go blind. use super::*; use buzz_core::CommunityId; use sqlx::postgres::PgPoolOptions; @@ -6962,297 +6430,6 @@ mod tests { drop_scratch_db(&admin, pool, &scratch_name).await; } - #[tokio::test] - #[ignore = "requires Postgres"] - async fn lookup_community_by_host_matches_case_insensitive_host_index() { - let db = setup_db().await; - let id = Uuid::new_v4(); - let lower_host = format!("lookup-community-{}.example", id.simple()); - let stored_host = lower_host.to_uppercase(); - - sqlx::query("INSERT INTO communities (id, host) VALUES ($1, $2)") - .bind(id) - .bind(&stored_host) - .execute(&db.pool) - .await - .expect("insert mixed-case community host"); - - let found = db - .lookup_community_by_host(&lower_host) - .await - .expect("lookup lower-case host") - .expect("community found by lower-case host"); - assert_eq!(found.id, CommunityId::from_uuid(id)); - assert_eq!(found.host, stored_host); - - let found = db - .lookup_community_by_host(&stored_host) - .await - .expect("lookup stored-case host") - .expect("community found by stored-case host"); - assert_eq!(found.id, CommunityId::from_uuid(id)); - } - - #[tokio::test] - #[ignore = "requires Postgres"] - async fn create_community_with_owner_is_atomic_and_create_only() { - let db = setup_db().await; - let host = format!("create-only-{}.example", Uuid::new_v4().simple()); - let owner = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let other = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; - - let created = db - .create_community_with_owner(&host, owner) - .await - .expect("create community"); - let CreateCommunityWithOwnerResult::Created(created) = created else { - panic!("expected new community"); - }; - assert_eq!(created.host, host); - let owner_role: Option = sqlx::query_scalar( - "SELECT role FROM relay_members WHERE community_id = $1 AND pubkey = $2", - ) - .bind(created.id.as_uuid()) - .bind(owner) - .fetch_optional(&db.pool) - .await - .expect("owner role"); - assert_eq!(owner_role.as_deref(), Some("owner")); - - let retry = db - .create_community_with_owner(&host.to_ascii_uppercase(), owner) - .await - .expect("same-owner retry"); - assert_eq!( - retry, - CreateCommunityWithOwnerResult::Created(created.clone()), - "retry returns the original row" - ); - - let collision = db - .create_community_with_owner(&host, other) - .await - .expect("collision result"); - assert_eq!(collision, CreateCommunityWithOwnerResult::HostExists); - let roles: Vec<(String, String)> = sqlx::query_as( - "SELECT pubkey, role FROM relay_members WHERE community_id = $1 ORDER BY pubkey", - ) - .bind(created.id.as_uuid()) - .fetch_all(&db.pool) - .await - .expect("community roles"); - assert_eq!(roles, vec![(owner.to_string(), "owner".to_string())]); - - db.bootstrap_owner(created.id, other) - .await - .expect("rotate owner"); - let post_rotation_retry = db - .create_community_with_owner(&host, owner) - .await - .expect("post-rotation retry"); - assert_eq!( - post_rotation_retry, - CreateCommunityWithOwnerResult::HostExists - ); - } - - #[tokio::test] - #[ignore = "requires Postgres"] - async fn unarchive_community_owned_by_restores_admission_idempotently() { - let db = setup_db().await; - let host = format!("unarchive-{}.example", Uuid::new_v4().simple()); - let owner = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple()); - let outsider = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple()); - let created = db - .create_community_with_owner(&host, &owner) - .await - .expect("create community"); - let CreateCommunityWithOwnerResult::Created(created) = created else { - panic!("expected new community"); - }; - - let archived = db - .archive_community_owned_by(&host, &owner, "protected.example") - .await - .expect("archive community") - .expect("owned community"); - assert_eq!(archived.id, created.id); - assert!( - db.lookup_community_by_host(&host) - .await - .expect("active lookup") - .is_none(), - "archived communities must fail admission" - ); - assert!(db - .unarchive_community_owned_by(&host, &outsider) - .await - .expect("wrong-owner unarchive") - .is_none()); - assert!(db - .unarchive_community_owned_by("missing.example", &owner) - .await - .expect("unknown-host unarchive") - .is_none()); - - let restored = db - .unarchive_community_owned_by(&host.to_ascii_uppercase(), &owner) - .await - .expect("unarchive community") - .expect("owned community"); - assert_eq!(restored.id, created.id); - assert_eq!(restored.host, host); - assert_eq!( - db.lookup_community_by_host(&host) - .await - .expect("restored lookup") - .expect("active community") - .id, - created.id - ); - assert_eq!( - db.get_relay_member(created.id, &owner) - .await - .expect("owner lookup") - .expect("owner remains") - .role, - "owner" - ); - - let retry = db - .unarchive_community_owned_by(&host, &owner) - .await - .expect("idempotent retry") - .expect("owned community"); - assert_eq!(retry, restored); - } - - #[tokio::test] - #[ignore = "requires Postgres"] - async fn create_community_with_owner_enforces_per_owner_limit() { - let db = setup_db().await; - let owner = format!("{:064x}", Uuid::new_v4().as_u128()); - - // Create 3 communities for this owner (the max). - for i in 0..3 { - let host = format!("limit-test-{}-{}.example", i, Uuid::new_v4().simple()); - assert!(matches!( - db.create_community_with_owner(&host, &owner) - .await - .expect("create community"), - CreateCommunityWithOwnerResult::Created(_) - )); - } - - let host = format!("limit-test-3-{}.example", Uuid::new_v4().simple()); - assert_eq!( - db.create_community_with_owner(&host, &owner) - .await - .expect("create community call"), - CreateCommunityWithOwnerResult::LimitReached - ); - assert!( - db.lookup_community_by_host(&host) - .await - .expect("look up rolled-back fresh host") - .is_none(), - "limit rejection must roll back the fresh community row" - ); - } - - #[tokio::test] - #[ignore = "requires Postgres"] - async fn concurrent_same_owner_create_returns_the_winning_row_to_both_callers() { - let db = setup_db().await; - let host = format!("concurrent-create-{}.example", Uuid::new_v4().simple()); - let owner = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - - let (first, second) = tokio::join!( - db.create_community_with_owner(&host, owner), - db.create_community_with_owner(&host, owner), - ); - let first = first.expect("first concurrent create"); - let second = second.expect("second concurrent create"); - - assert!(matches!(first, CreateCommunityWithOwnerResult::Created(_))); - assert_eq!(first, second, "conflict loser re-reads the winning row"); - } - - #[tokio::test] - #[ignore = "requires Postgres"] - async fn ensure_configured_community_reports_insert_winner() { - let db = setup_db().await; - let host = format!("ensure-community-{}.example", Uuid::new_v4().simple()); - - let first = db - .ensure_configured_community(&host) - .await - .expect("first ensure"); - assert!(first.created, "first ensure should report created"); - assert_eq!(first.host, host); - - let second = db - .ensure_configured_community(&host) - .await - .expect("second ensure"); - assert!(!second.created, "second ensure should report existed"); - assert_eq!(second.id, first.id); - assert_eq!(second.host, host); - } - - #[tokio::test] - #[ignore = "requires Postgres"] - async fn list_communities_owned_by_returns_only_owner_rows() { - let db = setup_db().await; - let community_a = CommunityId::from_uuid(make_community(&db.pool).await); - let community_b = CommunityId::from_uuid(make_community(&db.pool).await); - let community_c = CommunityId::from_uuid(make_community(&db.pool).await); - // Unique per run: `list_communities_owned_by` is keyed only by pubkey, - // so a shared fixed pubkey picks up communities leaked by sibling - // ignored tests running against the same database. - let owner = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple()); - let owner = owner.as_str(); - let other = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple()); - let other = other.as_str(); - - db.bootstrap_owner(community_a, owner) - .await - .expect("owner A"); - db.bootstrap_owner(community_b, other) - .await - .expect("other owner B"); - db.add_relay_member(community_c, owner, "admin", None) - .await - .expect("admin C"); - - let owned = db - .list_communities_owned_by(owner) - .await - .expect("list owned communities"); - - assert_eq!(owned.len(), 1); - assert_eq!(owned[0].id, community_a); - } - - async fn insert_channel(pool: &PgPool, community_id: Uuid, channel_id: Uuid) { - let creator: Vec = vec![0u8; 32]; - sqlx::query( - r#" - INSERT INTO channels - (id, community_id, name, channel_type, visibility, created_by) - VALUES - ($1, $2, $3, 'stream'::channel_type, 'open'::channel_visibility, $4) - "#, - ) - .bind(channel_id) - .bind(community_id) - .bind(format!("ch-{}", channel_id.simple())) - .bind(&creator) - .execute(pool) - .await - .expect("insert channel"); - } - #[tokio::test] #[ignore = "requires Postgres"] async fn allowlist_is_scoped_to_community() { @@ -7319,48 +6496,6 @@ mod tests { .expect("A not allowed after remove")); } - #[tokio::test] - #[ignore = "requires Postgres"] - async fn communities_of_channels_present_for_existing_absent_for_missing() { - let db = setup_db().await; - let community = make_community(&db.pool).await; - let existing = Uuid::new_v4(); - insert_channel(&db.pool, community, existing).await; - - // Channel that is NOT inserted — the load-bearing case. - let missing = Uuid::new_v4(); - - let result = db - .communities_of_channels(&[existing, missing]) - .await - .expect("communities_of_channels"); - - // (1) Existing channel → present with its true community. - assert_eq!( - result.get(&existing).copied(), - Some(CommunityId::from_uuid(community)), - "existing channel must map to its true community", - ); - - // (2) Missing channel → ABSENT from the map (never defaulted). - // This is the contract the relay-side `MissingLookup → ImplBug` - // fail-closed guard-rail depends on. If this assertion ever - // weakens to `result.get(&missing) != Some(community)`, the - // mutate-bite below stops biting. - assert!( - !result.contains_key(&missing), - "missing channel must be absent from the result map, got {:?}", - result.get(&missing), - ); - - // (3) Map size matches: exactly one entry, the existing one. - assert_eq!( - result.len(), - 1, - "result map must contain only existing channels" - ); - } - /// BUG-5 regression: the `reactions` table is community-scoped /// (`PK (community_id, event_created_at, event_id, pubkey, emoji)`), so a /// reaction added under community A must be invisible and unremovable from