pact-foundation / pact-js

JS version of Pact. Pact is a contract testing framework for HTTP APIs and non-HTTP asynchronous messaging systems.
https://pact.io
Other
1.58k stars 343 forks source link

Generators for headers are not handled properly #1144

Closed mario-martinez-se closed 5 months ago

mario-martinez-se commented 6 months ago

I have a pact that looks like (excerpt):

    "consumer": {
        "name": "Consumer"
    },
    "interactions": [
        {
            "description": "a request for bookings count",
            "providerStates": [
                {
                    "name": "A user exists",
                    "params": {
                        "email": "contract@mail.com"
                    }
                }
            ],
            "request": {
                "generators": {
                    "header": {
                        "$['se-token'][0]": {
                            "expression": "${seToken}",
                            "type": "ProviderState"
                        }
                    },
                    "path": {
                        "expression": "/v4/users/${userId}/bookings/count",
                        "type": "ProviderState"
                    }
                },
                "headers": {
                    "se-api-token": "156806cc-80cf-449d-b0ca-86c9252a36a1",
                    "se-token": "ABC123"
                },

                "method": "GET",
                "path": "/v4/users/1234/bookings/count"
            },
            "response": {

            }
        }
    ],
    "metadata": {
        "pact-js": {
            "version": "11.0.2"
        },
        "pactRust": {
            "ffi": "0.4.0",
            "models": "1.0.4"
        },
        "pactSpecification": {
            "version": "3.0.0"
        }
    },
    "provider": {
        "name": "Provider"
    }
}

When I run the tests for the provider, the status change url gets called as expected and it returns the required values, userId and seToken. These values are properly interpolated in the path, so my server receives a hit in the right url (/v4/users/${userId}/bookings/count), but the headers are not properly set in the request, so in my server logs I see:

 Note: further occurrences of HTTP request parsing errors will be logged at DEBUG level.

java.lang.IllegalArgumentException: The HTTP header line [$['se-token'][0]: eyJhbGciOiJSUzI1NiJ9.eyJ] does not conform to RFC 7230 and has been ignored.
    at org.apache.coyote.http11.Http11InputBuffer.skipLine(Http11InputBuffer.java:1093)
    at org.apache.coyote.http11.Http11InputBuffer.parseHeader(Http11InputBuffer.java:922)
    at org.apache.coyote.http11.Http11InputBuffer.parseHeaders(Http11InputBuffer.java:604)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:294)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:926)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1791)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:750)

If I modify my pact file to be


"request": {
                "generators": {
                    "header": {
                        "se-token": {
                            "expression": "${seToken}",
                            "type": "ProviderState"
                        }
                    },```

Then, everything works as expected, but with the generated pact file it's taking `$['se-token'][0]` as the headers name instead of just `se-token`.

I am using version gradle plugin version 4.1.42 .
rholshausen commented 6 months ago

Are you able to provide the test that generates that Pact file?

rholshausen commented 6 months ago

Moving this to Pact-JS, as the header name is being set incorrectly

mario-martinez-se commented 6 months ago

Are you able to provide the test that generates that Pact file?

Sure, this is the test:

test(
    '200 status for bookings count by userId',
    async () => {
      const expectedBody: any = bookingsCountResponseBody;

      const interaction: V3Interaction = {
        states: [
          {
            description: `A restClient exists`,
          },
          {
            description: `A user exists`,
            parameters: { email: 'contract@mail.com' },
          },
        ],
        uponReceiving: 'a request for bookings count',
        withRequest: {
          method: 'GET',
          headers: {
            'se-api-token': fromProviderState(`\${seApiToken}`, '15123-234234-234asd'),
            'se-token': fromProviderState(`\${seToken}`, 'ABC123'),
          },
          path: fromProviderState(
            `/v4/users/\${userId}/bookings/count`,
            `/v4/users/1234/bookings/count`
          ),
        },
        willRespondWith: {
          body: expectedBody,
          headers: {
            'Content-Type': 'application/json; charset=utf-8',
          },
          status: 200,
        },
      };

      await provider.addInteraction(interaction);

      await provider.executeTest(async mockServer => {
        const service: MockClient = new MockClient(mockServer.url);
        await service.getBookingsCount();
        flashSalesApi['get'] = jest.fn<Promise<any>, [string]>(async () => bookingsCountMock);
        const dataFromAPI: IBookingsCount = await flashSalesApi.getBookingsCount('7777777');
        expect(bookingsCountMock).toEqual(
          (reify(expectedBody) as unknown) as IFlashsalesBookingsCount
        );
        expect(dataFromAPI).toEqual(
          bookingCountFromApi((reify(expectedBody) as unknown) as IFlashsalesBookingsCount)
        );
      });
    },
    timeout
  );

And this is the generated Pact file:

{
  "consumer": {
    "name": "Consumer"
  },
  "interactions": [
    {
      "description": "a request for bookings count",
      "providerStates": [
        {
          "name": "A restClient exists"
        },
        {
          "name": "A user exists",
          "params": {
            "email": "contract@mail.com"
          }
        }
      ],
      "request": {
        "generators": {
          "header": {
            "$['se-api-token'][0]": {
              "expression": "${seApiToken}",
              "type": "ProviderState"
            },
            "$['se-token'][0]": {
              "expression": "${seToken}",
              "type": "ProviderState"
            }
          },
          "path": {
            "expression": "/v4/users/${userId}/bookings/count",
            "type": "ProviderState"
          }
        },
        "headers": {
          "se-api-token": "15123-234234-234asd",
          "se-token": "ABC123"
        },
        "matchingRules": {
          "header": {
            "$['se-api-token'][0]": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$['se-token'][0]": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            }
          },
          "path": {
            "combine": "AND",
            "matchers": [
              {
                "match": "type"
              }
            ]
          }
        },
        "method": "GET",
        "path": "/v4/users/1234/bookings/count"
      },
      "response": {
        "body": {
          "cancelled": 1,
          "past": 2,
          "upcoming": 3
        },
        "headers": {
          "Content-Type": "application/json; charset=utf-8"
        },
        "matchingRules": {
          "body": {
            "$.cancelled": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "integer"
                }
              ]
            },
            "$.past": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "integer"
                }
              ]
            },
            "$.upcoming": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "integer"
                }
              ]
            }
          },
          "header": {}
        },
        "status": 200
      }
    }
  ],
  "metadata": {
    "pact-js": {
      "version": "11.0.2"
    },
    "pactRust": {
      "ffi": "0.4.0",
      "models": "1.0.4"
    },
    "pactSpecification": {
      "version": "3.0.0"
    }
  },
  "provider": {
    "name": "Provider"
  }
}

Also, from package.json:

"@pact-foundation/pact": "^11.0.2",
mefellows commented 5 months ago

This should be fixed in the latest pact-core package. An update of that transitive dependency or fresh install of Pact should pull it in.