Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,424 @@
|
||||
use super::*;
|
||||
#[path = "http/choice.rs"]
|
||||
mod choice;
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
net::{TcpListener, TcpStream},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub(super) struct Server(pub(super) std::process::Child);
|
||||
impl Drop for Server {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.0.kill();
|
||||
let _ = self.0.wait();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn request(
|
||||
port: u16,
|
||||
method: &str,
|
||||
path: &str,
|
||||
host: &str,
|
||||
headers: &str,
|
||||
body: &str,
|
||||
) -> (u16, String) {
|
||||
let (status, _, body) = request_head(port, method, path, host, headers, body);
|
||||
(status, body)
|
||||
}
|
||||
|
||||
pub(super) fn request_head(
|
||||
port: u16,
|
||||
method: &str,
|
||||
path: &str,
|
||||
host: &str,
|
||||
headers: &str,
|
||||
body: &str,
|
||||
) -> (u16, String, String) {
|
||||
let mut socket = TcpStream::connect(("127.0.0.1", port)).unwrap();
|
||||
socket
|
||||
.set_read_timeout(Some(Duration::from_secs(3)))
|
||||
.unwrap();
|
||||
write!(socket, "{method} {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\nContent-Length: {}\r\n{headers}\r\n{body}", body.len()).unwrap();
|
||||
let mut response = String::new();
|
||||
socket.read_to_string(&mut response).unwrap();
|
||||
let (head, body) = response.split_once("\r\n\r\n").unwrap();
|
||||
(
|
||||
head.split_whitespace().nth(1).unwrap().parse().unwrap(),
|
||||
head.into(),
|
||||
body.into(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_cmd_api_budget_keeps_input_boundaries_and_raw_spelling() {
|
||||
let directory = Directory::new();
|
||||
let root = directory.0.join("module/demo/input");
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
std::fs::write(
|
||||
root.join("app.dever"),
|
||||
r#"type Packet { payload: Json
|
||||
note: Text?
|
||||
count: Int
|
||||
extra: Json? }
|
||||
|
||||
accept(value: Packet) (answer: Int) { answer = value.count }
|
||||
|
||||
echo(value: Json) (answer: Json) { answer = value }
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let mut api = "".to_owned();
|
||||
for (method, visibility) in [("cmd", ""), ("post", "public ")] {
|
||||
for name in ["accept", "echo"] {
|
||||
api.push_str(&format!("{visibility}{method} {name} = app.{name}\n"));
|
||||
}
|
||||
}
|
||||
std::fs::write(root.join("api.dever"), api).unwrap();
|
||||
let project = Project::load(&directory.0).unwrap();
|
||||
let program = project.check().unwrap();
|
||||
let binary =
|
||||
build::build(&program, &project.sources, "main", &directory.0.join("app")).unwrap();
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
drop(listener);
|
||||
let config = directory.0.join("setting.json");
|
||||
std::fs::write(&config, format!("{{\"http\":{{\"host\":\"127.0.0.1\",\"port\":{port}}},\"sites\":{{\"public\":{{\"path\":\"\",\"hosts\":[\"fixture.test\"]}}}}}}" )).unwrap();
|
||||
let raw = r#"{ "values": [1e9999,-0,1.00e+2,"\u96ea"] }"#;
|
||||
let input = format!("{{\"value\":{raw}}}");
|
||||
let expected = format!("{{\"code\":0,\"message\":\"ok\",\"data\":{raw}}}");
|
||||
let command = Command::new(&binary)
|
||||
.args([
|
||||
"--config",
|
||||
config.to_str().unwrap(),
|
||||
"demo.input.echo",
|
||||
&input,
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(command.status.success(), "{:?}", command.stderr);
|
||||
assert_eq!(
|
||||
String::from_utf8(command.stdout).unwrap(),
|
||||
format!("{expected}\n")
|
||||
);
|
||||
let mut server = Server(
|
||||
Command::new(&binary)
|
||||
.args(["--config", config.to_str().unwrap()])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
);
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while TcpStream::connect(("127.0.0.1", port)).is_err() {
|
||||
assert!(server.0.try_wait().unwrap().is_none());
|
||||
assert!(Instant::now() < deadline);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
let call = |name: &str, input: &str| {
|
||||
request(
|
||||
port,
|
||||
"POST",
|
||||
&format!("/demo/input/{name}"),
|
||||
"fixture.test",
|
||||
"Content-Type: application/json\r\n",
|
||||
input,
|
||||
)
|
||||
};
|
||||
assert_eq!(call("echo", &input), (200, expected));
|
||||
for input in [
|
||||
r#"[{"payload":null,"count":0}]"#,
|
||||
r#"{"value":[null,null,0,null]}"#,
|
||||
r#"{"value":{"payload":null,"note":null,"note":null,"count":0}}"#,
|
||||
r#"{"value":{"payload":{"a":0,"\u0061":1},"count":0}}"#,
|
||||
r#"{"value":{"payload":"\ud800","count":0}}"#,
|
||||
r#"{"value":{"payload":null,"count":0,"unknown":0}}"#,
|
||||
r#"{"value":{"payload":null,"count":0,"extra":null,"extra":null}}"#,
|
||||
r#"{"value":{"payload":null,"count":0}} {}"#,
|
||||
] {
|
||||
assert_eq!(call("accept", input).0, 400, "{input}");
|
||||
let command = Command::new(&binary)
|
||||
.args([
|
||||
"--config",
|
||||
config.to_str().unwrap(),
|
||||
"demo.input.accept",
|
||||
input,
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!command.status.success());
|
||||
assert!(String::from_utf8_lossy(&command.stderr).contains("invalid command input"));
|
||||
}
|
||||
// 总值数边界的正文超过 Linux 单个 argv 的限制,通过 HTTP 验证同一 codec。
|
||||
let limit = dever_runtime::wire::document::MAX_VALUES;
|
||||
for (count, extra) in [(limit - 4, ""), (limit - 6, ",\"extra\":[0]")] {
|
||||
let payload = vec!["0"; count].join(",");
|
||||
for (note, status) in [("", 200), (",\"note\":null", 400)] {
|
||||
let input =
|
||||
format!("{{\"value\":{{\"payload\":[{payload}],\"count\":0{extra}{note}}}}}");
|
||||
assert_eq!(call("accept", &input).0, status);
|
||||
}
|
||||
}
|
||||
for (depth, status) in [(62, 200), (63, 400)] {
|
||||
let payload = "[".repeat(depth) + &"]".repeat(depth);
|
||||
let input = format!("{{\"value\":{{\"payload\":{payload},\"count\":0}}}}");
|
||||
assert_eq!(call("accept", &input).0, status);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_http_runs_dever_app_strict_inputs_sites_faults_cmd_and_shutdown() {
|
||||
application(true);
|
||||
application(false);
|
||||
}
|
||||
|
||||
fn application(commands: bool) {
|
||||
let directory = Directory::new();
|
||||
let mut project = Project::load(&fixture().with_file_name("http-application")).unwrap();
|
||||
if !commands {
|
||||
let source = project
|
||||
.sources
|
||||
.iter_mut()
|
||||
.find(|source| source.path.ends_with("greeting/api.dever"))
|
||||
.unwrap();
|
||||
*source = dever::syntax::source::Source::new(
|
||||
source.id,
|
||||
&source.path,
|
||||
source.text.split("cmd ").next().unwrap().into(),
|
||||
);
|
||||
}
|
||||
if commands {
|
||||
let source = project
|
||||
.sources
|
||||
.iter_mut()
|
||||
.find(|source| source.path.ends_with("greeting/api.dever"))
|
||||
.unwrap();
|
||||
*source = dever::syntax::source::Source::new(
|
||||
source.id,
|
||||
&source.path,
|
||||
format!("{}\npublic post greet = app.greet\n", source.text),
|
||||
);
|
||||
}
|
||||
let program = project.check().unwrap();
|
||||
let binary =
|
||||
build::build(&program, &project.sources, "main", &directory.0.join("app")).unwrap();
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
drop(listener);
|
||||
let config = directory.0.join("setting.json");
|
||||
std::fs::write(&config, format!("{{\"http\":{{\"host\":\"127.0.0.1\",\"port\":{port}}},\"sites\":{{\"public\":{{\"path\":\"\",\"hosts\":[\"fixture.test\"]}}}}}}" )).unwrap();
|
||||
let fragment = serde_json::json!({"source": " \n{\"a\": [1e9999,-0]}\t "}).to_string();
|
||||
let expected_fragment = "{\"code\":0,\"message\":\"ok\",\"data\":{\"value\":{\"a\": [1e9999,-0]},\"label\":\"雪\\\"\"}}";
|
||||
if commands {
|
||||
let command = Command::new(&binary)
|
||||
.args([
|
||||
"--config",
|
||||
config.to_str().unwrap(),
|
||||
"demo.greeting.greet",
|
||||
"{\"name\":\"CMD\"}",
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
command.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&command.stderr)
|
||||
);
|
||||
assert!(String::from_utf8_lossy(&command.stdout).contains("Hello, CMD"));
|
||||
let command = Command::new(&binary)
|
||||
.args([
|
||||
"--config",
|
||||
config.to_str().unwrap(),
|
||||
"demo.greeting.fragment",
|
||||
&fragment,
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
command.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&command.stderr)
|
||||
);
|
||||
assert_eq!(
|
||||
String::from_utf8(command.stdout).unwrap(),
|
||||
format!("{expected_fragment}\n")
|
||||
);
|
||||
}
|
||||
let mut server = Server(
|
||||
Command::new(&binary)
|
||||
.args(["--config", config.to_str().unwrap()])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
);
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while TcpStream::connect(("127.0.0.1", port)).is_err() {
|
||||
assert!(
|
||||
server.0.try_wait().unwrap().is_none(),
|
||||
"server exited during startup"
|
||||
);
|
||||
assert!(Instant::now() < deadline, "server did not bind");
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
let (status, body) = request(
|
||||
port,
|
||||
"POST",
|
||||
"/demo/greeting/fragment",
|
||||
"fixture.test",
|
||||
"Content-Type: application/json\r\n",
|
||||
&fragment,
|
||||
);
|
||||
assert_eq!(status, 200, "{body}");
|
||||
assert_eq!(body, expected_fragment);
|
||||
let (status, head, body) =
|
||||
request_head(port, "PUT", "/demo/greeting/greet", "fixture.test", "", "");
|
||||
assert_eq!(status, 405);
|
||||
assert_eq!(
|
||||
body,
|
||||
"{\"code\":405,\"message\":\"method not allowed\",\"data\":null}"
|
||||
);
|
||||
let allowed = head
|
||||
.lines()
|
||||
.filter_map(|line| line.split_once(':'))
|
||||
.find(|(name, _)| name.eq_ignore_ascii_case("allow"))
|
||||
.unwrap()
|
||||
.1
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
allowed,
|
||||
if commands {
|
||||
vec!["GET", "POST"]
|
||||
} else {
|
||||
vec!["GET"]
|
||||
}
|
||||
.into_iter()
|
||||
.collect()
|
||||
);
|
||||
for (path, host) in [
|
||||
("/demo/greeting/greet", "wrong.test"),
|
||||
("/demo/greeting/absent", "fixture.test"),
|
||||
] {
|
||||
let (status, head, _) = request_head(port, "PUT", path, host, "", "");
|
||||
assert_eq!(status, 404);
|
||||
assert!(!head.to_ascii_lowercase().contains("\r\nallow:"));
|
||||
}
|
||||
for expected in [400, 401, 403, 404, 409, 429, 500] {
|
||||
let (status, body) = request(
|
||||
port,
|
||||
"GET",
|
||||
&format!("/demo/greeting/reject?status={expected}"),
|
||||
"fixture.test",
|
||||
"",
|
||||
"",
|
||||
);
|
||||
assert_eq!(status, expected, "{body}");
|
||||
let body: serde_json::Value = serde_json::from_str(&body).unwrap();
|
||||
assert_eq!(body["code"], expected);
|
||||
assert!(body["data"].is_null());
|
||||
assert!(!body.to_string().contains("Problem"));
|
||||
}
|
||||
let (status, body) = request(
|
||||
port,
|
||||
"GET",
|
||||
"/demo/greeting/greet?name=%E9%9B%AA",
|
||||
"fixture.test",
|
||||
"Cookie: stale=ignored\r\n",
|
||||
"",
|
||||
);
|
||||
assert_eq!(status, 200);
|
||||
assert_eq!(
|
||||
body,
|
||||
"{\"code\":0,\"message\":\"ok\",\"data\":\"Hello, 雪\"}"
|
||||
);
|
||||
for path in [
|
||||
"/demo/greeting/greet",
|
||||
"/demo/greeting/greet?name=a&name=b",
|
||||
"/demo/greeting/greet?name=a&%6Eame=b",
|
||||
"/demo/greeting/greet?name=%ZZ",
|
||||
"/demo/greeting/greet?name=%",
|
||||
"/demo/greeting/greet?name=%FF",
|
||||
"/demo/greeting/greet?name",
|
||||
"/demo/greeting/greet?name=a&extra=b",
|
||||
] {
|
||||
assert_eq!(request(port, "GET", path, "fixture.test", "", "").0, 400);
|
||||
}
|
||||
assert_eq!(
|
||||
request(
|
||||
port,
|
||||
"GET",
|
||||
"/demo/greeting/greet?name=a",
|
||||
"wrong.test",
|
||||
"",
|
||||
""
|
||||
)
|
||||
.0,
|
||||
404
|
||||
);
|
||||
assert_eq!(
|
||||
request(
|
||||
port,
|
||||
"GET",
|
||||
"/demo/greeting/greet?name=a",
|
||||
"fixture.test",
|
||||
"Authorization: Bearer invalid\r\n",
|
||||
""
|
||||
)
|
||||
.0,
|
||||
401
|
||||
);
|
||||
let json = "Content-Type: application/json\r\n";
|
||||
for (body, status) in [
|
||||
("{\"value\":41}", 200),
|
||||
("{\"value\":1,\"value\":2}", 400),
|
||||
("{\"value\":1.2}", 400),
|
||||
("{\"value\":1,\"extra\":2}", 400),
|
||||
("[1]", 400),
|
||||
("{\"value\":null}", 400),
|
||||
("{\"value\":9223372036854775807}", 500),
|
||||
] {
|
||||
let (actual, response) = request(
|
||||
port,
|
||||
"POST",
|
||||
"/demo/greeting/pair",
|
||||
"fixture.test",
|
||||
json,
|
||||
body,
|
||||
);
|
||||
assert_eq!(actual, status, "{response}");
|
||||
assert!(!response.contains("arithmetic fault"));
|
||||
}
|
||||
assert_eq!(
|
||||
request(
|
||||
port,
|
||||
"POST",
|
||||
"/demo/greeting/pair",
|
||||
"fixture.test",
|
||||
"",
|
||||
"{\"value\":1}"
|
||||
)
|
||||
.0,
|
||||
415
|
||||
);
|
||||
assert!(
|
||||
Command::new("sh")
|
||||
.args([
|
||||
"-c",
|
||||
"kill -TERM \"$1\"",
|
||||
"dever-test",
|
||||
&server.0.id().to_string()
|
||||
])
|
||||
.status()
|
||||
.unwrap()
|
||||
.success()
|
||||
);
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
loop {
|
||||
if let Some(status) = server.0.try_wait().unwrap() {
|
||||
assert!(status.success());
|
||||
break;
|
||||
}
|
||||
assert!(Instant::now() < deadline, "shutdown did not release server");
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user