use async_channel::Sender;
use indoc::{formatdoc, indoc};
use pretty_assertions::assert_eq;
use std::{
future::{Future, IntoFuture},
io,
net::SocketAddr,
};
use test_harness::test;
use trillium_client::{Client, Conn, Error, KnownHeaderName, Status, USER_AGENT};
use trillium_server_common::{Connector, Url};
use trillium_testing::{RuntimeTrait, TestResult, TestTransport, harness};
#[test(harness)]
async fn early_hints_then_final() -> TestResult {
let (transport, conn_fut) = test_conn(|client| client.get("http://example.com")).await;
let expected_request = formatdoc! {"
GET / HTTP/1.1\r
Host: example.com\r
Accept: */*\r
User-Agent: {USER_AGENT}\r
\r
"};
assert_eq!(expected_request, transport.read_available_string().await);
transport.write_all(indoc! {"
HTTP/1.1 103 Early Hints\r
Link: </styles.css>; rel=preload; as=style\r
Link: </script.js>; rel=preload; as=script\r
\r
"});
transport.write_all(formatdoc! {"
HTTP/1.1 200 Ok\r
Date: {TEST_DATE}\r
Connection: close\r
Content-Length: 5\r
Server: text\r
\r
hello\
"});
let mut conn = conn_fut.await.unwrap();
assert_eq!("hello", conn.response_body().read_string().await?);
assert_eq!(Some(Status::Ok), conn.status());
assert_eq!(
conn.response_headers().get_str(KnownHeaderName::Server),
Some("text")
);
assert!(conn.response_headers().get(KnownHeaderName::Link).is_none());
Ok(())
}
#[test(harness)]
async fn multiple_early_hints_then_final() -> TestResult {
let (transport, conn_fut) = test_conn(|client| client.get("http://example.com")).await;
let expected_request = formatdoc! {"
GET / HTTP/1.1\r
Host: example.com\r
Accept: */*\r
User-Agent: {USER_AGENT}\r
\r
"};
assert_eq!(expected_request, transport.read_available_string().await);
transport.write_all(indoc! {"
HTTP/1.1 103 Early Hints\r
Link: </styles.css>; rel=preload; as=style\r
\r
HTTP/1.1 103 Early Hints\r
Link: </script.js>; rel=preload; as=script\r
X-Hint: something\r
\r
"});
transport.write_all(formatdoc! {"
HTTP/1.1 200 Ok\r
Date: {TEST_DATE}\r
Connection: close\r
Content-Length: 5\r
Server: text\r
\r
hello\
"});
let mut conn = conn_fut.await.unwrap();
assert_eq!("hello", conn.response_body().read_string().await?);
assert_eq!(Some(Status::Ok), conn.status());
assert!(conn.response_headers().get(KnownHeaderName::Link).is_none());
assert!(conn.response_headers().get("X-Hint").is_none());
Ok(())
}
#[test(harness)]
async fn early_hints_then_continue_then_final() -> TestResult {
let (transport, conn_fut) =
test_conn(|client| client.post("http://example.com").with_body("body")).await;
let expected_request_head = formatdoc! {"
POST / HTTP/1.1\r
Host: example.com\r
Accept: */*\r
Content-Length: 4\r
Expect: 100-continue\r
User-Agent: {USER_AGENT}\r
\r
"};
assert_eq!(
expected_request_head,
transport.read_available_string().await
);
transport.write_all(indoc! {"
HTTP/1.1 103 Early Hints\r
Link: </styles.css>; rel=preload; as=style\r
\r
HTTP/1.1 100 Continue\r
Server: Caddy\r
\r
"});
assert_eq!("body", transport.read_available_string().await);
transport.write_all(formatdoc! {"
HTTP/1.1 200 Ok\r
Date: {TEST_DATE}\r
Connection: close\r
Content-Length: 5\r
Server: text\r
\r
hello\
"});
let mut conn = conn_fut.await.unwrap();
assert_eq!("hello", conn.response_body().read_string().await?);
assert_eq!(Some(Status::Ok), conn.status());
assert_eq!(
conn.response_headers()
.get_values(KnownHeaderName::Server)
.unwrap(),
["text"].as_slice()
);
assert!(conn.response_headers().get(KnownHeaderName::Link).is_none());
Ok(())
}
const TEST_DATE: &str = "Tue, 21 Nov 2023 21:27:21 GMT";
struct TestConnector<R>(Sender<TestTransport>, R);
impl<R: RuntimeTrait> Connector for TestConnector<R> {
type Runtime = R;
type Transport = TestTransport;
type Udp = ();
async fn connect(&self, _url: &Url) -> io::Result<Self::Transport> {
let (server, client) = TestTransport::new();
let _ = self.0.send(server).await;
Ok(client)
}
fn runtime(&self) -> Self::Runtime {
self.1.clone()
}
async fn resolve(&self, _host: &str, _port: u16) -> io::Result<Vec<SocketAddr>> {
Ok(vec![])
}
}
async fn test_conn(
setup: impl FnOnce(Client) -> Conn + Send + 'static,
) -> (TestTransport, impl Future<Output = Result<Conn, Error>>) {
let (sender, receiver) = async_channel::unbounded();
let client = Client::new(TestConnector(sender, trillium_testing::runtime()));
let runtime = client.connector().runtime();
let conn_fut = runtime.spawn(setup(client).into_future()).into_future();
let transport = receiver.recv().await.unwrap();
(transport, async move { conn_fut.await.unwrap() })
}