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.63k stars 348 forks source link

Storage full response of provider-side API calls. #836

Closed IngridCampos0502 closed 2 years ago

IngridCampos0502 commented 2 years ago

Checklist

This checklist is optional, but studies show that people who have followed it checklist are really excellent people and we like them

Before making a feature request, I have:

Feature description

Thank you very much for opening these spaces to improve :smiley:

At this moment, it is not possible to obtain the response of the calls to the APIs on the provider side; it is only possible to get a pass or fail result thrown by the following method:

return new Verifier(opts).verifyProvider() .then((res) => { console.log('Pact Verification Complete!: Get identity ', res); }).catch((res) => { console.log('Pact Verification FAIL!: Get identity ', res); });

Use case

What is the use case that motivates this feature request?

At this moment I have several tests that call an API that returns a guid; I need to obtain that guid to be able to eliminate the data created by this API; I must do this elimination when PACT finishes doing all the validations, but PACT is not storing that answer anywhere.

Please describe why you would like Pact-js to have this feature.

If PACT JS implements this solution, it could do the work normally done after executing the tests much more naturally, such as the elimination of information or the dechaining of tasks after completing the tests.

My code on the provider side is following.

describe("Validate pact of identity", () => {

    it("Validate pact of identity creation", () => {
        let opts = {
            afterEach: () => {
                console.log('------> I would want to get the complete response here <---------')
            },

            providerBaseUrl: baseUrl.BASE_URL,
            changeOrigin: true,
            provider: "Create identity",
            logLevel: "TRACE",
            pactUrls: [
                path.resolve(
                    process.cwd(),
                    `./__tests__/contract/pacts/${identity_data.nameConsumerPactFile}-${identity_data.nameProviderPactFile}.json`
                ),
            ],
            requestFilter: async (req, res, next) => {
                req.headers["authorization"] = `Bearer Bearer UQiQPSG-MtD3mNHG0JZT2mqfBh1`,
                    next()
            },
            consumerVersionTags: ["QA"],
            providerVersionTags: ["QA"],
            publishVerificationResult: false,
            providerVersion: "1.0.0"
        }

        return new Verifier(opts).verifyProvider()
            .then((res) => {
                console.log(' ------> Pact Verification Complete!: OR I would want to get the complete response here <---------', res);
            }).catch((res) => {
                console.log('Pact Verification FAIL!: Get identity ', res);
            });
    })

Additional information:

I made the consultation in the slack channel; I attached the conversation for more context:

image

mefellows commented 2 years ago

Thanks for the report!

I believe the issue is that you need a way to clear out specific data after a pact run has happened.

There are a few questions I have:

  1. Why do you need to be able to tear down the data at all? Pact is usually expected to run in an ephemeral environment - i.e. the provider and all of its state is usually run in a unit test like context. So needing to be able to clear out the environment afterwards doesn't make sense. This leads me to believe that you're running against a real environment, which I would strongly recommend against
  2. The request filter feature has access to the full request and response. You could capture any information you needed here and take whatever necessary action is required
  3. We already have hooks before/after tests runs: https://github.com/pact-foundation/pact-js/#before-and-after-hooks
  4. In the new V3 interface, there are also per provider state setup and teardown hooks (see docs).

I could see how 2+3 or 2+4 could be used together to address your use case.

Let me know.

IngridCampos0502 commented 2 years ago

Hi Matt :sunglasses:, thanks for the recommendations, below are my comments:

Why do you need to be able to tear down the data at all? Pact is usually expected to run in an ephemeral environment - i.e. the provider and all of its state is usually run in a unit test like context. So needing to be able to clear out the environment afterwards doesn't make sense. This leads me to believe that you're running against a real environment, which I would strongly recommend against

:point_up: you are absolutely right, we are working on implementing afterwards environments, but at the moment, due to some limitations that we have, it is not possible. For this reason, we are working on the real environment

The request filter feature has access to the full request and response. You could capture any information you needed here and take whatever necessary action is required

:point_up: I also tried this, but the res variable does not return the response of my API call, then I send you the information returned by the res variable

ServerResponse {
      _events: [Object: null prototype] { finish: [Function: bound resOnFinish] },
      _eventsCount: 1,
      _maxListeners: undefined,
      outputData: [],
      outputSize: 0,
      writable: true,
      destroyed: false,
      _last: false,
      chunkedEncoding: false,
      shouldKeepAlive: true,
      maxRequestsOnConnectionReached: false,
      _defaultKeepAlive: true,
      useChunkedEncodingByDefault: true,
      sendDate: true,
      _removedConnection: false,
      _removedContLen: false,
      _removedTE: false,
      _contentLength: null,
      _hasBody: true,
      _trailer: '',
      finished: false,
      _headerSent: false,
      _closed: false,
      socket: <ref *1> Socket {
        connecting: false,
        _hadError: false,
        _parent: null,
        _host: null,
        _readableState: ReadableState {
          objectMode: false,
          highWaterMark: 16384,
          buffer: BufferList { head: null, tail: null, length: 0 },
          length: 0,
          pipes: [],
          flowing: true,
          ended: false,
          endEmitted: false,
          reading: true,
          constructed: true,
          sync: false,
          needReadable: true,
          emittedReadable: false,
          readableListening: false,
          resumeScheduled: false,
          errorEmitted: false,
          emitClose: false,
          autoDestroy: true,
          destroyed: false,
          errored: null,
          closed: false,
          closeEmitted: false,
          defaultEncoding: 'utf8',
          awaitDrainWriters: null,
          multiAwaitDrain: false,
          readingMore: false,
          dataEmitted: false,
          decoder: null,
          encoding: null,
          [Symbol(kPaused)]: false
        },
        _events: [Object: null prototype] {
          end: [Array],
          timeout: [Function: socketOnTimeout],
          data: [Function: bound socketOnData],
          error: [Function: socketOnError],
          close: [Array],
          drain: [Function: bound socketOnDrain],
          resume: [Function: onSocketResume],
          pause: [Function: onSocketPause]
        },
        _eventsCount: 8,
        _maxListeners: undefined,
        _writableState: WritableState {
          objectMode: false,
          highWaterMark: 16384,
          finalCalled: false,
          needDrain: false,
          ending: false,
          ended: false,
          finished: false,
          destroyed: false,
          decodeStrings: false,
          defaultEncoding: 'utf8',
          length: 0,
          writing: false,
          corked: 0,
          sync: false,
          bufferProcessing: false,
          onwrite: [Function: bound onwrite],
          writecb: null,
          writelen: 0,
          afterWriteTickInfo: null,
          buffered: [],
          bufferedIndex: 0,
          allBuffers: true,
          allNoop: true,
          pendingcb: 0,
          constructed: true,
          prefinished: false,
          errorEmitted: false,
          emitClose: false,
          autoDestroy: true,
          errored: null,
          closed: false,
          closeEmitted: false,
          [Symbol(kOnFinished)]: []
        },
        allowHalfOpen: true,
        _sockname: null,
        _pendingData: null,
        _pendingEncoding: '',
        server: Server {
          maxHeaderSize: undefined,
          insecureHTTPParser: undefined,
          _events: [Object: null prototype],
          _eventsCount: 4,
          _maxListeners: undefined,
          _connections: 1,
          _handle: [TCP],
          _usingWorkers: false,
          _workers: [],
          _unref: false,
          allowHalfOpen: true,
          pauseOnConnect: false,
          httpAllowHalfOpen: false,
          timeout: 0,
          keepAliveTimeout: 5000,
          maxHeadersCount: null,
          maxRequestsPerSocket: 0,
          headersTimeout: 60000,
          requestTimeout: 0,
          _connectionKey: '6::::0',
          [Symbol(IncomingMessage)]: [Function: IncomingMessage],
          [Symbol(ServerResponse)]: [Function: ServerResponse],
          [Symbol(kCapture)]: false,
          [Symbol(async_id_symbol)]: 234
        },
        _server: Server {
          maxHeaderSize: undefined,
          insecureHTTPParser: undefined,
          _events: [Object: null prototype],
          _eventsCount: 4,
          _maxListeners: undefined,
          _connections: 1,
          _handle: [TCP],
          _usingWorkers: false,
          _workers: [],
          _unref: false,
          allowHalfOpen: true,
          pauseOnConnect: false,
          httpAllowHalfOpen: false,
          timeout: 0,
          keepAliveTimeout: 5000,
          maxHeadersCount: null,
          maxRequestsPerSocket: 0,
          headersTimeout: 60000,
          requestTimeout: 0,
          _connectionKey: '6::::0',
          [Symbol(IncomingMessage)]: [Function: IncomingMessage],
          [Symbol(ServerResponse)]: [Function: ServerResponse],
          [Symbol(kCapture)]: false,
          [Symbol(async_id_symbol)]: 234
        },
        parser: HTTPParser {
          '0': [Function: bound setRequestTimeout],
          '1': [Function: parserOnHeaders],
          '2': [Function: parserOnHeadersComplete],
          '3': [Function: parserOnBody],
          '4': [Function: parserOnMessageComplete],
          '5': [Function: bound onParserExecute],
          '6': [Function: bound onParserTimeout],
          _headers: [],
          _url: '',
          socket: [Circular *1],
          incoming: [IncomingMessage],
          outgoing: null,
          maxHeaderPairs: 2000,
          _consumed: true,
          onIncoming: [Function: bound parserOnIncoming],
          [Symbol(owner_symbol)]: [HTTPServerAsyncResource]
        },
        on: [Function: socketListenerWrap],
        addListener: [Function: socketListenerWrap],
        prependListener: [Function: socketListenerWrap],
        setEncoding: [Function: socketSetEncoding],
        _paused: false,
        _httpMessage: [Circular *2],
        timeout: 0,
        [Symbol(async_id_symbol)]: 452,
        [Symbol(kHandle)]: TCP {
          reading: true,
          onconnection: null,
          _consumed: true,
          [Symbol(owner_symbol)]: [Circular *1]
        },
        [Symbol(kSetNoDelay)]: false,
        [Symbol(lastWriteQueueSize)]: 0,
        [Symbol(timeout)]: Timeout {
          _idleTimeout: -1,
          _idlePrev: null,
          _idleNext: null,
          _idleStart: 6828,
          _onTimeout: null,
          _timerArgs: undefined,
          _repeat: null,
          _destroyed: true,
          [Symbol(refed)]: false,
          [Symbol(kHasPrimitive)]: false,
          [Symbol(asyncId)]: 481,
          [Symbol(triggerId)]: 479
        },
        [Symbol(kBuffer)]: null,
        [Symbol(kBufferCb)]: null,
        [Symbol(kBufferGen)]: null,
        [Symbol(kCapture)]: false,
        [Symbol(kBytesRead)]: 0,
        [Symbol(kBytesWritten)]: 0,
        [Symbol(RequestTimeout)]: undefined
      },
      _header: null,
      _keepAliveTimeout: 5000,
      _onPendingData: [Function: bound updateOutgoingData],
      req: IncomingMessage {
        _readableState: ReadableState {
          objectMode: false,
          highWaterMark: 16384,
          buffer: BufferList { head: null, tail: null, length: 0 },
          length: 0,
          pipes: [],
          flowing: null,
          ended: false,
          endEmitted: false,
          reading: false,
          constructed: true,
          sync: true,
          needReadable: false,
          emittedReadable: false,
          readableListening: false,
          resumeScheduled: false,
          errorEmitted: false,
          emitClose: true,
          autoDestroy: true,
          destroyed: false,
          errored: null,
          closed: false,
          closeEmitted: false,
          defaultEncoding: 'utf8',
          awaitDrainWriters: null,
          multiAwaitDrain: false,
          readingMore: true,
          dataEmitted: false,
          decoder: null,
          encoding: null,
          [Symbol(kPaused)]: null
        },
        _events: [Object: null prototype] { end: [Function: clearRequestTimeout] },
        _eventsCount: 1,
        _maxListeners: undefined,
        socket: <ref *1> Socket {
          connecting: false,
          _hadError: false,
          _parent: null,
          _host: null,
          _readableState: [ReadableState],
          _events: [Object: null prototype],
          _eventsCount: 8,
          _maxListeners: undefined,
          _writableState: [WritableState],
          allowHalfOpen: true,
          _sockname: null,
          _pendingData: null,
          _pendingEncoding: '',
          server: [Server],
          _server: [Server],
          parser: [HTTPParser],
          on: [Function: socketListenerWrap],
          addListener: [Function: socketListenerWrap],
          prependListener: [Function: socketListenerWrap],
          setEncoding: [Function: socketSetEncoding],
          _paused: false,
          _httpMessage: [Circular *2],
          timeout: 0,
          [Symbol(async_id_symbol)]: 452,
          [Symbol(kHandle)]: [TCP],
          [Symbol(kSetNoDelay)]: false,
          [Symbol(lastWriteQueueSize)]: 0,
          [Symbol(timeout)]: Timeout {
            _idleTimeout: -1,
            _idlePrev: null,
            _idleNext: null,
            _idleStart: 6828,
            _onTimeout: null,
            _timerArgs: undefined,
            _repeat: null,
            _destroyed: true,
            [Symbol(refed)]: false,
            [Symbol(kHasPrimitive)]: false,
            [Symbol(asyncId)]: 481,
            [Symbol(triggerId)]: 479
          },
          [Symbol(kBuffer)]: null,
          [Symbol(kBufferCb)]: null,
          [Symbol(kBufferGen)]: null,
          [Symbol(kCapture)]: false,
          [Symbol(kBytesRead)]: 0,
          [Symbol(kBytesWritten)]: 0,
          [Symbol(RequestTimeout)]: undefined
        },
        httpVersionMajor: 1,
        httpVersionMinor: 1,
        httpVersion: '1.1',
        complete: false,
        rawHeaders: [
          'authorization',
          'Bearer UQiQPSG-MtD3mNHG0JZT2mqfBh1',
          'content-type',
          'application/json',
          'accept',
          '*/*',
          'accept-encoding',
          'gzip, deflate',
          'host',
          'localhost:50258'
        ],
        rawTrailers: [],
        aborted: false,
        upgrade: false,
        url: '/transfers/alias-directory-service/clients/8e0e6d94-a0bf-4c67-a525-c7627424f4f2/identities/7d7de180-0624-40d4-8407-929cd89550d2',
        method: 'GET',
        statusCode: null,
        statusMessage: null,
        client: <ref *1> Socket {
          connecting: false,
          _hadError: false,
          _parent: null,
          _host: null,
          _readableState: [ReadableState],
          _events: [Object: null prototype],
          _eventsCount: 8,
          _maxListeners: undefined,
          _writableState: [WritableState],
          allowHalfOpen: true,
          _sockname: null,
          _pendingData: null,
          _pendingEncoding: '',
          server: [Server],
          _server: [Server],
          parser: [HTTPParser],
          on: [Function: socketListenerWrap],
          addListener: [Function: socketListenerWrap],
          prependListener: [Function: socketListenerWrap],
          setEncoding: [Function: socketSetEncoding],
          _paused: false,
          _httpMessage: [Circular *2],
          timeout: 0,
          [Symbol(async_id_symbol)]: 452,
          [Symbol(kHandle)]: [TCP],
          [Symbol(kSetNoDelay)]: false,
          [Symbol(lastWriteQueueSize)]: 0,
          [Symbol(timeout)]: Timeout {
            _idleTimeout: -1,
            _idlePrev: null,
            _idleNext: null,
            _idleStart: 6828,
            _onTimeout: null,
            _timerArgs: undefined,
            _repeat: null,
            _destroyed: true,
            [Symbol(refed)]: false,
            [Symbol(kHasPrimitive)]: false,
            [Symbol(asyncId)]: 481,
            [Symbol(triggerId)]: 479
          },
          [Symbol(kBuffer)]: null,
          [Symbol(kBufferCb)]: null,
          [Symbol(kBufferGen)]: null,
          [Symbol(kCapture)]: false,
          [Symbol(kBytesRead)]: 0,
          [Symbol(kBytesWritten)]: 0,
          [Symbol(RequestTimeout)]: undefined
        },
        _consuming: false,
        _dumped: false,
        next: [Function: next],
        baseUrl: '',
        originalUrl: '/transfers/alias-directory-service/clients/8e0e6d94-a0bf-4c67-a525-c7627424f4f2/identities/7d7de180-0624-40d4-8407-929cd89550d2',
        _parsedUrl: Url {
          protocol: null,
          slashes: null,
          auth: null,
          host: null,
          port: null,
          hostname: null,
          hash: null,
          search: null,
          query: null,
          pathname: '/transfers/alias-directory-service/clients/8e0e6d94-a0bf-4c67-a525-c7627424f4f2/identities/7d7de180-0624-40d4-8407-929cd89550d2',
          path: '/transfers/alias-directory-service/clients/8e0e6d94-a0bf-4c67-a525-c7627424f4f2/identities/7d7de180-0624-40d4-8407-929cd89550d2',
          href: '/transfers/alias-directory-service/clients/8e0e6d94-a0bf-4c67-a525-c7627424f4f2/identities/7d7de180-0624-40d4-8407-929cd89550d2',
          _raw: '/transfers/alias-directory-service/clients/8e0e6d94-a0bf-4c67-a525-c7627424f4f2/identities/7d7de180-0624-40d4-8407-929cd89550d2'
        },
        params: {},
        query: {},
        res: [Circular *2],
        [Symbol(kCapture)]: false,
        [Symbol(kHeaders)]: {
          authorization: 'Bearer UQiQPSG-MtD3mNHG0JZT2mqfBh1',
          'content-type': 'application/json',
          accept: '*/*',
          'accept-encoding': 'gzip, deflate',
          host: 'localhost:50258'
        },
        [Symbol(kHeadersCount)]: 10,
        [Symbol(kTrailers)]: null,
        [Symbol(kTrailersCount)]: 0,
        [Symbol(RequestTimeout)]: undefined
      },
      _sent100: false,
      _expect_continue: false,
      locals: [Object: null prototype] {},
      [Symbol(kCapture)]: false,
      [Symbol(kNeedDrain)]: false,
      [Symbol(corked)]: 0,
      [Symbol(kOutHeaders)]: [Object: null prototype] {
        'x-powered-by': [ 'X-Powered-By', 'Express' ]
      }
    }

this is the response that I am waiting

compare_maps: Comparing maps at $: {"alias": String("51788704902"), "aliasType": String("PHONE"), "consentDate": String("2019-05-17T08:02:02.000Z"), "countryCode": String("PER"), "dob": String("2019-05-17T08:02:02.000Z"), "email": String("ssuarez@gmail.com"), "firstName": String("Johann Sebastian"), "homeAddress": Object({"city": String("Lima"), "isoCountry3Char": String("PER"), "line1": String("calle 143 a # 128 - 81"), "line2": String("Dg. 61c #26-36, Bogotá"), "line3": String("Ak. 7 ##40b-53, Bogotá"), "postalCode": String("1111121")}), "identificationNumber": String("1022989871"), "identificationType": String("PASSPORT"), "identityId": String("7d7de180-0624-40d4-8407-929cd89550d2"), "lastName": String("Suarez"), "permanentAlias": String("1234567890"), "permanentAliasType": String("PHONE"), "qr": Null} -> {"alias": String("51788704902"), "aliasType": String("PHONE"), "consentDate": String("2019-05-17T08:02:02.000Z"), "countryCode": String("PER"), "dob": String("2019-05-17T08:02:02.000Z"), "email": String("ssuarez@gmail.com"), "firstName": String("Johann Sebastian"), "homeAddress": Object({"city": String("Lima"), "isoCountry3Char": String("PER"), "line1": String("calle 143 a # 128 - 81"), "line2": String("Dg. 61c #26-36, Bogotá"), "line3": String("Ak. 7 ##40b-53, Bogotá"), "postalCode": String("1111121")}), "identificationNumber": String("1022989871"), "identificationType": String("PASSPORT"), "identityId": String("7d7de180-0624-40d4-8407-929cd89550d2"), "lastName": String("Suarez"), "permanentAlias": String("1234567890"), "permanentAliasType": String("PHONE"), "qr": Null}

this is my code of the provider side

describe("Validate pact of identity", () => {

    it("Validate pact of identity creation", () => {
        let opts = {
            afterEach: () => {
                console.log('------> I would want to get the complete response here <---------')
            },

            providerBaseUrl: baseUrl.BASE_URL,
            changeOrigin: true,
            provider: "Create identity",
            logLevel: "TRACE",
            pactUrls: [
                path.resolve(
                    process.cwd(),
                    `./__tests__/contract/pacts/${identity_data.nameConsumerPactFile}-${identity_data.nameProviderPactFile}.json`
                ),
            ],
            requestFilter: async (req, res, next) => {
                console.log("the response is ::::: ", res)
                req.headers["authorization"] = `Bearer ${await postRequestTokenNonCDE(credentials.bbva_client)}`,
                    next()
            },
            consumerVersionTags: ["QA"],
            providerVersionTags: ["QA"],
            publishVerificationResult: false,
            providerVersion: "1.0.0"
        }

        return new Verifier(opts).verifyProvider()
            .then((res) => {
                console.log(' ------> Pact Verification Complete!: OR I would want to get the complete response here <---------', res);
            }).catch((res) => {
                console.log('Pact Verification FAIL!: Get identity ', res);
            });
    })

})

For additional information, I attach the complete log

logProviderSide.txt

We already have hooks before/after tests runs: https://github.com/pact-foundation/pact-js/#before-and-after-hooks

:point_up: yes, I plan to use these hooks when I get the response from the API

In the new V3 interface, there are also per provider state setup and teardown hooks (see docs).

:point_up: Oh cool :smile:, that might work too, but I need the API response :sleepy:

mefellows commented 2 years ago

The response object here is an instance of the standard NodeJS ServerResponse. So the body isn't just available as an object, you need to read it from the event stream.

Here is an example of how to extract a JSON body from it (note you should consider how to use this for your use cases, especially if you're expecting bigger payloads/non text formats):

const extractResponse = (res) => {
  return new Promise((resolve, reject) => {
    const [oldWrite, oldEnd] = [res.write, res.end];
    const chunks = [];

    res.write = (chunk) => {
      chunks.push(Buffer.from(chunk));
      return oldWrite.apply(res, [chunk]);
    };

    res.end = (chunk) => {
      if (chunk) {
        chunks.push(Buffer.from(chunk));
      }
      const body = Buffer.concat(chunks).toString('utf8');
      oldEnd.apply(res, [chunk]);

      resolve(body);
    };
  });
};

Here is an example of how you can use it in a requestFilter:

...
      requestFilter: async (req, res, next) => {
        // Read the body on requests you're interested in...
        const bodyPromise = extractResponse(res, next);

        // Invoke the next middleware, to ensure it passes to the
        // actual provider
        next();

        // Wait for the data to be read in from the event stream
        const lastBody = await bodyPromise

        // do something with body
        console.log(lastBody)
      }

I'm reluctant to add this sort of functionality to the framework itself for the following reasons:

  1. This is not just an edge case but actively working against how Pact should best be used
  2. I can't see it being immediately useful in other circumstances, therefore making the contribution not widely useful vs any additional long term maintenance obligation
  3. It's fairly easy to do without support by the framework itself
IngridCampos0502 commented 2 years ago

mefellows Thank you very much, this works for me :sunglasses: :ok_hand:

mefellows commented 2 years ago

Great! I'll be honest.. this post did lead to me pushing up a new branch that might improve this situation. But I won't make any promises just yet.

Glad to hear you're back on track!