ci(runner): rewrite collection_runner.sh in rust (#1604)

This commit is contained in:
Pa1NarK
2023-07-10 15:02:44 +05:30
committed by GitHub
parent 69454ec55c
commit e09077a75f
17 changed files with 496 additions and 240 deletions

View File

@ -0,0 +1,25 @@
[package]
name = "test_utils"
description = "Postman collection runner and utility"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
readme = "README.md"
license.workspace = true
[features]
default = ["dummy_connector"]
dummy_connector = ["api_models/dummy_connector"]
[dependencies]
clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage", "cargo"] }
serde = { version = "1.0.163", features = ["derive"] }
serde_json = "1.0.96"
serde_path_to_error = "0.1.11"
toml = "0.7.4"
# First party crates
router = { version = "0.2.0", path = "../router" }
api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] }
masking = { version = "0.1.0", path = "../masking" }

View File

@ -0,0 +1,13 @@
# Test Runner
The main part of running tests through `newman`.
# Usage
- Make sure you that you've the postman collection for the connector available in the `postman` dir with the name `<connector_name>.postman_collection.json`
- Add the connector credentials to the `connector_auth.toml` / `auth.toml`
- In terminal, execute:
```zsh
export CONNECTOR_AUTH_FILE_PATH=/path/to/auth.toml
cargo run --package test_utils --bin test_utils -- --connector_name=<connector_name> --base_url=<base_url> --admin_api_key=<admin_api_key>
```

View File

