diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5bb79cd..98bc711 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: - name: Update local toolchain run: | rustup update - rustup install nightly + rustup install 1.72 - name: Toolchain info run: | diff --git a/Cargo.toml b/Cargo.toml index 5e6c85c..f636e6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,3 +42,5 @@ vcr-cassette = "2" [dev-dependencies] tokio = { version = "1.17.0", features = ["full"] } tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] } +url = "2.4" + diff --git a/tests/integration/e2e.rs b/tests/integration/e2e.rs index fffa0c0..4972c74 100644 --- a/tests/integration/e2e.rs +++ b/tests/integration/e2e.rs @@ -1,50 +1,9 @@ use reqwest::Client; -use std::{path::PathBuf, sync::Arc, time::Duration}; -use tokio::sync::Mutex; +use std::{path::PathBuf, time::Duration}; use http::header::ACCEPT; use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; use rvcr::VCRMiddleware; -use tracing_subscriber::{ - filter, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer, -}; - -lazy_static::lazy_static! { - static ref SCOPE: TestScope = TestScope::default(); - static ref ADDRESS: String = String::from("http://127.0.0.1:38282"); -} - -#[derive(Clone)] -pub struct TestScope { - pub initialized: Arc>, -} - -impl Default for TestScope { - fn default() -> Self { - Self { - initialized: Arc::new(Mutex::new(false)), - } - } -} - -impl TestScope { - pub async fn init(self) { - let mut inited = self.initialized.lock().await; - if *inited == false { - if std::env::var("TEST_LOG").is_ok() { - let stdout_log = tracing_subscriber::fmt::layer().pretty(); - tracing_subscriber::registry() - .with( - stdout_log - // Add an `INFO` filter to the stdout logging layer - .with_filter(filter::LevelFilter::DEBUG), - ) - .init(); - } - *inited = true; - } - } -} async fn send_and_compare( method: reqwest::Method, @@ -54,9 +13,12 @@ async fn send_and_compare( vcr_client: ClientWithMiddleware, real_client: reqwest::Client, ) { - let mut req1 = vcr_client.request(method.clone(), format!("{}{}", ADDRESS.to_string(), path)); + let mut req1 = vcr_client.request( + method.clone(), + format!("{}{}", crate::ADDRESS.to_string(), path), + ); - let mut req2 = real_client.request(method, format!("{}{}", ADDRESS.to_string(), path)); + let mut req2 = real_client.request(method, format!("{}{}", crate::ADDRESS.to_string(), path)); for (header_name, header_value) in headers { req1 = req1.header(header_name.clone(), header_value); @@ -97,7 +59,7 @@ async fn send_and_compare( #[tokio::test] async fn test_rvcr_replay() { - SCOPE.clone().init().await; + crate::SCOPE.clone().init().await; let mut bundle = PathBuf::from(env!("CARGO_MANIFEST_DIR")); bundle.push("tests/resources/replay.vcr.json"); @@ -142,7 +104,7 @@ async fn test_rvcr_replay() { #[tokio::test] async fn test_rvcr_replay_search_all() { - SCOPE.clone().init().await; + crate::SCOPE.clone().init().await; let mut bundle = PathBuf::from(env!("CARGO_MANIFEST_DIR")); bundle.push("tests/resources/search-all.vcr.json"); @@ -188,7 +150,7 @@ async fn test_rvcr_replay_search_all() { let req1 = vcr_client .request( reqwest::Method::POST, - format!("{}{}", ADDRESS.to_string(), "/post"), + format!("{}{}", crate::ADDRESS.to_string(), "/post"), ) .send() .await @@ -201,7 +163,7 @@ async fn test_rvcr_replay_search_all() { let req2 = vcr_client .request( reqwest::Method::POST, - format!("{}{}", ADDRESS.to_string(), "/post"), + format!("{}{}", crate::ADDRESS.to_string(), "/post"), ) .send() .await @@ -217,7 +179,7 @@ async fn test_rvcr_replay_search_all() { #[tokio::test] async fn test_rvcr_replay_skip_found() { - SCOPE.clone().init().await; + crate::SCOPE.clone().init().await; let mut bundle = PathBuf::from(env!("CARGO_MANIFEST_DIR")); bundle.push("tests/resources/skip-found.vcr.json"); @@ -263,7 +225,7 @@ async fn test_rvcr_replay_skip_found() { let req1 = vcr_client .request( reqwest::Method::POST, - format!("{}{}", ADDRESS.to_string(), "/post"), + format!("{}{}", crate::ADDRESS.to_string(), "/post"), ) .send() .await @@ -274,7 +236,7 @@ async fn test_rvcr_replay_skip_found() { let req2 = vcr_client .request( reqwest::Method::POST, - format!("{}{}", ADDRESS.to_string(), "/post"), + format!("{}{}", crate::ADDRESS.to_string(), "/post"), ) .send() .await @@ -291,7 +253,7 @@ async fn test_rvcr_replay_skip_found() { #[cfg(feature = "compress")] #[tokio::test] async fn test_rvcr_replay_compressed() { - SCOPE.clone().init().await; + crate::SCOPE.clone().init().await; let mut bundle = PathBuf::from(env!("CARGO_MANIFEST_DIR")); bundle.push("tests/resources/replay.vcr.zip"); diff --git a/tests/integration/lib.rs b/tests/integration/lib.rs index b6ae44f..eccfa44 100644 --- a/tests/integration/lib.rs +++ b/tests/integration/lib.rs @@ -1 +1,46 @@ +use std::sync::Arc; + +use tokio::sync::Mutex; +use tracing_subscriber::{ + filter, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer, +}; + mod e2e; +mod modifiers; + +#[derive(Clone)] +pub struct TestScope { + pub initialized: Arc>, +} + +impl Default for TestScope { + fn default() -> Self { + Self { + initialized: Arc::new(Mutex::new(false)), + } + } +} + +impl TestScope { + pub async fn init(self) { + let mut inited = self.initialized.lock().await; + if *inited == false { + if std::env::var("TEST_LOG").is_ok() { + let stdout_log = tracing_subscriber::fmt::layer().pretty(); + tracing_subscriber::registry() + .with( + stdout_log + // Add an `INFO` filter to the stdout logging layer + .with_filter(filter::LevelFilter::DEBUG), + ) + .init(); + } + *inited = true; + } + } +} + +lazy_static::lazy_static! { + static ref SCOPE: TestScope = TestScope::default(); + static ref ADDRESS: String = String::from("http://127.0.0.1:38282"); +} diff --git a/tests/integration/modifiers.rs b/tests/integration/modifiers.rs new file mode 100644 index 0000000..23a07c1 --- /dev/null +++ b/tests/integration/modifiers.rs @@ -0,0 +1,135 @@ +use std::borrow::Cow; +use std::fs; +use std::path::PathBuf; + +use reqwest_middleware::ClientBuilder; +use reqwest_middleware::ClientWithMiddleware; +use rvcr::VCRMiddleware; +use rvcr::VCRMode; +use vcr_cassette::{Request, Response}; + +// Replace access_token and secret header values with dummy ones +fn filter_query_params(mut uri: url::Url) -> url::Url { + let sensitive_query_params = ["access_token", "secret"]; + let cloned = uri.clone(); + let filtered_query_params = cloned.query_pairs().map(|(k, v)| { + if sensitive_query_params.contains(&k.as_ref()) { + (k.clone(), Cow::from(format!("__{}__", k.to_uppercase()))) + } else { + (k, v) + } + }); + uri.query_pairs_mut() + .clear() + .extend_pairs(filtered_query_params) + .finish(); + uri +} + +fn request_modifier(req: &mut Request) { + // Overwrite query params with filtered ones + req.uri = filter_query_params(req.uri.clone()); +} + +fn response_modifier(resp: &mut Response) { + for (name, value) in &mut resp.headers { + if name == "server" { + (*value).pop().unwrap(); + (*value).push("Test Server Header Emulated Expect".to_string()); + } + } +} + +fn saved_fixture_path(path: &str) -> PathBuf { + let mut bundle = PathBuf::from(std::env::temp_dir()); + bundle.push(path); + + if bundle.exists() { + std::fs::remove_file(bundle.clone()).unwrap(); + } + bundle +} + +#[tokio::test] +pub async fn test_modifier_request() { + crate::SCOPE.clone().init().await; + let bundle = saved_fixture_path("request-modifer-test-case.vcr.json"); + + let middleware = VCRMiddleware::try_from(bundle.clone()) + .unwrap() + .with_mode(VCRMode::Record) + .with_modify_request(request_modifier); + + let vcr_client: ClientWithMiddleware = ClientBuilder::new(reqwest::Client::new()) + .with(middleware) + .build(); + + vcr_client + .request( + reqwest::Method::POST, + format!( + "{}{}", + crate::ADDRESS.to_string(), + "/post?access_token=s3cr3t&spam=eggs&secret=s3cr3t", + ), + ) + .send() + .await + .expect("Can not send request"); + // Drop triggers recording + drop(vcr_client); + + let vcr_content = fs::read(bundle.clone()).expect("VCR file not created during test case"); + + let cassette: vcr_cassette::Cassette = serde_json::from_slice(&vcr_content).unwrap(); + let interaction = cassette.http_interactions.first().unwrap(); + let recorded_url = interaction.request.uri.to_string(); + // Secret parameters were replaced + assert_eq!( + format!( + "{}/post?access_token=__ACCESS_TOKEN__&spam=eggs&secret=__SECRET__", + crate::ADDRESS.to_string() + ), + recorded_url, + ) +} + +#[tokio::test] +pub async fn test_modifier_response() { + crate::SCOPE.clone().init().await; + let bundle = saved_fixture_path("response-modifer-test-case.vcr.json"); + let middleware = VCRMiddleware::try_from(bundle.clone()) + .unwrap() + .with_mode(VCRMode::Record) + .with_modify_response(response_modifier); + + let vcr_client: ClientWithMiddleware = ClientBuilder::new(reqwest::Client::new()) + .with(middleware) + .build(); + + vcr_client + .request( + reqwest::Method::POST, + format!("{}{}", crate::ADDRESS.to_string(), "/post",), + ) + .send() + .await + .expect("Can not send request"); + // Drop triggers recording + drop(vcr_client); + + let vcr_content = fs::read(bundle.clone()).expect("VCR file not created during test case"); + + let cassette: vcr_cassette::Cassette = serde_json::from_slice(&vcr_content).unwrap(); + let interaction = cassette.http_interactions.first().unwrap(); + let recorded_server_header = interaction + .response + .headers + .get("server") + .unwrap() + .clone() + .pop() + .unwrap(); + // Server header was replaced + assert_eq!(recorded_server_header, "Test Server Header Emulated Expect",) +}