use std::net::{SocketAddr, TcpListener as StdTcpListener};
use reqwest::header::CONTENT_TYPE;
use reqwest::multipart::{Form, Part};
use reqwest::RequestBuilder;
use tokio::net::TcpListener;
use tower::ServiceExt;
use axum::body::{to_bytes, Body};
mod multipart;
use multipart::{MultipartFieldValue, MultipartFields};
use echoreq::router;
const DEMO_IMG: &'static [u8] = include_bytes!("../demo.png");
const DEMO_TXT: &'static [u8] = include_bytes!("../demo.txt");
struct TestClient {
addr: SocketAddr,
client: reqwest::Client,
}
impl TestClient {
fn new() -> Self {
let std_listener = StdTcpListener::bind("0.0.0.0:0").unwrap();
std_listener.set_nonblocking(true).unwrap();
let listener = TcpListener::from_std(std_listener).unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(listener, router()).await.unwrap();
});
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
Self { addr, client }
}
fn addr(&self) -> &SocketAddr {
&self.addr
}
async fn get(&self, url: &str) -> String {
self.client
.get(format!("http://{}{}", self.addr, url))
.send()
.await
.unwrap()
.text()
.await
.unwrap()
}
fn post(&self, url: &str) -> RequestBuilder {
self.client.post(format!("http://{}{}", self.addr, url))
}
async fn send(req: RequestBuilder) -> String {
req.send().await.unwrap().text().await.unwrap()
}
}
#[tokio::test]
async fn get() {
let client = TestClient::new();
let addr = client.addr();
let path = "/home";
let resp = client.get(path).await;
assert_eq!(resp, format!("GET {path}\naccept: */*\nhost: {addr}\n\n"));
}
#[tokio::test]
async fn post_simple() {
let client = TestClient::new();
let addr = client.addr();
let path = "/hello/name";
let req = client
.post(path)
.header("cookie", "tower.sid=abcd1234")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("param1=value1¶m2=value2");
let resp = TestClient::send(req).await;
assert_eq!(
resp,
format!(
"POST {path}
cookie: tower.sid=abcd1234
content-type: application/x-www-form-urlencoded
accept: */*
host: {addr}
content-length: 27
param1=value1¶m2=value2"
)
);
}
#[tokio::test]
async fn post_json() {
let client = TestClient::new();
let addr = client.addr();
let path = "/echo/post/json";
let req = client
.post(path)
.header(CONTENT_TYPE, "application/json")
.body("{\"productId\": 123456, \"quantity\": 100}");
let resp = TestClient::send(req).await;
assert_eq!(
resp,
format!(
"POST {path}
content-type: application/json
accept: */*
host: {addr}
content-length: 38
{{\"productId\": 123456, \"quantity\": 100}}"
)
);
}
#[tokio::test]
async fn post_formdata_text() {
let client = TestClient::new();
let addr = client.addr();
let form = Form::new()
.text("title", "Cool story")
.text("year", "2023")
.part(
"thumb",
Part::bytes(DEMO_TXT)
.file_name("demo.txt")
.mime_str("text/plain")
.unwrap(),
);
let boundary = form.boundary().to_owned();
let path = "/form-data/text";
let req = client.post(path).multipart(form);
let resp = TestClient::send(req).await;
assert_eq!(
resp,
format!(
"POST {path}
content-type: multipart/form-data; boundary={boundary}
content-length: 513
accept: */*
host: {addr}
--{boundary}\r
Content-Disposition: form-data; name=\"title\"\r
\r
Cool story\r
--{boundary}\r
Content-Disposition: form-data; name=\"year\"\r
\r
2023\r
--{boundary}\r
Content-Disposition: form-data; name=\"thumb\"; filename=\"demo.txt\"\r
Content-Type: text/plain\r
\r
hi there
a file
\r
--{boundary}--\r
"
)
);
}
#[tokio::test]
async fn post_formdata_file() {
let client = TestClient::new();
let addr = client.addr();
let form = Form::new()
.text("title", "Cool story")
.text("year", "2023")
.part(
"thumb",
Part::bytes(DEMO_IMG)
.file_name("demo.png")
.mime_str("image/png")
.unwrap(),
);
let boundary = form.boundary().to_owned();
let path = "/form-data/image";
let req = client.post(path).multipart(form);
let resp = TestClient::send(req).await;
assert_eq!(
resp,
format!(
"POST {path}
content-type: multipart/form-data; boundary={boundary}
content-length: 791
accept: */*
host: {addr}
--{boundary}\r
Content-Disposition: form-data; name=\"title\"\r
\r
Cool story\r
--{boundary}\r
Content-Disposition: form-data; name=\"year\"\r
\r
2023\r
--{boundary}\r
Content-Disposition: form-data; name=\"thumb\"; filename=\"demo.png\"\r
Content-Type: image/png\r
\r
.PNG\r
.
...\rIHDR.....................sRGB.........gAMA......a....\tpHYs...t...t..f.x....IDAT(S..A..p.....3(.'.V.RZ....J.a8)....Vn.b..\\......P.?...O==..^...,3.....;........R..=.S....Mr...2.K...X(.l.D..a...v......q.Nk...xWf.^n
:.7..#J.0.....(.l.d5...1.........I`..t.X.g..k..-.......!..r.....IEND.B`.\r
--{boundary}--\r
"
)
);
}
#[tokio::test]
async fn request_conversion() {
let mut rng = fastrand::Rng::new();
let boundary = format!(
"{:016x}-{:016x}-{:016x}-{:016x}",
rng.u64(..),
rng.u64(..),
rng.u64(..),
rng.u64(..)
);
let fields = MultipartFields::new(&[
("title", MultipartFieldValue::Text("Cool story")),
("year", MultipartFieldValue::Text("2023")),
(
"thumb",
MultipartFieldValue::File {
filename: "demo.png",
data: DEMO_IMG,
content_type: "image/png",
},
),
]);
let path = "/form-data/image";
let addr = "0.0.0.0:40123";
let req_body = fields.to_http(&boundary);
let request = http::Request::builder()
.method("POST")
.uri(path)
.header(
CONTENT_TYPE.to_string(),
format!("multipart/form-data; boundary={boundary}"),
)
.header("content-length", req_body.len())
.header("accept", "*/*")
.header("host", addr)
.body(Body::from(req_body))
.unwrap();
let response = router().oneshot(request).await.unwrap();
let body_bytes = to_bytes(response.into_body(), 1234).await.unwrap().to_vec();
let resp = unsafe { String::from_utf8_unchecked(body_bytes) };
assert_eq!(
resp,
format!(
"POST {path}
content-type: multipart/form-data; boundary={boundary}
content-length: 791
accept: */*
host: {addr}
--{boundary}\r
Content-Disposition: form-data; name=\"title\"\r
\r
Cool story\r
--{boundary}\r
Content-Disposition: form-data; name=\"year\"\r
\r
2023\r
--{boundary}\r
Content-Disposition: form-data; name=\"thumb\"; filename=\"demo.png\"\r
Content-Type: image/png\r
\r
.PNG\r
.
...\rIHDR.....................sRGB.........gAMA......a....\tpHYs...t...t..f.x....IDAT(S..A..p.....3(.'.V.RZ....J.a8)....Vn.b..\\......P.?...O==..^...,3.....;........R..=.S....Mr...2.K...X(.l.D..a...v......q.Nk...xWf.^n
:.7..#J.0.....(.l.d5...1.........I`..t.X.g..k..-.......!..r.....IEND.B`.\r
--{boundary}--\r
"
)
);
}