我有一个CLI应用程序,做一些http请求,以用户输入依赖的URL.

我曾经用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). 我做了一个快速的谷歌搜索,但没有mockitohttpmockwiremock 似乎支持这个开箱即用.

How can I create an equivalent test server with HTTPS support?

推荐答案

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

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;
        });
    }
}

货运量:

[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"

Rust相关问答推荐

在HashMap中插入Vacant条目的可变借位问题

从特征实现调用函数的Rust惯用方法

如何在Rust中表示仅具有特定大小的数组

具有对同一类型的另一个实例的可变引用的

无法定义名为&new&的关联函数,该函数的第一个参数不是self

在Rust中,Box:ed struct 与普通 struct 在删除顺序上有区别吗?

不能在Rust中使用OpenGL绘制三角形

Rust从关联函数启动线程

如何将 struct 数组放置在另一个 struct 的末尾而不进行内存分段

Rust ECDH 不会产生与 NodeJS/Javascript 和 C 实现相同的共享密钥

没有得到无法返回引用局部变量`queues`的值返回引用当前函数拥有的数据的值的重复逻辑

发生移动是因为 `data` 的类型为 `Vec`,它没有实现 `Copy` 特性

在 RefCell 上borrow

意外的正则表达式模式匹配

为什么这个值在上次使用后没有下降?

在使用大型表达式时(8k 行需要一小时编译),是否可以避免 Rust 中的二次编译时间?

如果我立即等待,为什么 `tokio::spawn` 需要一个 `'static` 生命周期?

如何重写这个通用参数?

当值是新类型包装器时,对键的奇怪 HashMap 生命周期要求

如何在 Rust 中构建一个 str