Skip to content

Commit 5362135

Browse files
ehsan6shaclaude
andcommitted
P2: real-server streaming upload e2e -- PASSES byte-exact (50MB / 200 chunks)
Real-gateway e2e (#[ignore]; run with the Mode A creds): drives the streaming sequence (begin -> plan-only encoder -> finalize_plan -> put_chunk loop -> finish) for a 50 MB / 200-chunk file, downloads via get_object_flat, asserts byte-exact. 200 chunks @ 256 KB pushes the index metadata past the 16 KB header budget, so it also exercises header_safe_enc_metadata stripping + body/forest fallback on the real server. Verified on the production gateway: 52,428,800 bytes round-tripped byte-exact in 108s. Complements the hermetic streaming_upload_roundtrip.rs (mock). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 517c292 commit 5362135

1 file changed

Lines changed: 154 additions & 0 deletions

File tree

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
//! E2E (real server): the PUSH-model streaming upload must produce a normally
2+
//! downloadable, BYTE-EXACT object on the real gateway. Validates P2 of
3+
//! docs/web-streaming-resumable-upload-plan.md against production — not just the
4+
//! hermetic mock in streaming_upload_roundtrip.rs.
5+
//!
6+
//! Drives the exact sequence the FRB handle uses:
7+
//! streaming_begin -> plan-only ChunkedEncoder (pass 1) -> streaming_finalize_plan
8+
//! -> streaming_put_chunk loop (pass 2) -> streaming_finish
9+
//! then downloads via get_object_flat and asserts byte-exact. The file is large
10+
//! enough (~50 MB, ~200 chunks at 256 KB) that the index metadata exceeds the
11+
//! gateway's 16 KB header budget, so this also exercises header_safe_enc_metadata
12+
//! stripping + the body/forest fallback on the real server.
13+
//!
14+
//! `#[ignore]` — needs network + real credentials. Run (PowerShell, env loaded
15+
//! from e2e-credentials.env):
16+
//! cargo test -p fula-client --test streaming_upload_e2e --release -- --ignored --nocapture
17+
//!
18+
//! Required env: FULA_S3, FULA_JWT, FULA_TEST_PROVIDER, FULA_TEST_OAUTH_SUB,
19+
//! FULA_TEST_EMAIL (the Mode A derivation triple).
20+
21+
#![cfg(not(target_arch = "wasm32"))]
22+
23+
use fula_client::{Config, EncryptedClient, EncryptionConfig};
24+
use fula_crypto::chunked::ChunkedEncoder;
25+
use fula_crypto::keys::SecretKey;
26+
27+
fn env(name: &str) -> String {
28+
std::env::var(name).unwrap_or_else(|_| panic!("missing required env {name}"))
29+
}
30+
31+
#[tokio::test]
32+
#[ignore = "real-server; needs FULA_S3 + FULA_JWT + Mode A triple"]
33+
async fn streaming_upload_large_file_roundtrips_on_real_server() {
34+
let s3 = env("FULA_S3");
35+
let jwt = env("FULA_JWT");
36+
let input = format!(
37+
"{}:{}:{}",
38+
env("FULA_TEST_PROVIDER"),
39+
env("FULA_TEST_OAUTH_SUB"),
40+
env("FULA_TEST_EMAIL"),
41+
);
42+
let kek = fula_crypto::hashing::derive_key_argon2id("fula-files-v1", input.as_bytes());
43+
let secret = SecretKey::from_bytes(&kek).expect("32-byte secret from Argon2id");
44+
45+
let mut config = Config::new(&s3).with_token(&jwt);
46+
// Match FxFiles production: stamps chunk_cids into the index metadata.
47+
config.walkable_v8_writer_enabled = true;
48+
let client = EncryptedClient::new(config, EncryptionConfig::from_secret_key(secret))
49+
.expect("EncryptedClient::new");
50+
51+
let epoch = std::time::SystemTime::now()
52+
.duration_since(std::time::UNIX_EPOCH)
53+
.unwrap()
54+
.as_secs();
55+
let bucket = format!("e2e-streaming-{epoch}-v8");
56+
eprintln!("[streaming_e2e] BUCKET={bucket}");
57+
if let Err(e) = client.create_bucket(&bucket).await {
58+
eprintln!("[streaming_e2e] create_bucket({bucket}) -> {e} (continuing)");
59+
}
60+
61+
// ~50 MB -> ~200 chunks at 256 KB -> index metadata over the 16 KB header
62+
// budget (exercises header_safe_enc_metadata stripping + body fallback).
63+
let size = 50 * 1024 * 1024;
64+
let mut data = vec![0u8; size];
65+
for (i, b) in data.iter_mut().enumerate() {
66+
*b = ((i * 31 + 7) % 251) as u8;
67+
}
68+
let key_path = "/big-streamed.bin";
69+
70+
// ---- streaming upload (push model; mirrors the FRB handle) ----
71+
eprintln!("[streaming_e2e] streaming_begin...");
72+
let (storage_key, dek, wrapped_dek, kek_version) = client
73+
.streaming_begin(&bucket, key_path)
74+
.await
75+
.expect("streaming_begin");
76+
77+
// pass 1: plan-only encoder (default 256 KB chunks, matching production),
78+
// fed in arbitrary 1 MiB streaming slices.
79+
let aad_prefix = format!("fula:v4:chunk:{}", storage_key);
80+
let mut encoder =
81+
ChunkedEncoder::with_aad(dek.clone(), aad_prefix.into_bytes()).into_plan_only();
82+
eprintln!("[streaming_e2e] pass 1 (plan)...");
83+
for slice in data.chunks(1024 * 1024) {
84+
encoder.update(slice).expect("plan update");
85+
}
86+
let (chunked_metadata, private_meta, encrypted_meta) = client
87+
.streaming_finalize_plan(encoder, &dek, &storage_key, key_path, Some("application/octet-stream"))
88+
.expect("streaming_finalize_plan");
89+
90+
let cs = chunked_metadata.chunk_size as usize;
91+
let num_chunks = chunked_metadata.num_chunks as usize;
92+
eprintln!("[streaming_e2e] {num_chunks} chunks @ {cs} bytes; pass 2 (upload)...");
93+
assert!(
94+
num_chunks > 120,
95+
"need >120 chunks to exceed the 16 KB header budget (got {num_chunks})"
96+
);
97+
98+
// pass 2: upload each chunk from its committed nonce.
99+
let mut chunk_cids = vec![None; num_chunks];
100+
for i in 0..num_chunks {
101+
let start = i * cs;
102+
let end = ((i + 1) * cs).min(data.len());
103+
let (_chunk_key, cid) = client
104+
.streaming_put_chunk(
105+
&bucket,
106+
&storage_key,
107+
&chunked_metadata,
108+
i as u32,
109+
&data[start..end],
110+
&dek,
111+
)
112+
.await
113+
.unwrap_or_else(|e| panic!("streaming_put_chunk[{i}] failed: {e}"));
114+
chunk_cids[i] = cid;
115+
if i % 50 == 0 {
116+
eprintln!("[streaming_e2e] uploaded chunk {i}/{num_chunks}");
117+
}
118+
}
119+
120+
eprintln!("[streaming_e2e] streaming_finish...");
121+
client
122+
.streaming_finish(
123+
&bucket,
124+
key_path,
125+
&storage_key,
126+
&wrapped_dek,
127+
&encrypted_meta,
128+
kek_version,
129+
&private_meta,
130+
chunked_metadata,
131+
chunk_cids,
132+
)
133+
.await
134+
.expect("streaming_finish");
135+
eprintln!("[streaming_e2e] upload OK");
136+
137+
// ---- download + verify byte-exact (the gate) ----
138+
eprintln!("[streaming_e2e] downloading via get_object_flat...");
139+
let downloaded = client
140+
.get_object_flat(&bucket, key_path)
141+
.await
142+
.expect("get_object_flat");
143+
assert_eq!(downloaded.len(), data.len(), "round-trip length mismatch");
144+
assert_eq!(
145+
downloaded.as_ref(),
146+
data.as_slice(),
147+
"streaming upload -> download MUST be byte-exact on the real gateway"
148+
);
149+
eprintln!(
150+
"[streaming_e2e] OK: {} bytes ({} chunks) round-tripped byte-exact via streaming on the real gateway",
151+
data.len(),
152+
num_chunks
153+
);
154+
}

0 commit comments

Comments
 (0)