Skip to content

Commit

Permalink
Use yaml format for conversions file and allow multiple random values
Browse files Browse the repository at this point in the history
  • Loading branch information
JordiPolo committed Aug 13, 2020
1 parent 9ba5e09 commit 860e8d1
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 110 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "minos"
version = "0.2.2"
version = "0.3.0"
authors = ["Jordi Polo Carres <[email protected]>"]
description = """
Minos is a command line tool to generate and run scenarios against
Expand Down
54 changes: 23 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,<piece to be converted>,<value>
path,<piece to be converted2>,<value2>
param,<piece to be converted2>,<value2>
...
```
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
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions conversions_example.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion src/authentication.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//use mauth_client::*;
// use mauth_client::*;

// pub struct Authentication {
// mauth_info: mauth_client::MAuthInfo
Expand Down
2 changes: 1 addition & 1 deletion src/cli_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down
136 changes: 68 additions & 68 deletions src/known_param.rs
Original file line number Diff line number Diff line change
@@ -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<String>);

// 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<Raw> 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<String>),
}

pub struct KnownParamCollection {
params: Vec<KnownParam>,
#[derive(Debug, PartialEq, Deserialize)]
pub struct Conversions {
paths: BTreeMap<String, BTreeMap<String, StringOrArray>>,
}

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<String> {
let conversion = self.find_by_path(path)?;
Some(str::replace(path, &conversion.pattern, &conversion.value))
fn _read(conversions: &str) -> Result<Conversions, std::io::Error> {
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<KnownParam> {
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<String> {
let mut result = String::new();

fn read_known_params(conversions: &str) -> Vec<KnownParam> {
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<Vec<KnownParam>, 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<String, StringOrArray> {
self.paths
.iter()
.find(|(name, _param)| **name == "/".to_string())
.unwrap()
.1
}
}
6 changes: 3 additions & 3 deletions src/mutation.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/mutation/params.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/reporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down

0 comments on commit 860e8d1

Please sign in to comment.