Skip to content

Commit

Permalink
Merge pull request #478 from tienvx/support-generator-for-form-urlenc…
Browse files Browse the repository at this point in the history
…oded

feat: Support generators for form urlencoded
  • Loading branch information
rholshausen authored Dec 4, 2024
2 parents 93ce0e9 + 707752a commit 1b6f0bc
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 1 deletion.
1 change: 1 addition & 0 deletions rust/Cargo.lock

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

4 changes: 3 additions & 1 deletion rust/pact_models/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ exclude = [
build = "build.rs"

[features]
default = ["datetime", "xml"]
default = ["datetime", "xml", "form_urlencoded"]
datetime = ["dep:chrono", "dep:chrono-tz", "dep:gregorian"] # Support for date/time matchers and expressions
xml = ["dep:sxd-document"] # support for matching XML documents
form_urlencoded = ["dep:serde_urlencoded"] # suport for matching form urlencoded

[dependencies]
ariadne = "0.5.0"
Expand All @@ -44,6 +45,7 @@ regex-syntax = "0.8.5"
semver = "1.0.23"
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.132"
serde_urlencoded = { version = "0.7.1", optional = true }
sxd-document = { version = "0.3.2", optional = true }
tracing = "0.1.40" # This needs to be the same version across all the libs (i.e. Pact FFI and plugin driver)

Expand Down
14 changes: 14 additions & 0 deletions rust/pact_models/src/content_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ impl ContentType {
self.main_type == *t && self.sub_type == *st
}).is_some()
}

pub fn is_form_urlencoded(&self) -> bool {
self.main_type == "application" && self.sub_type == "x-www-form-urlencoded"
}
}

