pact-foundation / pact-reference

Reference implementations for the pact specifications
https://pact.io
MIT License
91 stars 46 forks source link

Pact written to file and pact read from file are different #246

Closed NikZak closed 1 year ago

NikZak commented 1 year ago

Some context first (not relevant). I want to make testing of my applications faster so I want to exclude fetching data from web during testing. The way to do it is fetch data first time, save the response and then if there is saved request/response than match against the saved data. I decided to go with PACT as it seems that you are solving similar problem and have done a lot of work about how to save and read the http requests/response.

Problem. I get the response from the web. Make a pact object. Save it. Then read it and it is not same in some important aspects. E.g 1) There are added backslashes for escaped characters 2) The request query is not saved.

Here is an excerpt of a minimal reproducible example:

fn save_pact(pact: &RequestResponsePact, pact_path: &Path) -> Result<(), Box<dyn Error>> {
    let pact_json = pact.to_json(PactSpecification::V4)?;
    let mut file = File::create(pact_path)?;
    file.write_all(pact_json.to_string().as_bytes())?;
    Ok(())
}

fn read_pact(pact_path: &Path) -> Result<RequestResponsePact, Box<dyn Error>> {
    let pact = RequestResponsePact::read_pact(pact_path)?;
    Ok(pact)
}

fn main() {
    // get test reqwest https://data.binance.com/api/v3/klines?symbol=LUNCUSDT&interval=1w&limit=1
    let request = reqwest::blocking::Request::new(reqwest::Method::GET, "https://data.binance.com/api/v3/klines?symbol=LUNCUSDT&interval=1w&limit=1".parse().unwrap());
    let response = reqwest::blocking::Client::new().execute(request.try_clone().unwrap()).unwrap();
    // if we want to use response multiple times, we need to clone it. There is no clone for response but there is for its parts
    let response_headers = response.headers().clone();
    let response_text = response.text().unwrap();
    let pact = make_pact("consumer", &Url::parse("https://data.binance.com/api/v3/klines?symbol=LUNCUSDT&interval=1w&limit=1").unwrap(), &request, &response_text, &response_headers).unwrap();
    // save pact to file
    println!("Pact just received: {pact:?}");
    let pact_path = PathBuf::from("pact.json");
    save_pact(&pact, &pact_path).unwrap();
    // read pact from file
    let pact_from_file = read_pact(&pact_path).unwrap();
    println!("Pact just from file: {pact_from_file:?}");
}

And here is the output:

Pact just received: RequestResponsePact { consumer: Consumer { name: "consumer" }, provider: Provider { name: "https://data.binance.com" }, interactions: [RequestResponseInteraction { id: None, description: "https://data.binance.com/api/v3/klines?symbol=LUNCUSDT&interval=1w&limit=1", provider_states: [], request: Request { method: "GET", path: "/api/v3/klines", query: Some({"symbol": ["LUNCUSDT"], "interval": ["1w"], "limit": ["1"]}), headers: None, body: Empty, matching_rules: MatchingRules { rules: {} }, generators: Generators { categories: {} } }, response: Response { status: 200, headers: Some({"connection": ["keep-alive"], "pragma": ["no-cache"], "content-length": ["179"], "expires": ["0"], "access-control-allow-methods": ["GET, HEAD, OPTIONS"], "x-xss-protection": ["1; mode=block"], "date": ["Mon, 09 Jan 2023 14:10:45 GMT"], "strict-transport-security": ["max-age=31536000; includeSubdomains"], "via": ["1.1 791c1a1066b263a57d2ecaaa4381525a.cloudfront.net (CloudFront)"], "x-cache": ["Miss from cloudfront"], "server": ["nginx"], "x-amz-cf-pop": ["DXB52-P1"], "content-type": ["application/json;charset=UTF-8"], "x-content-security-policy": ["default-src 'self'"], "access-control-allow-origin": ["*"], "x-amz-cf-id": ["jVf8UxDQ6FM6_JkR4VnMo1CE4R72nZcBoeQCb1TgRU2NvZE1KVZr7Q=="], "x-content-type-options": ["nosniff"], "x-mbx-uuid": ["3c9bda84-8d6e-42ce-bddc-2867e6ec3b82"], "x-frame-options": ["SAMEORIGIN"], "x-mbx-used-weight": ["2"], "x-mbx-used-weight-1m": ["2"], "content-security-policy": ["default-src 'self'"], "x-webkit-csp": ["default-src 'self'"], "cache-control": ["no-cache, no-store, must-revalidate"]}), body: Present(b"[[1673222400000,\"0.00015856\",\"0.00016744\",\"0.00015744\",\"0.00016244\",\"103891875712.37000000\",1673827199999,\"16826713.22069334\",61574,\"53288057930.57000000\",\"8633012.83979493\",\"0\"]]", None, None), matching_rules: MatchingRules { rules: {} }, generators: Generators { categories: {} } } }], metadata: {}, specification_version: V4 }
Pact just from file: RequestResponsePact { consumer: Consumer { name: "consumer" }, provider: Provider { name: "https://data.binance.com" }, interactions: [RequestResponseInteraction { id: None, description: "https://data.binance.com/api/v3/klines?symbol=LUNCUSDT&interval=1w&limit=1", provider_states: [], request: Request { method: "GET", path: "/api/v3/klines", query: None, headers: None, body: Present(b"{\"content\":\"\"}", None, None), matching_rules: MatchingRules { rules: {} }, generators: Generators { categories: {} } }, response: Response { status: 200, headers: Some({"expires": ["0"], "strict-transport-security": ["max-age=31536000; includeSubdomains"], "x-mbx-used-weight-1m": ["2"], "access-control-allow-methods": ["GET, HEAD, OPTIONS"], "x-xss-protection": ["1; mode=block"], "date": ["Mon, 09 Jan 2023 14:10:45 GMT"], "access-control-allow-origin": ["*"], "connection": ["keep-alive"], "content-type": ["application/json;charset=UTF-8"], "x-mbx-used-weight": ["2"], "x-frame-options": ["SAMEORIGIN"], "via": ["1.1 791c1a1066b263a57d2ecaaa4381525a.cloudfront.net (CloudFront)"], "x-webkit-csp": ["default-src 'self'"], "pragma": ["no-cache"], "x-amz-cf-pop": ["DXB52-P1"], "server": ["nginx"], "cache-control": ["no-cache, no-store, must-revalidate"], "x-content-security-policy": ["default-src 'self'"], "x-amz-cf-id": ["jVf8UxDQ6FM6_JkR4VnMo1CE4R72nZcBoeQCb1TgRU2NvZE1KVZr7Q=="], "content-security-policy": ["default-src 'self'"], "x-mbx-uuid": ["3c9bda84-8d6e-42ce-bddc-2867e6ec3b82"], "content-length": ["179"], "x-content-type-options": ["nosniff"], "x-cache": ["Miss from cloudfront"]}), body: Present(b"{\"content\":\"[[1673222400000,\\\"0.00015856\\\",\\\"0.00016744\\\",\\\"0.00015744\\\",\\\"0.00016244\\\",\\\"103891875712.37000000\\\",1673827199999,\\\"16826713.22069334\\\",61574,\\\"53288057930.57000000\\\",\\\"8633012.83979493\\\",\\\"0\\\"]]\",\"contentType\":\"*/*\",\"encoded\":false}", None, None), matching_rules: MatchingRules { rules: {} }, generators: Generators { categories: {} } } }], metadata: {"pactRust": {"models": "1.0.3"}, "pactSpecification": {"version": "4.0"}}, specification_version: V4 }

