//! Integration tests for the volume server HTTP handlers. //! //! Uses axum's Router with tower::ServiceExt::oneshot to test //! end-to-end without starting a real TCP server. use std::sync::{Arc, RwLock}; use axum::body::Body; use axum::extract::connect_info::ConnectInfo; use axum::http::{Request, StatusCode}; use tower::ServiceExt; // for `oneshot` use seaweed_volume::security::{Guard, SigningKey}; use seaweed_volume::server::volume_server::{ build_admin_router, build_admin_router_with_ui, build_metrics_router, build_public_router, VolumeServerState, }; use seaweed_volume::storage::needle_map::NeedleMapKind; use seaweed_volume::storage::store::Store; use seaweed_volume::storage::types::{DiskType, Version, VolumeId}; use tempfile::TempDir; /// Create a test VolumeServerState with a temp directory, a single disk /// location, and one pre-created volume (VolumeId 1). fn test_state() -> (Arc, TempDir) { test_state_with_guard(Vec::new(), Vec::new()) } fn test_state_with_signing_key(signing_key: Vec) -> (Arc, TempDir) { test_state_with_guard(Vec::new(), signing_key) } fn test_state_with_whitelist(whitelist: Vec) -> (Arc, TempDir) { test_state_with_guard(whitelist, Vec::new()) } fn test_state_with_guard( whitelist: Vec, signing_key: Vec, ) -> (Arc, TempDir) { build_test_state(whitelist, signing_key, None, String::new()) } /// Build a test state with volume 1 created using the given replica placement /// (e.g. "001" for a two-copy volume) and master URL. An empty master URL plus /// a None placement is the single-copy default used by most tests. fn build_test_state( whitelist: Vec, signing_key: Vec, replication: Option<&str>, master_url: String, ) -> (Arc, TempDir) { let tmp = TempDir::new().expect("failed to create temp dir"); let dir = tmp.path().to_str().unwrap(); let replica_placement = replication.map(|s| { seaweed_volume::storage::super_block::ReplicaPlacement::from_string(s) .expect("invalid replica placement") }); let mut store = Store::new(NeedleMapKind::InMemory); store .add_location( dir, dir, 10, DiskType::HardDrive, seaweed_volume::config::MinFreeSpace::Percent(1.0), Vec::new(), ) .expect("failed to add location"); store .add_volume( VolumeId(1), "", replica_placement, None, 0, DiskType::HardDrive, Version::current(), ) .expect("failed to create volume"); let guard = Guard::new( &whitelist, SigningKey(signing_key), 0, SigningKey(vec![]), 0, ); let state = Arc::new(VolumeServerState { store: RwLock::new(store), guard: RwLock::new(guard), is_stopping: RwLock::new(false), maintenance: std::sync::atomic::AtomicBool::new(false), state_version: std::sync::atomic::AtomicU32::new(0), concurrent_upload_limit: 0, concurrent_download_limit: 0, inflight_upload_data_timeout: std::time::Duration::from_secs(60), inflight_download_data_timeout: std::time::Duration::from_secs(60), inflight_upload_bytes: std::sync::atomic::AtomicI64::new(0), inflight_download_bytes: std::sync::atomic::AtomicI64::new(0), upload_notify: tokio::sync::Notify::new(), download_notify: tokio::sync::Notify::new(), data_center: String::new(), rack: String::new(), file_size_limit_bytes: 0, maintenance_byte_per_second: 0, is_heartbeating: std::sync::atomic::AtomicBool::new(true), has_master: false, pre_stop_seconds: 0, volume_state_notify: tokio::sync::Notify::new(), write_queue: std::sync::OnceLock::new(), s3_tier_registry: std::sync::RwLock::new( seaweed_volume::remote_storage::s3_tier::S3TierRegistry::new(), ), read_mode: seaweed_volume::config::ReadMode::Local, allow_untrusted_remote_endpoints: false, master_url, master_urls: Vec::new(), seed_master_set: std::collections::HashSet::new(), current_master_url: tokio::sync::RwLock::new(String::new()), self_url: String::new(), http_client: reqwest::Client::new(), outgoing_http_scheme: "http".to_string(), outgoing_grpc_tls: None, metrics_runtime: std::sync::RwLock::new( seaweed_volume::server::volume_server::RuntimeMetricsConfig::default(), ), metrics_notify: tokio::sync::Notify::new(), fix_jpg_orientation: false, has_slow_read: false, read_buffer_size_bytes: 1024 * 1024, security_file: String::new(), cli_white_list: vec![], state_file_path: String::new(), }); (state, tmp) } /// Helper: read the entire response body as bytes. async fn body_bytes(response: axum::response::Response) -> Vec { let body = response.into_body(); axum::body::to_bytes(body, usize::MAX) .await .expect("failed to read body") .to_vec() } fn with_remote_addr(request: Request, remote_addr: &str) -> Request { let mut request = request; let remote_addr = remote_addr .parse::() .expect("invalid socket address"); request.extensions_mut().insert(ConnectInfo(remote_addr)); request } // ============================================================================ // 1. GET /healthz returns 200 when server is running // ============================================================================ #[tokio::test] async fn healthz_returns_200_when_running() { let (state, _tmp) = test_state(); let app = build_admin_router(state); let response = app .oneshot( Request::builder() .uri("/healthz") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } // ============================================================================ // 2. GET /healthz returns 503 when is_stopping=true // ============================================================================ #[tokio::test] async fn healthz_returns_503_when_stopping() { let (state, _tmp) = test_state(); *state.is_stopping.write().unwrap() = true; let app = build_admin_router(state); let response = app .oneshot( Request::builder() .uri("/healthz") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); } // ============================================================================ // 3. GET /status returns JSON with version and volumes array // ============================================================================ #[tokio::test] async fn status_returns_json_with_version_and_volumes() { let (state, _tmp) = test_state(); let app = build_admin_router(state); let response = app .oneshot( Request::builder() .uri("/status") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = body_bytes(response).await; let json: serde_json::Value = serde_json::from_slice(&body).expect("response is not valid JSON"); assert!(json.get("Version").is_some(), "missing 'Version' field"); assert!(json["Version"].is_string(), "'Version' should be a string"); assert!(json.get("Volumes").is_some(), "missing 'Volumes' field"); assert!(json["Volumes"].is_array(), "'Volumes' should be an array"); // We created one volume in test_state, so the array should have one entry let volumes = json["Volumes"].as_array().unwrap(); assert_eq!(volumes.len(), 1, "expected 1 volume"); assert_eq!(volumes[0]["Id"], 1); } #[tokio::test] async fn admin_router_does_not_expose_metrics() { let (state, _tmp) = test_state(); let app = build_admin_router(state); let response = app .oneshot( Request::builder() .uri("/metrics") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn metrics_router_serves_metrics() { let app = build_metrics_router(); let response = app .oneshot( Request::builder() .uri("/metrics") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn admin_router_rejects_non_whitelisted_uploads() { let (state, _tmp) = test_state_with_whitelist(vec!["127.0.0.1".to_string()]); let app = build_admin_router(state); let response = app .oneshot(with_remote_addr( Request::builder() .method("POST") .uri("/1,000000000000000001") .body(Body::from("blocked")) .unwrap(), "10.0.0.9:12345", )) .await .unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } #[tokio::test] async fn admin_router_rejects_non_whitelisted_deletes() { let (state, _tmp) = test_state_with_whitelist(vec!["127.0.0.1".to_string()]); let app = build_admin_router(state); let response = app .oneshot(with_remote_addr( Request::builder() .method("DELETE") .uri("/1,000000000000000001") .body(Body::empty()) .unwrap(), "10.0.0.9:12345", )) .await .unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } // Go's volume_server.go has /stats/* endpoints commented out (L130-134). // Requests to /stats/counter fall through to the store handler which returns 400. #[tokio::test] async fn admin_router_does_not_expose_stats_routes() { let (state, _tmp) = test_state_with_whitelist(vec!["127.0.0.1".to_string()]); let app = build_admin_router_with_ui(state, true); let response = app .oneshot(with_remote_addr( Request::builder() .uri("/stats/counter") .body(Body::empty()) .unwrap(), "127.0.0.1:12345", )) .await .unwrap(); // Falls through to store handler → 400 (bad volume id) assert_eq!(response.status(), StatusCode::BAD_REQUEST); } // ============================================================================ // 4. POST writes data, then GET reads it back // ============================================================================ #[tokio::test] async fn write_then_read_needle() { let (state, _tmp) = test_state(); // The fid "01637037d6" encodes NeedleId=0x01, Cookie=0x637037d6 let uri = "/1,01637037d6"; let payload = b"hello, seaweedfs!"; // --- POST (write) --- let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .method("POST") .uri(uri) .body(Body::from(payload.to_vec())) .unwrap(), ) .await .unwrap(); assert_eq!( response.status(), StatusCode::CREATED, "POST should return 201 Created" ); let body = body_bytes(response).await; let json: serde_json::Value = serde_json::from_slice(&body).expect("POST response is not valid JSON"); assert_eq!(json["size"], payload.len() as u64); // --- GET (read back) --- let app = build_admin_router(state.clone()); let response = app .oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap()) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK, "GET should return 200"); let body = body_bytes(response).await; assert_eq!(body, payload, "GET body should match written data"); } // A durable upload takes the same route through the handler; the flush is not // observable from here, but a broken wiring would show up as a failed write. #[tokio::test] async fn write_with_fsync_then_read_needle() { let (state, _tmp) = test_state(); let uri = "/1,01637037d6?fsync=true"; let payload = b"durable through the handler"; let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .method("POST") .uri(uri) .body(Body::from(payload.to_vec())) .unwrap(), ) .await .unwrap(); assert_eq!( response.status(), StatusCode::CREATED, "a durable POST should return 201 Created" ); let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .uri("/1,01637037d6") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); assert_eq!(body_bytes(response).await, payload); } // ============================================================================ // 5. DELETE deletes a needle, subsequent GET returns 404 // ============================================================================ #[tokio::test] async fn delete_then_get_returns_404() { let (state, _tmp) = test_state(); let uri = "/1,01637037d6"; let payload = b"to be deleted"; // Write the needle first let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .method("POST") .uri(uri) .body(Body::from(payload.to_vec())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::CREATED); // Delete let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .method("DELETE") .uri(uri) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!( response.status(), StatusCode::ACCEPTED, "DELETE should return 202 Accepted" ); // GET should now return 404 let app = build_admin_router(state.clone()); let response = app .oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap()) .await .unwrap(); assert_eq!( response.status(), StatusCode::NOT_FOUND, "GET after DELETE should return 404" ); } // ============================================================================ // 6. HEAD returns headers without body // ============================================================================ #[tokio::test] async fn head_returns_headers_without_body() { let (state, _tmp) = test_state(); let uri = "/1,01637037d6"; let payload = b"head test data"; // Write needle let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .method("POST") .uri(uri) .body(Body::from(payload.to_vec())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::CREATED); // HEAD let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .method("HEAD") .uri(uri) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK, "HEAD should return 200"); // Content-Length header should be present let content_length = response .headers() .get("content-length") .expect("HEAD should include Content-Length header"); let len: usize = content_length .to_str() .unwrap() .parse() .expect("Content-Length should be a number"); assert_eq!( len, payload.len(), "Content-Length should match payload size" ); // Body should be empty for HEAD let body = body_bytes(response).await; assert!(body.is_empty(), "HEAD body should be empty"); } // ============================================================================ // 7. Invalid URL path returns 400 // ============================================================================ #[tokio::test] async fn invalid_url_path_returns_400() { let (state, _tmp) = test_state(); let app = build_admin_router(state); // "invalidpath" has no comma or slash separator so parse_url_path returns None let response = app .oneshot( Request::builder() .uri("/invalidpath") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!( response.status(), StatusCode::BAD_REQUEST, "invalid URL path should return 400" ); } #[tokio::test] async fn deep_invalid_url_path_returns_400() { let (state, _tmp) = test_state(); let app = build_admin_router(state); let response = app .oneshot( Request::builder() .uri("/not/a/valid/volume/path") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn admin_root_get_returns_400() { let (state, _tmp) = test_state(); let app = build_admin_router(state); let response = app .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn public_root_get_returns_400() { let (state, _tmp) = test_state(); let app = build_public_router(state); let response = app .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn public_router_does_not_expose_healthz() { let (state, _tmp) = test_state(); let app = build_public_router(state); let response = app .oneshot( Request::builder() .uri("/healthz") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); } // Go's volume_server.go has /stats/* endpoints commented out (L130-134). #[tokio::test] async fn admin_router_stats_routes_not_registered() { let (state, _tmp) = test_state(); let app = build_admin_router(state); let response = app .oneshot( Request::builder() .uri("/stats/counter") .body(Body::empty()) .unwrap(), ) .await .unwrap(); // Falls through to store handler → 400 (bad volume id) assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn admin_router_hides_ui_when_write_jwt_is_configured() { let (state, _tmp) = test_state_with_signing_key(b"secret".to_vec()); let app = build_admin_router(state); let response = app .oneshot( Request::builder() .uri("/ui/index.html") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn admin_router_can_expose_ui_with_explicit_override() { let (state, _tmp) = test_state_with_signing_key(b"secret".to_vec()); let app = build_admin_router_with_ui(state, true); let response = app .oneshot( Request::builder() .uri("/ui/index.html") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = body_bytes(response).await; let html = String::from_utf8(body).unwrap(); assert!(html.contains("Disk Stats")); assert!(html.contains("System Stats")); assert!(html.contains("Volumes")); } #[tokio::test] async fn admin_router_ui_override_ignores_read_jwt_checks() { let (state, _tmp) = test_state_with_signing_key(b"write-secret".to_vec()); state.guard.write().unwrap().read_signing_key = SigningKey(b"read-secret".to_vec()); let app = build_admin_router_with_ui(state, true); let response = app .oneshot( Request::builder() .uri("/ui/index.html") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn admin_router_serves_volume_ui_static_assets() { let (state, _tmp) = test_state(); let app = build_admin_router(state); let response = app .oneshot( Request::builder() .uri("/seaweedfsstatic/bootstrap/3.3.1/css/bootstrap.min.css") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); assert_eq!( response .headers() .get("content-type") .and_then(|value| value.to_str().ok()), Some("text/css; charset=utf-8") ); let body = body_bytes(response).await; assert!(body.len() > 1000); } // ============================================================================ // Replicated (fan-out) writes // // A chunked S3 upload has the gateway write every replica holder directly with // `type=replicate` instead of relaying through the primary volume. Each holder // must accept the copy and store it locally without re-replicating. These tests // pin that contract on the Rust volume server. // ============================================================================ /// Raw-body `type=replicate` write is stored and reads back. #[tokio::test] async fn replicate_write_raw_body_is_stored() { let (state, _tmp) = test_state(); let uri = "/1,01637037d6?type=replicate"; let payload = b"fan-out replica bytes"; let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .method("POST") .uri(uri) .body(Body::from(payload.to_vec())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::CREATED); let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .uri("/1,01637037d6") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); assert_eq!(body_bytes(response).await, payload); } /// Go reads fsync through r.FormValue, which decodes the query, so a /// percent-encoded value has to reach the write path here too. #[tokio::test] async fn write_with_percent_encoded_fsync_is_accepted() { let (state, _tmp) = test_state(); let payload = b"encoded durable payload"; let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .method("POST") .uri("/1,01637037d6?fsync=%74rue") .body(Body::from(payload.to_vec())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::CREATED); let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .uri("/1,01637037d6") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); assert_eq!(body_bytes(response).await, payload); } /// The fan-out query a Go primary sends for a durable write: `fsync=true` rides /// along with `type=replicate`, and the replica has to honor it rather than ack /// out of the page cache. #[tokio::test] async fn replicate_write_with_fsync_is_stored() { let (state, _tmp) = test_state(); let uri = "/1,01637037d6?fsync=true&type=replicate"; let payload = b"durable replica bytes"; let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .method("POST") .uri(uri) .body(Body::from(payload.to_vec())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::CREATED); let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .uri("/1,01637037d6") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); assert_eq!(body_bytes(response).await, payload); } /// Multipart `type=replicate` write (the shape the Go gateway uploader sends) /// is stored and reads back. #[tokio::test] async fn replicate_write_multipart_body_is_stored() { let (state, _tmp) = test_state(); let boundary = "----replicateboundary"; let payload = b"fan-out replica bytes"; let mut body = Vec::new(); body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); body.extend_from_slice( b"Content-Disposition: form-data; name=\"file\"; filename=\"chunk\"\r\n", ); body.extend_from_slice(b"Content-Type: application/octet-stream\r\n\r\n"); body.extend_from_slice(payload); body.extend_from_slice(format!("\r\n--{}--\r\n", boundary).as_bytes()); let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .method("POST") .uri("/1,01637037d6?type=replicate") .header( "Content-Type", format!("multipart/form-data; boundary={}", boundary), ) .body(Body::from(body)) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::CREATED); let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .uri("/1,01637037d6") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); assert_eq!(body_bytes(response).await, payload); } /// On a multi-copy volume, a `type=replicate` write must store locally without /// re-replicating: it must not contact the master, even an unreachable one. /// A plain write to the same volume does attempt replication and fails against /// the dead master, which proves the `type=replicate` flag is what suppresses /// the fan-out (not a disabled replication path). #[tokio::test] async fn replicate_write_does_not_re_replicate() { // Port 0 is rejected by the socket layer immediately, so a stray lookup // fails fast instead of hanging on a connect timeout in firewalled CI. let dead_master = "127.0.0.1:0".to_string(); let (state, _tmp) = build_test_state(Vec::new(), Vec::new(), Some("001"), dead_master); // type=replicate: stored locally, no master lookup -> 201. let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .method("POST") .uri("/1,01637037d6?type=replicate") .body(Body::from(b"replicated copy".to_vec())) .unwrap(), ) .await .unwrap(); assert_eq!( response.status(), StatusCode::CREATED, "replicate write must not depend on the master" ); // plain write: tries to fan out to the dead master and fails. let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .method("POST") .uri("/1,02637037d7") .body(Body::from(b"primary copy".to_vec())) .unwrap(), ) .await .unwrap(); assert_eq!( response.status(), StatusCode::INTERNAL_SERVER_ERROR, "plain write to a multi-copy volume must attempt replication" ); } // ============================================================================ // Chunk-manifest expansion resolves chunks on EC volumes // // A chunked object whose data chunks live on an EC-encoded volume must expand // by reconstruct-on-read from the shards, not by a local regular-volume lookup // (which finds nothing once the volume is EC-encoded). Mirrors Go's // ChunkedFileReader, which resolves every chunk through the master. // ============================================================================ #[tokio::test] async fn chunk_manifest_expands_chunk_stored_on_ec_volume() { use seaweed_volume::storage::erasure_coding::ec_encoder::write_ec_files; use seaweed_volume::storage::needle::needle::{FileId, Needle}; use seaweed_volume::storage::types::{Cookie, NeedleId}; use seaweed_volume::storage::volume::Volume; let (state, tmp) = test_state(); let dir = tmp.path().to_str().unwrap(); // A chunk large enough to be worth EC-encoding; its bytes are the payload we // expect the manifest GET to return. let chunk_data: Vec = (0..4096u32).map(|i| (i % 251) as u8).collect(); let chunk_nid = NeedleId(0x2a); let chunk_cookie = Cookie(0x1234abcd); // Build regular volume 2 holding the chunk, then EC-encode it and mount all // 14 shards locally so reconstruct-on-read is a pure local read. { let mut v = Volume::new( dir, dir, "", VolumeId(2), NeedleMapKind::InMemory, None, None, 0, Version::current(), ) .unwrap(); let mut n = Needle { id: chunk_nid, cookie: chunk_cookie, data: chunk_data.clone(), data_size: chunk_data.len() as u32, ..Needle::default() }; v.write_needle(&mut n, true, false).unwrap(); v.sync_to_disk().unwrap(); v.close(); } write_ec_files(dir, dir, "", VolumeId(2), 10, 4).unwrap(); // Volume 2 was built standalone (never registered in the store), so it only // exists as EC shards — the chunk resolves through the EC path, as it would // after ec.encode retired the regular volume. { let mut store = state.store.write().unwrap(); let shard_ids: Vec = (0..14).collect(); store.mount_ec_shards(VolumeId(2), "", &shard_ids).unwrap(); } // Write the chunk-manifest needle to regular volume 1. let chunk_fid = FileId::new(VolumeId(2), chunk_nid, chunk_cookie).to_string(); let manifest = format!( r#"{{"name":"big.bin","mime":"application/octet-stream","size":{},"chunks":[{{"fid":"{}","offset":0,"size":{}}}]}}"#, chunk_data.len(), chunk_fid, chunk_data.len() ); let manifest_nid = NeedleId(0x7); let manifest_cookie = Cookie(0x55667788); { let mut store = state.store.write().unwrap(); let mut n = Needle { id: manifest_nid, cookie: manifest_cookie, data: manifest.into_bytes(), ..Needle::default() }; n.data_size = n.data.len() as u32; n.set_is_chunk_manifest(); store .write_volume_needle(VolumeId(1), &mut n, false) .unwrap(); } // GET the manifest object; expect the reconstructed chunk bytes. let manifest_fid = FileId::new(VolumeId(1), manifest_nid, manifest_cookie).to_string(); let app = build_admin_router(state.clone()); let response = app .oneshot( Request::builder() .uri(format!("/{}", manifest_fid)) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); assert_eq!(body_bytes(response).await, chunk_data); }