diff --git a/.generator/src/generator/templates/api_mod.j2 b/.generator/src/generator/templates/api_mod.j2 deleted file mode 100644 index 980159af0..000000000 --- a/.generator/src/generator/templates/api_mod.j2 +++ /dev/null @@ -1,96 +0,0 @@ -use std::error; -use std::fmt; - -#[derive(Debug, Clone)] -pub struct ResponseContent { - pub status: reqwest::StatusCode, - pub content: String, - pub entity: Option, -} - -#[derive(Debug)] -pub enum Error { - Reqwest(reqwest::Error), - Serde(serde_json::Error), - Io(std::io::Error), - ResponseError(ResponseContent), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let (module, e) = match self { - Error::Reqwest(e) => ("reqwest", e.to_string()), - Error::Serde(e) => ("serde", e.to_string()), - Error::Io(e) => ("IO", e.to_string()), - Error::ResponseError(e) => ("response", format!("status code {}", e.status)), - }; - write!(f, "error in {}: {}", module, e) - } -} - -impl error::Error for Error { - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - Some(match self { - Error::Reqwest(e) => e, - Error::Serde(e) => e, - Error::Io(e) => e, - Error::ResponseError(_) => return None, - }) - } -} - -impl From for Error { - fn from(e: reqwest::Error) -> Self { - Error::Reqwest(e) - } -} - -impl From for Error { - fn from(e: serde_json::Error) -> Self { - Error::Serde(e) - } -} - -impl From for Error { - fn from(e: std::io::Error) -> Self { - Error::Io(e) - } -} - -pub fn urlencode>(s: T) -> String { - ::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()).collect() -} - -pub fn parse_deep_object(prefix: &str, value: &serde_json::Value) -> Vec<(String, String)> { - if let serde_json::Value::Object(object) = value { - let mut params = vec![]; - - for (key, value) in object { - match value { - serde_json::Value::Object(_) => params.append(&mut parse_deep_object( - &format!("{}[{}]", prefix, key), - value, - )), - serde_json::Value::Array(array) => { - for (i, value) in array.iter().enumerate() { - params.append(&mut parse_deep_object( - &format!("{}[{}][{}]", prefix, key, i), - value, - )); - } - }, - serde_json::Value::String(s) => params.push((format!("{}[{}]", prefix, key), s.clone())), - _ => params.push((format!("{}[{}]", prefix, key), value.to_string())), - } - } - return params; - } - unimplemented!("Only objects are supported with style=deepObject") -} - -{%- for name, operations in all_operations|sort %} -{%- set classname = name + " api" %} -pub mod {{ classname | snake_case}}; -{%- endfor %} - -pub mod configuration; diff --git a/.generator/src/generator/templates/common_mod.j2 b/.generator/src/generator/templates/common_mod.j2 index 980159af0..19b9a3b2e 100644 --- a/.generator/src/generator/templates/common_mod.j2 +++ b/.generator/src/generator/templates/common_mod.j2 @@ -11,6 +11,7 @@ pub struct ResponseContent { #[derive(Debug)] pub enum Error { Reqwest(reqwest::Error), + ReqwestMiddleware(reqwest_middleware::Error), Serde(serde_json::Error), Io(std::io::Error), ResponseError(ResponseContent), @@ -20,6 +21,7 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let (module, e) = match self { Error::Reqwest(e) => ("reqwest", e.to_string()), + Error::ReqwestMiddleware(e) => ("reqwest_middleware", e.to_string()), Error::Serde(e) => ("serde", e.to_string()), Error::Io(e) => ("IO", e.to_string()), Error::ResponseError(e) => ("response", format!("status code {}", e.status)), @@ -32,6 +34,7 @@ impl error::Error for Error { fn source(&self) -> Option<&(dyn error::Error + 'static)> { Some(match self { Error::Reqwest(e) => e, + Error::ReqwestMiddleware(e) => e, Error::Serde(e) => e, Error::Io(e) => e, Error::ResponseError(_) => return None, @@ -45,6 +48,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: reqwest_middleware::Error) -> Self { + Error::ReqwestMiddleware(e) + } +} + impl From for Error { fn from(e: serde_json::Error) -> Self { Error::Serde(e) diff --git a/.generator/src/generator/templates/configuration.j2 b/.generator/src/generator/templates/configuration.j2 index c758d13ea..62f201e26 100644 --- a/.generator/src/generator/templates/configuration.j2 +++ b/.generator/src/generator/templates/configuration.j2 @@ -6,7 +6,7 @@ use std::env; pub struct Configuration { pub base_path: String, pub user_agent: Option, - pub client: reqwest::Client, + pub client: reqwest_middleware::ClientWithMiddleware, {%- set authMethods = openapi.security %} {%- if authMethods %} {%- for authMethod in authMethods %} @@ -29,6 +29,7 @@ impl Configuration { impl Default for Configuration { fn default() -> Self { + let http_client = reqwest_middleware::ClientBuilder::new(reqwest::Client::new()); Configuration { base_path: "https://api.datadoghq.com".to_owned(), user_agent: Some(format!( @@ -38,7 +39,7 @@ impl Default for Configuration { env::consts::OS, env::consts::ARCH, )), - client: reqwest::Client::new(), + client: http_client.build(), {%- set authMethods = openapi.security %} {%- if authMethods %} {%- for authMethod in authMethods %} diff --git a/.generator/src/generator/templates/function_mappings.j2 b/.generator/src/generator/templates/function_mappings.j2 index 90b07b2b4..9c9912a41 100644 --- a/.generator/src/generator/templates/function_mappings.j2 +++ b/.generator/src/generator/templates/function_mappings.j2 @@ -75,10 +75,8 @@ fn test_{{ operation['operationId'] | snake_case }}(world: &mut DatadogWorld, _p Ok(response) => response, Err(error) => { return match error { - Error::Reqwest(e) => panic!("reqwest error: {}", e), - Error::Serde(e) => panic!("serde error: {}", e), - Error::Io(e) => panic!("io error: {}", e), Error::ResponseError(e) => world.response.code = e.status.as_u16(), + _ => panic!("error parsing response: {}", error), }; } }; diff --git a/Cargo.toml b/Cargo.toml index c5663bc99..6fcaaf3ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,32 +3,31 @@ name = "datadog-api-client" version = "1.0.0" authors = ["support@datadoghq.com"] description = "Collection of all Datadog Public endpoints." -# Override this license by providing a License Object in the OpenAPI. license = "Apache-2.0" edition = "2021" [dependencies] +log = "0.4.20" +reqwest = { version = "^0.11", features = ["json", "multipart"] } +reqwest-middleware = "0.1.6" +rustc_version = "0.4.0" serde = "^1.0" serde_derive = "^1.0" -serde_with = "^2.0" serde_json = "^1.0" +serde_with = "^2.0" url = "^2.2" -uuid = { version = "^1.0", features = ["serde"] } -rustc_version = "0.4.0" -log = "0.4.20" -[dependencies.reqwest] -version = "^0.11" -features = ["json", "multipart"] [dev-dependencies] +chrono = "0.4.31" cucumber = "0.19.1" env_logger = "0.10.0" -tokio = { version = "1.10", features = ["macros", "rt-multi-thread", "time"] } +futures = "0.3.28" handlebars = "4.4.0" regex = "1.9.5" +rvcr = { git = "https://github.com/nkzou/rvcr.git", rev = "b8f84bc0dfacd539fdc6f7446637c6276dcbb57c" } sha256 = "1.4.0" -futures = "0.3.28" +tokio = { version = "1.10", features = ["macros", "rt-multi-thread", "time"] } [[test]] -name = "main" harness = false # allows Cucumber to print output instead of libtest +name = "main" diff --git a/src/datadog/configuration.rs b/src/datadog/configuration.rs index bc9adadc4..a4037869b 100644 --- a/src/datadog/configuration.rs +++ b/src/datadog/configuration.rs @@ -8,7 +8,7 @@ use std::env; pub struct Configuration { pub base_path: String, pub user_agent: Option, - pub client: reqwest::Client, + pub client: reqwest_middleware::ClientWithMiddleware, pub api_key_auth: Option, pub app_key_auth: Option, } @@ -21,6 +21,7 @@ impl Configuration { impl Default for Configuration { fn default() -> Self { + let http_client = reqwest_middleware::ClientBuilder::new(reqwest::Client::new()); Configuration { base_path: "https://api.datadoghq.com".to_owned(), user_agent: Some(format!( @@ -30,7 +31,7 @@ impl Default for Configuration { env::consts::OS, env::consts::ARCH, )), - client: reqwest::Client::new(), + client: http_client.build(), api_key_auth: env::var("DD_API_KEY").ok(), app_key_auth: env::var("DD_APP_KEY").ok(), } diff --git a/src/datadog/mod.rs b/src/datadog/mod.rs index 6a6d2c1f9..805146b8b 100644 --- a/src/datadog/mod.rs +++ b/src/datadog/mod.rs @@ -11,6 +11,7 @@ pub struct ResponseContent { #[derive(Debug)] pub enum Error { Reqwest(reqwest::Error), + ReqwestMiddleware(reqwest_middleware::Error), Serde(serde_json::Error), Io(std::io::Error), ResponseError(ResponseContent), @@ -20,6 +21,7 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let (module, e) = match self { Error::Reqwest(e) => ("reqwest", e.to_string()), + Error::ReqwestMiddleware(e) => ("reqwest_middleware", e.to_string()), Error::Serde(e) => ("serde", e.to_string()), Error::Io(e) => ("IO", e.to_string()), Error::ResponseError(e) => ("response", format!("status code {}", e.status)), @@ -32,6 +34,7 @@ impl error::Error for Error { fn source(&self) -> Option<&(dyn error::Error + 'static)> { Some(match self { Error::Reqwest(e) => e, + Error::ReqwestMiddleware(e) => e, Error::Serde(e) => e, Error::Io(e) => e, Error::ResponseError(_) => return None, @@ -45,6 +48,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: reqwest_middleware::Error) -> Self { + Error::ReqwestMiddleware(e) + } +} + impl From for Error { fn from(e: serde_json::Error) -> Self { Error::Serde(e) diff --git a/tests/scenarios/cassettes/v2/fastly_integration/Add-Fastly-account-returns-CREATED-response.frozen b/tests/scenarios/cassettes/v2/fastly_integration/Add-Fastly-account-returns-CREATED-response.frozen new file mode 100644 index 000000000..f0b6e19ec --- /dev/null +++ b/tests/scenarios/cassettes/v2/fastly_integration/Add-Fastly-account-returns-CREATED-response.frozen @@ -0,0 +1 @@ +2023-01-19T15:15:56.412Z \ No newline at end of file diff --git a/tests/scenarios/cassettes/v2/fastly_integration/Add-Fastly-account-returns-CREATED-response.json b/tests/scenarios/cassettes/v2/fastly_integration/Add-Fastly-account-returns-CREATED-response.json new file mode 100644 index 000000000..fa365e20b --- /dev/null +++ b/tests/scenarios/cassettes/v2/fastly_integration/Add-Fastly-account-returns-CREATED-response.json @@ -0,0 +1 @@ +{"http_interactions": [{"request": {"body": {"string": "{\"data\":{\"attributes\":{\"api_key\":\"TestAddFastlyaccountreturnsCREATEDresponse1674141356\",\"name\":\"Test-Add_Fastly_account_returns_CREATED_response-1674141356\",\"services\":[]},\"type\":\"fastly-accounts\"}}", "encoding": null}, "headers": {"Accept": ["application/json"], "Content-Type": ["application/json"]}, "method": "post", "uri": "https://api.datadoghq.com/api/v2/integrations/fastly/accounts"}, "response": {"body": {"string": "{\"data\":{\"attributes\":{\"services\":[],\"name\":\"Test-Add_Fastly_account_returns_CREATED_response-1674141356\"},\"type\":\"fastly-accounts\",\"id\":\"0427b05b6f56f454ca1477aa8df5e75d\"}}\n", "encoding": null}, "headers": {"Content-Type": ["application/json"]}, "status": {"code": 201, "message": "Created"}}, "recorded_at": "Thu, 19 Jan 2023 15:15:56 GMT"}, {"request": {"body": "", "headers": {"Accept": ["*/*"]}, "method": "delete", "uri": "https://api.datadoghq.com/api/v2/integrations/fastly/accounts/0427b05b6f56f454ca1477aa8df5e75d"}, "response": {"body": {"string": "", "encoding": null}, "headers": {"Content-Type": ["text/html; charset=utf-8"]}, "status": {"code": 204, "message": "No Content"}}, "recorded_at": "Thu, 19 Jan 2023 15:15:56 GMT"}], "recorded_with": "VCR 6.0.0"} \ No newline at end of file diff --git a/tests/scenarios/cassettes/v2/fastly_integration/Get-Fastly-account-returns-OK-response.frozen b/tests/scenarios/cassettes/v2/fastly_integration/Get-Fastly-account-returns-OK-response.frozen new file mode 100644 index 000000000..2ee7e978b --- /dev/null +++ b/tests/scenarios/cassettes/v2/fastly_integration/Get-Fastly-account-returns-OK-response.frozen @@ -0,0 +1 @@ +2023-03-13T10:11:14.475Z \ No newline at end of file diff --git a/tests/scenarios/cassettes/v2/fastly_integration/Get-Fastly-account-returns-OK-response.json b/tests/scenarios/cassettes/v2/fastly_integration/Get-Fastly-account-returns-OK-response.json new file mode 100644 index 000000000..1af728d7a --- /dev/null +++ b/tests/scenarios/cassettes/v2/fastly_integration/Get-Fastly-account-returns-OK-response.json @@ -0,0 +1 @@ +{"http_interactions": [{"request": {"body": {"string": "{\"data\":{\"attributes\":{\"api_key\":\"TestGetFastlyaccountreturnsOKresponse1678702274\",\"name\":\"Test-Get_Fastly_account_returns_OK_response-1678702274\",\"services\":[]},\"type\":\"fastly-accounts\"}}", "encoding": null}, "headers": {"Accept": ["application/json"], "Content-Type": ["application/json"]}, "method": "post", "uri": "https://api.datadoghq.com/api/v2/integrations/fastly/accounts"}, "response": {"body": {"string": "{\"data\":{\"type\":\"fastly-accounts\",\"id\":\"6d8f2860f9f3e953fb46d554b9a19627\",\"attributes\":{\"services\":[],\"name\":\"Test-Get_Fastly_account_returns_OK_response-1678702274\"}}}\n", "encoding": null}, "headers": {"Content-Type": ["application/json"]}, "status": {"code": 201, "message": "Created"}}, "recorded_at": "Mon, 13 Mar 2023 10:11:14 GMT"}, {"request": {"body": "", "headers": {"Accept": ["application/json"]}, "method": "get", "uri": "https://api.datadoghq.com/api/v2/integrations/fastly/accounts/6d8f2860f9f3e953fb46d554b9a19627"}, "response": {"body": {"string": "{\"data\":{\"type\":\"fastly-accounts\",\"id\":\"6d8f2860f9f3e953fb46d554b9a19627\",\"attributes\":{\"name\":\"Test-Get_Fastly_account_returns_OK_response-1678702274\",\"services\":[]}}}\n", "encoding": null}, "headers": {"Content-Type": ["application/json"]}, "status": {"code": 200, "message": "OK"}}, "recorded_at": "Mon, 13 Mar 2023 10:11:14 GMT"}, {"request": {"body": "", "headers": {"Accept": ["*/*"]}, "method": "delete", "uri": "https://api.datadoghq.com/api/v2/integrations/fastly/accounts/6d8f2860f9f3e953fb46d554b9a19627"}, "response": {"body": {"string": "", "encoding": null}, "headers": {"Content-Type": ["text/html; charset=utf-8"]}, "status": {"code": 204, "message": "No Content"}}, "recorded_at": "Mon, 13 Mar 2023 10:11:14 GMT"}], "recorded_with": "VCR 6.0.0"} \ No newline at end of file diff --git a/tests/scenarios/cassettes/v2/fastly_integration/List-Fastly-accounts-returns-OK-response.frozen b/tests/scenarios/cassettes/v2/fastly_integration/List-Fastly-accounts-returns-OK-response.frozen new file mode 100644 index 000000000..1e15453e8 --- /dev/null +++ b/tests/scenarios/cassettes/v2/fastly_integration/List-Fastly-accounts-returns-OK-response.frozen @@ -0,0 +1 @@ +2023-03-13T10:10:50.453Z \ No newline at end of file diff --git a/tests/scenarios/cassettes/v2/fastly_integration/List-Fastly-accounts-returns-OK-response.json b/tests/scenarios/cassettes/v2/fastly_integration/List-Fastly-accounts-returns-OK-response.json new file mode 100644 index 000000000..e322e188a --- /dev/null +++ b/tests/scenarios/cassettes/v2/fastly_integration/List-Fastly-accounts-returns-OK-response.json @@ -0,0 +1 @@ +{"http_interactions": [{"request": {"body": {"string": "{\"data\":{\"attributes\":{\"api_key\":\"TestListFastlyaccountsreturnsOKresponse1678702250\",\"name\":\"Test-List_Fastly_accounts_returns_OK_response-1678702250\",\"services\":[]},\"type\":\"fastly-accounts\"}}", "encoding": null}, "headers": {"Accept": ["application/json"], "Content-Type": ["application/json"]}, "method": "post", "uri": "https://api.datadoghq.com/api/v2/integrations/fastly/accounts"}, "response": {"body": {"string": "{\"data\":{\"type\":\"fastly-accounts\",\"attributes\":{\"name\":\"Test-List_Fastly_accounts_returns_OK_response-1678702250\",\"services\":[]},\"id\":\"07ec97dd43cd794c847ecf15cb25eb1c\"}}\n", "encoding": null}, "headers": {"Content-Type": ["application/json"]}, "status": {"code": 201, "message": "Created"}}, "recorded_at": "Mon, 13 Mar 2023 10:10:50 GMT"}, {"request": {"body": "", "headers": {"Accept": ["application/json"]}, "method": "get", "uri": "https://api.datadoghq.com/api/v2/integrations/fastly/accounts"}, "response": {"body": {"string": "{\"data\":[{\"type\":\"fastly-accounts\",\"attributes\":{\"name\":\"Test-List_Fastly_accounts_returns_OK_response-1678702250\",\"services\":[]},\"id\":\"07ec97dd43cd794c847ecf15cb25eb1c\"}]}\n", "encoding": null}, "headers": {"Content-Type": ["application/json"]}, "status": {"code": 200, "message": "OK"}}, "recorded_at": "Mon, 13 Mar 2023 10:10:50 GMT"}, {"request": {"body": "", "headers": {"Accept": ["*/*"]}, "method": "delete", "uri": "https://api.datadoghq.com/api/v2/integrations/fastly/accounts/07ec97dd43cd794c847ecf15cb25eb1c"}, "response": {"body": {"string": "", "encoding": null}, "headers": {"Content-Type": ["text/html; charset=utf-8"]}, "status": {"code": 204, "message": "No Content"}}, "recorded_at": "Mon, 13 Mar 2023 10:10:50 GMT"}], "recorded_with": "VCR 6.0.0"} \ No newline at end of file diff --git a/tests/scenarios/cassettes/v2/fastly_integration/Update-Fastly-account-returns-OK-response.frozen b/tests/scenarios/cassettes/v2/fastly_integration/Update-Fastly-account-returns-OK-response.frozen new file mode 100644 index 000000000..64bd5f8bd --- /dev/null +++ b/tests/scenarios/cassettes/v2/fastly_integration/Update-Fastly-account-returns-OK-response.frozen @@ -0,0 +1 @@ +2023-03-13T10:10:17.626Z \ No newline at end of file diff --git a/tests/scenarios/cassettes/v2/fastly_integration/Update-Fastly-account-returns-OK-response.json b/tests/scenarios/cassettes/v2/fastly_integration/Update-Fastly-account-returns-OK-response.json new file mode 100644 index 000000000..f7b1c27d3 --- /dev/null +++ b/tests/scenarios/cassettes/v2/fastly_integration/Update-Fastly-account-returns-OK-response.json @@ -0,0 +1 @@ +{"http_interactions": [{"request": {"body": {"string": "{\"data\":{\"attributes\":{\"api_key\":\"TestUpdateFastlyaccountreturnsOKresponse1678702217\",\"name\":\"Test-Update_Fastly_account_returns_OK_response-1678702217\",\"services\":[]},\"type\":\"fastly-accounts\"}}", "encoding": null}, "headers": {"Accept": ["application/json"], "Content-Type": ["application/json"]}, "method": "post", "uri": "https://api.datadoghq.com/api/v2/integrations/fastly/accounts"}, "response": {"body": {"string": "{\"data\":{\"type\":\"fastly-accounts\",\"id\":\"e37e834ae856fa24a2924973fdc7c276\",\"attributes\":{\"services\":[],\"name\":\"Test-Update_Fastly_account_returns_OK_response-1678702217\"}}}\n", "encoding": null}, "headers": {"Content-Type": ["application/json"]}, "status": {"code": 201, "message": "Created"}}, "recorded_at": "Mon, 13 Mar 2023 10:10:17 GMT"}, {"request": {"body": {"string": "{\"data\":{\"attributes\":{\"api_key\":\"update-secret\"},\"type\":\"fastly-accounts\"}}", "encoding": null}, "headers": {"Accept": ["application/json"], "Content-Type": ["application/json"]}, "method": "patch", "uri": "https://api.datadoghq.com/api/v2/integrations/fastly/accounts/e37e834ae856fa24a2924973fdc7c276"}, "response": {"body": {"string": "{\"data\":{\"type\":\"fastly-accounts\",\"id\":\"e37e834ae856fa24a2924973fdc7c276\",\"attributes\":{\"services\":[],\"name\":\"Test-Update_Fastly_account_returns_OK_response-1678702217\"}}}\n", "encoding": null}, "headers": {"Content-Type": ["application/json"]}, "status": {"code": 200, "message": "OK"}}, "recorded_at": "Mon, 13 Mar 2023 10:10:17 GMT"}, {"request": {"body": "", "headers": {"Accept": ["*/*"]}, "method": "delete", "uri": "https://api.datadoghq.com/api/v2/integrations/fastly/accounts/e37e834ae856fa24a2924973fdc7c276"}, "response": {"body": {"string": "", "encoding": null}, "headers": {"Content-Type": ["text/html; charset=utf-8"]}, "status": {"code": 204, "message": "No Content"}}, "recorded_at": "Mon, 13 Mar 2023 10:10:17 GMT"}], "recorded_with": "VCR 6.0.0"} \ No newline at end of file diff --git a/tests/scenarios/fixtures.rs b/tests/scenarios/fixtures.rs index 16ceff5da..d4eaa3cee 100644 --- a/tests/scenarios/fixtures.rs +++ b/tests/scenarios/fixtures.rs @@ -1,4 +1,5 @@ use crate::scenarios::function_mappings::{collect_function_calls, initialize_api_instance, ApiInstances}; +use chrono::DateTime; use cucumber::{ event::ScenarioFinished, gherkin::{Feature, Rule, Scenario}, @@ -6,13 +7,18 @@ use cucumber::{ }; use datadog_api_client::datadog::configuration::Configuration; use handlebars::Handlebars; +use log::debug; use regex::Regex; +use reqwest_middleware::ClientBuilder; +use rvcr::{VCRMiddleware, VCRMode}; use serde_json::{json, Value}; use sha256::digest; use std::{ collections::HashMap, - fs::{read_to_string, File}, + env, + fs::{create_dir_all, read_to_string, File}, io::BufReader, + path::PathBuf, time::SystemTime, }; @@ -47,7 +53,7 @@ pub struct DatadogWorld { } pub async fn before_scenario(feature: &Feature, _rule: Option<&Rule>, scenario: &Scenario, world: &mut DatadogWorld) { - let api_version_re = Regex::new(r"tests/scenarios/features/v(\d+)/").unwrap(); + let api_version_re = Regex::new(r"tests/scenarios/features/v(\d+)/").expect("api version regex failed"); // TODO: refactor this lol world.api_version = api_version_re .captures(feature.path.as_ref().unwrap().to_str().unwrap()) @@ -59,32 +65,104 @@ pub async fn before_scenario(feature: &Feature, _rule: Option<&Rule>, scenario: .unwrap(); collect_function_calls(world); - let given_file = File::open(format!("tests/scenarios/features/v{}/given.json", world.api_version)).unwrap(); - world.given_map = serde_json::from_reader(BufReader::new(given_file)).unwrap(); - let undo_file = File::open(format!("tests/scenarios/features/v{}/undo.json", world.api_version)).unwrap(); - world.undo_map = serde_json::from_reader(BufReader::new(undo_file)).unwrap(); + let given_file = File::open(format!("tests/scenarios/features/v{}/given.json", world.api_version)) + .expect("failed to open given.json file"); + world.given_map = serde_json::from_reader(BufReader::new(given_file)).expect("failed to deserialize given.json"); + let undo_file = File::open(format!("tests/scenarios/features/v{}/undo.json", world.api_version)) + .expect("failed to open undo.json file"); + world.undo_map = serde_json::from_reader(BufReader::new(undo_file)).expect("failed to deserialize undo.json"); - let mut config = Configuration::new(); - config.api_key_auth = Some("00000000000000000000000000000000".to_string()); - config.app_key_auth = Some("0000000000000000000000000000000000000000".to_string()); - world.config = config; - - let non_alnum_re = Regex::new(r"[^A-Za-z0-9]+").unwrap(); - let prefix = match true { - true => "Test-Rust", - false => "Test", + let non_alnum_re = Regex::new(r"[^A-Za-z0-9]+").expect("non alnum regex failed"); + let escaped_filename = non_alnum_re.replace_all(scenario.name.as_str(), "-").to_string(); + let filename = match escaped_filename.len() > 100 { + true => escaped_filename[..100].to_string(), + false => escaped_filename, }; - let now = SystemTime::now() + let mut prefix = "Test".to_string(); + let mut cassette_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + cassette_dir.push(format!( + "tests/scenarios/cassettes/v{}/{}", + world.api_version, + feature.name.replace(' ', "_").to_lowercase() + )); + create_dir_all(&cassette_dir).expect("failed to create cassette directory"); + let mut cassette = cassette_dir.clone(); + cassette.push(format!("{}.json", filename)); + let mut freeze = cassette_dir.clone(); + freeze.push(format!("{}.frozen", filename)); + let mut config = Configuration::new(); + let mut frozen_time = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs(); + let vcr_client_builder = ClientBuilder::new(reqwest::Client::new()); + config.client = match env::var("RECORD").unwrap_or("false".to_string()).as_str() { + "none" => { + prefix.push_str("-Rust"); + vcr_client_builder.build() + } + "true" => { + // let _ = remove_file(cassette.clone()); + // let _ = remove_file(freeze.clone()); + // let mut freeze_file = File::create(freeze).expect("failed to write freeze file"); + // freeze_file + // .write_all( + // DateTime::to_rfc3339( + // &DateTime::from_timestamp(frozen_time as i64, 0) + // .expect("failed to convert timestamp to datetime"), + // ) + // .as_bytes(), + // ) + // .expect("failed to write freeze file"); + // let middleware: VCRMiddleware = VCRMiddleware::try_from(cassette) + // .expect("Failed to initialize rVCR middleware") + // .with_mode(VCRMode::Record) + // .with_modify_request(|req| { + // req.headers.remove_entry("dd-api-key"); + // req.headers.remove_entry("dd-application-key"); + // }) + // .with_modify_response(|res| { + // res.headers.remove_entry("content-security-policy"); + // }); + // vcr_client_builder.with(middleware).build() + panic!("sdk's shouldn't be recording, that's the spec repo's job."); + } + _ => { + frozen_time = + DateTime::parse_from_rfc3339(read_to_string(freeze).expect("Failed to read freeze file").as_str()) + .expect("Failed to parse freeze file time") + .signed_duration_since(DateTime::UNIX_EPOCH) + .num_seconds() as u64; + debug!("{}", frozen_time); + let middleware: VCRMiddleware = VCRMiddleware::try_from(cassette) + .expect("Failed to initialize rVCR middleware") + .with_mode(VCRMode::Replay) + .with_modify_request(|req| { + req.headers.remove_entry("dd-api-key"); + req.headers.remove_entry("dd-application-key"); + }) + .with_modify_response(|res| { + res.headers.remove_entry("content-security-policy"); + }) + .with_request_matcher(|vcr_req, req| { + vcr_req.uri.to_string() == req.uri.to_string() + && vcr_req.body.string == req.body.string + && vcr_req.method == req.method + }); + vcr_client_builder.with(middleware).build() + } + }; + config.api_key_auth = Some("00000000000000000000000000000000".to_string()); + config.app_key_auth = Some("0000000000000000000000000000000000000000".to_string()); + world.config = config; + let escaped_name = non_alnum_re.replace_all(scenario.name.as_str(), "_").to_string(); let name = match escaped_name.len() > 100 { true => escaped_name[..100].to_string(), false => escaped_name, }; - let unique = format!("{}-{}-{}", prefix, name, now); + let unique = format!("{}-{}-{}", prefix, name, frozen_time); let unique_alnum = non_alnum_re.replace_all(unique.as_str(), "").to_string(); world.fixtures = json!({ "unique": unique, @@ -94,7 +172,7 @@ pub async fn before_scenario(feature: &Feature, _rule: Option<&Rule>, scenario: "unique_lower_alnum": unique_alnum.to_ascii_lowercase(), "unique_upper_alnum": unique_alnum.to_ascii_uppercase(), "unique_hash": digest(unique)[..16], - "now": now, + "now": frozen_time, }); } @@ -107,19 +185,22 @@ pub async fn after_scenario( ) { if let Some(world) = world { for undo in world.undo_operations.clone().iter().rev() { - world.function_mappings.get(&undo.operation_id).unwrap()(world, &undo.parameters); + world + .function_mappings + .get(&undo.operation_id) + .expect("undo operation not found")(world, &undo.parameters); } } } #[given(expr = "a valid \"apiKeyAuth\" key in the system")] fn valid_apikey_auth(world: &mut DatadogWorld) { - world.config.api_key_auth = std::env::var("DD_TEST_CLIENT_API_KEY").ok(); + world.config.api_key_auth = env::var("DD_TEST_CLIENT_API_KEY").ok(); } #[given(expr = "a valid \"appKeyAuth\" key in the system")] fn valid_appkey_auth(world: &mut DatadogWorld) { - world.config.app_key_auth = std::env::var("DD_TEST_CLIENT_APP_KEY").ok(); + world.config.app_key_auth = env::var("DD_TEST_CLIENT_APP_KEY").ok(); } #[given(expr = "an instance of {string} API")] @@ -167,11 +248,12 @@ fn given_resource_in_system(world: &mut DatadogWorld, given_key: String) { .expect("failed to parse given operation id as str") .to_string(); - world.function_mappings.get(&operation_id).unwrap()(world, &given_parameters); + world + .function_mappings + .get(&operation_id) + .expect("given operation not found")(world, &given_parameters); match build_undo(world, &operation_id) { - Ok(Some(undo)) => { - world.undo_operations.push(undo); - } + Ok(Some(undo)) => world.undo_operations.push(undo), Ok(None) => {} Err(err) => panic!("{err}"), } @@ -201,7 +283,7 @@ fn body_with_value(world: &mut DatadogWorld, body: String) { #[given(regex = r"^body from file (.*)$")] fn body_from_file(world: &mut DatadogWorld, path: String) { - let body = read_to_string(format!("tests/scenarios/features/v{}/{}", world.api_version, path,)).unwrap(); + let body = read_to_string(format!("tests/scenarios/features/v{}/{}", world.api_version, path)).unwrap(); let rendered = template(body, &world.fixtures); let body_struct = serde_json::from_str(rendered.as_str()).unwrap(); world.parameters.insert("body".to_string(), body_struct); @@ -221,7 +303,10 @@ fn request_parameter_with_value(world: &mut DatadogWorld, param: String, value: #[when(regex = r"^the request is sent$")] fn request_sent(world: &mut DatadogWorld) { - world.function_mappings.get(&world.operation_id).unwrap()(world, &world.parameters.clone()); + world + .function_mappings + .get(&world.operation_id) + .expect("request operation not found")(world, &world.parameters.clone()); match build_undo(world, &world.operation_id.clone()) { Ok(Some(undo)) => { world.undo_operations.push(undo); @@ -238,7 +323,7 @@ fn response_status_is(world: &mut DatadogWorld, status_code: u16, _status_messag #[then(expr = "the response {string} is equal to {}")] fn response_equal_to(world: &mut DatadogWorld, path: String, value: String) { - let lookup = lookup(&path, &world.response.object).unwrap(); + let lookup = lookup(&path, &world.response.object).expect("value not found in response"); let rendered_value = template(value, &world.fixtures); let expected: Value = serde_json::from_str(rendered_value.as_str()).unwrap(); assert_eq!(lookup, expected); @@ -251,17 +336,18 @@ fn response_has_length(world: &mut DatadogWorld, path: String, expected_len: usi } fn lookup(path: &String, object: &Value) -> Option { - let index_re = Regex::new(r"\[(\d+)\]+").unwrap(); + let index_re = Regex::new(r"\[(\d+)\]+").expect("index regex failed"); let mut json_pointer = format!("/{}", path).replace('.', "/"); for (_, [idx]) in index_re.captures_iter(&json_pointer.clone()).map(|c| c.extract()) { json_pointer = index_re.replace(&json_pointer, format!("/{idx}")).to_string(); } - return object.pointer(&json_pointer).cloned(); } fn template(string: String, fixtures: &Value) -> String { - Handlebars::new().render_template(string.as_str(), &fixtures).unwrap() + Handlebars::new() + .render_template(string.as_str(), &fixtures) + .expect("failed to apply template") } fn build_undo(world: &mut DatadogWorld, operation_id: &String) -> Result, Value> { diff --git a/tests/scenarios/function_mappings.rs b/tests/scenarios/function_mappings.rs index f24f88fd2..7b15f202d 100644 --- a/tests/scenarios/function_mappings.rs +++ b/tests/scenarios/function_mappings.rs @@ -67,10 +67,8 @@ fn test_list_fastly_accounts(world: &mut DatadogWorld, _parameters: &HashMap response, Err(error) => { return match error { - Error::Reqwest(e) => panic!("reqwest error: {}", e), - Error::Serde(e) => panic!("serde error: {}", e), - Error::Io(e) => panic!("io error: {}", e), Error::ResponseError(e) => world.response.code = e.status.as_u16(), + _ => panic!("error parsing response: {}", error), }; } }; @@ -91,10 +89,8 @@ fn test_create_fastly_account(world: &mut DatadogWorld, _parameters: &HashMap response, Err(error) => { return match error { - Error::Reqwest(e) => panic!("reqwest error: {}", e), - Error::Serde(e) => panic!("serde error: {}", e), - Error::Io(e) => panic!("io error: {}", e), Error::ResponseError(e) => world.response.code = e.status.as_u16(), + _ => panic!("error parsing response: {}", error), }; } }; @@ -115,10 +111,8 @@ fn test_delete_fastly_account(world: &mut DatadogWorld, _parameters: &HashMap response, Err(error) => { return match error { - Error::Reqwest(e) => panic!("reqwest error: {}", e), - Error::Serde(e) => panic!("serde error: {}", e), - Error::Io(e) => panic!("io error: {}", e), Error::ResponseError(e) => world.response.code = e.status.as_u16(), + _ => panic!("error parsing response: {}", error), }; } }; @@ -139,10 +133,8 @@ fn test_get_fastly_account(world: &mut DatadogWorld, _parameters: &HashMap response, Err(error) => { return match error { - Error::Reqwest(e) => panic!("reqwest error: {}", e), - Error::Serde(e) => panic!("serde error: {}", e), - Error::Io(e) => panic!("io error: {}", e), Error::ResponseError(e) => world.response.code = e.status.as_u16(), + _ => panic!("error parsing response: {}", error), }; } }; @@ -164,10 +156,8 @@ fn test_update_fastly_account(world: &mut DatadogWorld, _parameters: &HashMap response, Err(error) => { return match error { - Error::Reqwest(e) => panic!("reqwest error: {}", e), - Error::Serde(e) => panic!("serde error: {}", e), - Error::Io(e) => panic!("io error: {}", e), Error::ResponseError(e) => world.response.code = e.status.as_u16(), + _ => panic!("error parsing response: {}", error), }; } }; @@ -188,10 +178,8 @@ fn test_list_fastly_services(world: &mut DatadogWorld, _parameters: &HashMap response, Err(error) => { return match error { - Error::Reqwest(e) => panic!("reqwest error: {}", e), - Error::Serde(e) => panic!("serde error: {}", e), - Error::Io(e) => panic!("io error: {}", e), Error::ResponseError(e) => world.response.code = e.status.as_u16(), + _ => panic!("error parsing response: {}", error), }; } }; @@ -213,10 +201,8 @@ fn test_create_fastly_service(world: &mut DatadogWorld, _parameters: &HashMap response, Err(error) => { return match error { - Error::Reqwest(e) => panic!("reqwest error: {}", e), - Error::Serde(e) => panic!("serde error: {}", e), - Error::Io(e) => panic!("io error: {}", e), Error::ResponseError(e) => world.response.code = e.status.as_u16(), + _ => panic!("error parsing response: {}", error), }; } }; @@ -238,10 +224,8 @@ fn test_delete_fastly_service(world: &mut DatadogWorld, _parameters: &HashMap response, Err(error) => { return match error { - Error::Reqwest(e) => panic!("reqwest error: {}", e), - Error::Serde(e) => panic!("serde error: {}", e), - Error::Io(e) => panic!("io error: {}", e), Error::ResponseError(e) => world.response.code = e.status.as_u16(), + _ => panic!("error parsing response: {}", error), }; } }; @@ -263,10 +247,8 @@ fn test_get_fastly_service(world: &mut DatadogWorld, _parameters: &HashMap response, Err(error) => { return match error { - Error::Reqwest(e) => panic!("reqwest error: {}", e), - Error::Serde(e) => panic!("serde error: {}", e), - Error::Io(e) => panic!("io error: {}", e), Error::ResponseError(e) => world.response.code = e.status.as_u16(), + _ => panic!("error parsing response: {}", error), }; } }; @@ -289,10 +271,8 @@ fn test_update_fastly_service(world: &mut DatadogWorld, _parameters: &HashMap response, Err(error) => { return match error { - Error::Reqwest(e) => panic!("reqwest error: {}", e), - Error::Serde(e) => panic!("serde error: {}", e), - Error::Io(e) => panic!("io error: {}", e), Error::ResponseError(e) => world.response.code = e.status.as_u16(), + _ => panic!("error parsing response: {}", error), }; } };