Skip to content

Commit

Permalink
Remove dependency to real HTTP client in local mode
Browse files Browse the repository at this point in the history
  • Loading branch information
alexliesenfeld committed Oct 15, 2023
1 parent ce8a5e4 commit c51d7fa
Show file tree
Hide file tree
Showing 20 changed files with 270 additions and 114 deletions.
37 changes: 30 additions & 7 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,57 @@ name: Build
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust:
- stable
- beta
- nightly
- 1.64.0 # MSRV
# Default features enabled
- version: stable
- version: beta
- version: nightly
- version: 1.65.0
# Also remote feature enabled
- version: stable
features: remote
- version: beta
features: remote
- version: nightly
features: remote
- version: 1.70.0
features: remote

steps:
- uses: actions/checkout@v2

- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
toolchain: ${{ matrix.rust.version }}
override: true
components: rustfmt, clippy

- name: Set feature arguments
id: set_features
run: |
if [ "${{ matrix.rust.features }}" == "remote" ]; then
echo "::set-output name=FEATURES_ARGS::--features remote"
else
echo "::set-output name=FEATURES_ARGS::"
fi
- uses: actions-rs/cargo@v1
with:
command: build
args: ${{ steps.set_features.outputs.FEATURES_ARGS }}

- uses: actions-rs/cargo@v1
with:
command: test
args: ${{ steps.set_features.outputs.FEATURES_ARGS }}

- uses: actions-rs/cargo@v1
with:
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## Version 0.7.0

