mirror of
https://github.com/block/buzz.git
synced 2026-11-02 07:22:55 +00:00
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:
committed by
Brother Darryl
parent
2a35fd6ade
commit
46283f4038
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
95
crates/buzz-relay/src/test_support.rs
Normal file
95
crates/buzz-relay/src/test_support.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user