The added backslashes is a bigger nuisance than missing query string.

Probably I am using wrong api, so leaving it for the experts

The whole minimal reproducible example is a bit long so I placed it here. It is not compiling as you need to install dependencies:

[dependencies]
pact_models = "1.0.3"
reqwest = {version = "0.11.13", features = ["blocking"]}
url = "2.3.1"
tracing = "0.1.37"
serde_json = "1.0.91"
rholshausen commented 1 year ago

/jira ticket

github-actions[bot] commented 1 year ago

👋 Thanks, Jira [PACT-530] ticket created.

rholshausen commented 1 year ago

The issue is that you have specified the PactSpecification as V4, but you are using the V3 model classes to read and write the Pact file. While this is an issue (it is probably converting the formats and losing information), setting the spec version to V3 does not have this issue.

rholshausen commented 1 year ago

There are already functions you can use that can read and write the Pact file correctly, taking into account the format. See https://docs.rs/pact_models/1.0.3/pact_models/pact/fn.read_pact.html and https://docs.rs/pact_models/1.0.3/pact_models/pact/fn.write_pact.html.

Example:

  let pact_path = PathBuf::from("pact.json");
  write_pact(pact.boxed(), &pact_path, PactSpecification::V3, true).unwrap();
  // read pact from file
  let pact_from_file = read_pact(&pact_path).unwrap().as_request_response_pact().unwrap();
  println!("Pact just from file: {pact_from_file:?}");

Or if you want to use V4 format:

  let pact_path = PathBuf::from("pact.json");
  write_pact(pact.boxed(), &pact_path, PactSpecification::V4, true).unwrap();
  // read pact from file
  let pact_from_file = read_pact(&pact_path).unwrap().as_v4_pact().unwrap();
  println!("Pact just from file: {pact_from_file:?}");
NikZak commented 1 year ago

Thank you for your prompt response! It works! Do you recommend using V3 or V4 pact specification? And could you point me to the docs describing differences between those? Or briefly describe it here

NikZak commented 1 year ago

Do I understand correctly that RequestResponsePact structure can only be used for V3 and for V4 the structure is V4Pact?

RequestResponsePact has an attribute PactSpecification which can take V4 as value. If it can't then it should probably be written somewhere or checks added. RequestResponsePact

You can also make some attributes of RequestResponsePact private then ppl would not be able to directly fill them as I did. Only through methods

rholshausen commented 1 year ago

All good suggestions.

V4 is newer and not as well supported as V3, but supports more types of workflows (like gRPC and Websockets). It depends on how you want to share your Pact files. From when you are doing, V3 will work fine for you. You can read up on the differences at https://github.com/pact-foundation/pact-specification/tree/version-3 and https://github.com/pact-foundation/pact-specification/tree/version-4

NikZak commented 1 year ago

Eventually I did this repo to solve the initial problem https://github.com/NikZak/pact-proxy-rs