Files
core/test/rust/native/application/upload/mod.rs
T
2026-11-03 03:32:07 +00:00

284 lines
11 KiB
Rust

use super::{
http::{Server, request, request_head},
*,
};
use std::{
io::{BufRead, BufReader, Read, Write},
net::{TcpListener, TcpStream},
time::{Duration, Instant},
};
fn copy_sources(source: &Path, target: &Path) {
std::fs::create_dir_all(target).unwrap();
for entry in std::fs::read_dir(source).unwrap() {
let entry = entry.unwrap();
if entry.file_type().unwrap().is_dir() {
copy_sources(&entry.path(), &target.join(entry.file_name()));
} else {
std::fs::copy(entry.path(), target.join(entry.file_name())).unwrap();
}
}
}
fn multipart(bytes: usize, fields: bool) -> String {
let mut body = String::new();
if fields {
for (name, value) in [("label", "snow"), ("count", "7"), ("active", "true")] {
body.push_str(&format!(
"--boundary\r\nContent-Disposition: form-data; name={name}\r\n\r\n{value}\r\n"
));
}
}
body.push_str("--boundary\r\nContent-Disposition: form-data; name=file; filename=\"scene;one.bin\"\r\nContent-Type: application/octet-stream\r\n\r\n");
body.push_str(&"x".repeat(bytes));
body.push_str("\r\n--boundary--\r\n");
body
}
fn wait_until(mut condition: impl FnMut() -> bool) {
let deadline = Instant::now() + Duration::from_secs(5);
while !condition() {
assert!(
Instant::now() < deadline,
"upload condition did not complete"
);
std::thread::sleep(Duration::from_millis(5));
}
}
#[test]
fn automatic_upload_streams_after_auth_and_cleans_failure_timeout_and_disconnect() {
let directory = Directory::new();
let root = directory.0.join("source");
copy_sources(&fixture().with_file_name("auth"), &root);
for (path, text) in [
(
"app/uploads.dever",
include_str!("../../../fixtures/upload/app.dever"),
),
(
"api/admin/uploads.dever",
include_str!("../../../fixtures/upload/api.dever"),
),
(
"port.dever",
include_str!("../../../fixtures/upload/port.dever"),
),
(
"adapter.dever",
include_str!("../../../fixtures/upload/adapter.dever"),
),
] {
std::fs::write(root.join("module/demo/account").join(path), text).unwrap();
}
let mut project = Project::load(&root).unwrap();
// 临时检出仅隔离夹具写入,诊断源码仍指向稳定文件,允许重复验收复用构建。
for source in &mut project.sources {
if let Ok(relative) = source.path.strip_prefix(&root) {
let target = match relative.to_str().unwrap() {
"module/demo/account/app/uploads.dever" => Some("app"),
"module/demo/account/api/admin/uploads.dever" => Some("api"),
"module/demo/account/port.dever" => Some("port"),
"module/demo/account/adapter.dever" => Some("adapter"),
_ => None,
};
let path = target.map_or_else(
|| fixture().with_file_name("auth").join(relative),
|name| {
fixture()
.with_file_name("upload")
.join(format!("{name}.dever"))
},
);
*source = dever::syntax::source::Source::new(source.id, path, source.text.clone());
}
}
let program = project.check().unwrap();
let binary = build::build(
&program,
&project.sources,
"main",
&directory.0.join("upload"),
)
.unwrap();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
std::fs::create_dir(directory.0.join("config")).unwrap();
let settings = serde_json::json!({
"http":{"host":"127.0.0.1","port":port,"limits":{"body_bytes":1024,"timeout_ms":1200},"upload":{"total_bytes":131072,"file_bytes":65536,"field_bytes":256}},
"sites":{"admin":{"path":"admin","auth":"session","hosts":["fixture.test"],"origin":"https://fixture.test"}},
"database":{"default":{"type":"sqlite","path":"data/account.db","max_connections":4}},
"runtime":{"mode":"api","shutdown_ms":1000},
"auth":{"providers":{"session":{"verify":"demo.account.verify","jwtSecret":"upload-native-test-key-not-for-deployment","ttlSeconds":60,"cookie":"session"}}}
});
std::fs::write(
directory.0.join("config/setting.json"),
settings.to_string(),
)
.unwrap();
let seeded = execute(
&binary,
"demo.account.seed",
r#"{"subject":"user","session":"valid-session"}"#,
);
assert!(
seeded.status.success(),
"{}",
String::from_utf8_lossy(&seeded.stderr)
);
let mut server = Server(Command::new(&binary).spawn().unwrap());
wait_until(|| {
assert!(server.0.try_wait().unwrap().is_none());
TcpStream::connect(("127.0.0.1", port)).is_ok()
});
let temporary = directory.0.join("data/tmp");
let count = || std::fs::read_dir(&temporary).map_or(0, |entries| entries.count());
let target = "/demo/account/admin/uploads/inspect";
// 未认证请求不必上传正文即可收到拒绝,也不能创建临时文件。
let mut unauthenticated = TcpStream::connect(("127.0.0.1", port)).unwrap();
unauthenticated
.set_read_timeout(Some(Duration::from_secs(3)))
.unwrap();
write!(unauthenticated, "POST {target} HTTP/1.1\r\nHost: fixture.test\r\nConnection: close\r\nContent-Type: multipart/form-data; boundary=boundary\r\nContent-Length: 60000\r\n\r\n").unwrap();
let mut response = String::new();
unauthenticated.read_to_string(&mut response).unwrap();
assert!(response.starts_with("HTTP/1.1 401"), "{response}");
assert_eq!(count(), 0);
let (status, head, _) = request_head(
port,
"POST",
"/demo/account/admin/session/login",
"fixture.test",
"Origin: https://fixture.test\r\nContent-Type: application/json\r\n",
r#"{"subject":"user","session":"valid-session"}"#,
);
assert_eq!(status, 200, "{head}");
let cookie = head
.lines()
.find_map(|line| line.strip_prefix("set-cookie: "))
.unwrap()
.split(';')
.next()
.unwrap();
let headers = format!(
"Cookie: {cookie}\r\nOrigin: https://fixture.test\r\nContent-Type: multipart/form-data; boundary=boundary\r\n"
);
let input = multipart(50_000, true);
let (status, body) = request(port, "POST", target, "fixture.test", &headers, &input);
assert_eq!(status, 200, "{body}");
let value: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(
value["data"],
serde_json::json!({"size":50000,"filename":"scene;one.bin","echo":"snow","increment":8,"enabled":true})
);
assert_eq!(count(), 0);
let (status, body) = request(
port,
"POST",
"/demo/account/admin/uploads/store",
"fixture.test",
&headers,
&multipart(50_000, false),
);
assert_eq!(status, 200, "{body}");
let stored: serde_json::Value = serde_json::from_str(&body).unwrap();
let key = stored["data"].as_str().unwrap();
assert_eq!(
std::fs::read(directory.0.join("data/upload").join(key)).unwrap(),
vec![b'x'; 50_000]
);
assert_eq!(count(), 0);
let mut connection = BufReader::new(TcpStream::connect(("127.0.0.1", port)).unwrap());
connection
.get_mut()
.set_read_timeout(Some(Duration::from_secs(4)))
.unwrap();
let json_headers = format!(
"Cookie: {cookie}\r\nOrigin: https://fixture.test\r\nContent-Type: application/json\r\n"
);
for (path, headers, body) in [
(target, headers.as_str(), input.as_str()),
(
"/demo/account/admin/session/me",
json_headers.as_str(),
"{}",
),
(target, headers.as_str(), input.as_str()),
] {
write!(connection.get_mut(), "POST {path} HTTP/1.1\r\nHost: fixture.test\r\nContent-Length: {}\r\n{headers}\r\n{body}", body.len()).unwrap();
let mut status = String::new();
connection.read_line(&mut status).unwrap();
assert!(status.starts_with("HTTP/1.1 200"), "{status}");
let mut length = None;
loop {
let mut line = String::new();
connection.read_line(&mut line).unwrap();
if line == "\r\n" {
break;
}
if let Some(value) = line.to_ascii_lowercase().strip_prefix("content-length:") {
length = Some(value.trim().parse::<usize>().unwrap());
}
}
let mut output = vec![0; length.unwrap()];
connection.read_exact(&mut output).unwrap();
assert_eq!(count(), 0);
}
drop(connection);
for (path, input, expected) in [
(
"/demo/account/admin/uploads/reject",
multipart(32_768, false),
409,
),
(target, multipart(70_000, true), 413),
(target, input.replace("name=count", "name=unknown"), 400),
(
target,
multipart(10, true).replace("\r\n7\r\n", "\r\nwrong\r\n"),
400,
),
(
target,
multipart(10, true).replace("scene;one.bin", "../escape"),
400,
),
] {
let (status, body) = request(port, "POST", path, "fixture.test", &headers, &input);
assert_eq!(status, expected, "{body}");
assert_eq!(count(), 0);
}
// 正在接收文件时分别断开与超时,等待真实文件清理后才确认返回。
for disconnect in [true, false] {
let mut socket = TcpStream::connect(("127.0.0.1", port)).unwrap();
socket
.set_read_timeout(Some(Duration::from_secs(4)))
.unwrap();
let mut input = multipart(32_768, true);
input.truncate(input.len() - 16);
write!(socket, "POST {target} HTTP/1.1\r\nHost: fixture.test\r\nConnection: close\r\nContent-Length: 60000\r\n{headers}\r\n{input}").unwrap();
wait_until(|| count() == 1);
if !disconnect {
let mut response = String::new();
socket.read_to_string(&mut response).unwrap();
assert!(response.starts_with("HTTP/1.1 504"), "{response}");
}
drop(socket);
wait_until(|| count() == 0);
}
// 同一服务中的旧 JSON/REST 入口仍通过原有有界读取。
assert_eq!(
request(
port,
"GET",
"/demo/account/admin/session/health",
"fixture.test",
"",
""
)
.0,
200
);
}