- **BREAKING CHANGES**:
- For connecting to **remote** httpmock servers during tests using any of the `connect` methods like
[MockServer::connect](struct.MockServer.html#method.connect),
[MockServer::connect_async](struct.MockServer.html#method.connect_async),
[MockServer::connect_async](struct.MockServer.html#method.connect_from_env), or
[MockServer::connect_from_env](struct.MockServer.html#method.connect_from_env_async), you must now activate the
`remote` feature. This feature is not enabled by default.

- Improvements:
- The dependency tree has been significantly slimmed down when the `remote` feature is not enabled.
- If the new `remote` feature is not enabled, `httpmock` no longer has a dependency on a real HTTP client.
As a result, certain [TLS issues previously reported by users](https://github.com/alexliesenfeld/httpmock/issues/82)
should no longer arise.

- This release also updates all dependencies to the most recent version.
- The minimum Rust version has been bumped to 1.65 (and 1.70 for the standalone mode).

## Version 0.6.8

- This is a maintenance release that updates all dependencies to the most recent version.
Expand Down
29 changes: 16 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,45 +15,48 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_regex = "1.1"
lazy_static = "1.4"
hyper = { version = "0.14", features = ["server", "http1", "tcp"] }
tokio = { version = "1.29", features = ["sync", "macros", "rt-multi-thread", "signal"] }
isahc = "1.7"

base64 = "0.21"
regex = "1.9"
regex = "1.10"
log = "0.4"
url = "2.4"
assert-json-diff = "2.0"
async-trait = "0.1"
async-object-pool = "0.1"
crossbeam-utils = "0.8"
futures-util = "0.3"
similar = "2.2"
similar = "2.3"
levenshtein = "1.0"
form_urlencoded = "1.2"

hyper = { version = "0.14", features = ["server", "http1", "tcp"] }
tokio = { version = "1.33", features = ["sync", "macros", "rt-multi-thread", "signal"] }

isahc = { version = "1.7", optional = true }
basic-cookies = { version = "0.1", optional = true }
colored = { version = "2.0", optional = true }
clap = { version = "4.3", features = ["derive", "env"], optional = true }
clap = { version = "4.4", features = ["derive", "env"], optional = true }
env_logger = { version = "0.10", optional = true }
serde_yaml = { version = "0.9", optional = true }
async-std = { version = "1.12", features = ["attributes", "unstable"] }

[dev-dependencies]
env_logger = "0.10"
tokio-test = "0.4"
async-std = { version = "1.12", features = ["attributes", "unstable"] }
isahc = { version = "1.7", features = ["json"] }
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
actix-rt = "2.8"
actix-rt = "2.9"
colored = "2.0"
ureq = "2.7"
ureq = "2.8"

isahc = { version = "1.7", features = ["json"] }
syn = { version = "2.0", features = ["full"] }

reqwest = "0.11.22"
[features]
default = ["cookies"]
standalone = ["clap", "env_logger", "serde_yaml"]
standalone = ["clap", "env_logger", "serde_yaml", "remote"]
color = ["colored"]
cookies = ["basic-cookies"]
remote = ["isahc"]

[[bin]]
name = "httpmock"
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ================================================================================
# Builder
# ================================================================================
FROM rust:1.64 as builder
FROM rust:1.70 as builder
WORKDIR /usr/src/httpmock

COPY Cargo.toml .
Expand Down
15 changes: 15 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.PHONY: build
build:
cargo build

.PHONY: test-local
test-local:
cargo test

.PHONY: test-remote
test-local:
cargo test --features=remote

.PHONY: build-docker
build-docker:
docker build -t alexliesenfeld/httpmock:latest .
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
[![codecov](https://codecov.io/gh/alexliesenfeld/httpmock/branch/master/graph/badge.svg)](https://codecov.io/gh/alexliesenfeld/httpmock)
[![crates.io](https://img.shields.io/crates/d/httpmock.svg)](https://crates.io/crates/httpmock)
[![Mentioned in Awesome](https://camo.githubusercontent.com/e5d3197f63169393ee5695f496402136b412d5e3b1d77dc5aa80805fdd5e7edb/68747470733a2f2f617765736f6d652e72652f6d656e74696f6e65642d62616467652e737667)](https://github.com/rust-unofficial/awesome-rust#testing)
[![Rust](https://img.shields.io/badge/rust-1.64%2B-blue.svg?maxAge=3600)](https://github.com/rust-lang/rust/blob/master/RELEASES.md#version-1540-2021-07-29)
[![Rust](https://img.shields.io/badge/rust-1.65%2B-blue.svg?maxAge=3600)](https://github.com/rust-lang/rust/blob/master/RELEASES.md#version-1540-2021-07-29)

</div>

Expand Down Expand Up @@ -44,7 +44,7 @@ Add `httpmock` to `Cargo.toml`:

```toml
[dev-dependencies]
httpmock = "0.6"
httpmock = "0.7"
```

You can then use `httpmock` as follows:
Expand Down
35 changes: 29 additions & 6 deletions src/api/adapter/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ use std::fmt::Debug;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use async_std::io::WriteExt;

use async_trait::async_trait;
use isahc::prelude::*;

use crate::api::adapter::{build_http_client, http_ping, InternalHttpClient, MockServerAdapter};
use async_std::net::TcpStream;
use async_std::prelude::*;

use crate::api::adapter::{MockServerAdapter};

use crate::common::data::{ActiveMock, ClosestMatch, MockDefinition, MockRef, RequestRequirements};
use crate::server::web::handlers::{
add_new_mock, delete_all_mocks, delete_history, delete_one_mock, read_one_mock, verify,
Expand All @@ -17,16 +21,13 @@ use crate::server::MockServerState;
pub struct LocalMockServerAdapter {
pub addr: SocketAddr,
local_state: Arc<MockServerState>,
client: Arc<InternalHttpClient>,
}

impl LocalMockServerAdapter {
pub fn new(addr: SocketAddr, local_state: Arc<MockServerState>) -> Self {
let client = build_http_client();
LocalMockServerAdapter {
addr,
local_state,
client,
}
}
}
Expand Down Expand Up @@ -81,6 +82,28 @@ impl MockServerAdapter for LocalMockServerAdapter {
}

async fn ping(&self) -> Result<(), String> {
http_ping(&self.addr, self.client.borrow()).await
let addr = self.addr.to_string();

let mut stream = TcpStream::connect(&addr).await
.map_err(|err| format!("Cannot connect to mock server: {}", err))?;

let request = format!(
"GET /__httpmock__/ping HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
addr
);

stream.write_all(request.as_bytes()).await
.map_err(|err| format!("Cannot send request to mock server: {}", err))?;

let mut buf = vec![0u8; 1024];
stream.read(&mut buf).await
.map_err(|err| format!("Cannot read response from mock server: {}", err))?;

let response = String::from_utf8_lossy(&buf);
if !response.contains("200 OK") {
return Err(format!("Unexpected mock server response. Expected '{}' to contain '200 OK'", response))
}

Ok(())
}
}
61 changes: 3 additions & 58 deletions src/api/adapter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ use std::sync::Arc;
use std::time::Duration;

use async_trait::async_trait;
use isahc::http::Request;
use isahc::prelude::Configurable;
use isahc::{AsyncReadResponseExt, ResponseExt};

use serde::{Deserialize, Serialize};

use crate::common::data::{ActiveMock, ClosestMatch, MockDefinition, MockRef, RequestRequirements};
Expand All @@ -15,13 +13,13 @@ use crate::server::web::handlers::{
};

pub mod local;

#[cfg(feature = "remote")]
pub mod standalone;

/// Type alias for [regex::Regex](../regex/struct.Regex.html).
pub type Regex = regex::Regex;

pub type InternalHttpClient = isahc::HttpClient;

/// Represents an HTTP method.
#[derive(Serialize, Deserialize, Debug)]
pub enum Method {
Expand Down Expand Up @@ -80,56 +78,3 @@ pub trait MockServerAdapter {
async fn delete_history(&self) -> Result<(), String>;
async fn ping(&self) -> Result<(), String>;
}

async fn http_ping(
server_addr: &SocketAddr,
http_client: &InternalHttpClient,
) -> Result<(), String> {
let request_url = format!("http://{}/__httpmock__/ping", server_addr);
let request = Request::builder()
.method("GET")
.uri(request_url)
.body("".to_string())
.unwrap();

let (status, _body) = match execute_request(request, http_client).await {
Err(err) => return Err(format!("cannot send request to mock server: {}", err)),
Ok(sb) => sb,
};

if status != 200 {
return Err(format!(
"Could not create mock. Mock server response: status = {}",
status
));
}

Ok(())
}

async fn execute_request(
req: Request<String>,
http_client: &InternalHttpClient,
) -> Result<(u16, String), String> {
let mut response = match http_client.send_async(req).await {
Err(err) => return Err(format!("cannot send request to mock server: {}", err)),
Ok(r) => r,
};

// Evaluate the response status
let body = match response.text().await {
Err(err) => return Err(format!("cannot send request to mock server: {}", err)),
Ok(b) => b,
};

Ok((response.status().as_u16(), body))
}

fn build_http_client() -> Arc<InternalHttpClient> {
Arc::new(
InternalHttpClient::builder()
.tcp_keepalive(Duration::from_secs(60 * 60 * 24))
.build()
.expect("Cannot build HTTP client"),
)
}

0 comments on commit c51d7fa

Please sign in to comment.