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.59k stars 343 forks source link

Two StateHandler types defined #1057

Closed paul-kuihi closed 1 year ago

paul-kuihi commented 1 year ago

Software versions

Issue Checklist

Please confirm the following:

I raised this in the pact-js slack channel and was asked to raise this issue.

Expected behaviour

Creating StateHandlers as per the documentation results in no Typing errors from TS.

Actual behaviour

When attempting to use the setup/teardown properties on the provider state handlers Typescript complains that it doesn't match the type it's expecting. TS is finding two types with the same name and merging them (StateHandlers I believe). It gives the error:

(property) 'my state': {
    setup: () => Promise<void>;
    teardown: () => Promise<void>;
}
Type '{ setup: () => Promise<void>; teardown: () => Promise<void>; }' is not assignable to type 'StateHandler & ((state: string, params?: { [name: string]: string; } | undefined) => Promise<unknown>)'.
  Type '{ setup: () => Promise<void>; teardown: () => Promise<void>; }' is not assignable to type 'StateFuncWithSetup & ((state: string, params?: { [name: string]: string; } | undefined) => Promise<unknown>)'.
    Type '{ setup: () => Promise<void>; teardown: () => Promise<void>; }' is not assignable to type '(state: string, params?: { [name: string]: string; } | undefined) => Promise<unknown>'.
      Type '{ setup: () => Promise<void>; teardown: () => Promise<void>; }' provides no match for the signature '(state: string, params?: { [name: string]: string; } | undefined): Promise<unknown>'.ts(2322)

Steps to reproduce

Create a state handler definition such as:

'Whatever your state name is': {
    setup: (parameters) => {
      // do your setup here
      // return a promise if you need to
    },
    teardown: (parameters) => {
      // do your teardown here
      // return a promise if you need to
    },
  },

TS will complain as above.

As a workaround, we've created a shim type. An alternative as mentioned by @TimothyJones in the slack thread is to define your own StateHandler type until this is fixed up.

Relevant log files

N/A

TimothyJones commented 1 year ago

I raised a PR that fixes this.

iamchughmayank commented 1 year ago

Hi there 👋

I was facing the same problem and stumbled upon this issue. I followed the slack thread linked in the issue and tried to workaround this but I still faced some issues.

While the PR is under review, I was hoping to get some help with the suggested workaround:

type ShimmedVerifierOptions = Omit<VerifierOptions, 'stateHandlers'> & {
    stateHandlers: StateHandlers;
  };

let baseOptions: ShimmedVerifierOptions;

        baseOptions = {
...
            stateHandlers: {
                'my consumer state': {
                    setup: async () => {
                        console.log('setup');
                    }
                }
            }
        };

Error:

Type '{ setup: () => Promise<void>; }' is not assignable to type '(state: string, params?: { [name: string]: string; }) => Promise<unknown>'.
  Object literal may only specify known properties, and 'setup' does not exist in type '(state: string, params?: { [name: string]: string; }) => Promise<unknown>'.ts(2322)

TIA 💙

TimothyJones commented 1 year ago

I don't think that workaround would work unless you defined your own StateHandler type and didn't import Pact's one (which is what I think the OP did).

The workaround I suggested was in the thread was:

Define your state handlers explicitly as one of the types it accepts (so typescript doesn’t try to figure it out)

const myConsumerState: StateFuncWithSetup = {
  setup: async (params) => {
    console.log(params);
  },
};

const baseOptions: ShimmedVerifierOptions = {
...
            stateHandlers: {
                'my consumer state': myConsumerState,
            },
        };

PS: The PR is #1062. Sorry I forgot to link it before.

iamchughmayank commented 1 year ago

Thanks, @TimothyJones :) I am quite new to TypeScript and I think I will take some time to learn a lot of concepts here (probably the PR will merge in before that 😓 )

If I understand correctly, OP did not use import { StateHandlers } from '@pact-foundation/pact';

However, while checking out the StatefuncWithSetup, I also saw that StateHandlers are exposed from the same place. And, I tried doing:

import { StateHandlers } from '@pact-foundation/pact/src/dsl/verifier/proxy/types';

Then, I "shimmy-ed" the VerifierOptions like so:

import { Verifier, LogLevel, VerifierOptions } from '@pact-foundation/pact';
import { StateHandlers } from '@pact-foundation/pact/src/dsl/verifier/proxy/types';

type ShimmedVerifierOptions = Omit<VerifierOptions, 'stateHandlers'> & {
    stateHandlers: StateHandlers;
  };

