Skip to content

Commit

Permalink
feat: support for matchers on headers
Browse files Browse the repository at this point in the history
  • Loading branch information
Ronald Holshausen committed Nov 19, 2020
1 parent a3dfdd1 commit aa3d55e
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 427 deletions.
488 changes: 136 additions & 352 deletions examples/v3/todo-consumer/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion examples/v3/todo-consumer/package.json
Expand Up @@ -28,7 +28,7 @@
"xml2json": "^0.11.2"
},
"devDependencies": {
"@pact-foundation/pact": "^10.0.0-beta",
"@pact-foundation/pact": "^10.0.0-beta.22",
"ts-node": "^3.3.0",
"neon-cli": "^0.3.3"
},
Expand Down
9 changes: 6 additions & 3 deletions examples/v3/todo-consumer/provider.js
Expand Up @@ -2,9 +2,8 @@ const express = require("express")
const server = express()

server.get("/projects", (req, res) => {
console.log(req.headers)
if (req.headers.accept == "application/xml") {
res.header("Content-Type", "application/xml")
if (req.headers.accept.endsWith("xml")) {
res.header("Content-Type", "application/todo+xml")
res.send(
"<?xml version='1.0'?><ns1:projects id='1234' xmlns:ns1='http://some.namespace/and/more/stuff'><ns1:project id='1' name='Project 1' type='activity'><ns1:tasks><ns1:task done='true' id='1' name='Task 1'/><ns1:task done='true' id='1' name='Task 1'/><ns1:task done='true' id='1' name='Task 1'/><ns1:task done='true' id='1' name='Task 1'/><ns1:task done='true' id='1' name='Task 1'/></ns1:tasks></ns1:project><ns1:project id='1' name='Project 1' type='activity'><ns1:tasks><ns1:task done='true' id='1' name='Task 1'/><ns1:task done='true' id='1' name='Task 1'/><ns1:task done='true' id='1' name='Task 1'/><ns1:task done='true' id='1' name='Task 1'/><ns1:task done='true' id='1' name='Task 1'/></ns1:tasks></ns1:project></ns1:projects>"
)
Expand Down Expand Up @@ -43,6 +42,10 @@ server.get("/projects", (req, res) => {
}
})

server.post("/projects/:id/images", (req, res) => {
res.status(201).end()
})

module.exports = {
server,
}
2 changes: 1 addition & 1 deletion examples/v3/todo-consumer/src/todo.js
Expand Up @@ -17,7 +17,7 @@ module.exports = {
.then(response => {
console.log("todo response:")
eyes.inspect(response.data)
if (format === "xml") {
if (format.endsWith("xml")) {
const result = JSON.parse(parser.toJson(response.data))
console.dir(result, { depth: 10 })
return R.path(["ns1:projects"], result)
Expand Down
20 changes: 4 additions & 16 deletions examples/v3/todo-consumer/test/consumer.spec.js
Expand Up @@ -9,6 +9,7 @@ const {
boolean,
atLeastOneLike,
timestamp,
regex
} = MatchersV3

const TodoApp = require("../src/todo")
Expand Down Expand Up @@ -94,11 +95,11 @@ describe("Pact V3", () => {
method: "GET",
path: "/projects",
query: { from: "today" },
headers: { Accept: "application/xml" },
headers: { Accept: regex("application/.*xml", "application/xml") },
})
.willRespondWith({
status: 200,
headers: { "Content-Type": "application/xml" },
headers: { "Content-Type": regex("application/.*xml(;.*)?", "application/xml") },
body: new XmlBuilder("1.0", "UTF-8", "ns1:projects").build(el => {
el.setAttributes({
id: "1234",
Expand Down Expand Up @@ -132,20 +133,7 @@ describe("Pact V3", () => {
},
{ examples: 2 }
)
}),
// body: `<?xml version="1.0" encoding="UTF-8"?>
// <projects foo="bar">
// <project id="1" name="Project 1" due="2016-02-11T09:46:56.023Z">
// <tasks>
// <task id="1" name="Do the laundry" done="true"/>
// <task id="2" name="Do the dishes" done="false"/>
// <task id="3" name="Do the backyard" done="false"/>
// <task id="4" name="Do nothing" done="false"/>
// </tasks>
// </project>
// <project/>
// </projects>
// `,
})
})
})

Expand Down
7 changes: 1 addition & 6 deletions examples/v3/todo-consumer/test/provider.spec.js
Expand Up @@ -20,14 +20,9 @@ describe("Pact XML Verification", () => {
provider: "XML Service",
providerBaseUrl: "http://localhost:8081",
pactUrls: ["./pacts/TodoApp-TodoServiceV3.json"],
// pactUrls: [
// path.resolve(
// process.cwd(),
// "./pacts/matching_service-animal_profile_service.json"
// ),
// ],
stateHandlers: {
"i have a list of projects": setup => {},
"i have a project": setup => {},
},
}

Expand Down
18 changes: 5 additions & 13 deletions native/Cargo.lock

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

135 changes: 101 additions & 34 deletions native/src/lib.rs
Expand Up @@ -214,7 +214,7 @@ fn get_parameter(cx: &mut CallContext<JsPact>, param: &Handle<JsValue>) -> NeonR
}
}

fn process_query(cx: &mut CallContext<JsPact>, js_query: Handle<JsValue>, request: Handle<JsObject>) -> NeonResult<(HashMap<String, Vec<String>>, MatchingRuleCategory, HashMap<String, Generator>)> {
fn process_query(cx: &mut CallContext<JsPact>, js_query: Handle<JsValue>) -> NeonResult<(HashMap<String, Vec<String>>, MatchingRuleCategory, HashMap<String, Generator>)> {
let mut map = hashmap!{};
let mut rules = MatchingRuleCategory::empty("query");
let mut generators = hashmap!{};
Expand Down Expand Up @@ -256,20 +256,47 @@ fn process_query(cx: &mut CallContext<JsPact>, js_query: Handle<JsValue>, reques
}
}

fn get_headers(cx: &mut CallContext<JsPact>, obj: Handle<JsObject>) -> NeonResult<HashMap<String, Vec<String>>> {
let js_headers = obj.get(cx, "headers");
js_headers.map(|val| {
let mut map = hashmap!{};
if let Ok(header_map) = val.downcast::<JsObject>() {
let props = header_map.get_own_property_names(cx).unwrap();
for prop in props.to_vec(cx).unwrap() {
let prop_name = prop.downcast::<JsString>().unwrap().value();
let prop_val = header_map.get(cx, prop_name.as_str()).unwrap();
map.insert(prop_name, vec![prop_val.downcast::<JsString>().unwrap().value()]);
fn process_headers(cx: &mut CallContext<JsPact>, obj: Handle<JsObject>) -> NeonResult<(HashMap<String, Vec<String>>, MatchingRuleCategory, HashMap<String, Generator>)> {
let mut map = hashmap!{};
let mut rules = MatchingRuleCategory::empty("header");
let mut generators = hashmap!{};
let js_headers = obj.get(cx, "headers")?;
if let Ok(header_map) = js_headers.downcast::<JsObject>() {
let props = header_map.get_own_property_names(cx)?;
for prop in props.to_vec(cx).unwrap() {
let prop_name = prop.downcast::<JsString>().unwrap().value();
let prop_val = header_map.get(cx, prop_name.as_str())?;
if let Ok(array) = prop_val.downcast::<JsArray>() {
let vec = array.to_vec(cx)?;
let mut params = vec![];
for (index, item) in vec.iter().enumerate() {
let (value, matcher, generator) = get_parameter(cx, &item)?;
if let Some(rule) = matcher {
rules.add_rule(prop_name.clone(), rule, &RuleLogic::And);
}
if let Some(generator) = generator {
generators.insert(format!("{}[{}]", prop_name.clone(), index), generator);
}
params.push(value);
}
map.insert(prop_name, params);
} else {
let (value, matcher, generator) = get_parameter(cx, &prop_val)?;
if let Some(rule) = matcher {
rules.add_rule(prop_name.clone(), rule, &RuleLogic::And);
}
if let Some(generator) = generator {
generators.insert(prop_name.clone(), generator);
};
map.insert(prop_name, vec![value]);
}
}
map
})
Ok((map, rules, generators))
} else if js_headers.is_a::<JsUndefined>() || js_headers.is_a::<JsNull>() {
Ok((map, rules, generators))
} else {
cx.throw_type_error(format!("Headers must be a map of key/values"))
}
}

fn load_file(file_path: &String) -> Result<OptionalBody, std::io::Error> {
Expand Down Expand Up @@ -363,8 +390,8 @@ declare_types! {
let js_method = request.get(&mut cx, "method");
let path = get_request_path(&mut cx, request);
let js_query = request.get(&mut cx, "query")?;
let (query_vals, query_rules, query_gens) = process_query(&mut cx, js_query, request)?;
let js_header_props = get_headers(&mut cx, request);
let (query_vals, query_rules, query_gens) = process_query(&mut cx, js_query)?;
let (headers, header_rules, header_gens) = process_headers(&mut cx, request)?;
let js_body = match cx.argument::<JsValue>(1) {
Ok(body) => body.downcast::<JsString>().map(|val| val.value()).ok(),
Err(_) => None
Expand Down Expand Up @@ -406,9 +433,16 @@ declare_types! {
}
}

if let Ok(header_props) = js_header_props {
last.request.headers = Some(header_props)
if !headers.is_empty() {
last.request.headers = Some(headers);
if header_rules.is_not_empty() {
last.request.matching_rules.rules.insert("header".into(), header_rules);
}
if !header_gens.is_empty() {
last.request.generators.categories.insert(GeneratorCategory::HEADER, header_gens);
}
}

if let Some(body) = js_body {
match process_body(body, last.request.content_type(), &mut last.request.matching_rules,
&mut last.request.generators) {
Expand Down Expand Up @@ -444,8 +478,8 @@ declare_types! {
let js_method = request.get(&mut cx, "method");
let path = get_request_path(&mut cx, request);
let js_query = request.get(&mut cx, "query")?;
let (query_vals, query_rules, query_gens) = process_query(&mut cx, js_query, request)?;
let js_header_props = get_headers(&mut cx, request);
let (query_vals, query_rules, query_gens) = process_query(&mut cx, js_query)?;
let (headers, header_rules, header_gens) = process_headers(&mut cx, request)?;

let mut this = cx.this();

Expand Down Expand Up @@ -483,8 +517,14 @@ declare_types! {
}
}

if let Ok(header_props) = js_header_props {
last.request.headers = Some(header_props)
if !headers.is_empty() {
last.request.headers = Some(headers);
if header_rules.is_not_empty() {
last.request.matching_rules.rules.insert("header".into(), header_rules);
}
if !header_gens.is_empty() {
last.request.generators.categories.insert(GeneratorCategory::HEADER, header_gens);
}
}

match load_file(&file_path.value()) {
Expand Down Expand Up @@ -530,8 +570,8 @@ declare_types! {
let js_method = request.get(&mut cx, "method");
let path = get_request_path(&mut cx, request);
let js_query = request.get(&mut cx, "query")?;
let (query_vals, query_rules, query_gens) = process_query(&mut cx, js_query, request)?;
let js_header_props = get_headers(&mut cx, request);
let (query_vals, query_rules, query_gens) = process_query(&mut cx, js_query)?;
let (headers, header_rules, header_gens) = process_headers(&mut cx, request)?;

let mut this = cx.this();

Expand Down Expand Up @@ -569,8 +609,14 @@ declare_types! {
}
}

if let Ok(header_props) = js_header_props {
last.request.headers = Some(header_props)
if !headers.is_empty() {
last.request.headers = Some(headers);
if header_rules.is_not_empty() {
last.request.matching_rules.rules.insert("header".into(), header_rules);
}
if !header_gens.is_empty() {
last.request.generators.categories.insert(GeneratorCategory::HEADER, header_gens);
}
}

let boundary = last.description.replace(" ", "_");
Expand Down Expand Up @@ -600,7 +646,7 @@ declare_types! {
method addResponse(mut cx) {
let response = cx.argument::<JsObject>(0)?;
let js_status = response.get(&mut cx, "status");
let js_header_props = get_headers(&mut cx, response);
let (headers, header_rules, header_gens) = process_headers(&mut cx, response)?;
let js_body = match cx.argument::<JsValue>(1) {
Ok(body) => body.downcast::<JsString>().map(|val| val.value()).ok(),
Err(_) => None
Expand All @@ -618,8 +664,15 @@ declare_types! {
Err(err) => warn!("Response status is not a number - {}", err)
}
}
if let Ok(header_props) = js_header_props {
last.response.headers = Some(header_props)

if !headers.is_empty() {
last.response.headers = Some(headers);
if header_rules.is_not_empty() {
last.response.matching_rules.rules.insert("header".into(), header_rules);
}
if !header_gens.is_empty() {
last.response.generators.categories.insert(GeneratorCategory::HEADER, header_gens);
}
}

if let Some(body) = js_body {
Expand Down Expand Up @@ -652,7 +705,7 @@ declare_types! {
let file_path = cx.argument::<JsString>(2)?;

let js_status = response.get(&mut cx, "status");
let js_header_props = get_headers(&mut cx, response);
let (headers, header_rules, header_gens) = process_headers(&mut cx, response)?;

let mut this = cx.this();

Expand All @@ -667,8 +720,15 @@ declare_types! {
Err(err) => warn!("Response status is not a number - {}", err)
}
}
if let Ok(header_props) = js_header_props {
last.response.headers = Some(header_props)

if !headers.is_empty() {
last.response.headers = Some(headers);
if header_rules.is_not_empty() {
last.response.matching_rules.rules.insert("header".into(), header_rules);
}
if !header_gens.is_empty() {
last.response.generators.categories.insert(GeneratorCategory::HEADER, header_gens);
}
}

match load_file(&file_path.value()) {
Expand Down Expand Up @@ -712,7 +772,7 @@ declare_types! {
let part_name = cx.argument::<JsString>(3)?;

let js_status = response.get(&mut cx, "status");
let js_header_props = get_headers(&mut cx, response);
let (headers, header_rules, header_gens) = process_headers(&mut cx, response)?;

let mut this = cx.this();

Expand All @@ -727,8 +787,15 @@ declare_types! {
Err(err) => warn!("Response status is not a number - {}", err)
}
}
if let Ok(header_props) = js_header_props {
last.response.headers = Some(header_props)

if !headers.is_empty() {
last.response.headers = Some(headers);
if header_rules.is_not_empty() {
last.response.matching_rules.rules.insert("header".into(), header_rules);
}
if !header_gens.is_empty() {
last.response.generators.categories.insert(GeneratorCategory::HEADER, header_gens);
}
}

let boundary = last.description.replace(" ", "_");
Expand Down
1 change: 0 additions & 1 deletion native/src/verify.rs
Expand Up @@ -8,7 +8,6 @@ use ansi_term::Colour::*;
use url::Url;
use std::sync::mpsc;
use std::time::Duration;
use std::ops::Deref;
use async_trait::async_trait;
use pact_matching::models::provider_states::ProviderState;
use maplit::*;
Expand Down

0 comments on commit aa3d55e

Please sign in to comment.