Skip to content

Commit

Permalink
test(http): more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
lennartkloock committed Feb 2, 2025
1 parent 4ead112 commit 6da65b5
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 103 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ scuffle-workspace-hack.workspace = true
[dev-dependencies]
reqwest = { version = "0.12.12", default-features = false, features = ["rustls-tls"] }
rustls-pemfile = "2.2.0"
scuffle-future-ext.workspace = true
162 changes: 162 additions & 0 deletions crates/http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,165 @@ pub use server::builder::ServerBuilder;
pub use server::HttpServer;

pub type IncomingRequest = http::Request<body::IncomingBody>;

#[cfg(test)]
#[cfg_attr(all(test, coverage_nightly), coverage(off))]
mod tests {
use std::convert::Infallible;
use std::fmt::{Debug, Display};
use std::fs;
use std::io::BufReader;
use std::time::Duration;

use scuffle_future_ext::FutureExt;

use super::ServerBuilder;
use crate::service::{fn_http_service, service_clone_factory, HttpService, HttpServiceFactory};

fn get_available_addr() -> std::io::Result<std::net::SocketAddr> {
let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
listener.local_addr()
}

const RESPONSE_TEXT: &str = "Hello, world!";

async fn test_server<F>(builder: ServerBuilder<F>, tls: bool)
where
F: HttpServiceFactory + Debug + Clone + Send + 'static,
F::Error: Debug + Display,
F::Service: Clone + Debug + Send + 'static,
<F::Service as HttpService>::Error: std::error::Error + Debug + Display + Send + Sync,
<F::Service as HttpService>::ResBody: Send,
<<F::Service as HttpService>::ResBody as http_body::Body>::Data: Send,
<<F::Service as HttpService>::ResBody as http_body::Body>::Error: std::error::Error + Send + Sync,
{
let addr = get_available_addr().expect("failed to get available address");
let (ctx, handler) = scuffle_context::Context::new();

let server = builder.bind(addr).with_ctx(ctx).build().unwrap();

let handle = tokio::spawn(async move {
server.run().await.expect("server run failed");
});

// Wait for the server to start
tokio::time::sleep(std::time::Duration::from_millis(100)).await;

let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.https_only(tls)
.build()
.expect("failed to build client");
let resp = client
.get(format!("{}://{}/", if tls { "https" } else { "http" }, addr))
.send()
.await
.expect("failed to get response")
.text()
.await
.expect("failed to get text");

assert_eq!(resp, RESPONSE_TEXT);

handler.shutdown().await;
handle.await.expect("task failed");
}

#[tokio::test]
async fn http2_server() {
let builder = ServerBuilder::default()
.with_service_factory(service_clone_factory(fn_http_service(|_| async {
Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
})))
.disable_http1();

test_server(builder, false).await;
}

#[tokio::test]
async fn http12_server() {
let server = ServerBuilder::default().with_service_factory(service_clone_factory(fn_http_service(|_| async {
Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
})));

test_server(server, false).await;
}

fn rustls_config() -> rustls::ServerConfig {
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.expect("failed to install aws lc provider");

let certfile = fs::File::open("assets/cert.pem").expect("cert not found");
let certs = rustls_pemfile::certs(&mut BufReader::new(certfile))
.collect::<Result<Vec<_>, _>>()
.expect("failed to load certs");
let keyfile = fs::File::open("assets/key.pem").expect("key not found");
let key = rustls_pemfile::private_key(&mut BufReader::new(keyfile))
.expect("failed to load key")
.expect("no key found");

rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.expect("failed to build config")
}

#[tokio::test]
async fn rustls_http1_server() {
let builder = ServerBuilder::default()
.with_service_factory(service_clone_factory(fn_http_service(|_| async {
Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
})))
.with_rustls(rustls_config())
.disable_http2();

test_server(builder, true).await;
}

#[tokio::test]
async fn rustls_http12_server() {
let builder = ServerBuilder::default()
.with_service_factory(service_clone_factory(fn_http_service(|_| async {
Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
})))
.with_rustls(rustls_config());

test_server(builder, true).await;
}

#[tokio::test]
async fn rustls_http123_server() {
let builder = ServerBuilder::default()
.with_service_factory(service_clone_factory(fn_http_service(|_| async {
Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
})))
.with_rustls(rustls_config())
.enable_http3();

test_server(builder, true).await;
}

#[tokio::test]
async fn rustls_no_server() {
let addr = get_available_addr().expect("failed to get available address");

let builder = ServerBuilder::default()
.with_service_factory(service_clone_factory(fn_http_service(|_| async {
Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
})))
.with_rustls(rustls_config())
.bind(addr)
.disable_http1()
.disable_http2();

builder
.build()
.unwrap()
.run()
.with_timeout(Duration::from_millis(100))
.await
.expect("server timed out")
.expect("server failed");
}
}
106 changes: 12 additions & 94 deletions crates/http/src/server/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,85 +266,29 @@ where
#[cfg_attr(all(test, coverage_nightly), coverage(off))]
mod tests {
use std::convert::Infallible;
use std::fs;
use std::io::BufReader;

use super::ServerBuilder;
use crate::server::builder::ServerBuilderError;
use crate::service::{fn_http_service, service_clone_factory};

fn get_available_addr() -> std::io::Result<std::net::SocketAddr> {
let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
listener.local_addr()
}

const RESPONSE_TEXT: &str = "Hello, world!";

#[test]
fn builder_missing_bind() {
fn missing_bind() {
let builder = ServerBuilder::default().with_service_factory(service_clone_factory(fn_http_service(|_| async {
Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
})));

assert_eq!(builder.build().unwrap_err(), ServerBuilderError::MissingBind);
}

#[tokio::test]
async fn builder_rustls() {
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.expect("failed to install aws lc provider");

let certfile = fs::File::open("assets/cert.pem").expect("cert not found");
let certs = rustls_pemfile::certs(&mut BufReader::new(certfile))
.collect::<Result<Vec<_>, _>>()
.expect("failed to load certs");
let keyfile = fs::File::open("assets/key.pem").expect("key not found");
let key = rustls_pemfile::private_key(&mut BufReader::new(keyfile))
.expect("failed to load key")
.expect("no key found");

let rustls_config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.expect("failed to build config");

let (ctx, handler) = scuffle_context::Context::new();
let addr = get_available_addr().expect("failed to get available address");

let builder = ServerBuilder::default()
.with_ctx(ctx)
.with_service_factory(service_clone_factory(fn_http_service(|_| async {
Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
})))
.with_rustls(rustls_config)
.bind(addr)
.enable_http3();

let server = builder.build().expect("failed to build server");

let handle = tokio::spawn(async move {
server.run().await.expect("server run failed");
});

// Wait for the server to start
tokio::time::sleep(std::time::Duration::from_millis(50)).await;

let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.build()
.expect("failed to build client");

let resp = client
.get(format!("https://{}/", addr))
.send()
.await
.expect("failed to get response")
.text()
.await
.expect("failed to get text");

assert_eq!(resp, RESPONSE_TEXT);

handler.shutdown().await;
handle.await.expect("task failed");
}

#[test]
fn builder_missing_rustls() {
fn missing_rustls() {
let builder = ServerBuilder::default()
.with_service_factory(service_clone_factory(fn_http_service(|_| async {
Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
Expand All @@ -354,17 +298,10 @@ mod tests {
assert_eq!(builder.build().unwrap_err(), ServerBuilderError::MissingRustlsConfig);
}

fn get_available_addr() -> std::io::Result<std::net::SocketAddr> {
let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
listener.local_addr()
}

const RESPONSE_TEXT: &str = "Hello, world!";

#[tokio::test]
async fn simple_server() {
async fn simple() {
let mut builder = ServerBuilder::default().with_service_factory(service_clone_factory(fn_http_service(|_| async {
Ok::<_, Infallible>(http::Response::new(RESPONSE_TEXT.to_string()))
Ok::<_, Infallible>(http::Response::new("".to_string()))
})));

assert!(builder.ctx.is_none());
Expand Down Expand Up @@ -400,7 +337,7 @@ mod tests {
builder = builder.bind(addr);
assert!(builder.bind.is_some());

let (ctx, handler) = scuffle_context::Context::new();
let (ctx, _) = scuffle_context::Context::new();

builder = builder.with_ctx(ctx);
assert!(builder.ctx.is_some());
Expand All @@ -410,24 +347,5 @@ mod tests {
assert!(server.enable_http1);
assert!(server.enable_http2);
assert!(!server.enable_http3);

let handle = tokio::spawn(async move {
server.run().await.expect("server run failed");
});

// Wait for the server to start
tokio::time::sleep(std::time::Duration::from_millis(50)).await;

let resp = reqwest::get(format!("http://{}/", addr))
.await
.expect("failed to get response")
.text()
.await
.expect("failed to get text");

assert_eq!(resp, RESPONSE_TEXT);

handler.shutdown().await;
handle.await.expect("task failed");
}
}
18 changes: 9 additions & 9 deletions crates/http/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ where
}
}

impl<S> HttpServer<S>
impl<F> HttpServer<F>
where
S: HttpServiceFactory + Clone + Send + 'static,
S::Error: Debug + Display,
S::Service: Clone + Send + 'static,
<S::Service as HttpService>::Error: std::error::Error + Debug + Display + Send + Sync,
<S::Service as HttpService>::ResBody: Send,
<<S::Service as HttpService>::ResBody as http_body::Body>::Data: Send,
<<S::Service as HttpService>::ResBody as http_body::Body>::Error: std::error::Error + Send + Sync,
F: HttpServiceFactory + Clone + Send + 'static,
F::Error: Debug + Display,
F::Service: Clone + Send + 'static,
<F::Service as HttpService>::Error: std::error::Error + Debug + Display + Send + Sync,
<F::Service as HttpService>::ResBody: Send,
<<F::Service as HttpService>::ResBody as http_body::Body>::Data: Send,
<<F::Service as HttpService>::ResBody as http_body::Body>::Error: std::error::Error + Send + Sync,
{
/// Run the server.
///
Expand All @@ -54,7 +54,7 @@ where
/// - Start listening on all configured interfaces for incoming connections.
/// - Accept all incoming connections.
/// - Handle incoming requests by passing them to the configured service factory.
pub async fn run(self) -> Result<(), Error<S>> {
pub async fn run(self) -> Result<(), Error<F>> {
let start_tcp_backend = self.enable_http1 || self.enable_http2;

if let Some(rustls_config) = self.rustls_config {
Expand Down

0 comments on commit 6da65b5

Please sign in to comment.