parent
154f0b97bf
commit
ccdbf1307c
@ -1,4 +1,9 @@ |
|||||||
.PHONY: make |
.PHONY: make update-psl |
||||||
|
|
||||||
make: |
make: |
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
|
# Refresh the vendored Public Suffix List used for cookie domain checks
|
||||||
|
# (compiled into the binary by stdlib::http).
|
||||||
|
update-psl: |
||||||
|
curl -sfL https://publicsuffix.org/list/public_suffix_list.dat -o assets/public_suffix_list.dat
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,543 @@ |
|||||||
|
//! End-to-end tests for the http module against a local `tiny_http` server:
|
||||||
|
//! request building (form/json/headers/auth), redirect behaviour, the session
|
||||||
|
//! cookie jar, and jar persistence. Cookie *policy* (public suffix, Secure,
|
||||||
|
//! expiry) is unit-tested in `stdlib::http`.
|
||||||
|
|
||||||
|
use std::sync::{Arc, Mutex}; |
||||||
|
use std::thread::JoinHandle; |
||||||
|
|
||||||
|
use super::lua as sandboxed_lua; |
||||||
|
|
||||||
|
/// The stdlib Lua with networking and filesystem allowed (the default
|
||||||
|
/// test policy is deny-all), plus a `BASE` global pointing at `server`.
|
||||||
|
fn lua_net(server: &TestServer) -> mlua::Lua { |
||||||
|
let lua = sandboxed_lua(); |
||||||
|
lua.set_app_data(Arc::new(crate::stdlib::fs::FsPolicy::allow_all())); |
||||||
|
lua.globals().set("BASE", server.base.clone()).unwrap(); |
||||||
|
lua |
||||||
|
} |
||||||
|
|
||||||
|
/// A request as the server saw it, for asserting on from the test thread
|
||||||
|
/// (a panic inside the server thread would be silently swallowed).
|
||||||
|
#[derive(Clone, Debug)] |
||||||
|
struct Captured { |
||||||
|
method: String, |
||||||
|
url: String, |
||||||
|
headers: Vec<(String, String)>, |
||||||
|
body: String, |
||||||
|
} |
||||||
|
|
||||||
|
impl Captured { |
||||||
|
fn header(&self, name: &str) -> Option<&str> { |
||||||
|
self.headers.iter().find(|(k, _)| k == name).map(|(_, v)| v.as_str()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
struct TestServer { |
||||||
|
base: String, |
||||||
|
seen: Arc<Mutex<Vec<Captured>>>, |
||||||
|
server: Arc<tiny_http::Server>, |
||||||
|
thread: Option<JoinHandle<()>>, |
||||||
|
} |
||||||
|
|
||||||
|
impl TestServer { |
||||||
|
/// All requests observed so far, in arrival order.
|
||||||
|
fn requests(&self) -> Vec<Captured> { |
||||||
|
self.seen.lock().unwrap().clone() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl Drop for TestServer { |
||||||
|
fn drop(&mut self) { |
||||||
|
self.server.unblock(); |
||||||
|
if let Some(t) = self.thread.take() { |
||||||
|
let _ = t.join(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Spawn a local HTTP server; `handler` maps each captured request to a
|
||||||
|
/// response. Every request is also recorded for later assertions.
|
||||||
|
fn serve( |
||||||
|
handler: impl Fn(&Captured) -> tiny_http::Response<std::io::Cursor<Vec<u8>>> |
||||||
|
+ Send |
||||||
|
+ Sync |
||||||
|
+ 'static, |
||||||
|
) -> TestServer { |
||||||
|
let server = Arc::new(tiny_http::Server::http("127.0.0.1:0").unwrap()); |
||||||
|
let base = format!("http://{}", server.server_addr().to_ip().unwrap()); |
||||||
|
let seen: Arc<Mutex<Vec<Captured>>> = Arc::default(); |
||||||
|
|
||||||
|
let thread = { |
||||||
|
let server = server.clone(); |
||||||
|
let seen = seen.clone(); |
||||||
|
std::thread::spawn(move || { |
||||||
|
for mut req in server.incoming_requests() { |
||||||
|
let mut body = String::new(); |
||||||
|
req.as_reader().read_to_string(&mut body).unwrap(); |
||||||
|
let captured = Captured { |
||||||
|
method: req.method().to_string(), |
||||||
|
url: req.url().to_string(), |
||||||
|
headers: req |
||||||
|
.headers() |
||||||
|
.iter() |
||||||
|
.map(|h| (h.field.to_string().to_ascii_lowercase(), h.value.to_string())) |
||||||
|
.collect(), |
||||||
|
body, |
||||||
|
}; |
||||||
|
seen.lock().unwrap().push(captured.clone()); |
||||||
|
let _ = req.respond(handler(&captured)); |
||||||
|
} |
||||||
|
}) |
||||||
|
}; |
||||||
|
|
||||||
|
TestServer { base, seen, server, thread: Some(thread) } |
||||||
|
} |
||||||
|
|
||||||
|
fn resp( |
||||||
|
code: u16, |
||||||
|
headers: &[(&str, &str)], |
||||||
|
body: &str, |
||||||
|
) -> tiny_http::Response<std::io::Cursor<Vec<u8>>> { |
||||||
|
let mut r = tiny_http::Response::from_data(body.as_bytes().to_vec()).with_status_code(code); |
||||||
|
for (k, v) in headers { |
||||||
|
r.add_header(tiny_http::Header::from_bytes(k.as_bytes(), v.as_bytes()).unwrap()); |
||||||
|
} |
||||||
|
r |
||||||
|
} |
||||||
|
|
||||||
|
fn path_of(c: &Captured) -> &str { |
||||||
|
c.url.split('?').next().unwrap() |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_get_response_fields() { |
||||||
|
let server = serve(|_| resp(200, &[("X-One", "1")], "hello")); |
||||||
|
let lua = lua_net(&server); |
||||||
|
|
||||||
|
let (status, ok, body, x_one, url): (u16, bool, String, String, String) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local r = http.get(BASE) |
||||||
|
return r.status, r.ok, r.body, r.headers["x-one"], r.url |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
assert_eq!((status, ok), (200, true)); |
||||||
|
assert_eq!(body, "hello"); |
||||||
|
assert_eq!(x_one, "1"); |
||||||
|
assert_eq!(url, format!("{}/", server.base)); |
||||||
|
|
||||||
|
// The default User-Agent goes out unless overridden.
|
||||||
|
let reqs = server.requests(); |
||||||
|
assert!(reqs[0].header("user-agent").unwrap().starts_with("a/"), "{reqs:?}"); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_form_body_is_urlencoded() { |
||||||
|
let server = serve(|_| resp(200, &[], "")); |
||||||
|
let lua = lua_net(&server); |
||||||
|
|
||||||
|
lua.load(r#"http.post(BASE, { form = { q = "a b&c=d", plain = "x", num = 5 } })"#) |
||||||
|
.exec_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
let req = &server.requests()[0]; |
||||||
|
assert_eq!(req.method, "POST"); |
||||||
|
assert_eq!(req.header("content-type"), Some("application/x-www-form-urlencoded")); |
||||||
|
// Lua table order is unspecified; compare as a set of encoded pairs.
|
||||||
|
let mut pairs: Vec<&str> = req.body.split('&').collect(); |
||||||
|
pairs.sort(); |
||||||
|
assert_eq!(pairs, ["num=5", "plain=x", "q=a+b%26c%3Dd"]); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_json_body_and_resp_json() { |
||||||
|
let server = |
||||||
|
serve(|c| resp(200, &[("Content-Type", "application/json")], &c.body.clone())); |
||||||
|
let lua = lua_net(&server); |
||||||
|
|
||||||
|
let hello: String = lua |
||||||
|
.load(r#"return http.postJSON(BASE, { hello = "world" }).json().hello"#) |
||||||
|
.eval_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(hello, "world"); |
||||||
|
|
||||||
|
let req = &server.requests()[0]; |
||||||
|
assert_eq!(req.header("content-type"), Some("application/json")); |
||||||
|
assert!(super::json_eq(&req.body, r#"{"hello":"world"}"#)); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_user_content_type_wins_over_implied() { |
||||||
|
let server = serve(|_| resp(200, &[], "")); |
||||||
|
let lua = lua_net(&server); |
||||||
|
|
||||||
|
lua.load( |
||||||
|
r#" |
||||||
|
http.post(BASE, { |
||||||
|
form = { a = "1" }, |
||||||
|
headers = { ["Content-Type"] = "text/plain" }, |
||||||
|
}) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.exec_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
let req = &server.requests()[0]; |
||||||
|
let cts: Vec<&str> = |
||||||
|
req.headers.iter().filter(|(k, _)| k == "content-type").map(|(_, v)| v.as_str()).collect(); |
||||||
|
assert_eq!(cts, ["text/plain"], "exactly one Content-Type, the caller's"); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_redirect_302_degrades_post_to_get() { |
||||||
|
let server = serve(|c| match path_of(c) { |
||||||
|
"/a" => resp(302, &[("Location", "/b")], ""), |
||||||
|
"/b" => resp(200, &[], "final"), |
||||||
|
p => resp(404, &[], p), |
||||||
|
}); |
||||||
|
let lua = lua_net(&server); |
||||||
|
|
||||||
|
let (status, body, url): (u16, String, String) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local r = http.post(BASE .. "/a", { json = { x = 1 } }) |
||||||
|
return r.status, r.body, r.url |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
assert_eq!((status, body.as_str()), (200, "final")); |
||||||
|
assert!(url.ends_with("/b"), "{url}"); |
||||||
|
|
||||||
|
let reqs = server.requests(); |
||||||
|
assert_eq!(reqs[1].method, "GET"); |
||||||
|
assert_eq!(reqs[1].body, "", "302-degraded GET must not carry the POST body"); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_redirect_307_preserves_method_and_body() { |
||||||
|
let server = serve(|c| match path_of(c) { |
||||||
|
"/a" => resp(307, &[("Location", "/b")], ""), |
||||||
|
_ => resp(200, &[], ""), |
||||||
|
}); |
||||||
|
let lua = lua_net(&server); |
||||||
|
|
||||||
|
lua.load(r#"http.post(BASE .. "/a", { body = "payload" })"#).exec_async().await.unwrap(); |
||||||
|
|
||||||
|
let reqs = server.requests(); |
||||||
|
assert_eq!(reqs[1].method, "POST"); |
||||||
|
assert_eq!(reqs[1].body, "payload"); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_too_many_redirects_errors() { |
||||||
|
let server = serve(|_| resp(302, &[("Location", "/loop")], "")); |
||||||
|
let lua = lua_net(&server); |
||||||
|
|
||||||
|
let err = |
||||||
|
lua.load(r#"http.get(BASE .. "/loop")"#).exec_async().await.unwrap_err().to_string(); |
||||||
|
assert!(err.contains("http:") && err.contains("redirect"), "{err}"); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_session_captures_set_cookie_on_redirect() { |
||||||
|
// The login pattern: Set-Cookie arrives on the 302 itself, and the
|
||||||
|
// redirected hop must already send it back.
|
||||||
|
let server = serve(|c| match path_of(c) { |
||||||
|
"/login" => resp(302, &[("Location", "/dash"), ("Set-Cookie", "sid=abc; Path=/")], ""), |
||||||
|
"/dash" => resp(200, &[], ""), |
||||||
|
p => resp(404, &[], p), |
||||||
|
}); |
||||||
|
let lua = lua_net(&server); |
||||||
|
|
||||||
|
let (sid, n): (String, i64) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local s = http.session() |
||||||
|
s:get(BASE .. "/login") |
||||||
|
local jar = s:cookies() |
||||||
|
local domains = 0 |
||||||
|
for _ in pairs(jar) do domains = domains + 1 end |
||||||
|
return jar["127.0.0.1"].sid, domains |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(sid, "abc"); |
||||||
|
assert_eq!(n, 1); |
||||||
|
|
||||||
|
let reqs = server.requests(); |
||||||
|
assert_eq!(reqs[0].header("cookie"), None); |
||||||
|
assert_eq!(reqs[1].header("cookie"), Some("sid=abc"), "jar cookie must reach /dash"); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_session_clear_cookies() { |
||||||
|
let server = serve(|c| match path_of(c) { |
||||||
|
"/set" => resp(200, &[("Set-Cookie", "sid=abc")], ""), |
||||||
|
_ => resp(200, &[], ""), |
||||||
|
}); |
||||||
|
let lua = lua_net(&server); |
||||||
|
|
||||||
|
lua.load( |
||||||
|
r#" |
||||||
|
local s = http.session() |
||||||
|
s:get(BASE .. "/set") |
||||||
|
s:clearCookies() |
||||||
|
assert(next(s:cookies()) == nil, "jar must be empty") |
||||||
|
s:get(BASE .. "/check") |
||||||
|
"#, |
||||||
|
) |
||||||
|
.exec_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
let reqs = server.requests(); |
||||||
|
assert_eq!(reqs[1].header("cookie"), None); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_per_request_cookies_merge_with_jar() { |
||||||
|
let server = serve(|c| match path_of(c) { |
||||||
|
"/set" => resp(200, &[("Set-Cookie", "a=1"), ("Set-Cookie", "keep=yes")], ""), |
||||||
|
_ => resp(200, &[], ""), |
||||||
|
}); |
||||||
|
let lua = lua_net(&server); |
||||||
|
|
||||||
|
lua.load( |
||||||
|
r#" |
||||||
|
local s = http.session() |
||||||
|
s:get(BASE .. "/set") |
||||||
|
s:get(BASE .. "/check", { cookies = { a = "2", b = "3" } }) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.exec_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
let reqs = server.requests(); |
||||||
|
let mut got: Vec<&str> = reqs[1].header("cookie").unwrap().split("; ").collect(); |
||||||
|
got.sort(); |
||||||
|
// Jar cookie `keep` rides along, per-request `a` wins over the jar's.
|
||||||
|
assert_eq!(got, ["a=2", "b=3", "keep=yes"]); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_cookies_opt_conflicts_with_cookie_header() { |
||||||
|
let server = serve(|_| resp(200, &[], "")); |
||||||
|
let lua = lua_net(&server); |
||||||
|
|
||||||
|
let err = lua |
||||||
|
.load(r#"http.get(BASE, { cookies = { a = "1" }, headers = { Cookie = "b=2" } })"#) |
||||||
|
.exec_async() |
||||||
|
.await |
||||||
|
.unwrap_err() |
||||||
|
.to_string(); |
||||||
|
assert!(err.contains("not both"), "{err}"); |
||||||
|
assert!(server.requests().is_empty(), "the request must not go out"); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_session_save_load_roundtrip() { |
||||||
|
let server = serve(|c| match path_of(c) { |
||||||
|
"/set" => resp(200, &[("Set-Cookie", "sid=abc")], ""), |
||||||
|
_ => resp(200, &[], ""), |
||||||
|
}); |
||||||
|
let lua = lua_net(&server); |
||||||
|
let dir = tempfile::tempdir().unwrap(); |
||||||
|
let jar_path = dir.path().join("jar.jsonl"); |
||||||
|
lua.globals().set("JAR", jar_path.to_str().unwrap()).unwrap(); |
||||||
|
|
||||||
|
lua.load( |
||||||
|
r#" |
||||||
|
local s = http.session() |
||||||
|
s:get(BASE .. "/set") |
||||||
|
s:save(JAR) |
||||||
|
|
||||||
|
-- A fresh session preloaded from the file sends the cookie again. |
||||||
|
local s2 = http.session(JAR) |
||||||
|
assert(s2:cookies()["127.0.0.1"].sid == "abc", "cookie must survive save/load") |
||||||
|
s2:get(BASE .. "/check") |
||||||
|
"#, |
||||||
|
) |
||||||
|
.exec_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
let reqs = server.requests(); |
||||||
|
assert_eq!(reqs[1].header("cookie"), Some("sid=abc")); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_basic_auth_header() { |
||||||
|
let server = serve(|_| resp(200, &[], "")); |
||||||
|
let lua = lua_net(&server); |
||||||
|
|
||||||
|
lua.load(r#"http.get(BASE, { auth = { username = "alice", password = "secret" } })"#) |
||||||
|
.exec_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
// base64("alice:secret")
|
||||||
|
let auth = server.requests()[0].header("authorization").unwrap().to_string(); |
||||||
|
assert_eq!(auth, "Basic YWxpY2U6c2VjcmV0"); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_digest_auth_answers_challenge() { |
||||||
|
let server = serve(|c| { |
||||||
|
if c.header("authorization").is_none() { |
||||||
|
resp( |
||||||
|
401, |
||||||
|
&[("WWW-Authenticate", r#"Digest realm="test", nonce="abc123", qop="auth""#)], |
||||||
|
"", |
||||||
|
) |
||||||
|
} else { |
||||||
|
resp(200, &[], "in") |
||||||
|
} |
||||||
|
}); |
||||||
|
let lua = lua_net(&server); |
||||||
|
|
||||||
|
let (ok, body): (bool, String) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local r = http.get(BASE .. "/p", { |
||||||
|
auth = { username = "alice", password = "secret", scheme = "digest" }, |
||||||
|
}) |
||||||
|
return r.ok, r.body |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
assert!(ok); |
||||||
|
assert_eq!(body, "in"); |
||||||
|
|
||||||
|
let reqs = server.requests(); |
||||||
|
assert_eq!(reqs.len(), 2, "one challenge, one answer"); |
||||||
|
let auth = reqs[1].header("authorization").unwrap(); |
||||||
|
assert!(auth.starts_with("Digest "), "{auth}"); |
||||||
|
for part in [r#"username="alice""#, r#"uri="/p""#, "response="] { |
||||||
|
assert!(auth.contains(part), "{auth} missing {part}"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_cross_host_redirect_strips_credentials() { |
||||||
|
// Two servers = two hosts (the port differs). Authorization and Cookie
|
||||||
|
// must not follow the redirect off the original host; ordinary custom
|
||||||
|
// headers do (matching browser/curl behaviour).
|
||||||
|
let target = serve(|_| resp(200, &[], "landed")); |
||||||
|
let origin = { |
||||||
|
let to = format!("{}/land", target.base); |
||||||
|
serve(move |_| resp(302, &[("Location", &to.clone())], "")) |
||||||
|
}; |
||||||
|
let lua = lua_net(&origin); |
||||||
|
|
||||||
|
let body: String = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local r = http.get(BASE .. "/go", { |
||||||
|
auth = { username = "alice", password = "secret" }, |
||||||
|
cookies = { sid = "abc" }, |
||||||
|
headers = { ["X-Custom"] = "keep" }, |
||||||
|
}) |
||||||
|
return r.body |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(body, "landed"); |
||||||
|
|
||||||
|
let first = &origin.requests()[0]; |
||||||
|
assert!(first.header("authorization").is_some()); |
||||||
|
assert!(first.header("cookie").is_some()); |
||||||
|
|
||||||
|
let landed = &target.requests()[0]; |
||||||
|
assert_eq!(landed.header("authorization"), None, "auth leaked cross-host"); |
||||||
|
assert_eq!(landed.header("cookie"), None, "cookies leaked cross-host"); |
||||||
|
assert_eq!(landed.header("x-custom"), Some("keep")); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_multi_value_headers_and_set_cookies() { |
||||||
|
let server = serve(|_| { |
||||||
|
resp( |
||||||
|
200, |
||||||
|
&[ |
||||||
|
("X-Multi", "one"), |
||||||
|
("X-Multi", "two"), |
||||||
|
("Set-Cookie", "a=1; Path=/"), |
||||||
|
("Set-Cookie", "b=2; Path=/"), |
||||||
|
], |
||||||
|
"", |
||||||
|
) |
||||||
|
}); |
||||||
|
let lua = lua_net(&server); |
||||||
|
|
||||||
|
let (multi, first_sc, sc1, sc2): (String, String, String, String) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local r = http.get(BASE) |
||||||
|
assert(#r.setCookies == 2, "expected two Set-Cookie values") |
||||||
|
return r.headers["x-multi"], r.headers["set-cookie"], r.setCookies[1], r.setCookies[2] |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
assert_eq!(multi, "one, two"); |
||||||
|
assert_eq!(first_sc, "a=1; Path=/"); |
||||||
|
assert_eq!((sc1.as_str(), sc2.as_str()), ("a=1; Path=/", "b=2; Path=/")); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_timeout_errors() { |
||||||
|
let server = serve(|_| { |
||||||
|
std::thread::sleep(std::time::Duration::from_secs(2)); |
||||||
|
resp(200, &[], "late") |
||||||
|
}); |
||||||
|
let lua = lua_net(&server); |
||||||
|
|
||||||
|
let start = std::time::Instant::now(); |
||||||
|
let err = lua |
||||||
|
.load(r#"http.get(BASE, { timeout = 0.1 })"#) |
||||||
|
.exec_async() |
||||||
|
.await |
||||||
|
.unwrap_err() |
||||||
|
.to_string(); |
||||||
|
assert!(err.contains("http:"), "{err}"); |
||||||
|
assert!(start.elapsed() < std::time::Duration::from_secs(1), "timeout did not apply"); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_invalid_url_and_method() { |
||||||
|
let server = serve(|_| resp(200, &[], "")); |
||||||
|
let lua = lua_net(&server); |
||||||
|
|
||||||
|
let err = lua.load(r#"http.get("not a url")"#).exec_async().await.unwrap_err().to_string(); |
||||||
|
assert!(err.contains("invalid URL"), "{err}"); |
||||||
|
|
||||||
|
let err = lua |
||||||
|
.load(r#"http.request("BAD METHOD", BASE)"#) |
||||||
|
.exec_async() |
||||||
|
.await |
||||||
|
.unwrap_err() |
||||||
|
.to_string(); |
||||||
|
assert!(err.contains("invalid method"), "{err}"); |
||||||
|
|
||||||
|
assert!(server.requests().is_empty()); |
||||||
|
} |
||||||
Loading…
Reference in new issue