Files
core/test/llvm_api_source.rs

896 lines
33 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Checked-source HTTP applications linked to the explicit runtime-only archive.
#[cfg(all(
target_os = "linux",
target_arch = "x86_64",
feature = "runtime-sqlite"
))]
#[path = "llvm_job_security_cases.rs"]
mod jobs;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[path = "dever-tests/tests/support/llvm_module.rs"]
mod llvm_module;
#[cfg(all(
target_os = "linux",
target_arch = "x86_64",
feature = "runtime-postgres"
))]
#[path = "dever-tests/tests/support/postgres.rs"]
pub mod postgres;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[path = "dever-tests/tests/support/process.rs"]
mod process;
#[cfg(all(
target_os = "linux",
target_arch = "x86_64",
feature = "runtime-sqlite"
))]
#[path = "llvm_api_security_cases.rs"]
mod security;
#[cfg(all(
target_os = "linux",
target_arch = "x86_64",
feature = "runtime-sqlite"
))]
#[path = "dever-tests/tests/support/sqlite_inspect.rs"]
mod sqlite_inspect;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[path = "dever-tests/tests/support/temp.rs"]
mod temp;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
mod native {
use super::{llvm_module, process, temp};
use dever_backend_bridge::{Target, emit_object};
use dever_core::{hir::Program, source::SourceMap};
use dever_runtime::config::Settings;
use serde_json::{Value, json};
use std::fs;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
pub(super) const TRANSPORT: &[(&str, &str)] = &[
(
"user/account/model.dever",
"global type Account { email: Text(1, 254) }",
),
(
"platform/tenant/model.dever",
"global type Tenant { name: Text(1, 64) }",
),
(
"user/account/app.dever",
r#"type Identity {
id: Text
user_id: user.account.model.id?
tenant_id: platform.tenant.model.id?
}
verify(claims: dever.auth.Claims) (identity: Identity) {
identity = Identity { id = claims.subject
user_id = null
tenant_id = null }
}
"#,
),
(
"sample/echo/app.dever",
r#"type Payload {
name: Text
count: Int
tags: List<Text?>
note: Text?
}
type Snapshot {
request_id: Text
method: Text
path: Text
header: Text?
cookie: Text?
peer: Text?
}
type Outcome {
error Rejected(message: Text)
}
increment(q: Int, note: Text?) (value: Int) { value = q + 1 }
echo(payload: Payload) (value: Payload) { value = payload }
inspect() (value: Snapshot) {
dever.api.set_header("x-response-probe", "committed")
dever.task.sleep(1)
value = Snapshot {
request_id = dever.api.request_id()
method = dever.api.method()
path = dever.api.path()
header = dever.api.header("x-probe")
cookie = dever.api.cookie("probe")
peer = dever.api.client_address()
}
}
invalid() (value: Text) {
dever.api.set_header("x-response-probe", "must-not-commit")
fail(dever.api.Error.Invalid)
}
unauthorized() (value: Text) { fail(dever.api.Error.Unauthorized) }
forbidden() (value: Text) { fail(dever.api.Error.Forbidden) }
missing() (value: Text) { fail(dever.api.Error.NotFound) }
conflict() (value: Text) { fail(dever.api.Error.Conflict) }
limited() (value: Text) { fail(dever.api.Error.TooManyRequests) }
fault() (value: Text) { fail(Outcome.Rejected("must-not-appear-in-http-or-log")) }
upload(file: Upload, caption: Text) (size: Int) {
size = dever.api.upload_size(file)
dever.api.close_upload(file)
}
"#,
),
(
"sample/echo/api.dever",
"public get increment = app.increment\npublic post echo = app.echo\npublic get inspect = app.inspect\npublic get invalid = app.invalid\npublic get unauthorized = app.unauthorized\npublic get forbidden = app.forbidden\npublic get missing = app.missing\npublic get conflict = app.conflict\npublic get limited = app.limited\npublic get fault = app.fault\npublic post upload = app.upload\ncmd echo = app.echo",
),
];
fn echo_files(markdown: bool) -> Vec<(&'static str, &'static str)> {
let mut files = TRANSPORT[..3].to_vec();
if markdown {
files.extend([
(
"sample/echo/app.dever.md",
r#"# Echo application
Owned JSON echo contract.
- 包:`sample.echo.app`
- 公开类型:
- `Payload`
- 公开方法:
- `echo`
- 使用:
- `sample.echo.echo(payload)`
## Payload
Keep concrete input fields.
- 类型:`Payload`
- 字段:
- `name: Text`:Name.
- `count: Int`:Count.
- `tags: List<Text?>`:Tags.
- `note: Text?`:Optional note.
```dever
type Payload { name: Text
count: Int
tags: List<Text?>
note: Text? }
```
## Echo
Return the checked input.
- 函数:`echo`
- 输入:
- `payload: Payload`:Payload.
- 输出:
- `value: Payload`:Payload.
```dever
echo(payload: Payload) (value: Payload) { value = payload }
```
"#,
),
(
"sample/echo/api.dever.md",
r#"# Echo interfaces
HTTP and command-line entry bindings.
- 包:`sample.echo.api`
- 公开类型:无
- 公开方法:无
- 使用:无
## HTTP
Anonymous typed JSON echo.
- 声明:`post echo`
```dever
public post echo = app.echo
```
## Command
Command-line JSON echo.
- 声明:`cmd echo`
```dever
cmd echo = app.echo
```
"#,
),
]);
} else {
files.extend([
("sample/echo/app.dever", "type Payload { name: Text\ncount: Int\ntags: List<Text?>\nnote: Text? }\necho(payload: Payload) (value: Payload) { value = payload }"),
("sample/echo/api.dever", "public post echo = app.echo\ncmd echo = app.echo"),
]);
}
files
}
pub(super) fn settings(port: u16) -> String {
json!({
"http": {"host": "127.0.0.1", "port": port},
"runtime": {"mode": "api", "shutdown_ms": 2000},
"log": {"level": "error"},
"database": {"default": {"type": "sqlite", "path": "data/control.db", "max_connections": 2}},
"auth": {"providers": {"session": {"verify": "user.account.verify", "jwtSecret": "0123456789abcdef0123456789abcdef", "cookie": "test_session"}}},
"sites": {"test": {"path": "", "auth": "session", "hosts": ["localhost"], "origin": "https://localhost"}}
})
.to_string()
}
fn checked(files: &[(&str, &str)], setting: &str) -> (SourceMap, Program) {
let directory = temp::TemporaryDirectory::new();
fs::create_dir(directory.path().join("config")).unwrap();
fs::write(directory.path().join("config/setting.json"), setting).unwrap();
let settings = Settings::load_project(directory.path()).unwrap();
let mut sources = SourceMap::default();
for (path, source) in files {
sources.add(*path, *source);
}
let program =
dever_core::check_with_settings(&sources, &settings).unwrap_or_else(|errors| {
panic!(
"{}",
errors
.iter()
.map(|error| error.render(&sources))
.collect::<String>()
)
});
(sources, program)
}
pub(super) fn application_ir(
files: &[(&str, &str)],
setting: &str,
marker: bool,
failure: Option<u32>,
) -> String {
let (sources, program) = checked(files, setting);
let mut ir = dever_core::llvm::emit_application(&program, &sources).unwrap();
if marker {
ir.push_str("\n@api_probe_cycle = private constant [19 x i8] c\"api-cycle-complete\\00\"\ndeclare i32 @puts(ptr)\ndeclare i32 @fflush(ptr)\n@stderr = external global ptr\ndeclare i64 @fwrite(ptr, i64, i64, ptr)\n");
}
ir.push_str("\ndefine i32 @dever_test_run() {\nentry:\n %out = alloca i8\n %fault = alloca %dever.fault\n %status = call i32 @dever_application_entry(ptr %out, ptr %fault)\n");
match failure {
Some(code) => ir.push_str(&llvm_module::fault_check(code, None)),
None => ir.push_str(" %passed = icmp eq i32 %status, 0"),
}
if marker {
// Owned fixture diagnostics only, while the failed root still owns its buffer.
ir.push_str("\n %diagnostic_ptr = getelementptr %dever.fault, ptr %fault, i32 0, i32 4\n %diagnostic = load { ptr, i64 }, ptr %diagnostic_ptr\n %diagnostic_data = extractvalue { ptr, i64 } %diagnostic, 0\n %diagnostic_size = extractvalue { ptr, i64 } %diagnostic, 1\n %has_diagnostic = icmp ugt i64 %diagnostic_size, 0\n %failed_root = xor i1 %passed, true\n %print_diagnostic = and i1 %failed_root, %has_diagnostic\n br i1 %print_diagnostic, label %diagnose, label %release_root\ndiagnose:\n %diagnostic_stream = load ptr, ptr @stderr\n %diagnostic_written = call i64 @fwrite(ptr %diagnostic_data, i64 1, i64 %diagnostic_size, ptr %diagnostic_stream)\n br label %release_root\nrelease_root:\n");
}
ir.push_str("\n call void @dever_outputs_release(ptr %out)\n call void @dever_fault_release(ptr %fault)\n");
if marker {
ir.push_str(" %printed = call i32 @puts(ptr @api_probe_cycle)\n %flushed = call i32 @fflush(ptr null)\n");
}
ir.push_str(" %exit = select i1 %passed, i32 0, i32 1\n ret i32 %exit\n}\n");
ir
}
#[test]
#[ignore = "requires explicit runtime-api archive; invalid descriptor must not bind a listener"]
fn runtime_rejects_missing_authentication_before_serving() {
let reservation = TcpListener::bind(("127.0.0.1", 0)).unwrap();
let setting = settings(reservation.local_addr().unwrap().port());
let ir = application_ir(TRANSPORT, &setting, false, Some(3));
assert!(ir.contains("ptr @dever_api_auth_0"));
let ir = ir.replace("ptr @dever_api_auth_0", "ptr null");
llvm_module::execute(
&ir,
"invalid HTTP metadata",
64,
Duration::from_secs(15),
&[],
Some(&setting),
);
}
#[test]
#[ignore = "requires explicit runtime-api archive; occupied listener belongs to this test only"]
fn failed_listener_start_drains_application_resources() {
let reservation = TcpListener::bind(("127.0.0.1", 0)).unwrap();
let setting = settings(reservation.local_addr().unwrap().port());
let ir = application_ir(TRANSPORT, &setting, false, Some(3));
llvm_module::execute(
&ir,
"occupied owned listener",
64,
Duration::from_secs(15),
&[],
Some(&setting),
);
}
#[test]
#[ignore = "requires explicit runtime-api archive; reads only owned setting.json and captured stderr"]
fn log_only_cmd_uses_configured_owner_and_flushes_before_return() {
let files = [
(
"sample/log/app.dever",
"write() (okay: Bool) { dever.log.info(\"filtered-info\", {})\n dever.log.warn(\"configured-warning\", {\"component\" = \"probe\"})\n okay = true }",
),
("sample/log/api.dever", "cmd write = app.write"),
];
let mut setting: Value = serde_json::from_str(&settings(8080)).unwrap();
setting["log"]["level"] = "warn".into();
let setting = setting.to_string();
let ir = application_ir(&files, &setting, false, None);
let output = llvm_module::execute(
&ir,
"log-only CMD configuration",
64,
Duration::from_secs(15),
&["sample.log.write", "{}"],
Some(&setting),
);
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(!stderr.contains("filtered-info"));
assert_eq!(stderr.matches("configured-warning").count(), 65);
assert!(stderr.contains("probe"));
}
#[test]
fn http_and_cmd_sources_preserve_checker_and_markdown_contracts() {
let setting = settings(8080);
let (_, dever) = checked(&echo_files(false), &setting);
let (_, markdown) = checked(&echo_files(true), &setting);
assert_eq!(dever.api_snapshot(), markdown.api_snapshot());
assert!(dever.api_snapshot().contains("function sample.echo.echo"));
}
#[test]
fn http_application_emits_objects_for_all_six_targets() {
let ir = application_ir(TRANSPORT, &settings(8080), false, None);
for target in Target::ALL {
assert!(!emit_object(&ir, target).unwrap().is_empty());
}
}
#[test]
fn http_lowering_rejects_unbound_authentication_metadata() {
for declaration in ["get echo = app.echo", "public get echo = app.echo"] {
let mut sources = SourceMap::default();
sources.add(
"sample/echo/app.dever",
"echo() (value: Text) { value = \"owned\" }",
);
sources.add("sample/echo/api.dever", declaration);
let program = dever_core::check(&sources).unwrap();
let error = dever_core::llvm::emit_application(&program, &sources).unwrap_err();
assert!(error.contains("sample/echo/api.dever:1:1"), "{error}");
assert!(error.contains("requires a configured site"), "{error}");
}
}
pub(super) struct Server {
pub(super) directory: temp::TemporaryDirectory,
child: Child,
port: u16,
}
impl Server {
fn start(files: &[(&str, &str)], iterations: usize) -> Self {
Self::start_with(
files,
iterations,
serde_json::from_str(&settings(8080)).unwrap(),
&[],
)
}
pub(super) fn start_with(
files: &[(&str, &str)],
iterations: usize,
mut setting: Value,
commands: &[Vec<String>],
) -> Self {
let reservation = TcpListener::bind(("127.0.0.1", 0)).unwrap();
let port = reservation.local_addr().unwrap().port();
setting["http"]["port"] = port.into();
let setting = setting.to_string();
let ir = application_ir(files, &setting, true, None);
let directory = temp::TemporaryDirectory::new();
fs::create_dir(directory.path().join("config")).unwrap();
fs::write(directory.path().join("config/setting.json"), setting).unwrap();
if !commands.is_empty() {
let program =
llvm_module::link_in(&directory, &ir, "checked management command", 1);
for arguments in commands {
let stderr = directory.path().join("command-stderr");
let status = process::status(
Command::new(&program)
.env_clear()
.args(arguments)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(fs::File::create(&stderr).unwrap()),
Duration::from_secs(10),
)
.unwrap();
assert!(
status.success(),
"management command failed {arguments:?}: {status}\n{}",
fs::read_to_string(stderr).unwrap()
);
}
}
let program =
llvm_module::link_in(&directory, &ir, "checked HTTP application", iterations);
drop(reservation);
let child = Command::new(program)
.env_clear()
.stdin(Stdio::null())
.stdout(fs::File::create(directory.path().join("stdout")).unwrap())
.stderr(fs::File::create(directory.path().join("stderr")).unwrap())
.spawn()
.unwrap();
Self {
directory,
child,
port,
}
}
pub(super) fn ready(&mut self) {
self.ready_at("/sample/echo/increment?q=1");
}
pub(super) fn ready_at(&mut self, path: &str) {
let deadline = Instant::now() + Duration::from_secs(10);
loop {
if let Ok(response) = self.request("GET", path, &[], "") {
assert_eq!(response.status, 200);
assert_eq!(response.body["data"], 2);
return;
}
if let Some(status) = self.child.try_wait().unwrap() {
panic!(
"HTTP application exited before readiness: {status}\n{}",
self.stderr()
);
}
assert!(
Instant::now() < deadline,
"HTTP application did not become ready\n{}",
self.stderr()
);
std::thread::sleep(Duration::from_millis(5));
}
}
pub(super) fn request(
&self,
method: &str,
path: &str,
headers: &[(&str, &str)],
body: &str,
) -> std::io::Result<Reply> {
let mut connection = TcpStream::connect_timeout(
&std::net::SocketAddr::from(([127, 0, 0, 1], self.port)),
Duration::from_secs(1),
)?;
connection.set_read_timeout(Some(Duration::from_secs(5)))?;
connection.set_write_timeout(Some(Duration::from_secs(5)))?;
write!(
connection,
"{method} {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nContent-Length: {}\r\n",
body.len()
)?;
for (name, value) in headers {
write!(connection, "{name}: {value}\r\n")?;
}
write!(connection, "\r\n{body}")?;
let mut raw = Vec::new();
connection.take(1_048_577).read_to_end(&mut raw)?;
assert!(
raw.len() <= 1_048_576,
"fixture response exceeded its bound"
);
if !raw.windows(4).any(|bytes| bytes == b"\r\n\r\n") {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!(
"{method} {path}: no complete HTTP response\n{}",
self.stderr()
),
));
}
Ok(Reply::parse(&raw))
}
pub(super) fn stop_cycle(&mut self, completed: usize) {
assert!(
process::status(
Command::new("/usr/bin/kill")
.env_clear()
.arg("-TERM")
.arg(self.child.id().to_string()),
Duration::from_secs(2),
)
.unwrap()
.success()
);
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let output = fs::read_to_string(self.directory.path().join("stdout")).unwrap();
if output
.lines()
.filter(|line| *line == "api-cycle-complete")
.count()
== completed
{
return;
}
if let Some(status) = self.child.try_wait().unwrap() {
panic!(
"HTTP application exited before completing cycle {completed}: {status}\n{}",
self.stderr()
);
}
assert!(
Instant::now() < deadline,
"HTTP cycle did not drain\n{}",
self.stderr()
);
std::thread::sleep(Duration::from_millis(5));
}
}
pub(super) fn finish(&mut self) {
let status = process::wait(&mut self.child, Duration::from_secs(5)).unwrap();
assert!(
status.success(),
"HTTP application failed allocation/fault oracle: {status}\n{}",
self.stderr()
);
}
pub(super) fn restart(&mut self, mode: &str) {
assert!(
self.child
.try_wait()
.unwrap()
.is_some_and(|status| status.success())
);
let path = self.directory.path().join("config/setting.json");
let mut setting: Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
setting["runtime"]["mode"] = mode.into();
fs::write(path, setting.to_string()).unwrap();
self.child = Command::new(self.directory.path().join("managed-kernel"))
.env_clear()
.stdin(Stdio::null())
.stdout(fs::File::create(self.directory.path().join("stdout")).unwrap())
.stderr(fs::File::create(self.directory.path().join("stderr")).unwrap())
.spawn()
.unwrap();
}
pub(super) fn stderr(&self) -> String {
fs::read_to_string(self.directory.path().join("stderr")).unwrap()
}
fn disconnect_partial_upload(&self) {
let directory = self.directory.path().join("data/tmp");
let mut connection = TcpStream::connect_timeout(
&std::net::SocketAddr::from(([127, 0, 0, 1], self.port)),
Duration::from_secs(1),
)
.unwrap();
connection
.set_write_timeout(Some(Duration::from_secs(1)))
.unwrap();
write!(connection, "POST /media/image/store HTTP/1.1\r\nHost: localhost\r\nContent-Type: multipart/form-data; boundary=boundary\r\nContent-Length: 65536\r\n\r\n--boundary\r\nContent-Disposition: form-data; name=\"file\"; filename=\"partial.txt\"\r\n\r\n").unwrap();
// 超过解码器的一块正文,避免请求头/正文的 TCP 合并决定是否写盘;
// 仍少于 Content-Length,关闭连接才会结束这次不完整上传。
connection.write_all(&[b'x'; 32 * 1024]).unwrap();
let deadline = Instant::now() + Duration::from_secs(3);
while !fs::read_dir(&directory)
.unwrap()
.any(|entry| entry.unwrap().metadata().unwrap().len() > 1024)
{
assert!(
Instant::now() < deadline,
"incomplete body never reached the upload owner"
);
std::thread::sleep(Duration::from_millis(5));
}
connection.shutdown(std::net::Shutdown::Both).unwrap();
drop(connection);
while fs::read_dir(&directory).unwrap().count() != 0 {
assert!(
Instant::now() < deadline,
"disconnected upload retained its temporary file"
);
std::thread::sleep(Duration::from_millis(5));
}
}
}
impl Drop for Server {
fn drop(&mut self) {
if !matches!(self.child.try_wait(), Ok(Some(_))) {
let _ = process::terminate(&mut self.child);
}
}
}
pub(super) struct Reply {
pub(super) status: u16,
pub(super) headers: Vec<(String, String)>,
pub(super) body: Value,
}
impl Reply {
fn parse(raw: &[u8]) -> Self {
let split = raw
.windows(4)
.position(|bytes| bytes == b"\r\n\r\n")
.expect("HTTP response headers");
let header = std::str::from_utf8(&raw[..split]).unwrap();
let mut lines = header.split("\r\n");
let status = lines
.next()
.unwrap()
.split_whitespace()
.nth(1)
.unwrap()
.parse()
.unwrap();
let headers = lines
.map(|line| {
let (name, value) = line.split_once(':').unwrap();
(name.to_ascii_lowercase(), value.trim().to_owned())
})
.collect::<Vec<_>>();
assert!(
!headers
.iter()
.any(|(name, value)| name == "transfer-encoding" && value != "identity"),
"fixture requires a bounded Content-Length response"
);
let body = serde_json::from_slice(&raw[split + 4..]).unwrap();
Self {
status,
headers,
body,
}
}
}
#[test]
#[ignore = "requires explicit runtime-api archive; does not start a listener"]
fn mixed_http_application_dispatches_cmd_without_binding_http() {
let reservation = TcpListener::bind(("127.0.0.1", 0)).unwrap();
let setting = settings(reservation.local_addr().unwrap().port());
let payload = json!({"name": "owned", "count": 4, "tags": [], "note": null});
let ir = application_ir(&echo_files(true), &setting, false, None);
let body = json!({"payload": payload}).to_string();
let output = llvm_module::execute(
&ir,
"API plus CMD",
64,
Duration::from_secs(15),
&["sample.echo.echo", &body],
Some(&setting),
);
let lines = std::str::from_utf8(&output.stdout)
.unwrap()
.lines()
.collect::<Vec<_>>();
assert_eq!(lines.len(), 65);
for line in lines {
assert_eq!(
serde_json::from_str::<Value>(line).unwrap()["data"],
payload
);
}
let ir = application_ir(TRANSPORT, &setting, false, Some(3));
llvm_module::execute(
&ir,
"API CMD rejects invalid input",
64,
Duration::from_secs(15),
&["sample.echo.echo", "{}"],
Some(&setting),
);
}
#[test]
#[ignore = "requires explicit runtime-api archive; starts only test-owned loopback services"]
fn public_http_inputs_context_errors_and_repeated_roots_release_owners() {
let mut server = Server::start(TRANSPORT, 64);
let mut request_ids = std::collections::BTreeSet::new();
for cycle in 1..=65 {
server.ready();
let context = server
.request(
"GET",
"/sample/echo/inspect?ignored=1",
&[("x-probe", "owned"), ("cookie", "probe=ordinary")],
"",
)
.unwrap();
assert_eq!(context.status, 400);
let context = server
.request(
"GET",
"/sample/echo/inspect",
&[("x-probe", "owned"), ("cookie", "probe=ordinary")],
"",
)
.unwrap();
assert_eq!(context.status, 200);
let data = &context.body["data"];
assert_eq!(data["method"], "GET");
assert_eq!(data["path"], "/sample/echo/inspect");
assert_eq!(data["header"], "owned");
assert_eq!(data["cookie"], "ordinary");
assert_eq!(data["peer"], "127.0.0.1");
assert!(request_ids.insert(data["request_id"].as_str().unwrap().to_owned()));
assert!(
context
.headers
.contains(&("x-response-probe".into(), "committed".into()))
);
for path in [
"/sample/echo/increment",
"/sample/echo/increment?q=x",
"/sample/echo/increment?q=1&q=2",
"/sample/echo/increment?q=1&unknown=2",
] {
assert_eq!(server.request("GET", path, &[], "").unwrap().status, 400);
}
let payload = json!({"name": "你好 🌍", "count": 9_007_199_254_740_993_i64, "tags": ["owned", null], "note": null});
let body = json!({"payload": payload}).to_string();
let response = server
.request(
"POST",
"/sample/echo/echo",
&[("content-type", "application/json")],
&body,
)
.unwrap();
assert_eq!(response.status, 200, "{}", response.body);
assert_eq!(
response.body,
json!({"code": 0, "message": "ok", "data": payload})
);
let multipart = "--boundary\r\nContent-Disposition: form-data; name=\"caption\"\r\n\r\nowned\r\n--boundary\r\nContent-Disposition: form-data; name=\"file\"; filename=\"owned.txt\"\r\nContent-Type: text/plain\r\n\r\n你好\r\n--boundary--\r\n";
let response = server
.request(
"POST",
"/sample/echo/upload",
&[("content-type", "multipart/form-data; boundary=boundary")],
multipart,
)
.unwrap();
assert_eq!(response.status, 200);
assert_eq!(response.body["data"], 6);
assert_eq!(
fs::read_dir(server.directory.path().join("data/tmp"))
.unwrap()
.count(),
0
);
for (action, status) in [
("invalid", 400),
("unauthorized", 401),
("forbidden", 403),
("missing", 404),
("conflict", 409),
("limited", 429),
("fault", 500),
] {
let response = server
.request("GET", &format!("/sample/echo/{action}"), &[], "")
.unwrap();
assert_eq!(response.status, status);
assert_eq!(response.body["code"], status);
assert_eq!(response.body["data"], Value::Null);
assert!(
!response
.headers
.iter()
.any(|(name, _)| name == "x-response-probe")
);
assert!(!response.body.to_string().contains("must-not-appear"));
}
assert_eq!(
server
.request("DELETE", "/sample/echo/increment", &[], "")
.unwrap()
.status,
405
);
assert_eq!(
server.request("GET", "/missing", &[], "").unwrap().status,
404
);
server.stop_cycle(cycle);
}
server.finish();
assert!(!server.stderr().contains("must-not-appear-in-http-or-log"));
}
#[test]
#[ignore = "requires explicit runtime-api archive; owns listener, upload files and storage outputs"]
fn upload_adapter_store_and_failed_input_release_affine_owners() {
let mut files = TRANSPORT.to_vec();
files.extend([
(
"media/image/app.dever",
"accept(file: Upload) (key: Uuid) { key = port.store(file) }",
),
(
"media/image/port.dever",
"store(file: Upload) (key: Uuid) fails dever.storage.PutResult",
),
(
"media/image/adapter.dever",
"port.store(file: Upload) (key: Uuid) { key = dever.storage.put(file) }",
),
("media/image/api.dever", "public post store = app.accept"),
]);
let mut server = Server::start(&files, 64);
let multipart = "--boundary\r\nContent-Disposition: form-data; name=\"file\"; filename=\"owned.txt\"\r\nContent-Type: text/plain\r\n\r\n你好\r\n--boundary--\r\n";
for cycle in 1..=65 {
server.ready();
let response = server
.request(
"POST",
"/media/image/store",
&[("content-type", "multipart/form-data; boundary=boundary")],
multipart,
)
.unwrap();
assert_eq!(response.status, 200, "{}", response.body);
let key = response.body["data"].as_str().unwrap();
assert_eq!(
dever_runtime::orm::Uuid::parse(key).unwrap().to_string(),
key
);
let stored = server.directory.path().join("data/upload").join(key);
assert_eq!(fs::read(&stored).unwrap(), "你好".as_bytes());
fs::remove_file(stored).unwrap();
server.disconnect_partial_upload();
// File was decoded before the missing caption was discovered.
assert_eq!(
server
.request(
"POST",
"/sample/echo/upload",
&[("content-type", "multipart/form-data; boundary=boundary")],
multipart
)
.unwrap()
.status,
400
);
assert_eq!(
fs::read_dir(server.directory.path().join("data/tmp"))
.unwrap()
.count(),
0
);
server.stop_cycle(cycle);
}
server.finish();
}
}