我有一个 CLI 应用程序,它对用户输入相关 url 执行一些 http 请求。
我曾经使用 https://httpbin.org 对此进行了测试,但这很脆弱并且不 在没有网络连接的情况下工作(由于某种原因,该网站现在也很慢)。
所以我切换到
mockito
,作为一个简单的回显测试服务器,类似于httpbin的base64端点。
fn setup_mockito_test_server() -> mockito::ServerGuard {
let mut server = mockito::Server::new();
server
.mock("GET", mockito::Matcher::Regex(r"^/echo/.*$".to_string()))
.with_status(200)
.with_header("content-type", "text/plain")
.with_body_from_request(|req| req.path()[6..].as_bytes().to_owned())
.create();
server
}
#[test]
fn example_test() {
let server = setup_mockito_test_server();
let resp = reqwest::blocking::get(format!("{}/echo/foobar", server.url()))
.unwrap().text().unwrap();
assert_eq!(resp, "foobar");
}
这效果很好,但要测试一些不同的代码路径,我需要使用 TLS (https)。 我快速进行了谷歌搜索,但既没有
mockito
,httpmock
也没有wiremock
似乎开箱即用地支持这一点。
如何创建具有 HTTPS 支持的等效测试服务器?
我最终得到了以下测试代码:
use reqwest::{Certificate, ClientBuilder};
#[tokio::test]
async fn example_test() {
let server = tokio::spawn(async {
run_https_test_server(1234, echo_handler).await.unwrap()
});
let client = ClientBuilder::new()
.add_root_certificate(
Certificate::from_pem(EXAMPLE_HOST_CERT).unwrap(),
)
.build()
.unwrap();
let request = client
.get("https://localhost:1234/echo/foobar")
.build()
.unwrap();
let response =
client.execute(request).await.unwrap().text().await.unwrap();
assert_eq!(response, "foobar");
server.abort();
assert!(server.await.unwrap_err().is_cancelled());
}
服务器代码:
use std::{
future::Future,
net::{Ipv4Addr, SocketAddr},
sync::Arc,
};
use http::{Request, Response, StatusCode};
use http_body_util::Full;
use hyper::{
body::{Body, Bytes, Incoming},
service::service_fn,
};
use hyper_util::{
rt::{TokioExecutor, TokioIo},
server::conn::auto::Builder,
};
use rustls::ServerConfig;
use tokio::net::TcpListener;
use tokio_rustls::TlsAcceptor;
// can be generated using
// `openssl genrsa 2048 > example_host.key`
pub const EXAMPLE_HOST_KEY: &[u8] = include_bytes!("example_host.key");
// can be generated using
// `openssl req -new -x509 -subj "/CN=localhost" -key example_host.key -out example_host.crt`
pub const EXAMPLE_HOST_CERT: &[u8] = include_bytes!("example_host.crt");
pub async fn echo_handler(
req: Request<Incoming>,
) -> Result<Response<Full<Bytes>>, hyper::Error> {
const PREFIX: &str = "/echo/";
let mut response = Response::new(Full::default());
let path = req.uri().path();
if !path.starts_with(PREFIX) {
*response.status_mut() = StatusCode::NOT_FOUND;
return Ok(response);
}
*response.body_mut() =
Full::from(path[PREFIX.len()..].as_bytes().to_owned());
Ok(response)
}
pub async fn run_https_test_server<
E: std::error::Error + Send + Sync + 'static,
D: Send + 'static,
B: Body<Error = E, Data = D> + Send + 'static,
R: Future<Output = Result<Response<B>, hyper::Error>> + Send + 'static,
F: Fn(Request<Incoming>) -> R + Send + Copy + 'static,
>(
port: u16,
request_handler: F,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), port);
let mut certs_file = std::io::Cursor::new(EXAMPLE_HOST_CERT);
let certs = rustls_pemfile::certs(&mut certs_file)
.collect::<std::io::Result<Vec<_>>>()?;
let mut key_file = std::io::Cursor::new(EXAMPLE_HOST_KEY);
let key =
rustls_pemfile::private_key(&mut key_file).map(|key| key.unwrap())?;
let incoming = TcpListener::bind(&addr).await?;
let mut server_config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)?;
server_config.alpn_protocols = ["h2" as &str, "http/1.1", "http/1.0"]
.iter()
.map(|v| v.as_bytes().to_vec())
.collect();
let acceptor = TlsAcceptor::from(Arc::new(server_config));
let service = service_fn(request_handler);
loop {
let (tcp_stream, _remote_addr) = incoming.accept().await?;
let tls_acceptor = acceptor.clone();
tokio::spawn(async move {
let tls_stream = match tls_acceptor.accept(tcp_stream).await {
Ok(tls_stream) => tls_stream,
Err(err) => {
panic!("tls handshake failed: {err:#}");
}
};
// Aborting the server might raise an error, so we ignore it.
let _ = Builder::new(TokioExecutor::new())
.serve_connection_with_upgrades(
TokioIo::new(tls_stream),
service,
)
.await;
});
}
}
Cargo.toml:
[dev-dependencies]
reqwest = "0.11.23"
tokio = "1.35.1"
hyper = "1.1.0"
hyper-tls = "0.6.0"
http = "1.0.0"
hyper-util = { version = "0.1.2", features = ["server-auto"] }
http-body-util = "0.1"
tokio-rustls = "0.25"
WireMock 具有 HTTPs 支持。如果您不介意在单独的进程中运行测试服务器,这可能是一个解决方案:
docker run --rm -p 8080:8080 -p 8443:8443 -v $PWD/target/cert:/home/wiremock/cert --name wiremock \
wiremock/wiremock:3.3.1 \
--https-port 8443 \
--keystore-type pkcs12 \
--https-keystore /home/wiremock/cert/localhost.p12
它接受 PKCS #12 存档中的 TLS 密钥/证书,该存档可以使用 openssl 生成(存档必须具有“password”密码,如下所示):
openssl pkcs12 -export -out localhost.p12 -inkey localhost.key -in localhost.crt -passout pass:password
这是一个通过管理端点定义 WireMock 存根的示例测试:
fn setup_wiremock_test_server() {
let client = reqwest::blocking::Client::new();
// Using plain HTTP port to setup mappings, but HTTPs is also possible
let wm_mappings_endpoint = "http://localhost:8080/__admin/mappings";
// Remove any existing mappings
let _ = client.delete(wm_mappings_endpoint).send();
// Setup a mapping for GET /foobar
let _ = client.post(wm_mappings_endpoint)
.body(r#"{
"request": {
"method": "GET",
"url": "/foobar"
},
"response": {
"status": 200,
"body": "Hello, world!"
}
}"#)
.send();
}
/// A solution based on Java Wiremock
#[test]
fn wiremock_test() {
setup_wiremock_test_server();
let cert = std::fs::read("target/cert/rootCA.crt").unwrap();
let cert = reqwest::Certificate::from_pem(&cert).unwrap();
let client = reqwest::blocking::Client::builder()
.add_root_certificate(
cert
)
.use_rustls_tls()
.build()
.unwrap();
let resp = client.get("https://localhost:8443/foobar").send().unwrap();
assert_eq!(resp.text().unwrap().as_str(), "Hello, world!");
}