Once, I did that, the type compatibility issues did not occur:

cones baseOptions: ShimmedVerifierOptions = {
            stateHandlers: {
                'fetch all blogs for site': {
                    setup: async () => {
                        const siteId = await funcToGetSiteId();
                        return Promise.resolve({site: siteId});
                    }
                  }
            }
}

However, with this implementation, I am getting a 500 error in pact setup:


INFO (30342): pact@10.4.1: Verifying provider
INFO (30342): pact@10.4.1: debug request/response logging enabled
INFO (30342): pact-core@13.13.4: Verifying Pacts.
INFO (30342): pact-core@13.13.4: Verifying Pact Files
DEBUG (30342): pact-core@13.13.4: Initalising native core at log level 'debug'
2023-02-28T13:25:51.035753Z DEBUG ThreadId(02) verify_interaction{interaction="a request to get blog by siteId"}: hyper::client::pool: pooling idle connection for ("http", 127.0.0.1:57203)
DEBUG (30342): pact@10.4.1: incoming request: {"body":{"action":"setup","params":{},"state":"fetch all blogs for site"},"headers":{"content-type":"application/json","accept":"*/*","accept-encoding":"gzip, deflate","host":"127.0.0.1:57203","content-length":"65"},"method":"POST","path":"/_pactSetup"}
DEBUG (30342): pact@10.4.1: setting up state '{"action":"setup","params":{},"state":"fetch all blogs for site"}'
DEBUG (30342): pact@10.4.1: setting up state 'fetch all blogs for site'
DEBUG (30342): pact@10.4.1: outgoing response: {"body":"{}","headers":{"x-powered-by":"Express","content-type":"application/json; charset=utf-8","content-length":"2","etag":"W/\"2-vyGp6PvFo4RvsFtPoIWeCReyIC8\""},"status":500}
DEBUG (30342): pact@10.4.1: incoming request: {"body":{"action":"setup","params":{},"state":"fetch all blogs for site"},"headers":{"content-type":"application/json","accept":"*/*","accept-encoding":"gzip, deflate","host":"127.0.0.1:57203","content-length":"65"},"method":"POST","path":"/_pactSetup"}
DEBUG (30342): pact@10.4.1: setting up state '{"action":"setup","params":{},"state":"fetch all blogs for site"}'
DEBUG (30342): pact@10.4.1: setting up state 'fetch all blogs for site'
DEBUG (30342): pact@10.4.1: outgoing response: {"body":"{}","headers":{"x-powered-by":"Express","content-type":"application/json; charset=utf-8","content-length":"2","etag":"W/\"2
 RUNS  test/contract/my-spec.ts
DEBUG ThreadId(02) verify_interaction{interaction="a request to get blog by siteId"}: hyper::client::pool: reuse idle connection for ("http", 127.0.0.1:57203)
DEBUG tokio-runtime-worker hyper::proto::h1::io: flushed 214 bytes
DEBUG tokio-runtime-worker hyper::proto::h1::io: parsed 7 headers
DEBUG tokio-runtime-worker hyper::proto::h1::conn: incoming body is content-length (2 bytes)
DEBUG tokio-runtime-worker hyper::proto::h1::conn: incoming body completed
DEBUG ThreadId(02) verify_interaction{interaction="a request to get blog by siteId"}: hyper::client::pool: pooling idle connection for ("http", 127.0.0.1:57203)
DEBUG ThreadId(02) verify_interaction{interaction="a request to get blog by siteId"}: pact_verifier::provider_client: State change request: Response { url: Url { scheme: "http", cannot_be_a_base: false, username: "", password: None, host: Some(Ipv4(127.0.0.1)), port: Some(57203), path: "/_pactSetup", query: None, fragment: None }, status: 500, headers: {"x-powered-by": "Express", "content-type": "application/json; charset=utf-8", "content-length": "2", "etag": "W/\"2-vyGp6PvFo4RvsFtPoIWeCReyIC8\"", "date": "Tue, 28 Feb 2023 13:25:52 GMT", "connection": "keep-alive", "keep-alive": "timeout=5"} }
DEBUG ThreadId(02) verify_interaction{interaction="a request to get blog by siteId"}: pact_verifier: State Change: "ProviderState { name: "fetch all blogs for site", params: {} }" -> Err(Provider state failed: Invalid status code: 500)
ERROR ThreadId(02) verify_interaction{interaction="a request to get blog by siteId"}: pact_verifier: Provider setup state change for 'fetch all blogs for site' has failed - MismatchResult::Error("Invalid status code: 500", None)