impl Default for ContentType {
Expand Down Expand Up @@ -348,6 +352,7 @@ impl TryFrom<&str> for ContentTypeHint {
mod tests {
use expectest::prelude::*;
use maplit::btreemap;
use rstest::rstest;

use super::ContentType;

Expand Down Expand Up @@ -573,4 +578,13 @@ mod tests {
expect!(content_type2.is_equivalent_to(&content_type3)).to(be_true());
expect!(content_type2.is_equivalent_to(&content_type4)).to(be_false());
}

#[rstest]
#[case("text/plain", false)]
#[case("multipart/form-data", false)]
#[case("application/x-www-form-urlencoded", true)]
#[case("application/json", false)]
fn is_form_urlencoded_test(#[case] content_type: &str, #[case] result: bool) {
expect!(ContentType::parse(content_type).unwrap().is_form_urlencoded()).to(be_eq(result));
}
}
174 changes: 174 additions & 0 deletions rust/pact_models/src/generators/form_urlencoded.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@

use std::collections::HashMap;

use serde_json::Value;
use tracing::debug;
use anyhow::{anyhow, Result};

use crate::generators::{ContentTypeHandler, Generator, GeneratorTestMode, VariantMatcher, GenerateValue};
use crate::path_exp::DocPath;
use crate::bodies::OptionalBody;

pub type QueryParams = Vec<(String, String)>;

/// Implementation of a content type handler for FORM URLENCODED
pub struct FormUrlEncodedHandler {
/// Query params to apply the generators to.
pub params: QueryParams
}

impl ContentTypeHandler<String> for FormUrlEncodedHandler {
fn process_body(
&mut self,
generators: &HashMap<DocPath, Generator>,
mode: &GeneratorTestMode,
context: &HashMap<&str, Value>,
matcher: &Box<dyn VariantMatcher + Send + Sync>
) -> Result<OptionalBody, String> {
for (key, generator) in generators {
if generator.corresponds_to_mode(mode) {
debug!("Applying generator {:?} to key {}", generator, key);
self.apply_key(key, generator, context, matcher);
}
};
debug!("Query Params {:?}", self.params);
match serde_urlencoded::to_string(self.params.clone()) {
Ok(query_string) => Ok(OptionalBody::Present(query_string.into(), Some("application/x-www-form-urlencoded".into()), None)),
Err(err) => Err(anyhow!("Failed to convert query params to query string: {}", err).to_string())
}
}

fn apply_key(
&mut self,
key: &DocPath,
generator: &dyn GenerateValue<String>,
context: &HashMap<&str, Value>,
matcher: &Box<dyn VariantMatcher + Send + Sync>,
) {
let mut map: HashMap<String, usize> = HashMap::new();
for (param_key, param_value) in self.params.iter_mut() {
let index = map.entry(param_key.clone()).or_insert(0);
if key.eq(&DocPath::root().join(param_key.clone())) || key.eq(&DocPath::root().join(param_key.clone()).join_index(*index)) {
return match generator.generate_value(&param_value, context, matcher) {
Ok(new_value) => *param_value = new_value,
Err(_) => ()
}
}
*index += 1;
}
}
}


#[cfg(test)]
mod tests {
use expectest::expect;
use expectest::prelude::*;
use test_log::test;
use maplit::hashmap;

use crate::generators::NoopVariantMatcher;

use super::*;
use super::Generator;

#[test]
fn applies_the_generator_to_a_valid_param() {
let params = vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B".to_string()), ("c".to_string(), "C".to_string()));
let mut form_urlencoded_handler = FormUrlEncodedHandler { params };

form_urlencoded_handler.apply_key(&DocPath::new_unwrap("$.b"), &Generator::RandomInt(0, 10), &hashmap!{}, &NoopVariantMatcher.boxed());

expect!(&form_urlencoded_handler.params[1].1).to_not(be_equal_to("B"));
}

#[test]
fn does_not_apply_the_generator_to_invalid_param() {
let params = vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B".to_string()), ("c".to_string(), "C".to_string()));
let mut form_urlencoded_handler = FormUrlEncodedHandler { params };

form_urlencoded_handler.apply_key(&DocPath::new_unwrap("$.d"), &Generator::RandomInt(0, 10), &hashmap!{}, &NoopVariantMatcher.boxed());

expect!(form_urlencoded_handler.params).to(be_equal_to(vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B".to_string()), ("c".to_string(), "C".to_string()))));
}

#[test]
fn applies_the_generator_to_a_list_item() {
let params = vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B1".to_string()), ("b".to_string(), "B2".to_string()), ("c".to_string(), "C".to_string()));
let mut form_urlencoded_handler = FormUrlEncodedHandler { params };

form_urlencoded_handler.apply_key(&DocPath::new_unwrap("$.b[1]"), &Generator::RandomInt(0, 10), &hashmap!{}, &NoopVariantMatcher.boxed());

expect!(&form_urlencoded_handler.params[2].1).to_not(be_equal_to("B2"));
}

#[test]
fn does_not_apply_the_generator_when_index_is_not_in_list() {
let params = vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B".to_string()), ("c".to_string(), "C".to_string()));
let mut form_urlencoded_handler = FormUrlEncodedHandler { params };

form_urlencoded_handler.apply_key(&DocPath::new_unwrap("$.b[3]"), &Generator::RandomInt(0, 10), &hashmap!{}, &NoopVariantMatcher.boxed());

expect!(form_urlencoded_handler.params).to(be_equal_to(vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B".to_string()), ("c".to_string(), "C".to_string()))));
}

#[test]
fn does_not_apply_the_generator_when_not_a_list() {
let params = vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B".to_string()), ("c".to_string(), "C".to_string()));
let mut form_urlencoded_handler = FormUrlEncodedHandler { params };

form_urlencoded_handler.apply_key(&DocPath::new_unwrap("$.a[0]"), &Generator::RandomInt(0, 10), &hashmap!{}, &NoopVariantMatcher.boxed());

expect!(&form_urlencoded_handler.params[0].1).to_not(be_equal_to("100"));
}

#[test]
fn applies_the_generator_to_the_root() {
let params = vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B".to_string()), ("c".to_string(), "C".to_string()));
let mut form_urlencoded_handler = FormUrlEncodedHandler { params };

form_urlencoded_handler.apply_key(&DocPath::root(), &Generator::RandomInt(0, 10), &hashmap!{}, &NoopVariantMatcher.boxed());

expect!(form_urlencoded_handler.params).to(be_equal_to(vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B".to_string()), ("c".to_string(), "C".to_string()))));
}

#[test]
fn does_not_apply_the_generator_to_long_path() {
let params = vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B".to_string()), ("c".to_string(), "C".to_string()));
let mut form_urlencoded_handler = FormUrlEncodedHandler { params };

form_urlencoded_handler.apply_key(&DocPath::new_unwrap("$.a[1].b['2']"), &Generator::RandomInt(0, 10), &hashmap!{}, &NoopVariantMatcher.boxed());

expect!(form_urlencoded_handler.params).to(be_equal_to(vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B".to_string()), ("c".to_string(), "C".to_string()))));
}

#[test]
fn applies_the_generator_to_all_map_entries() {
let params = vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B".to_string()), ("c".to_string(), "C".to_string()));
let mut form_urlencoded_handler = FormUrlEncodedHandler { params };

form_urlencoded_handler.apply_key(&DocPath::new_unwrap("$.*"), &Generator::RandomInt(0, 10), &hashmap!{}, &NoopVariantMatcher.boxed());

expect!(form_urlencoded_handler.params).to(be_equal_to(vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B".to_string()), ("c".to_string(), "C".to_string()))));
}

#[test]
fn applies_the_generator_to_all_list_items() {
let params = vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B".to_string()), ("c".to_string(), "C".to_string()));
let mut form_urlencoded_handler = FormUrlEncodedHandler { params };

form_urlencoded_handler.apply_key(&DocPath::new_unwrap("$[*]"), &Generator::RandomInt(0, 10), &hashmap!{}, &NoopVariantMatcher.boxed());

expect!(form_urlencoded_handler.params).to(be_equal_to(vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B".to_string()), ("c".to_string(), "C".to_string()))));
}

#[test]
fn applies_the_generator_to_long_path_with_wildcard() {
let params = vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B".to_string()), ("c".to_string(), "C".to_string()));
let mut form_urlencoded_handler = FormUrlEncodedHandler { params };

form_urlencoded_handler.apply_key(&DocPath::new_unwrap("$.*[1].b[*]"), &Generator::RandomInt(3, 10), &hashmap!{}, &NoopVariantMatcher.boxed());

expect!(form_urlencoded_handler.params).to(be_equal_to(vec!(("a".to_string(), "100".to_string()), ("b".to_string(), "B".to_string()), ("c".to_string(), "C".to_string()))));
}
}
1 change: 1 addition & 0 deletions rust/pact_models/src/generators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use crate::path_exp::{DocPath, PathToken};
#[cfg(feature = "datetime")] pub mod datetime_expressions;
#[cfg(feature = "datetime")] mod date_expression_parser;
#[cfg(feature = "datetime")] mod time_expression_parser;
#[cfg(feature = "form_urlencoded")] pub mod form_urlencoded;

/// Trait to represent matching logic to find a matching variant for the Array Contains generator
pub trait VariantMatcher: Debug {
Expand Down

0 comments on commit 1b6f0bc

Please sign in to comment.