#![allow(
missing_docs,
clippy::unwrap_used,
clippy::expect_used,
clippy::uninlined_format_args,
clippy::items_after_statements,
clippy::use_self,
clippy::semicolon_if_nothing_returned,
clippy::similar_names,
clippy::let_underscore_future,
clippy::option_if_let_else
)]
use cookie::Cookie;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tachyon_web::http::response::{Body, Html, Json};
use tachyon_web::routing::extract::{Cookies, Form};
use tachyon_web::{Router, Server, get, post};
use tokio::net::TcpListener;
#[derive(Clone, Default)]
struct AppState {
pub _counter: usize,
}
#[derive(Debug, Deserialize, Serialize)]
struct SubmitData {
username: String,
age: u8,
}
async fn handle_cookies(cookies: Cookies) -> (Cookies, Html<String>) {
let visited = if let Some(cookie) = cookies.get("visited") {
cookie.value().to_string()
} else {
"no".to_string()
};
let cookies = cookies.add(Cookie::new("visited", "yes"));
(cookies, Html(format!("Visited before: {}", visited)))
}
async fn handle_form(Form(data): Form<SubmitData>) -> Json<SubmitData> {
Json(data)
}
async fn handle_stream() -> hyper::Response<Body> {
use http_body_util::StreamBody;
use hyper::body::Frame;
use tokio_stream::wrappers::ReceiverStream;
let (sender, receiver) = tokio::sync::mpsc::channel::<
Result<Frame<bytes::Bytes>, tachyon_web::http::error::Error>,
>(2);
let _handle = tokio::spawn(async move {
sender
.send(Ok(Frame::data(bytes::Bytes::from("Chunk 1\n"))))
.await
.expect("expected result");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
sender
.send(Ok(Frame::data(bytes::Bytes::from("Chunk 2\n"))))
.await
.expect("expected result");
});
let body = StreamBody::new(ReceiverStream::new(receiver));
hyper::Response::builder()
.status(200)
.body(Body::stream(body))
.expect("expected result")
}
#[tokio::test]
async fn test_advanced_features() {
let api_router = Router::new()
.route("/form", post(handle_form))
.route("/stream", get(handle_stream));
let app = Router::new()
.route("/cookies", get(handle_cookies))
.nest("/api/v1", api_router)
.with_state(AppState::default());
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("expected result");
let port = listener.local_addr().expect("expected result").port();
let server = Server::new(app);
let _handle = tokio::spawn(async move {
server.serve_http(listener).await.expect("expected result");
});
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let client = Client::builder()
.cookie_store(true)
.build()
.expect("expected result");
let base_url = format!("http://127.0.0.1:{}", port);
let res = client
.get(format!("{}/cookies", base_url))
.send()
.await
.expect("expected result");
assert_eq!(
res.text().await.expect("expected result"),
"Visited before: no"
);
let res2 = client
.get(format!("{}/cookies", base_url))
.send()
.await
.expect("expected result");
assert_eq!(
res2.text().await.expect("expected result"),
"Visited before: yes"
);
let form_data = [("username", "hacer"), ("age", "30")];
let res_form = client
.post(format!("{}/api/v1/form", base_url))
.form(&form_data)
.send()
.await
.expect("expected result");
let response_data: SubmitData = res_form.json().await.expect("expected result");
assert_eq!(response_data.username, "hacer");
assert_eq!(response_data.age, 30);
let res_stream = client
.get(format!("{}/api/v1/stream", base_url))
.send()
.await
.expect("expected result");
assert_eq!(
res_stream.text().await.expect("expected result"),
"Chunk 1\nChunk 2\n"
);
}
#[tokio::test]
#[cfg(feature = "tls")]
async fn test_custom_crypto_provider() {
use std::sync::Arc;
let router = Router::new();
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
let _server = Server::new(router).crypto_provider(provider);
}
#[tokio::test]
async fn test_hoop_middleware() {
use hyper::{Request, Response};
use tachyon_web::routing::middleware::Next;
async fn add_header(req: Request<Body>, next: Next<()>) -> Response<Body> {
let mut res = next.run(req).await;
let _ = res.headers_mut().insert(
hyper::header::SERVER,
hyper::header::HeaderValue::from_static("Tachyon"),
);
res
}
async fn handler() -> &'static str {
"hello"
}
let app = Router::new().route("/hello", get(handler)).hoop(add_header);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let server = Server::new(app);
let _ = tokio::spawn(async move {
server.serve_http(listener).await.unwrap();
});
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let res = reqwest::get(format!("http://127.0.0.1:{}/hello", port))
.await
.unwrap();
assert_eq!(res.headers().get("server").unwrap(), "Tachyon");
assert_eq!(res.text().await.unwrap(), "hello");
}
#[tokio::test]
async fn test_hoop_at_ordering_and_state() {
use hyper::{Request, Response};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tachyon_web::MiddlewarePosition;
use tachyon_web::routing::middleware::Next;
#[derive(Clone)]
struct OrderState {
counter: Arc<AtomicUsize>,
}
async fn first_mw(req: Request<Body>, next: Next<OrderState>) -> Response<Body> {
let counter = next.state().counter.fetch_add(1, Ordering::SeqCst);
let mut res = next.run(req).await;
let _ = res.headers_mut().insert(
hyper::header::HeaderName::from_static("x-first-order"),
hyper::header::HeaderValue::from_str(&counter.to_string()).unwrap(),
);
res
}
async fn last_mw(req: Request<Body>, next: Next<OrderState>) -> Response<Body> {
let counter = next.state().counter.fetch_add(1, Ordering::SeqCst);
let mut res = next.run(req).await;
let _ = res.headers_mut().insert(
hyper::header::HeaderName::from_static("x-last-order"),
hyper::header::HeaderValue::from_str(&counter.to_string()).unwrap(),
);
res
}
async fn handler() -> &'static str {
"hello"
}
let state = OrderState {
counter: Arc::new(AtomicUsize::new(0)),
};
let app = Router::new()
.route("/hello", get(handler))
.hoop_at(MiddlewarePosition::First, first_mw)
.hoop_at(MiddlewarePosition::Last, last_mw)
.with_state(state);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let server = Server::new(app);
let _ = tokio::spawn(async move {
server.serve_http(listener).await.unwrap();
});
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let res = reqwest::get(format!("http://127.0.0.1:{}/hello", port))
.await
.unwrap();
let first_order = res
.headers()
.get("x-first-order")
.unwrap()
.to_str()
.unwrap();
let last_order = res.headers().get("x-last-order").unwrap().to_str().unwrap();
assert_eq!(first_order, "0");
assert_eq!(last_order, "1");
assert_eq!(res.text().await.unwrap(), "hello");
}
#[tokio::test]
async fn test_eager_polling_safety_and_allocations() {
use std::sync::Arc;
use std::time::Duration;
use tachyon_web::http::Request;
use tachyon_web::routing::handler::{Handler, ResponseFuture};
async fn get_time() -> String {
format!(
"time: {:?}",
std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos()
)
}
let req1 = Request::builder().body(Body::empty()).unwrap();
let res_fut1 = get_time.call(req1, Arc::new(()));
assert!(
matches!(res_fut1, ResponseFuture::Ready(_)),
"Expected Ready for instant route"
);
let response1 = res_fut1.await;
let body1 = response_to_string(response1).await;
tokio::time::sleep(Duration::from_millis(1)).await;
let req2 = Request::builder().body(Body::empty()).unwrap();
let res_fut2 = get_time.call(req2, Arc::new(()));
assert!(matches!(res_fut2, ResponseFuture::Ready(_)));
let response2 = res_fut2.await;
let body2 = response_to_string(response2).await;
assert_ne!(
body1, body2,
"Dynamic time handler must return different values on different requests"
);
async fn wait_a_bit() -> &'static str {
tokio::time::sleep(Duration::from_millis(5)).await;
"done"
}
let req3 = Request::builder().body(Body::empty()).unwrap();
let res_fut3 = wait_a_bit.call(req3, Arc::new(()));
assert!(
matches!(res_fut3, ResponseFuture::Boxed(_)),
"Expected Boxed for yielding route"
);
let response3 = res_fut3.await;
let body3 = response_to_string(response3).await;
assert_eq!(body3, "done");
fn sync_handler() -> &'static str {
"sync-ok"
}
let req4 = Request::builder().body(Body::empty()).unwrap();
let res_fut4 = sync_handler.call(req4, Arc::new(()));
assert!(
matches!(res_fut4, ResponseFuture::Ready(_)),
"Expected Ready for sync route"
);
let response4 = res_fut4.await;
let body4 = response_to_string(response4).await;
assert_eq!(body4, "sync-ok");
use tachyon_web::routing::extract::State;
fn sync_state_handler(State(state_val): State<String>) -> String {
format!("state: {}", state_val)
}
let req5 = Request::builder().body(Body::empty()).unwrap();
let res_fut5 = sync_state_handler.call(req5, Arc::new("app-state-value".to_string()));
assert!(
matches!(res_fut5, ResponseFuture::Boxed(_)),
"Expected Boxed for extractor route (last extractor is async)"
);
let response5 = res_fut5.await;
let body5 = response_to_string(response5).await;
assert_eq!(body5, "state: app-state-value");
}
async fn response_to_string(res: hyper::Response<tachyon_web::http::response::Body>) -> String {
use http_body_util::BodyExt;
let body_bytes = res.into_body().collect().await.unwrap().to_bytes();
String::from_utf8(body_bytes.to_vec()).unwrap()
}