makepad/libs/asset/store/tests/http/blob_refs_http.rs
Arena Agent d8d5ea8b6a Repair integration manifests and port nigig exports onto upstream widget families
Restore missing required asset/flow packages from the pinned nigig revision; retain upstream script isolation, X11 hover, and widget-core fixes. Validated makepad-widgets data feature with Rust 1.97.1.
2026-10-26 11:23:26 +00:00

241 lines
8.9 KiB
Rust

//! Reference blobs over the wire: the real client publishing a real file it
//! never reads, against a real server on a real socket.
//!
//! What this proves that the core-level suite cannot:
//! - the route DOES NOT EXIST unless the embedder turned the policy on —
//! an untouched server answers 404, so no deployment gains a file-read
//! surface by upgrading,
//! - a bundle published by reference produces a manifest that a normal
//! client fetches normally: same digest, same length, same bytes,
//! - the store's directory gained no copy of the payload,
//! - and when the file underneath changes, the fetch REFUSES rather than
//! serving whatever is there now.
use makepad_asset_client::{
ApiEndpoints, AssetClient, ClientConfig, ClientError, PublishBundle, PublishBundleFile,
PublishRights, PublishThumbnail,
};
use makepad_asset_data::{AssetKind, BlobId, FileRole, MediaType, ThumbnailMedia};
use makepad_asset_store::{AssetServer, BlobRefPolicy, ServerConfig};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
static DIR_COUNTER: AtomicU64 = AtomicU64::new(0);
fn test_root(name: &str) -> PathBuf {
let n = DIR_COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("mp_blobref_http_{}_{}_{}", std::process::id(), n, name))
}
fn start_server(name: &str, refs_on: bool) -> (AssetServer, String, PathBuf) {
let root = test_root(name);
let mut cfg = ServerConfig::new(root.clone());
cfg.control_addr = "127.0.0.1:0".parse().unwrap();
cfg.data_addr = "127.0.0.1:0".parse().unwrap();
cfg.bootstrap_admin = true;
cfg.log = false;
if refs_on {
cfg.blob_refs = BlobRefPolicy::local_host();
}
let server = AssetServer::start(cfg).expect("server start");
let token = std::fs::read_to_string(root.join("admin-token"))
.expect("admin token")
.trim()
.to_string();
(server, token, root)
}
fn connect(server: &AssetServer, token: &str, cache: &str) -> AssetClient {
let mut cfg = ClientConfig::new(test_root(cache));
cfg.token = Some(token.to_string());
let endpoints = ApiEndpoints { control: server.control_addr(), data: server.data_addr() };
AssetClient::connect(cfg, endpoints, Some(server.server_id())).expect("connect")
}
fn thumbnail() -> PublishThumbnail {
PublishThumbnail {
bytes: vec![9u8; 1200],
media: ThumbnailMedia::Jpeg,
width: 512,
height: 512,
views: Vec::new(),
}
}
/// Count regular files under a directory — used to show the CAS did not grow.
fn file_count(root: &Path) -> usize {
let mut n = 0;
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else { continue };
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else {
n += 1;
}
}
}
n
}
#[test]
fn reference_admission_is_absent_until_an_embedder_enables_it() {
let (server, token, _root) = start_server("policy_off", false);
let client = connect(&server, &token, "policy_off_cache");
let dir = test_root("policy_off_media");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("clip.mp4");
std::fs::write(&path, b"UNREACHABLE").unwrap();
match client.admit_blob_ref("gen", path.to_str().unwrap()) {
Err(ClientError::NotFound { .. }) => {}
other => panic!("a server with the policy off must not offer the route: {other:?}"),
}
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_bundle_published_by_reference_fetches_like_any_other() {
let (server, token, root) = start_server("publish_ref", true);
let mut client = connect(&server, &token, "publish_ref_cache");
// The user's media directory, entirely outside the store.
let media = test_root("publish_ref_media");
std::fs::create_dir_all(&media).unwrap();
let clip: Vec<u8> = (0..40_000u32).map(|i| (i % 251) as u8).collect();
let path = media.join("opener.mp4");
std::fs::write(&path, &clip).unwrap();
let cas_before = file_count(&root.join("cas"));
let mut bundle = PublishBundle::new(
"gen",
AssetKind::Video,
"Opener",
vec![PublishBundleFile::reference(
FileRole::Video,
MediaType::Mp4,
path.clone(),
None,
)],
thumbnail(),
PublishRights::generated_cc0(),
);
bundle.media_millis = 4000;
let published = client.publish_bundle(&bundle).expect("publish by reference");
// The manifest pins the file's REAL digest and length, measured by the
// server on a file the client never opened.
assert_eq!(published.files.len(), 1);
assert_eq!(published.files[0].blob, BlobId::hash_of(&clip));
assert_eq!(published.files[0].byte_len, clip.len() as u64);
// The CAS gained exactly ONE object — the thumbnail. The 40 KB payload
// was not copied.
let cas_after = file_count(&root.join("cas"));
assert_eq!(
cas_after - cas_before,
1,
"the payload should not have been copied into the CAS"
);
// And it fetches over the wire like anything else, digest-verified by
// the client's own cache on the way in.
let fetched = client.fetch_blob_bytes(&published.files[0].blob, None).expect("fetch");
assert_eq!(fetched, clip);
// Break the file underneath: the store refuses rather than serving
// whatever is at that path now.
let mut tampered = clip.clone();
tampered[0] ^= 0xff;
std::fs::write(&path, &tampered).unwrap();
let mut fresh = connect(&server, &token, "publish_ref_cache2");
match fresh.fetch_blob_bytes(&published.files[0].blob, None) {
Ok(_) => panic!("a drifted reference must never serve"),
Err(_) => {}
}
// Restore, and it works again: the reference was unavailable, not wrong.
std::fs::write(&path, &clip).unwrap();
let mut again = connect(&server, &token, "publish_ref_cache3");
assert_eq!(
again.fetch_blob_bytes(&published.files[0].blob, None).expect("restored"),
clip
);
std::fs::remove_dir_all(&media).ok();
}
#[test]
fn the_prefix_allowlist_is_enforced() {
let root = test_root("allowlist");
let allowed = test_root("allowlist_allowed");
let forbidden = test_root("allowlist_forbidden");
std::fs::create_dir_all(&allowed).unwrap();
std::fs::create_dir_all(&forbidden).unwrap();
std::fs::write(allowed.join("ok.mp4"), b"INSIDE").unwrap();
std::fs::write(forbidden.join("no.mp4"), b"OUTSIDE").unwrap();
let mut cfg = ServerConfig::new(root.clone());
cfg.control_addr = "127.0.0.1:0".parse().unwrap();
cfg.data_addr = "127.0.0.1:0".parse().unwrap();
cfg.bootstrap_admin = true;
cfg.log = false;
cfg.blob_refs = BlobRefPolicy { roots: vec![allowed.clone()], ..BlobRefPolicy::local_host() };
let server = AssetServer::start(cfg).expect("server start");
let token = std::fs::read_to_string(root.join("admin-token")).unwrap().trim().to_string();
let client = connect(&server, &token, "allowlist_cache");
client
.admit_blob_ref("gen", allowed.join("ok.mp4").to_str().unwrap())
.expect("a path under an allowed root is admitted");
assert!(
client
.admit_blob_ref("gen", forbidden.join("no.mp4").to_str().unwrap())
.is_err(),
"a path outside every allowed root must be refused"
);
// …including one that tries to walk out of an allowed root.
let escape = allowed.join("..").join("allowlist_forbidden").join("no.mp4");
assert!(
client.admit_blob_ref("gen", escape.to_str().unwrap()).is_err(),
"a traversal out of an allowed root must be refused"
);
std::fs::remove_dir_all(&allowed).ok();
std::fs::remove_dir_all(&forbidden).ok();
}
#[test]
fn rescan_over_http_reports_what_went_stale() {
let (server, token, _root) = start_server("rescan_http", true);
let client = connect(&server, &token, "rescan_http_cache");
let media = test_root("rescan_http_media");
std::fs::create_dir_all(&media).unwrap();
let good = media.join("good.mp4");
let gone = media.join("gone.mp4");
std::fs::write(&good, b"STILL-HERE").unwrap();
std::fs::write(&gone, b"NOT-FOR-LONG").unwrap();
client.admit_blob_ref("gen", good.to_str().unwrap()).unwrap();
client.admit_blob_ref("gen", gone.to_str().unwrap()).unwrap();
std::fs::remove_file(&gone).unwrap();
let page = client.blob_refs_page(None, 32).expect("rescan");
assert_eq!(page.total, 2);
assert_eq!(page.refs.len(), 2);
let state_of = |name: &str| {
page.refs
.iter()
.find(|r| r.path.ends_with(name))
.map(|r| r.state.clone())
.unwrap_or_default()
};
assert_eq!(state_of("good.mp4"), "present");
assert_eq!(state_of("gone.mp4"), "missing");
std::fs::remove_dir_all(&media).ok();
}