#[cfg(test)]
mod tests;
#[cfg(test)]
mod helper_tests;
pub mod sse;
pub mod snapshot;
#[cfg(not(target_arch = "wasm32"))]
pub mod websocket;
#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
pub mod db;
#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
pub mod factory;
pub use snapshot::{assert_json_snapshot, assert_openapi_snapshot, OpenApiSource};
pub use sse::{SseFrame, SseParser, TestSse};
#[cfg(not(target_arch = "wasm32"))]
pub use websocket::{TestWebSocket, TestWebSocketRequest};
#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
pub use db::{TestDb, TestDbBuilder};
use crate::application::Application;
use crate::header::Header;
use crate::http::VERSION;
use crate::mime_type::MimeType;
use crate::range::Range;
use crate::request::{METHOD, Request};
use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
use crate::server::{Address, ConnectionInfo};
pub struct TestClient<A: Application> {
app: A,
connection: ConnectionInfo,
}
impl<A: Application> TestClient<A> {
pub fn new(app: A) -> Self {
TestClient {
app,
connection: ConnectionInfo {
client: Address { ip: "127.0.0.1".to_string(), port: 12345 },
server: Address { ip: "127.0.0.1".to_string(), port: 7878 },
request_size: 16000,
sni_hostname: None,
tls: false,
},
}
}
pub fn get(&self, path: &str) -> TestRequest<'_, A> {
TestRequest::new(METHOD.get.to_string(), path, self)
}
pub fn post(&self, path: &str) -> TestRequest<'_, A> {
TestRequest::new(METHOD.post.to_string(), path, self)
}
pub fn put(&self, path: &str) -> TestRequest<'_, A> {
TestRequest::new(METHOD.put.to_string(), path, self)
}
pub fn patch(&self, path: &str) -> TestRequest<'_, A> {
TestRequest::new(METHOD.patch.to_string(), path, self)
}
pub fn delete(&self, path: &str) -> TestRequest<'_, A> {
TestRequest::new(METHOD.delete.to_string(), path, self)
}
pub fn options(&self, path: &str) -> TestRequest<'_, A> {
TestRequest::new(METHOD.options.to_string(), path, self)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn websocket(&self, path: &str) -> TestWebSocketRequest<'_, A> {
TestWebSocketRequest::new(TestRequest::new(METHOD.get.to_string(), path, self))
}
pub fn app(&self) -> &A {
&self.app
}
}
pub struct TestRequest<'a, A: Application> {
method: String,
path: String,
headers: Vec<Header>,
body: Vec<u8>,
client: &'a TestClient<A>,
}
impl<'a, A: Application> TestRequest<'a, A> {
fn new(method: String, path: &str, client: &'a TestClient<A>) -> Self {
TestRequest {
method,
path: path.to_string(),
headers: vec![],
body: Default::default(),
client,
}
}
pub fn header(mut self, name: &str, value: &str) -> Self {
self.headers.push(Header { name: name.to_string().into(), value: value.to_string().into() });
self
}
pub fn body_bytes(mut self, body: Vec<u8>) -> Self {
self.body = body;
self
}
pub fn body_text(mut self, text: &str) -> Self {
self.body = text.as_bytes().to_vec();
self
}
pub fn body_json(mut self, json: &str) -> Self {
self.headers.push(Header { name: Header::_CONTENT_TYPE.into(), value: "application/json".into() });
self.body = json.as_bytes().to_vec();
self
}
pub fn send(self) -> TestResponse {
TestResponse::from_response(self.dispatch())
}
#[cfg(not(target_arch = "wasm32"))]
fn has_header(&self, name: &str) -> bool {
self.headers.iter().any(|h| h.name.eq_ignore_ascii_case(name))
}
fn into_request(self) -> (Request, &'a TestClient<A>) {
let request = Request {
method: self.method,
request_uri: self.path,
http_version: VERSION.http_1_1.to_string(),
headers: self.headers,
body: self.body.into(),
};
(request, self.client)
}
fn dispatch(self) -> Response {
let (request, client) = self.into_request();
client.app.execute(&request, &client.connection).unwrap_or_else(error_response)
}
}
#[cfg(feature = "http2")]
impl<'a, S: Send + Sync + 'static> TestRequest<'a, crate::async_state::AsyncAppWithState<S>> {
pub async fn send_async(self) -> TestResponse {
let (request, client) = self.into_request();
let result = client.app.respond(&request, &client.connection).await;
TestResponse::from_response(result.unwrap_or_else(error_response))
}
}
fn error_response(message: String) -> Response {
let dummy = Request {
method: "GET".to_string(),
request_uri: "/".to_string(),
http_version: VERSION.http_1_1.to_string(),
headers: vec![],
body: Default::default(),
};
let header_list = Header::get_header_list(&dummy);
let cr = Range::get_content_range(message.into_bytes(), MimeType::TEXT_PLAIN.to_string());
Response::get_response(STATUS_CODE_REASON_PHRASE.n500_internal_server_error, Some(header_list), Some(vec![cr]))
}
pub struct TestResponse {
status: i16,
reason: String,
headers: Vec<Header>,
body: Vec<u8>,
stream: StreamSlot,
}
struct StreamSlot(Option<Box<dyn std::io::Read + Send>>);
unsafe impl Sync for StreamSlot {}
impl std::panic::UnwindSafe for StreamSlot {}
impl std::panic::RefUnwindSafe for StreamSlot {}
impl std::fmt::Debug for TestResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TestResponse")
.field("status", &self.status)
.field("reason", &self.reason)
.field("headers", &self.headers.iter().map(|h| format!("{}: {}", h.name, h.value)).collect::<Vec<_>>())
.field("body", &String::from_utf8_lossy(&self.body))
.field("streamed", &self.is_streamed())
.finish()
}
}
impl TestResponse {
fn from_response(mut r: Response) -> Self {
let body: Vec<u8> = r.content_range_list.iter()
.flat_map(|cr| cr.body.iter().copied())
.collect();
if r.content_range_list.len() == 1 {
let content_range = r.content_range_list.get(0).unwrap();
r.headers.push(Header {
name: Header::_CONTENT_TYPE.into(),
value: content_range.content_type.to_string().into(),
});
if let Some(value) = crate::response::content_range_header_value(r.status_code, content_range) {
r.headers.push(Header {
name: Header::_CONTENT_RANGE.into(),
value: value.into(),
});
}
r.headers.push(Header {
name: Header::_CONTENT_LENGTH.into(),
value: content_range.body.len().to_string().into(),
});
}
TestResponse {
status: r.status_code,
reason: r.reason_phrase,
headers: r.headers,
body,
stream: StreamSlot(r.stream_pipe.take()),
}
}
pub fn status(&self) -> i16 {
self.status
}
pub fn reason(&self) -> &str {
&self.reason
}
pub fn header(&self, name: &str) -> Option<&str> {
let lower = name.to_lowercase();
self.headers
.iter()
.find(|h| h.name.to_lowercase() == lower)
.map(|h| h.value.as_ref())
}
pub fn headers(&self) -> &[Header] {
&self.headers
}
pub fn body_bytes(&self) -> &[u8] {
&self.body
}
pub fn body_text(&self) -> &str {
std::str::from_utf8(&self.body).expect("response body is not valid UTF-8")
}
pub fn is_success(&self) -> bool {
(200..300).contains(&self.status)
}
pub fn is_streamed(&self) -> bool {
self.stream.0.is_some()
}
pub fn take_stream(&mut self) -> Option<Box<dyn std::io::Read + Send>> {
self.stream.0.take()
}
pub fn sse(mut self) -> TestSse {
match self.take_stream() {
Some(reader) => TestSse::from_reader(reader),
None => TestSse::from_bytes(&self.body),
}
}
pub fn sse_events(self, n: usize, timeout: std::time::Duration) -> Vec<SseFrame> {
self.sse().events(n, timeout)
}
}