use std::time::Duration;
use anyhow::{Context, Result};
use isola::{
host::NoopOutputSink,
sandbox::{Arg, CallOutput, Sandbox, SandboxOptions, args},
};
use wiremock::{
Mock, MockServer, ResponseTemplate,
matchers::{body_string, body_string_contains, header, header_regex, method, path},
};
use super::common::{TestHost, build_module};
async fn call_with_timeout<I>(
sandbox: &mut Sandbox<TestHost>,
function: &str,
args: I,
timeout: Duration,
) -> Result<CallOutput>
where
I: IntoIterator<Item = Arg>,
{
tokio::time::timeout(timeout, sandbox.call(function, args))
.await
.map_or_else(
|_| {
Err(anyhow::anyhow!(
"sandbox call timed out after {}ms",
timeout.as_millis()
))
},
|result| result.map_err(Into::into),
)
}
#[tokio::test]
#[cfg_attr(debug_assertions, ignore = "integration tests run in release mode")]
async fn integration_python_http_client_roundtrip() -> Result<()> {
let Some(module) = build_module().await? else {
return Ok(());
};
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/echo"))
.and(header("content-type", "application/json"))
.and(body_string(r#"{"hello":"world"}"#))
.respond_with(
ResponseTemplate::new(200)
.insert_header("content-type", "application/json")
.set_body_string(r#"{"ok":true}"#),
)
.expect(1)
.mount(&server)
.await;
let mut sandbox = module
.instantiate(TestHost::default(), SandboxOptions::default())
.await
.context("failed to instantiate sandbox")?;
let script = r#"
from sandbox.http import fetch
def main(url):
with fetch(
"POST",
url,
headers={"content-type": "application/json"},
body=b'{"hello":"world"}',
) as resp:
return resp.text()
"#;
sandbox
.eval_script(script, NoopOutputSink::shared())
.await
.context("failed to evaluate http fetch script")?;
let url_arg = format!("{}/echo", server.uri());
let output = call_with_timeout(
&mut sandbox,
"main",
args![url_arg]?,
Duration::from_secs(5),
)
.await
.context("failed to call http fetch function")?;
assert!(output.items.is_empty(), "expected no partial outputs");
let value: String = output
.result
.as_ref()
.context("expected exactly one end output")?
.to_serde()
.context("failed to decode response body")?;
assert_eq!(value, r#"{"ok":true}"#);
Ok(())
}
#[tokio::test]
#[cfg_attr(debug_assertions, ignore = "integration tests run in release mode")]
async fn integration_python_http_status_errors_surface() -> Result<()> {
let Some(module) = build_module().await? else {
return Ok(());
};
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/status/503"))
.respond_with(ResponseTemplate::new(503))
.expect(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/status/500"))
.and(header("content-type", "application/json"))
.and(body_string(r#"{"value":"test"}"#))
.respond_with(ResponseTemplate::new(500))
.expect(1)
.mount(&server)
.await;
let mut sandbox = module
.instantiate(TestHost::default(), SandboxOptions::default())
.await
.context("failed to instantiate sandbox")?;
let script = r#"
from sandbox.http import fetch
def main(url):
with fetch("GET", f"{url}/status/503") as first:
first_status = first.status
with fetch("POST", f"{url}/status/500", body={"value": "test"}) as second:
second_status = second.status
return (first_status, second_status)
"#;
sandbox
.eval_script(script, NoopOutputSink::shared())
.await
.context("failed to evaluate status script")?;
let url_arg = server.uri();
let output = call_with_timeout(
&mut sandbox,
"main",
args![url_arg]?,
Duration::from_secs(5),
)
.await
.context("failed to call status function")?;
assert!(output.items.is_empty(), "expected no partial outputs");
let value: (i64, i64) = output
.result
.as_ref()
.context("expected exactly one end output")?
.to_serde()
.context("failed to decode status tuple")?;
assert_eq!(value, (503, 500));
Ok(())
}
#[tokio::test]
#[cfg_attr(debug_assertions, ignore = "integration tests run in release mode")]
async fn integration_python_http_multipart_files() -> Result<()> {
let Some(module) = build_module().await? else {
return Ok(());
};
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/multipart"))
.and(header_regex(
"content-type",
r"^multipart/form-data;\s*boundary=.*$",
))
.and(body_string_contains(r#"name="file"; filename="file""#))
.and(body_string_contains("\r\n\r\ntest\r\n"))
.and(body_string_contains(r#"name="file2"; filename="a.txt""#))
.and(body_string_contains("Content-Type: text/plain"))
.and(body_string_contains("\r\n\r\ntest2\r\n"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let mut sandbox = module
.instantiate(TestHost::default(), SandboxOptions::default())
.await
.context("failed to instantiate sandbox")?;
let script = r#"
import io
from sandbox.http import fetch
def main(url):
with fetch(
"POST",
f"{url}/multipart",
files={
"file": b"test",
"file2": ("a.txt", io.BytesIO(b"test2"), "text/plain"),
},
) as resp:
return resp.status
"#;
sandbox
.eval_script(script, NoopOutputSink::shared())
.await
.context("failed to evaluate multipart script")?;
let url_arg = server.uri();
let output = call_with_timeout(
&mut sandbox,
"main",
args![url_arg]?,
Duration::from_secs(5),
)
.await
.context("failed to call multipart function")?;
assert!(output.items.is_empty(), "expected no partial outputs");
let value: i64 = output
.result
.as_ref()
.context("expected exactly one end output")?
.to_serde()
.context("failed to decode multipart status")?;
assert_eq!(value, 200);
Ok(())
}
#[tokio::test]
#[cfg_attr(debug_assertions, ignore = "integration tests run in release mode")]
async fn integration_python_http_read_twice_errors() -> Result<()> {
let Some(module) = build_module().await? else {
return Ok(());
};
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/read-twice"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("content-type", "application/json")
.set_body_string(r#"{"ok":true}"#),
)
.expect(1)
.mount(&server)
.await;
let mut sandbox = module
.instantiate(TestHost::default(), SandboxOptions::default())
.await
.context("failed to instantiate sandbox")?;
let script = r#"
from sandbox.http import fetch
def main(url):
with fetch("GET", f"{url}/read-twice") as resp:
_ = resp.json()
try:
_ = resp.json()
return "expected-second-read-error"
except Exception as e:
return str(e)
"#;
sandbox
.eval_script(script, NoopOutputSink::shared())
.await
.context("failed to evaluate read-twice script")?;
let url_arg = server.uri();
let output = call_with_timeout(
&mut sandbox,
"main",
args![url_arg]?,
Duration::from_secs(5),
)
.await
.context("failed to call read-twice function")?;
assert!(output.items.is_empty(), "expected no partial outputs");
let value: String = output
.result
.as_ref()
.context("expected exactly one end output")?
.to_serde()
.context("failed to decode read-twice result")?;
assert!(
value.contains("Response already read"),
"unexpected second-read error message: {value}"
);
Ok(())
}