diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index 46f215a9d..59b4f3e0c 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -92,6 +92,13 @@ pub struct TestECStoreEnv { /// `init_local_disks` + `ECStore::new` on `127.0.0.1:0` (random port keeps /// nextest's process-per-test parallelism safe). pub ecstore: Arc, + /// The single-pool, single-set topology the store was built from. + /// + /// The bootstrap does **not** publish it on the instance context (server + /// startup is what calls `set_endpoints`, and that write is once-only), so + /// a test that needs `get_global_endpoints` to resolve — admin server-info + /// and other topology readers — publishes this value itself. + pub endpoint_pools: EndpointServerPools, } impl TestECStoreEnv { @@ -234,7 +241,7 @@ impl TestECStoreEnvBuilder { // Port 0 keeps ECStore-backed integration binaries parallel-safe under // nextest: no fixed peer port is ever shared between test processes. let server_addr: std::net::SocketAddr = "127.0.0.1:0".parse().expect("parse test addr"); - let ecstore = ECStore::new(server_addr, endpoint_pools, CancellationToken::new()) + let ecstore = ECStore::new(server_addr, endpoint_pools.clone(), CancellationToken::new()) .await .expect("build test ECStore"); @@ -254,6 +261,7 @@ impl TestECStoreEnvBuilder { temp_root, disk_paths, ecstore, + endpoint_pools, } } } diff --git a/rustfs/src/admin/handlers/system.rs b/rustfs/src/admin/handlers/system.rs index 0909568e5..9df481c11 100644 --- a/rustfs/src/admin/handlers/system.rs +++ b/rustfs/src/admin/handlers/system.rs @@ -1541,6 +1541,84 @@ mod tests { assert_eq!(error.code(), &S3ErrorCode::InvalidRequest); } + /// `ServerInfoHandler` must answer an authorized admin request with the + /// per-pool erasure-set topology (rustfs/backlog#1839). That map is only + /// filled when the server-info query is issued with pools included, so a + /// handler that stopped asking for them would still return 200 with an + /// empty `pools` object instead of failing. + #[tokio::test] + #[serial_test::serial] + async fn server_info_response_carries_pool_topology() { + use crate::admin::runtime_sources::{AppContext, publish_test_app_context}; + use crate::admin::storage_api::runtime::bootstrap_ctx; + use http_body_util::BodyExt as _; + use rustfs_iam::store::{Store as _, object::IAM_CONFIG_PREFIX}; + use std::sync::Arc; + + const ROOT_ACCESS_KEY: &str = "SERVERINFOROOTACCESSKEY"; + const ROOT_SECRET_KEY: &str = "serverInfoRootSecret123"; + + let _ = rustfs_credentials::init_global_action_credentials( + Some(ROOT_ACCESS_KEY.to_string()), + Some(ROOT_SECRET_KEY.to_string()), + ); + + let env = rustfs_test_utils::TestECStoreEnv::builder() + .prefix("admin_server_info_pools") + .disk_count(1) + .init_bucket_metadata(false) + .build() + .await; + // Server startup owns this write in production; the test bootstrap + // stops short of it, and without a topology the server-info query + // returns before it ever looks at drives. + bootstrap_ctx().set_endpoints(env.endpoint_pools.clone()); + rustfs_iam::store::object::ObjectStore::new(Arc::clone(&env.ecstore)) + .save_iam_config(serde_json::json!({"version": 1}), format!("{}/format.json", *IAM_CONFIG_PREFIX)) + .await + .expect("seed IAM format"); + let iam = rustfs_iam::build_iam_sys(Arc::clone(&env.ecstore)) + .await + .expect("build test IAM"); + publish_test_app_context(Arc::new(AppContext::with_default_interfaces( + Arc::clone(&env.ecstore), + iam, + Arc::new(rustfs_kms::KmsServiceManager::new()), + ))); + + let request = S3Request { + input: Body::empty(), + method: Method::GET, + uri: Uri::from_static("/rustfs/admin/v3/info"), + headers: HeaderMap::new(), + extensions: Extensions::new(), + credentials: Some(s3s::auth::Credentials { + access_key: ROOT_ACCESS_KEY.to_string(), + secret_key: s3s::auth::SecretKey::from(ROOT_SECRET_KEY.to_string()), + }), + region: None, + service: None, + trailing_headers: None, + }; + + let (status, body) = super::ServerInfoHandler {} + .call(request, Params::new()) + .await + .expect("root admin credentials must be served server info") + .output; + assert_eq!(status, hyper::StatusCode::OK); + + let bytes = body.collect().await.expect("server info body should read").to_bytes(); + let payload: serde_json::Value = serde_json::from_slice(&bytes).expect("server info must be json"); + let pools = payload["info"]["pools"] + .as_object() + .expect("server info must carry a pools object"); + assert!( + pools.contains_key("0"), + "server info must report the erasure-set topology of pool 0, got {pools:?}" + ); + } + /// Authorization denial for this exact action is pinned to AccessDenied by /// `crate::admin::auth::tests::non_admin_credential_is_denied`. #[test] diff --git a/rustfs/src/admin/route_registration_test.rs b/rustfs/src/admin/route_registration_test.rs index e81f13d17..e48e09c94 100644 --- a/rustfs/src/admin/route_registration_test.rs +++ b/rustfs/src/admin/route_registration_test.rs @@ -1412,23 +1412,6 @@ fn test_health_routes_not_registered_when_disabled_by_env() { }); } -#[test] -fn test_phase5_admin_info_contract() { - let system_src = include_str!("handlers/system.rs"); - - let server_info_impl_marker = "impl Operation for ServerInfoHandler"; - let server_info_impl_start = system_src - .find(server_info_impl_marker) - .expect("Expected impl Operation for ServerInfoHandler in handlers/system.rs"); - let server_info_impl_block = &system_src[server_info_impl_start..]; - - assert!( - server_info_impl_block.contains("default_admin_usecase()") - && server_info_impl_block.contains("execute_query_server_info(QueryServerInfoRequest { include_pools: true })"), - "admin server info path must be served through admin runtime-source DefaultAdminUsecase::execute_query_server_info" - ); -} - fn extract_block_between_markers<'a>(src: &'a str, start_marker: &str, end_marker: &str) -> &'a str { let start = src .find(start_marker) diff --git a/rustfs/src/admin/storage_api.rs b/rustfs/src/admin/storage_api.rs index ec89f5e4e..dbf3a81df 100644 --- a/rustfs/src/admin/storage_api.rs +++ b/rustfs/src/admin/storage_api.rs @@ -948,6 +948,10 @@ pub(crate) mod runtime { #[cfg(test)] pub(crate) use super::{Endpoint, Endpoints, PoolEndpoints}; + /// Test-only: the process instance context, so a handler test can publish + /// the endpoint topology that server startup normally installs. + #[cfg(test)] + pub(crate) use crate::storage::storage_api::ecstore_runtime::bootstrap_ctx; } pub(crate) mod s3 {