@ -0,0 +1,258 @@
use std::{collections::HashMap, env};
use masking::{PeekInterface, Secret};
use router::types::ConnectorAuthType;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ConnectorAuthentication {
pub aci: Option<BodyKey>,
pub adyen: Option<BodyKey>,
pub adyen_uk: Option<BodyKey>,
pub airwallex: Option<BodyKey>,
pub authorizedotnet: Option<BodyKey>,
pub bambora: Option<BodyKey>,
pub bitpay: Option<HeaderKey>,
pub bluesnap: Option<BodyKey>,
pub cashtocode: Option<BodyKey>,
pub checkout: Option<SignatureKey>,
pub coinbase: Option<HeaderKey>,
pub cryptopay: Option<BodyKey>,
pub cybersource: Option<SignatureKey>,
pub dlocal: Option<SignatureKey>,
#[cfg(feature = "dummy_connector")]
pub dummyconnector: Option<HeaderKey>,
pub fiserv: Option<SignatureKey>,
pub forte: Option<MultiAuthKey>,
pub globalpay: Option<BodyKey>,
pub globepay: Option<HeaderKey>,
pub iatapay: Option<SignatureKey>,
pub mollie: Option<HeaderKey>,
pub multisafepay: Option<HeaderKey>,
pub nexinets: Option<BodyKey>,
pub noon: Option<SignatureKey>,
pub nmi: Option<HeaderKey>,
pub nuvei: Option<SignatureKey>,
pub opayo: Option<HeaderKey>,
pub opennode: Option<HeaderKey>,
pub payeezy: Option<SignatureKey>,
pub payme: Option<BodyKey>,
pub paypal: Option<BodyKey>,
pub payu: Option<BodyKey>,
pub powertranz: Option<HeaderKey>,
pub rapyd: Option<BodyKey>,
pub shift4: Option<HeaderKey>,
pub stripe: Option<HeaderKey>,
pub stripe_au: Option<HeaderKey>,
pub stripe_uk: Option<HeaderKey>,
pub trustpay: Option<SignatureKey>,
pub worldpay: Option<BodyKey>,
pub worldline: Option<SignatureKey>,
pub zen: Option<HeaderKey>,
pub automation_configs: Option<AutomationConfigs>,
}
impl Default for ConnectorAuthentication {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
impl ConnectorAuthentication {
#[allow(clippy::expect_used)]
pub fn new() -> Self {
// Do `export CONNECTOR_AUTH_FILE_PATH="/hyperswitch/crates/router/tests/connectors/sample_auth.toml"`
// before running tests in shell
let path = env::var("CONNECTOR_AUTH_FILE_PATH")
.expect("Connector authentication file path not set");
toml::from_str(
&std::fs::read_to_string(path).expect("connector authentication config file not found"),
)
.expect("Failed to read connector authentication config file")
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct ConnectorAuthenticationMap(HashMap<String, ConnectorAuthType>);
impl Default for ConnectorAuthenticationMap {
fn default() -> Self {
Self::new()
}
}
// This is a temporary solution to avoid rust compiler from complaining about unused function
#[allow(dead_code)]
impl ConnectorAuthenticationMap {
pub fn inner(&self) -> &HashMap<String, ConnectorAuthType> {
&self.0
}
#[allow(clippy::expect_used)]
pub fn new() -> Self {
// Do `export CONNECTOR_AUTH_FILE_PATH="/hyperswitch/crates/router/tests/connectors/sample_auth.toml"`
// before running tests in shell
let path = env::var("CONNECTOR_AUTH_FILE_PATH")
.expect("connector authentication file path not set");
// Read the file contents to a JsonString
let contents =
&std::fs::read_to_string(path).expect("Failed to read connector authentication file");
// Deserialize the JsonString to a HashMap
let auth_config: HashMap<String, toml::Value> =
toml::from_str(contents).expect("Failed to deserialize TOML file");
// auth_config contains the data in below given format:
// {
// "connector_name": Table(
// {
// "api_key": String(
// "API_Key",
// ),
// "api_secret": String(
// "Secret key",
// ),
// "key1": String(
// "key1",
// ),
// "key2": String(
// "key2",
// ),
// },
// ),
// "connector_name": Table(
// ...
// }
// auth_map refines and extracts required information
let auth_map = auth_config
.into_iter()
.map(|(connector_name, config)| {
let auth_type = match config {
toml::Value::Table(table) => {
match (
table.get("api_key"),
table.get("key1"),
table.get("api_secret"),
table.get("key2"),
) {
(Some(api_key), None, None, None) => ConnectorAuthType::HeaderKey {
api_key: api_key.as_str().unwrap_or_default().to_string(),
},
(Some(api_key), Some(key1), None, None) => ConnectorAuthType::BodyKey {
api_key: api_key.as_str().unwrap_or_default().to_string(),
key1: key1.as_str().unwrap_or_default().to_string(),
},
(Some(api_key), Some(key1), Some(api_secret), None) => {
ConnectorAuthType::SignatureKey {
api_key: api_key.as_str().unwrap_or_default().to_string(),
key1: key1.as_str().unwrap_or_default().to_string(),
api_secret: api_secret.as_str().unwrap_or_default().to_string(),
}
}
(Some(api_key), Some(key1), Some(api_secret), Some(key2)) => {
ConnectorAuthType::MultiAuthKey {
api_key: api_key.as_str().unwrap_or_default().to_string(),
key1: key1.as_str().unwrap_or_default().to_string(),
api_secret: api_secret.as_str().unwrap_or_default().to_string(),
key2: key2.as_str().unwrap_or_default().to_string(),
}
}
_ => ConnectorAuthType::NoKey,
}
}
_ => ConnectorAuthType::NoKey,
};
(connector_name, auth_type)
})
.collect();
Self(auth_map)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct HeaderKey {
pub api_key: Secret<String>,
}
impl From<HeaderKey> for ConnectorAuthType {
fn from(key: HeaderKey) -> Self {
Self::HeaderKey {
api_key: key.api_key.peek().to_string(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BodyKey {
pub api_key: Secret<String>,
pub key1: Secret<String>,
}
impl From<BodyKey> for ConnectorAuthType {
fn from(key: BodyKey) -> Self {
Self::BodyKey {
api_key: key.api_key.peek().to_string(),
key1: key.key1.peek().to_string(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SignatureKey {
pub api_key: Secret<String>,
pub key1: Secret<String>,
pub api_secret: Secret<String>,
}
impl From<SignatureKey> for ConnectorAuthType {
fn from(key: SignatureKey) -> Self {
Self::SignatureKey {
api_key: key.api_key.peek().to_string(),
key1: key.key1.peek().to_string(),
api_secret: key.api_secret.peek().to_string(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct MultiAuthKey {
pub api_key: Secret<String>,
pub key1: Secret<String>,
pub api_secret: Secret<String>,
pub key2: Secret<String>,
}
impl From<MultiAuthKey> for ConnectorAuthType {
fn from(key: MultiAuthKey) -> Self {
Self::MultiAuthKey {
api_key: key.api_key.peek().to_string(),
key1: key.key1.peek().to_string(),
api_secret: key.api_secret.peek().to_string(),
key2: key.key2.peek().to_string(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AutomationConfigs {
pub hs_base_url: Option<String>,
pub hs_api_key: Option<String>,
pub hs_test_browser: Option<String>,
pub chrome_profile_path: Option<String>,
pub firefox_profile_path: Option<String>,
pub pypl_email: Option<String>,
pub pypl_pass: Option<String>,
pub gmail_email: Option<String>,
pub gmail_pass: Option<String>,
pub configs_url: Option<String>,
pub stripe_pub_key: Option<String>,
pub testcases_path: Option<String>,
pub bluesnap_gateway_merchant_id: Option<String>,
pub globalpay_gateway_merchant_id: Option<String>,
pub run_minimum_steps: Option<bool>,
pub airwallex_merchant_name: Option<String>,
}

View File

@ -0,0 +1 @@
pub mod connector_auth;

View File

@ -0,0 +1,156 @@
use std::{
env,
process::{exit, Command as cmd},
};
use clap::{arg, command, Parser};
use router::types::ConnectorAuthType;
use test_utils::connector_auth::ConnectorAuthenticationMap;
// Just by the name of the connector, this function generates the name of the collection
// Example: CONNECTOR_NAME="stripe" -> OUTPUT: postman/stripe.postman_collection.json
#[inline]
fn path_generation(name: impl AsRef<str>) -> String {
format!("postman/{}.postman_collection.json", name.as_ref())
}
#[derive(Parser)]
#[command(version, about = "Postman collection runner using newman!", long_about = None)]
struct Args {
/// Name of the connector
#[arg(short, long = "connector_name")]
connector_name: String,
/// Base URL of the Hyperswitch environment
#[arg(short, long = "base_url")]
base_url: String,
/// Admin API Key of the environment
#[arg(short, long = "admin_api_key")]
admin_api_key: String,
}
fn main() {
let args = Args::parse();
let connector_name = args.connector_name;
let base_url = args.base_url;
let admin_api_key = args.admin_api_key;
let collection_path = path_generation(&connector_name);
let auth_map = ConnectorAuthenticationMap::new();
let inner_map = auth_map.inner();
// Newman runner
// Depending on the conditions satisfied, variables are added. Since certificates of stripe have already
// been added to the postman collection, those conditions are set to true and collections that have
// variables set up for certificate, will consider those variables and will fail.
let mut newman_command = cmd::new("newman");
newman_command.args(["run", &collection_path]);
newman_command.args(["--env-var", &format!("admin_api_key={admin_api_key}")]);
newman_command.args(["--env-var", &format!("baseUrl={base_url}")]);
if let Some(auth_type) = inner_map.get(&connector_name) {
match auth_type {
ConnectorAuthType::HeaderKey { api_key } => {
newman_command.args(["--env-var", &format!("connector_api_key={api_key}")]);
}
ConnectorAuthType::BodyKey { api_key, key1 } => {
newman_command.args([
"--env-var",
&format!("connector_api_key={api_key}"),
"--env-var",
&format!("connector_key1={key1}"),
]);
}
ConnectorAuthType::SignatureKey {
api_key,
key1,
api_secret,
} => {
newman_command.args([
"--env-var",
&format!("connector_api_key={api_key}"),
"--env-var",
&format!("connector_key1={key1}"),
"--env-var",
&format!("connector_api_secret={api_secret}"),
]);
}
ConnectorAuthType::MultiAuthKey {
api_key,
key1,
key2,
api_secret,
} => {
newman_command.args([
"--env-var",
&format!("connector_api_key={api_key}"),
"--env-var",
&format!("connector_key1={key1}"),
"--env-var",
&format!("connector_key1={key2}"),
"--env-var",
&format!("connector_api_secret={api_secret}"),
]);
}
// Handle other ConnectorAuthType variants
_ => {
eprintln!("Invalid authentication type.");
}
}
} else {
eprintln!("Connector not found.");
}
// Add additional environment variables if present
if let Ok(gateway_merchant_id) = env::var("GATEWAY_MERCHANT_ID") {
newman_command.args([
"--env-var",
&format!("gateway_merchant_id={gateway_merchant_id}"),
]);
}
if let Ok(gpay_certificate) = env::var("GPAY_CERTIFICATE") {
newman_command.args(["--env-var", &format!("certificate={gpay_certificate}")]);
}
if let Ok(gpay_certificate_keys) = env::var("GPAY_CERTIFICATE_KEYS") {
newman_command.args([
"--env-var",
&format!("certificate_keys={gpay_certificate_keys}"),
]);
}
newman_command.arg("--delay-request").arg("5");
// Execute the newman command
let output = newman_command.spawn();
let mut child = match output {
Ok(child) => child,
Err(err) => {
eprintln!("Failed to execute command: {err}");
exit(1);
}
};
let status = child.wait();
let exit_code = match status {
Ok(exit_status) => {
if exit_status.success() {
println!("Command executed successfully!");
exit_status.code().unwrap_or(0)
} else {
eprintln!("Command failed with exit code: {:?}", exit_status.code());
exit_status.code().unwrap_or(1)
}
}
Err(err) => {
eprintln!("Failed to wait for command execution: {}", err);
exit(1);
}
};
exit(exit_code);
}