diff --git a/Cargo.lock b/Cargo.lock index e79c8dd..61ce650 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1704,9 +1704,9 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] name = "smallvec" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3757cb9d89161a2f24e1cf78efa0c1fcff485d18e3f55e0aa3480824ddaa0f3f" +checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" [[package]] name = "socket2" diff --git a/Cargo.toml b/Cargo.toml index fd0dd97..56d57c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "minos" -version = "0.2.2" +version = "0.3.0" authors = ["Jordi Polo Carres "] description = """ Minos is a command line tool to generate and run scenarios against diff --git a/README.md b/README.md index 21929cc..67686d6 100644 --- a/README.md +++ b/README.md @@ -79,19 +79,9 @@ Now let's see how good is our performance, let's not use `-a` to avoid measuring ## Conversions file This step is optional. -If no conversions file is found, Minos will still generate scenarios for all the endpoints that do not have required parameters. - -Minos can't discover IDs of resources for itself yet. -It will call all the `index` routes which do not have required parameters. -To be able to call routes with required parameters in the path or query string, Minos needs a conversions file. -The default location for the file is `./conversions.minos` but this value can be overwriten with the `-c` parameter. -The format of the file is simply: -``` -path,, -path,, -param,, -... -``` +Minos allows you to specify the value of any parameter. Minos will use this information to create requests. +This is typically used to provide IDs that need to exist in your database (user_id, app_uuid, etc.). +The default location for the file is `./conversions.yml` but this value can be overwriten with the `-c` parameter. ### Example Our Openapi spec has the following routes @@ -104,33 +94,35 @@ Our Openapi spec has the following routes ... /houses/{house_id}/renters/{renter_id}: ... +/house_owners/{house_id}: ``` -When we have a `conversions.minos` file like: -``` -path,{house_id},55505 -path,{renter_id},60000 -query,city_id,1000 -``` -Minos will test: -``` -/houses/55505?city_id=1000 -/houses/55505/renters/60000 +When we have a `conversions.yaml` file like: ``` +paths: + "/": + house_id: [55505, 55506, 55507, 55508] + renter_id: 6000 + city_id: 1000 -If we want to use a different house_id for testing renting, with a file like: -``` -path,{house_id},55505 -path,{house_id}/renters/{renter_id},1111/renters/60000 -query,city_id,1000 + house_owners: + house_id: 19991 ``` -Minos will test: +Minos would test: ``` /houses/55505?city_id=1000 -/houses/1111/renters/60000 +/houses/55508/renters/60000 +/houses/19991 ``` -In short, Minos is quite dumb and will just sustitute the strings, no questions asked. + +The parameters within the "/" path will match any parameter in the openapi file. +If you need to match only a specific path, you can add the parameter within that specific path. +That is useful if you use the same param name everywhere (id, etc.) and you want to specify it per endpoint. +It this is not your case, where possible use "/" so you match as widely as possible. + +Note that when an array of values is passed for a parameter, Minos will choose one random value from the array. +This is specially useful when running performance tests. # Scenarios diff --git a/conversions_example.yaml b/conversions_example.yaml new file mode 100644 index 0000000..91a1877 --- /dev/null +++ b/conversions_example.yaml @@ -0,0 +1,9 @@ + +paths: + "/": + user_uuid: [03b97130-1be2-42f9-bdaf-e1f6a2b9e269] + + teams: + uuid: 03b97130-1be2-42f9-bdaf-e1f6a2b9e269 + roles: + uuid: effb3dfd-26be-4948-8786-4a04ea208461 diff --git a/src/authentication.rs b/src/authentication.rs index 118b602..1e3aa34 100644 --- a/src/authentication.rs +++ b/src/authentication.rs @@ -1,4 +1,4 @@ -//use mauth_client::*; +// use mauth_client::*; // pub struct Authentication { // mauth_info: mauth_client::MAuthInfo diff --git a/src/cli_args.rs b/src/cli_args.rs index fc0833b..88c75da 100644 --- a/src/cli_args.rs +++ b/src/cli_args.rs @@ -30,7 +30,7 @@ pub struct CLIArgs { short = "c", long = "conversions", help = "The location of the conversions file with parameter values for this run.", - default_value = "./conversions.minos" + default_value = "./conversions.yml" )] pub conv_filename: String, diff --git a/src/known_param.rs b/src/known_param.rs index 2154fe7..e2a7937 100644 --- a/src/known_param.rs +++ b/src/known_param.rs @@ -1,94 +1,94 @@ -#[derive(Debug, Clone)] -pub struct KnownParam { - pub pattern: String, - pub value: String, - pub context: String, -} +use serde::Deserialize; +use std::collections::BTreeMap; +use rand::seq::SliceRandom; -impl KnownParam { - fn new(data: &[&str]) -> Self { - KnownParam { - pattern: data[1].to_string(), - value: data[2].to_string(), - context: data[0].to_string(), - } - } +// StringOrArray and Raw to allow either strings or list of strings +#[derive(Deserialize, Debug, PartialEq)] +#[serde(from = "Raw")] +struct StringOrArray(Vec); - // We want: - // pattern {user_uuid} path: /v1/users/{user_uuid} -> true - // pattern {user_uuid} path: /v1/users/{user_uuid}/onboard -> true - // pattern {user_uuid} path: /v1/users/{user_uuid}/friends/{friend_uuid} -> false - fn path_matches(&self, full_path: &str) -> bool { - (self.context == "path" - || (self.context != "query" && full_path.contains(&self.context))) - && full_path.contains(&self.pattern) - && full_path.matches('}').count() == self.pattern.matches('}').count() +impl From for StringOrArray { + fn from(raw: Raw) -> StringOrArray { + match raw { + Raw::String(s) => StringOrArray(vec![s]), + Raw::List(l) => StringOrArray(l), + } } +} - fn query_matches(&self, query_param_name: &str) -> bool { - self.context == "query" && query_param_name == self.pattern - } +#[derive(Deserialize)] +#[serde(untagged)] +enum Raw { + String(String), + List(Vec), } -pub struct KnownParamCollection { - params: Vec, +#[derive(Debug, PartialEq, Deserialize)] +pub struct Conversions { + paths: BTreeMap>, } -impl KnownParamCollection { - pub fn new(conversions: &str) -> Self { - KnownParamCollection { - params: KnownParamCollection::read_known_params(conversions), +impl Conversions { + pub fn new(filename: &str) -> Self { + match Self::_read(filename) { + Err(_) => { + println!( + "Conversions file {} not found. Conversions will not be used", + filename + ); + Conversions { + paths: BTreeMap::new(), + } + } + Ok(d) => d, } } - // a path may be /users/{uuid}/friends/{uuid2} - pub fn retrieve_known_path(&self, path: &str) -> Option { - let conversion = self.find_by_path(path)?; - Some(str::replace(path, &conversion.pattern, &conversion.value)) + fn _read(conversions: &str) -> Result { + let filename = shellexpand::tilde(conversions).into_owned(); + let filedata = std::fs::read_to_string(&filename)?; + let deserialized_map: Conversions = serde_yaml::from_str(&filedata).expect(&format!( + "The file {} could not be deserialized as a conversions YAML file.", + conversions + )); + Ok(deserialized_map) } pub fn param_known(&self, name: &str) -> bool { - self.params.iter().any(|param| param.query_matches(name)) + self.paths + .iter() + .any(|(_name, path)| path.get(name).is_some()) } pub fn param_value(&self, name: &str) -> String { - self.params - .iter() - .find(|param| param.query_matches(name)) - .unwrap() - .value - .to_string() + let path = self.get_generic(); + let list = &path.get(name).unwrap().0; // .0 to get the inner object from StringOrArray + list.choose(&mut rand::thread_rng()).unwrap().to_string() } - fn find_by_path(&self, path: &str) -> Option { - self.params - .clone() - .into_iter() - .find(|param| param.path_matches(path)) - } + // a path may be /users/{uuid}/friends/{uuid2} + pub fn retrieve_known_path(&self, pattern: &str) -> Option { + let mut result = String::new(); - fn read_known_params(conversions: &str) -> Vec { - match KnownParamCollection::_read_known_params(conversions) { - Err(_) => { - println!( - "Conversions file {} not found. Conversions will not be used", - conversions - ); - vec![] + for (path, keys) in &self.paths { + if pattern.contains(path) { + for (key, value) in keys { + let random_value = &value.0.choose(&mut rand::thread_rng()).unwrap(); + result = str::replace(pattern, &format!("{{{}}}", key), random_value) + } + if !result.contains("{") { + return Some(result); + } } - Ok(d) => d, } + None } - fn _read_known_params(conversions: &str) -> Result, std::io::Error> { - let filename = shellexpand::tilde(conversions).into_owned(); - let filedata = std::fs::read_to_string(&filename)?; - Ok(filedata.split('\n').fold(vec![], |mut acc, line| { - let parts: Vec<&str> = line.split(',').collect(); - if parts.len() == 3 { - acc.push(KnownParam::new(&parts)) - } - acc - })) + fn get_generic(&self) -> &BTreeMap { + self.paths + .iter() + .find(|(name, _param)| **name == "/".to_string()) + .unwrap() + .1 } } diff --git a/src/mutation.rs b/src/mutation.rs index 7880f82..a108e16 100644 --- a/src/mutation.rs +++ b/src/mutation.rs @@ -1,4 +1,4 @@ -use crate::known_param::KnownParamCollection; +use crate::known_param::Conversions; use crate::operation::Endpoint; use crate::request_param::RequestParam; use crate::scenario::Scenario; @@ -102,14 +102,14 @@ impl fmt::Display for Mutation { } pub struct Mutator { - known_params: KnownParamCollection, + known_params: Conversions, run_all_codes: bool, } impl Mutator { pub fn new(conversions: &str, run_all_codes: bool) -> Self { Mutator { - known_params: KnownParamCollection::new(conversions), + known_params: Conversions::new(conversions), run_all_codes, } } diff --git a/src/mutation/params.rs b/src/mutation/params.rs index 4c4722e..caf6d9a 100644 --- a/src/mutation/params.rs +++ b/src/mutation/params.rs @@ -1,4 +1,4 @@ -use crate::known_param::KnownParamCollection; +use crate::known_param::Conversions; use crate::mutation::bool_type; use crate::mutation::integer_type; use crate::mutation::param_mutation::ParamMutation; @@ -10,7 +10,7 @@ use openapiv3::Type; /// TODO: How to make sure we generate for all the Mutagens? pub fn mutate( param: &openapiv3::Parameter, - known_params: &KnownParamCollection, + known_params: &Conversions, run_all_scenarios: bool, ) -> ParamMutation { let data = param.parameter_data(); diff --git a/src/reporter.rs b/src/reporter.rs index d0ae285..b9f036a 100644 --- a/src/reporter.rs +++ b/src/reporter.rs @@ -44,7 +44,7 @@ pub fn print_mutation_scenario(scenario: &Scenario) { } else { Color::Blue }; - printer.print_color(mutation, color); + // printer.print_color(mutation, color); } printer.print_color( format!(