支持 TLS 的模拟 HTTP 服务器

问题描述 投票:0回答:2

我有一个 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 支持的等效测试服务器?

testing rust https integration-testing
2个回答
2
投票

我最终得到了以下测试代码:

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"

0
投票
来自 Java 世界的

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!");
    }
© www.soinside.com 2019 - 2024. All rights reserved.