Skip to content

Commit

Permalink
Implement VCRReplaySearch modes: SearchAll, SkipFound
Browse files Browse the repository at this point in the history
  • Loading branch information
mksh committed Apr 30, 2023
1 parent cb37f76 commit 8bba88c
Show file tree
Hide file tree
Showing 7 changed files with 710 additions and 36 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rvcr"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
description = "Record-and-replay HTTP testing for requests"
authors = ["[email protected]"]
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ To record HTTP requests, initialize client like following
use rvcr::{VCRMiddleware, VCRMode};

let mut bundle = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
bundle.push("tests/resources/test.vcr");
bundle.push("tests/resources/replay.vcr.json");

let middleware: VCRMiddleware = VCRMiddleware::try_from(bundle.clone())
.unwrap()
Expand All @@ -35,7 +35,14 @@ To record HTTP requests, initialize client like following
```

Now `ClientWithMiddleware` instance will be recording requests to a file
located in `tests/resources/test.vcr` inside the project.
located in `tests/resources/replay.vcr.json` inside the project.

To use recorded VCR cassette files, replace `.with_mode(VCRMode::Record)`
with `.with_mode(VCRMode::Replay)`, or omit it, since replay is used by default.

## Search mode

When replaying, rVCR can skip requests already found when searching for
subsequent requests (the default). To disable skipping requests,
which is useful, for example, if requests are done in parallel and responses
may come in random order, use `.with_search(VCRReplaySearch::SearchAll)`.
35 changes: 30 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//! use rvcr::{VCRMiddleware, VCRMode};
//!
//! let mut bundle = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
//! bundle.push("tests/resources/test.vcr");
//! bundle.push("tests/resources/replay.vcr.json");
//!
//! let middleware: VCRMiddleware = VCRMiddleware::try_from(bundle.clone())
//! .unwrap()
Expand All @@ -30,7 +30,7 @@ use std::{collections::HashMap, fs, path::PathBuf, str::FromStr, sync::Mutex};

use base64::{engine::general_purpose, Engine};
use reqwest_middleware::Middleware;
use vcr_cassette::RecorderId;
use vcr_cassette::{HttpInteraction, RecorderId};

pub const VERSION: &str = env!("CARGO_PKG_VERSION");

Expand All @@ -44,6 +44,7 @@ pub struct VCRMiddleware {
path: Option<PathBuf>,
storage: Mutex<vcr_cassette::Cassette>,
mode: VCRMode,
search: VCRReplaySearch,
skip: Mutex<usize>,
}

Expand All @@ -56,6 +57,16 @@ pub enum VCRMode {
Replay,
}

/// Skip requests
#[derive(Eq, PartialEq)]
pub enum VCRReplaySearch {
/// Skip requests which already have been found. Useful for
/// verifying use-cases with strict request order.
SkipFound,
/// Search through all requests every time
SearchAll,
}

pub type VCRError = &'static str;

/// Implements boilerplate for converting between vcr_cassette
Expand All @@ -70,6 +81,12 @@ impl VCRMiddleware {
self
}

/// Adjust search behavior for responses
pub fn with_search(mut self, search: VCRReplaySearch) -> Self {
self.search = search;
self
}

/// Adjust path in the middleware and return it
pub fn with_path(mut self, path: impl Into<PathBuf>) -> Self {
self.path = Some(path.into());
Expand Down Expand Up @@ -178,9 +195,16 @@ impl VCRMiddleware {

fn find_response_in_vcr(&self, req: vcr_cassette::Request) -> Option<vcr_cassette::Response> {
let cassette = self.storage.lock().unwrap();
let skip = *self.skip.lock().unwrap();
*self.skip.lock().unwrap() += 1;
for interaction in cassette.http_interactions.iter().skip(skip) {
let iteractions: Vec<&HttpInteraction> = match self.search {
VCRReplaySearch::SkipFound => {
let skip = *self.skip.lock().unwrap();
*self.skip.lock().unwrap() += 1;
cassette.http_interactions.iter().skip(skip).collect()
}
VCRReplaySearch::SearchAll => cassette.http_interactions.iter().collect(),
};

for interaction in iteractions {
if interaction.request == req {
return Some(interaction.response.clone());
}
Expand Down Expand Up @@ -285,6 +309,7 @@ impl From<vcr_cassette::Cassette> for VCRMiddleware {
mode: VCRMode::Replay,
path: None,
skip: Mutex::new(0),
search: VCRReplaySearch::SkipFound,
}
}
}
Expand Down
152 changes: 150 additions & 2 deletions tests/integration/e2e.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use reqwest::Client;
use std::{path::PathBuf, sync::Arc};
use std::{path::PathBuf, sync::Arc, time::Duration};
use tokio::sync::Mutex;

use http::header::ACCEPT;
Expand Down Expand Up @@ -99,7 +99,7 @@ async fn send_and_compare(
async fn test_rvcr_replay() {
SCOPE.clone().init().await;
let mut bundle = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
bundle.push("tests/resources/test.vcr");
bundle.push("tests/resources/replay.vcr.json");

let middleware = VCRMiddleware::try_from(bundle.clone()).unwrap();

Expand Down Expand Up @@ -139,3 +139,151 @@ async fn test_rvcr_replay() {
)
.await;
}

#[tokio::test]
async fn test_rvcr_replay_search_all() {
SCOPE.clone().init().await;
let mut bundle = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
bundle.push("tests/resources/search-all.vcr.json");

let middleware = VCRMiddleware::try_from(bundle.clone())
.unwrap()
.with_search(rvcr::VCRReplaySearch::SearchAll);

let vcr_client: ClientWithMiddleware = ClientBuilder::new(reqwest::Client::new())
.with(middleware)
.build();

let real_client = Client::new();
send_and_compare(
reqwest::Method::GET,
"/get",
vec![(ACCEPT, "application/json")],
None,
vcr_client.clone(),
real_client.clone(),
)
.await;

send_and_compare(
reqwest::Method::POST,
"/post",
vec![(ACCEPT, "application/json")],
Some("test93"),
vcr_client.clone(),
real_client.clone(),
)
.await;

send_and_compare(
reqwest::Method::POST,
"/post",
vec![(ACCEPT, "application/json")],
Some("test93"),
vcr_client.clone(),
real_client,
)
.await;

let req1 = vcr_client
.request(
reqwest::Method::POST,
format!("{}{}", ADDRESS.to_string(), "/post"),
)
.send()
.await
.expect("Failed to get response");

// Ensure next request will get Date with 1 second more
// when recording
tokio::time::sleep(Duration::from_secs(1)).await;

let req2 = vcr_client
.request(
reqwest::Method::POST,
format!("{}{}", ADDRESS.to_string(), "/post"),
)
.send()
.await
.expect("Failed to get response");

let header_date_1 = req1.headers().get("date").unwrap();
let header_date_2 = req2.headers().get("date").unwrap();

// Since first request was identical to second, first response
// was returned for second request with SearchAll
assert_eq!(header_date_1, header_date_2);
}

#[tokio::test]
async fn test_rvcr_replay_skip_found() {
SCOPE.clone().init().await;
let mut bundle = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
bundle.push("tests/resources/skip-found.vcr.json");

let middleware = VCRMiddleware::try_from(bundle.clone())
.unwrap()
.with_search(rvcr::VCRReplaySearch::SkipFound);

let vcr_client: ClientWithMiddleware = ClientBuilder::new(reqwest::Client::new())
.with(middleware)
.build();

let real_client = Client::new();
send_and_compare(
reqwest::Method::GET,
"/get",
vec![(ACCEPT, "application/json")],
None,
vcr_client.clone(),
real_client.clone(),
)
.await;

send_and_compare(
reqwest::Method::POST,
"/post",
vec![(ACCEPT, "application/json")],
Some("test93"),
vcr_client.clone(),
real_client.clone(),
)
.await;

send_and_compare(
reqwest::Method::POST,
"/post",
vec![(ACCEPT, "application/json")],
Some("test93"),
vcr_client.clone(),
real_client,
)
.await;

let req1 = vcr_client
.request(
reqwest::Method::POST,
format!("{}{}", ADDRESS.to_string(), "/post"),
)
.send()
.await
.expect("Failed to get response");

tokio::time::sleep(Duration::from_secs(1)).await;

let req2 = vcr_client
.request(
reqwest::Method::POST,
format!("{}{}", ADDRESS.to_string(), "/post"),
)
.send()
.await
.expect("Failed to get response");

let header_date_1 = req1.headers().get("date").unwrap();
let header_date_2 = req2.headers().get("date").unwrap();

// Despite first request was identical to second, second response
// was returned for second request with SkipFound
assert_ne!(header_date_1, header_date_2);
}
Loading

0 comments on commit 8bba88c

Please sign in to comment.