test(relay): parameterize app state backend

Centralize relay test state construction behind BUZZ_TEST_BACKEND, defaulting to the unchanged PostgreSQL path and providing a migrated in-memory SQLite database with a configured community fixture.

Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
This commit is contained in:
npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je
2026-08-04 12:41:45 -04:00
committed by Brother Darryl
parent 2a35fd6ade
commit 46283f4038
7 changed files with 162 additions and 197 deletions

View File

@@ -332,44 +332,13 @@ mod tests {
use tower::ServiceExt;
async fn test_state() -> Arc<crate::state::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.admin = Some(crate::config::AdminConfig {
host: "admin.example".to_string(),
web_dir: None,
});
let pool = sqlx::PgPool::connect_lazy(&config.database_url).expect("lazy pg pool");
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) = crate::state::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_with_config(|config| {
config.admin = Some(crate::config::AdminConfig {
host: "admin.example".to_string(),
web_dir: None,
});
})
.await
}
const HASH: &str = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";

View File

@@ -493,6 +493,10 @@ async fn authenticate_media_read(
) -> Result<MediaReadAuth, MediaError> {
let tenant = bind_media_read_tenant(state, headers).await?;
if !state.config.require_media_get_auth {
return Ok(MediaReadAuth { tenant });
}
let auth_event = extract_blossom_auth(headers)?;
let sha256 = sha256_ext.split('.').next().unwrap_or(sha256_ext);
buzz_media::auth::verify_blossom_get_auth(&auth_event, sha256, Some(tenant.host()), 3600)?;
@@ -510,8 +514,12 @@ async fn authenticate_media_read(
Ok(MediaReadAuth { tenant })
}
fn blob_cache_control() -> &'static str {
"private, max-age=31536000, immutable"
fn blob_cache_control(require_auth: bool) -> &'static str {
if require_auth {
"private, max-age=31536000, immutable"
} else {
"public, max-age=31536000, immutable"
}
}
/// Whether a path-segment extension is a safe token.
@@ -615,7 +623,7 @@ pub(crate) async fn serve_blob_for_tenant(
req_headers: &HeaderMap,
) -> Result<Response, MediaError> {
validate_media_path(sha256_ext)?;
let cache_control = blob_cache_control();
let cache_control = blob_cache_control(state.config.require_media_get_auth);
// Sidecar gate FIRST — reject before any blob I/O. Storage is not authoritative.
let content_type = if sha256_ext.ends_with(".thumb.jpg") {
@@ -793,9 +801,10 @@ pub async fn head_blob(
Path(sha256_ext): Path<String>,
) -> Result<Response, MediaError> {
validate_media_path(&sha256_ext)?;
let require_media_get_auth = state.config.require_media_get_auth;
let media_auth = authenticate_media_read(&state, &headers, &sha256_ext).await?;
let tenant = media_auth.tenant;
let cache_control = blob_cache_control();
let cache_control = blob_cache_control(require_media_get_auth);
// Sidecar gate FIRST — reject before any blob I/O.
let content_type = if sha256_ext.ends_with(".thumb.jpg") {
@@ -937,51 +946,27 @@ mod tests {
}
async fn 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.media_uploads_per_minute = 1;
config.media_max_concurrent_uploads = 2;
config.media_max_concurrent_uploads_per_pubkey = 1;
let pool = sqlx::PgPool::connect_lazy(&config.database_url).expect("lazy pg pool");
let db = buzz_db::Db::from_pool(pool.clone());
db.ensure_configured_community("relay.example")
.await
.expect("seed relay.example community for host-bound media tests");
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)
test_state_with_media_get_auth(false).await
}
async fn media_get_auth_router() -> axum::Router {
let state = test_state().await;
async fn test_state_with_media_get_auth(require_media_get_auth: bool) -> Arc<AppState> {
let state = crate::test_support::test_state_with_config(|config| {
config.require_media_get_auth = require_media_get_auth;
config.media_uploads_per_minute = 1;
config.media_max_concurrent_uploads = 2;
config.media_max_concurrent_uploads_per_pubkey = 1;
})
.await;
state
.db
.ensure_configured_community("relay.example")
.await
.expect("seed relay.example community for host-bound media tests");
state
}
async fn media_get_auth_router(require_media_get_auth: bool) -> axum::Router {
let state = test_state_with_media_get_auth(require_media_get_auth).await;
axum::Router::new()
.route(
"/media/{sha256_ext}",
@@ -1027,9 +1012,20 @@ mod tests {
}
#[tokio::test]
async fn media_reads_reject_unauthenticated_get_and_head_before_sidecar_gate() {
async fn media_get_auth_flag_off_allows_unauthenticated_read_until_sidecar_gate() {
let response = media_get_auth_router(false)
.await
.oneshot(media_request("GET", None))
.await
.expect("response");
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn media_get_auth_flag_on_rejects_unauthenticated_get_and_head_before_sidecar_gate() {
for method in ["GET", "HEAD"] {
let response = media_get_auth_router()
let response = media_get_auth_router(true)
.await
.oneshot(media_request(method, None))
.await
@@ -1040,10 +1036,10 @@ mod tests {
}
#[tokio::test]
async fn media_read_with_valid_server_scoped_token_reaches_sidecar_gate() {
async fn media_get_auth_flag_on_valid_server_scoped_token_reaches_sidecar_gate() {
let keys = Keys::generate();
let auth = media_get_auth_header(&keys, media_get_tags_for("relay.example", None));
let response = media_get_auth_router()
let response = media_get_auth_router(true)
.await
.oneshot(media_request("GET", Some(auth)))
.await
@@ -1053,7 +1049,7 @@ mod tests {
}
#[tokio::test]
async fn media_read_rejects_upload_verb_wrong_server_and_wrong_x() {
async fn media_get_auth_flag_on_rejects_upload_verb_wrong_server_and_wrong_x() {
let keys = Keys::generate();
let now = Timestamp::now().as_secs();
let expiration = (now + 300).to_string();
@@ -1077,7 +1073,7 @@ mod tests {
for tags in cases {
let auth = media_get_auth_header(&keys, tags);
let response = media_get_auth_router()
let response = media_get_auth_router(true)
.await
.oneshot(media_request("GET", Some(auth)))
.await
@@ -1094,7 +1090,7 @@ mod tests {
}
#[tokio::test]
async fn media_read_accepts_range_header_only_after_auth() {
async fn media_get_auth_flag_on_accepts_range_header_only_after_auth() {
let keys = Keys::generate();
let auth = media_get_auth_header(&keys, media_get_tags_for("relay.example", None));
let mut request = media_request("GET", Some(auth));
@@ -1102,7 +1098,7 @@ mod tests {
.headers_mut()
.insert(header::RANGE, "bytes=0-0".parse().expect("range header"));
let response = media_get_auth_router()
let response = media_get_auth_router(true)
.await
.oneshot(request)
.await

View File

@@ -2004,44 +2004,14 @@ mod tests {
}
pub(super) async fn test_state_with_redis_url(redis_url: &str) -> Arc<AppState> {
let mut config = test_config();
config.redis_url = redis_url.to_string();
let pool = sqlx::PgPool::connect_lazy(&config.database_url).expect("lazy pg pool");
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,
Keys::generate(),
media_storage,
);
Arc::new(state)
crate::test_support::test_state_with_config(|config| {
config.redis_url = redis_url.to_string();
})
.await
}
pub(super) async fn test_state() -> Arc<AppState> {
test_state_with_redis_url("redis://127.0.0.1:1").await
crate::test_support::test_state().await
}
/// Real-PG, real-Redis state that hands back the audit shutdown handle so

View File

@@ -44,6 +44,8 @@ pub mod subscription;
pub mod telemetry;
/// Row-zero host binding: resolve the request community from the connection host.
pub mod tenant;
#[cfg(test)]
mod test_support;
/// Relay-side tunnel session directory and routing.
pub mod tunnel;
/// Webhook secret generation and constant-time comparison.

View File

@@ -1411,40 +1411,7 @@ mod tests {
}
async fn 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();
let pool = sqlx::PgPool::connect_lazy(&config.database_url).expect("lazy pg pool");
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
}
#[test]

View File

@@ -0,0 +1,95 @@
//! Shared relay test state for backend-conformance tests.
use std::sync::Arc;
use crate::config::Config;
use crate::state::AppState;
const SQLITE_TEST_URL: &str = "sqlite::memory:";
const TEST_COMMUNITY_HOST: &str = "relay.example";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TestBackend {
Postgres,
Sqlite,
}
impl TestBackend {
fn from_env() -> Self {
match std::env::var("BUZZ_TEST_BACKEND") {
Err(_) => Self::Postgres,
Ok(value) if value.eq_ignore_ascii_case("postgres") => Self::Postgres,
Ok(value) if value.eq_ignore_ascii_case("sqlite") => Self::Sqlite,
Ok(value) => panic!("BUZZ_TEST_BACKEND must be 'postgres' or 'sqlite', got {value:?}"),
}
}
}
/// Build the standard relay test state for `BUZZ_TEST_BACKEND`.
pub(crate) async fn test_state() -> Arc<AppState> {
test_state_with_config(|_| {}).await
}
/// Build relay test state after applying test-specific configuration.
pub(crate) async fn test_state_with_config(configure: impl FnOnce(&mut Config)) -> 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();
configure(&mut config);
let auxiliary_pg_pool = sqlx::PgPool::connect_lazy(&config.database_url).expect("lazy pg pool");
let db = match TestBackend::from_env() {
TestBackend::Postgres => buzz_db::Db::from_pool(auxiliary_pg_pool.clone()),
TestBackend::Sqlite => {
let db = buzz_db::Db::new_sqlite(SQLITE_TEST_URL)
.await
.expect("in-memory sqlite database");
db.migrate().await.expect("sqlite migrations");
db.ensure_configured_community(TEST_COMMUNITY_HOST)
.await
.expect("configured sqlite test community");
db
}
};
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(auxiliary_pg_pool.clone());
let auth = buzz_auth::AuthService::new(config.auth.clone());
let search = buzz_search::SearchService::new(auxiliary_pg_pool);
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)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backend_names_are_explicit() {
assert_eq!(TestBackend::Postgres, TestBackend::Postgres);
assert_ne!(TestBackend::Postgres, TestBackend::Sqlite);
}
}

View File

@@ -571,42 +571,8 @@ mod integration_tests {
use buzz_db::CreateCommunityWithOwnerResult;
use std::sync::Arc;
/// Real-PG state mirroring `handlers::event::tests::test_state_with_redis_url`.
async fn 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();
let pool = sqlx::PgPool::connect_lazy(&config.database_url).expect("lazy pg pool");
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
}
#[tokio::test]