diff --git a/.editorconfig b/.editorconfig index 9cee0ca..403d31e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,7 @@ trim_trailing_whitespace = true [*.rs] indent_size = 4 + +[*.json] +indent_size = 2 +insert_final_newline = true diff --git a/.github/workflows/test-pull.yml b/.github/workflows/test-pull.yml new file mode 100644 index 0000000..77003cb --- /dev/null +++ b/.github/workflows/test-pull.yml @@ -0,0 +1,33 @@ +name: Test - Pull Request + +on: [pull_request] + +jobs: + test: + timeout-minutes: 20 + permissions: + contents: read + strategy: + fail-fast: true + matrix: + include: + - os: ubuntu-24.04 + targets: ["x86_64-unknown-linux-gnu"] + - os: macos-latest + targets: ["x86_64-apple-darwin"] + runs-on: ${{ matrix.os }} + + steps: + - name: Check out code + uses: actions/checkout@v4.2.2 + + - name: Rust setup + uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 + + - name: Rust cache + uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 + with: + shared-key: "test-${{ matrix.os }}" + + - name: Run tests + run: cargo test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7ef95cd --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,36 @@ +name: Test + +on: + push: + branches: + - main + +jobs: + test: + timeout-minutes: 20 + permissions: + contents: read + strategy: + fail-fast: true + matrix: + include: + - os: ubuntu-24.04 + targets: ["x86_64-unknown-linux-gnu"] + - os: macos-latest + targets: ["x86_64-apple-darwin"] + runs-on: ${{ matrix.os }} + + steps: + - name: Check out code + uses: actions/checkout@v4.2.2 + + - name: Rust setup + uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 + + - name: Rust cache + uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 + with: + shared-key: "test-${{ matrix.os }}" + + - name: Run tests + run: cargo test diff --git a/.gitignore b/.gitignore index 0592138..7efd2a4 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ bws* target meili_data supabase + +tests/test_results diff --git a/README.md b/README.md index ef2dd94..bd616d9 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Type "help", "copyright", "credits" or "license" for more information. Secrets are read from the following sources, in this order: -1. A dotenv file (.env) +1. ~~A dotenv file (.env)~~ (TODO) 2. Environment variables 3. Secret Sources (e.g. Bitwarden) 4. Keyring diff --git a/src/cli.rs b/src/cli.rs index 005fb42..9d8db7f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -15,7 +15,7 @@ pub fn build() -> Command { .subcommand(Command::new("run") .about("Run a command defined in the secrets_machine.toml configuration file") .arg( - arg!( "Name of the command to run, as defined in the configuration file") + arg!([command_name] "Name of the command to run, as defined in the configuration file") .required(true) .value_parser(value_parser!(String)) ) diff --git a/src/library/commands/execute.rs b/src/library/commands/execute.rs index c03ed73..a02bbb7 100644 --- a/src/library/commands/execute.rs +++ b/src/library/commands/execute.rs @@ -1,4 +1,4 @@ -use std::env; +use std::{env, error::Error}; use tokio::process::Command; @@ -11,12 +11,20 @@ use super::prepare; /// Executes a command with the secrets machine /// -/// # Panics -/// - If the command fails to execute -/// - If function fails to get the output of the command -/// - If function fails to kill the process -pub async fn execute(config: Config, secrets: serde_json::Value, command_to_run: &str) { - prepare(&config, &secrets).await; +/// # Errors +/// - When `return_output` is true: +/// - If the command fails to execute +/// - If function fails to get the output of the command +/// - If function fails to kill the process +/// +/// - When `return_output` is false: +/// - Never +pub async fn execute( + config: Config, + secrets: serde_json::Value, + command_to_run: &str, +) -> Result<(), Box> { + prepare(&config, &secrets).await?; logging::nl().await; logging::print_color(logging::BG_GREEN, " Executing command ").await; @@ -27,49 +35,56 @@ pub async fn execute(config: Config, secrets: serde_json::Value, command_to_run: .await; // Get the default shell from the SHELL environment variable - let default_shell = env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()); + let default_shell = match env::var("SHELL") { + Ok(shell) => shell, + Err(_) => "/bin/sh".to_string(), + }; - let child: tokio::process::Child = Command::new(default_shell) + let Ok(child) = Command::new(default_shell) .arg("-c") .arg(command_to_run) .envs(env::vars()) .spawn() - .expect("Failed to execute command"); + else { + return Err(Box::from("Failed to execute command")); + }; - let pid = child.id().expect("Failed to get child pid"); + let Some(pid) = child.id() else { + return Err(Box::from("Failed to get child pid")); + }; let handle = child.wait_with_output(); tokio::spawn(async move { - tokio::signal::ctrl_c().await.unwrap(); logging::nl().await; - logging::info("πŸ‘ Shutting down gracefully...").await; - let result = Command::new("kill").arg(pid.to_string()).status().await; + match tokio::signal::ctrl_c().await { + Ok(()) => { + logging::info("πŸ‘ Shutting down gracefully...").await; + } + Err(e) => { + logging::error(&format!("πŸ›‘ Failed to listen for Ctrl+C: {e}")).await; + } + } - match result { + match Command::new("kill").arg(pid.to_string()).status().await { Ok(_) => { logging::info("βœ… All processes have been terminated.").await; - std::process::exit(0); } Err(e) => { logging::error(&format!("πŸ›‘ Failed to kill process: {e}")).await; - std::process::exit(1); } } }); - let output = handle.await; - - match output { + match handle.await { Ok(output) => { if output.status.success() { - std::process::exit(0); + Ok(()) } else { - std::process::exit(1); + Err(Box::from("Command failed")) } } - Err(e) => { - logging::error(&format!("πŸ›‘ Failed to wait for command execution: {e}")).await; - std::process::exit(1); - } + Err(e) => Err(Box::from(format!( + "πŸ›‘ Failed to wait for command execution: {e}" + ))), } } diff --git a/src/library/commands/prepare.rs b/src/library/commands/prepare.rs index 6a829ad..63e7b81 100644 --- a/src/library/commands/prepare.rs +++ b/src/library/commands/prepare.rs @@ -1,15 +1,19 @@ -use std::{collections::HashMap, env}; +use std::{collections::HashMap, env, error::Error}; use crate::{ - library::{secrets_sources, utils::env_vars}, + library::{ + secrets_sources, + utils::{env_vars, logging}, + }, models::config::Config, }; /// Sync secrets and set environment variables /// -/// # Panics +/// # Errors +/// - If the secrets map is not found /// - If the environment variables fail to be set -pub async fn prepare(config: &Config, secrets: &serde_json::Value) { +pub async fn prepare(config: &Config, secrets: &serde_json::Value) -> Result<(), Box> { let vars_iter = env::vars(); let mut original_env_vars: HashMap = HashMap::new(); @@ -20,13 +24,17 @@ pub async fn prepare(config: &Config, secrets: &serde_json::Value) { let mut env_vars: Vec<(String, String, String)> = Vec::new(); + let Some(secrets_map) = secrets.as_object() else { + return Err(Box::from("Secrets map not found")); + }; + // Add keyring secrets to the environment variables - for (key, value) in secrets.as_object().unwrap() { - env_vars.push(( - key.to_string(), - value.as_str().unwrap().to_string(), - "keyring".to_string(), - )); + for (key, value) in secrets_map { + if let Some(value) = value.as_str() { + env_vars.push((key.to_string(), value.to_string(), "keyring".to_string())); + } else { + logging::error(&format!("Failed to set secret {key} from keyring")).await; + } } secrets_sources::sync(config, secrets, &mut env_vars).await; @@ -36,4 +44,6 @@ pub async fn prepare(config: &Config, secrets: &serde_json::Value) { if config.general.print_secrets_table { env_vars::print_variables_box(original_env_vars, &env_vars).await; } + + Ok(()) } diff --git a/src/library/commands/run.rs b/src/library/commands/run.rs index 871ffa8..281ac92 100644 --- a/src/library/commands/run.rs +++ b/src/library/commands/run.rs @@ -1,3 +1,5 @@ +use std::error::Error; + use tokio::process::Command; use crate::{ @@ -12,7 +14,8 @@ use super::prepare; /// Runs a command specified in the config file with the secrets machine /// -/// # Panics +/// # Errors +/// - If the command is not found in the config file /// - If the command fails to execute /// - If the function fails to get the output of the command /// - If the function fails to kill the process @@ -22,8 +25,8 @@ pub async fn run( secrets: serde_json::Value, command_name: String, command_args: String, -) { - prepare(&config, &secrets).await; +) -> Result<(), Box> { + prepare(&config, &secrets).await?; if let Some(pre_command) = commands_config.pre_commands.get(&command_name) { let result = command::run(pre_command).await; @@ -35,59 +38,63 @@ pub async fn run( Err(e) => { logging::error(e.to_string().as_str().trim()).await; logging::error("πŸ›‘ Failed to run pre command").await; - std::process::exit(1); + return Err(e); } } } - let command = commands_config.commands.get(&command_name).unwrap(); - let command = format!("{command} {command_args}"); + let Some(command) = commands_config.commands.get(&command_name) else { + return Err(Box::from("Command not found")); + }; + + let full_command = format!("{command} {command_args}"); logging::nl().await; logging::print_color(logging::BG_GREEN, " Running command ").await; logging::info(&format!( "Running: {}", - env_vars::replace(&command, true).await + env_vars::replace(&full_command, true).await )) .await; - let child = Command::new("sh") - .arg("-c") - .arg(&command) - .spawn() - .expect("Failed to start main command"); - let pid = child.id().expect("Failed to get child pid"); + let Ok(child) = Command::new("sh").arg("-c").arg(&full_command).spawn() else { + return Err(Box::from("Failed to execute command")); + }; + + let Some(pid) = child.id() else { + return Err(Box::from("Failed to get child pid")); + }; let handle = child.wait_with_output(); tokio::spawn(async move { - tokio::signal::ctrl_c().await.unwrap(); logging::nl().await; - logging::info("πŸ‘ Shutting down gracefully...").await; - let result = Command::new("kill").arg(pid.to_string()).status().await; - - match result { + match tokio::signal::ctrl_c().await { + Ok(()) => { + logging::info("πŸ‘ Shutting down gracefully...").await; + } + Err(e) => { + logging::error(&format!("πŸ›‘ Failed to listen for Ctrl+C: {e}")).await; + } + } + match Command::new("kill").arg(pid.to_string()).status().await { Ok(_) => { logging::info("βœ… All processes have been terminated.").await; - std::process::exit(0); } Err(e) => { logging::error(&format!("πŸ›‘ Failed to kill process: {e}")).await; - std::process::exit(1); } } }); - let output = handle.await; - - match output { + match handle.await { Ok(output) => { if output.status.success() { - std::process::exit(0); + Ok(()) } else { - std::process::exit(1); + Err(Box::from("Command failed")) } } - Err(e) => { - logging::error(&format!("πŸ›‘ Failed to wait for main command: {e}")).await; - } + Err(e) => Err(Box::from(format!( + "πŸ›‘ Failed to wait for command execution: {e}" + ))), } } diff --git a/src/main.rs b/src/main.rs index 0cee51d..ec693a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -67,7 +67,17 @@ async fn handle_run_mode(matches: ArgMatches) { secrets_sources::check(&config, &secrets).await; - run(commands_config, config, secrets, command_name, command_args).await; + match run(commands_config, config, secrets, command_name, command_args).await { + Ok(()) => (), + Err(e) => { + // Only print the error log if there was an error on our side + let formatted_error = format!("{e}"); + if formatted_error != "Command failed" { + logging::error(&format!("{e}")).await; + } + std::process::exit(1); + } + } } async fn handle_exec_mode(matches: ArgMatches) { @@ -88,7 +98,17 @@ async fn handle_exec_mode(matches: ArgMatches) { secrets_sources::check(&config, &secrets).await; - execute(config, secrets, command_to_run.as_str()).await; + match execute(config, secrets, command_to_run.as_str()).await { + Ok(()) => (), + Err(e) => { + // Only print the error log if there was an error on our side + let formatted_error = format!("{e}"); + if formatted_error != "Command failed" { + logging::error(&format!("{e}")).await; + } + std::process::exit(1); + } + } } #[tokio::main] diff --git a/tests/assets/config.toml b/tests/assets/config.toml new file mode 100644 index 0000000..fe0a0fa --- /dev/null +++ b/tests/assets/config.toml @@ -0,0 +1,6 @@ +[general] +print_secrets_table = true + +[[secrets_sources]] +name = "bitwarden" +access_token_name = "BWS_ACCESS_TOKEN" diff --git a/tests/assets/secrets.json b/tests/assets/secrets.json new file mode 100644 index 0000000..310e849 --- /dev/null +++ b/tests/assets/secrets.json @@ -0,0 +1,6 @@ +{ + "TEST": "test", + "HELLO": "world", + "SPIDER": "El Hombre", + "MAN": "AraΓ±a" +} diff --git a/tests/assets/secrets_machine.toml b/tests/assets/secrets_machine.toml index efa8fe1..b0bd532 100644 --- a/tests/assets/secrets_machine.toml +++ b/tests/assets/secrets_machine.toml @@ -1,7 +1,17 @@ [commands] - dev = "cargo run" - test = "cargo test" + test_run_simple = "echo hello $TEST_ENV_VAR $HELLO > tests/test_results/test_run_simple.txt" + test_run_single_quotes = "echo 'hello $TEST_ENV_VAR $HELLO' > tests/test_results/test_run_single_quotes.txt" + test_run_double_quotes = "echo \"hello $TEST_ENV_VAR $HELLO\" > tests/test_results/test_run_double_quotes.txt" + + test_run_simple_braces = "echo hello ${TEST_ENV_VAR} ${HELLO} > tests/test_results/test_run_simple_braces.txt" + test_run_single_quotes_braces = "echo 'hello ${TEST_ENV_VAR} ${HELLO}' > tests/test_results/test_run_single_quotes_braces.txt" + test_run_double_quotes_braces = "echo \"hello ${TEST_ENV_VAR} ${HELLO}\" > tests/test_results/test_run_double_quotes_braces.txt" + + test_run_pre_commands_simple = "export SPIDER=Peter && export MAN=Parker && echo $SPIDER ${MAN} >> tests/test_results/test_run_pre_commands_simple.txt" + test_run_pre_commands_single_quotes = "export SPIDER=Peter && export MAN=Parker && echo '$SPIDER $MAN' >> tests/test_results/test_run_pre_commands_single_quotes.txt" + test_run_pre_commands_double_quotes = "export SPIDER=Peter && export MAN=Parker && echo \"${SPIDER} $MAN\" >> tests/test_results/test_run_pre_commands_double_quotes.txt" [pre_commands] - dev = "unset ARGV0" - test = "unset ARGV0" + test_run_pre_commands_simple = "echo 'My name is' > tests/test_results/test_run_pre_commands_simple.txt" + test_run_pre_commands_single_quotes = "echo 'My name is' > tests/test_results/test_run_pre_commands_single_quotes.txt" + test_run_pre_commands_double_quotes = "echo 'My name is' > tests/test_results/test_run_pre_commands_double_quotes.txt" diff --git a/tests/common.rs b/tests/common.rs new file mode 100644 index 0000000..4760b61 --- /dev/null +++ b/tests/common.rs @@ -0,0 +1,83 @@ +use std::{env, error::Error, path::PathBuf}; + +use sm::{ + library::system::{commands_config, config}, + models::{commands_config::CommandsConfig, config::Config}, +}; +use tokio::fs; + +/// Sets up the test environment +/// +/// # Errors +#[allow(dead_code)] +pub async fn setup() -> Result<(CommandsConfig, Config, serde_json::Value), Box> { + env::set_var("TEST_ENV_VAR", "beautiful"); + // Make test_results directory + if let Err(e) = fs::create_dir_all("tests/test_results").await { + return Err(Box::from(format!( + "Failed to create test_results directory: {e}" + ))); + } + let commands_config_path = PathBuf::from("tests/assets/secrets_machine.toml"); + let commands_config = commands_config::parse(commands_config_path).await; + + let config_path = PathBuf::from("tests/assets/secrets_machine.toml"); + let config = config::parse(Some(config_path)).await; + + let Ok(secrets) = get_mock_secrets().await else { + return Err(Box::from("Failed to get mock secrets")); + }; + Ok((commands_config, config, secrets)) +} + +/// Cleans up the test environment +/// +/// # Errors +/// - If the test results directory cannot be deleted +#[allow(dead_code)] +pub async fn teardown() { + // Delete the test file + let _ = fs::remove_dir_all("tests/test_results").await; +} + +/// Sets up the test environment +/// +/// # Errors +/// - If the file cannot be read +/// - If the file value does not match the expected value +#[allow(dead_code)] +pub async fn assert_text_result( + test_name: &str, + expected_value: &str, +) -> Result<(), Box> { + let file_path = format!("tests/test_results/{test_name}.txt"); + // Make test_results directory + match fs::read_to_string(file_path).await { + Ok(value) => { + if value.trim() == expected_value.trim() { + Ok(()) + } else { + Err(Box::from(format!( + "File value does not match expected value.\nExpected: {expected_value}\nActual: {value}" + ))) + } + } + Err(e) => Err(Box::from(format!("Failed to read file: {e}"))), + } +} + +/// Gets the mock secrets +/// +/// # Errors +/// - If the file cannot be read +/// - If the file cannot be parsed as JSON +#[allow(dead_code)] +pub async fn get_mock_secrets() -> Result> { + match fs::read_to_string("tests/assets/secrets.json").await { + Ok(value) => { + let secrets: serde_json::Value = serde_json::from_str(&value)?; + Ok(secrets) + } + Err(e) => Err(Box::from(format!("Failed to read file: {e}"))), + } +} diff --git a/tests/test_bitwarden.rs b/tests/test_bitwarden.rs deleted file mode 100644 index ebce361..0000000 --- a/tests/test_bitwarden.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::path::PathBuf; - -use serde_json::json; -use sm::{ - library::system::config, - models::config::{BitwardenCredentials, SecretsSource}, -}; - -use sm::library::secrets_sources::bitwarden; - -#[tokio::test] -async fn test_legacy_bitwarden_parse_env_vars() { - dotenv::dotenv().ok(); - - let credentials = BitwardenCredentials { - access_token_name: "BWS_ACCESS_TOKEN".to_string(), - }; - - let env_vars = bitwarden::get_env_variables(Some(&credentials), &json!({})).await; - - assert!(!env_vars.is_empty()); -} - -#[tokio::test] -async fn test_bitwarden_parse_env_vars() { - let config_path = PathBuf::from("tests/assets/config.toml"); - let config = config::parse(Some(config_path)).await; - - dotenv::dotenv().ok(); - - for secrets_source in &config.secrets_sources { - match secrets_source { - SecretsSource::Bitwarden(credentials) => { - let env_vars = bitwarden::get_env_variables(Some(credentials), &json!({})).await; - assert!(!env_vars.is_empty()); - } - } - } -} diff --git a/tests/test_execution.rs b/tests/test_execution.rs new file mode 100644 index 0000000..c3c7877 --- /dev/null +++ b/tests/test_execution.rs @@ -0,0 +1,68 @@ +use sm::library::commands::execute; + +mod common; + +async fn execute_test(echo_value: &str, test_name: &str) { + let (_, config, secrets) = common::setup().await.unwrap(); + + let _ = execute( + config, + secrets, + &format!("echo {echo_value} > tests/test_results/{test_name}.txt"), + ) + .await; +} + +#[tokio::test] +async fn test_execution_simple() { + let test_name = "test_execution_simple"; + execute_test("hello $TEST_ENV_VAR $HELLO", test_name).await; + common::assert_text_result(test_name, "hello beautiful world") + .await + .unwrap(); +} + +#[tokio::test] +async fn test_execution_single_quotes() { + let test_name = "test_execution_single_quotes"; + execute_test("'Hello $TEST_ENV_VAR $HELLO'", test_name).await; + common::assert_text_result(test_name, "Hello $TEST_ENV_VAR $HELLO") + .await + .unwrap(); +} + +#[tokio::test] +async fn test_execution_double_quotes() { + let test_name = "test_execution_double_quotes"; + execute_test("\"Hello $TEST_ENV_VAR $HELLO\"", test_name).await; + common::assert_text_result(test_name, "Hello beautiful world") + .await + .unwrap(); +} + +#[tokio::test] +async fn test_execution_simple_braces() { + let test_name = "test_execution_simple_braces"; + execute_test("hello ${TEST_ENV_VAR} ${HELLO}", test_name).await; + common::assert_text_result(test_name, "hello beautiful world") + .await + .unwrap(); +} + +#[tokio::test] +async fn test_execution_single_quotes_braces() { + let test_name = "test_execution_single_quotes_braces"; + execute_test("'Hello ${TEST_ENV_VAR} ${HELLO}'", test_name).await; + common::assert_text_result(test_name, "Hello ${TEST_ENV_VAR} ${HELLO}") + .await + .unwrap(); +} + +#[tokio::test] +async fn test_execution_double_quotes_braces() { + let test_name = "test_execution_double_quotes_braces"; + execute_test("\"Hello ${TEST_ENV_VAR} ${HELLO}\"", test_name).await; + common::assert_text_result(test_name, "Hello beautiful world") + .await + .unwrap(); +} diff --git a/tests/test_run.rs b/tests/test_run.rs new file mode 100644 index 0000000..fc9ab9a --- /dev/null +++ b/tests/test_run.rs @@ -0,0 +1,97 @@ +use sm::library::commands::run; + +mod common; + +async fn run_test(test_name: &str) { + let (commands_config, config, secrets) = common::setup().await.unwrap(); + + let _ = run( + commands_config, + config, + secrets, + test_name.to_string(), + String::new(), + ) + .await; +} + +#[tokio::test] +async fn test_run_simple() { + let test_name = "test_run_simple"; + run_test(test_name).await; + common::assert_text_result(test_name, "hello beautiful world") + .await + .unwrap(); +} + +#[tokio::test] +async fn test_run_single_quotes() { + let test_name = "test_run_single_quotes"; + run_test(test_name).await; + common::assert_text_result(test_name, "hello $TEST_ENV_VAR $HELLO") + .await + .unwrap(); +} + +#[tokio::test] +async fn test_run_double_quotes() { + let test_name = "test_run_double_quotes"; + run_test(test_name).await; + common::assert_text_result(test_name, "hello beautiful world") + .await + .unwrap(); +} + +#[tokio::test] +async fn test_run_simple_braces() { + let test_name = "test_run_simple_braces"; + run_test(test_name).await; + common::assert_text_result(test_name, "hello beautiful world") + .await + .unwrap(); +} + +#[tokio::test] +async fn test_run_single_quotes_braces() { + let test_name = "test_run_single_quotes_braces"; + run_test(test_name).await; + common::assert_text_result(test_name, "hello ${TEST_ENV_VAR} ${HELLO}") + .await + .unwrap(); +} + +#[tokio::test] +async fn test_run_double_quotes_braces() { + let test_name = "test_run_double_quotes_braces"; + run_test(test_name).await; + common::assert_text_result(test_name, "hello beautiful world") + .await + .unwrap(); +} + +#[tokio::test] +async fn test_run_pre_commands_simple() { + let test_name = "test_run_pre_commands_simple"; + run_test(test_name).await; + common::assert_text_result(test_name, "My name is\nPeter Parker") + .await + .unwrap(); +} + +#[tokio::test] +async fn test_run_pre_commands_single_quotes() { + let test_name = "test_run_pre_commands_single_quotes"; + run_test(test_name).await; + common::assert_text_result(test_name, "My name is\n$SPIDER $MAN") + .await + .unwrap(); +} + +#[tokio::test] +async fn test_run_pre_commands_double_quotes() { + let test_name = "test_run_pre_commands_double_quotes"; + run_test(test_name).await; + common::assert_text_result(test_name, "My name is\nPeter Parker") + .await + .unwrap(); +} diff --git a/tests/test_z_teardown.rs b/tests/test_z_teardown.rs new file mode 100644 index 0000000..c211ab5 --- /dev/null +++ b/tests/test_z_teardown.rs @@ -0,0 +1,9 @@ +use sm::library::utils::logging; + +mod common; + +#[tokio::test] +async fn test_teardown() { + logging::info("Tearing down test environment").await; + common::teardown().await; +}