diff --git a/pact-tests/application-creation.pact.spec.ts b/pact-tests/application-creation.pact.spec.ts index 5086ff3d3..7c25dfe34 100644 --- a/pact-tests/application-creation.pact.spec.ts +++ b/pact-tests/application-creation.pact.spec.ts @@ -1,12 +1,12 @@ import { pactWith } from 'jest-pact/dist/v3'; import { contract, params } from './contracts/application-service/create-application'; import { mockK8sCreateResource } from './contracts/contracts'; +import { setState, ProviderStates } from './states/states'; pactWith({ consumer: 'HACdev', provider: 'HAS' }, (interaction) => { interaction('App creation', ({ provider, execute }) => { beforeEach(() => { - provider - .given("Application doesn't exist", params) + setState(provider, ProviderStates.appNotExists, params) .uponReceiving('Create an application.') .withRequest(contract.request) .willRespondWith(contract.response); diff --git a/pact-tests/contracts/application-service/create-application.ts b/pact-tests/contracts/application-service/create-application.ts index fa2ec687b..17fb0ff5c 100644 --- a/pact-tests/contracts/application-service/create-application.ts +++ b/pact-tests/contracts/application-service/create-application.ts @@ -2,8 +2,8 @@ import { like, regex } from '@pact-foundation/pact/src/v3/matchers'; import { ApplicationGroupVersionKind, ApplicationModel } from '../../../src/models/application'; import { ApplicationKind } from '../../../src/types'; import { matchers } from '../../matchers'; +import { ApplicationParams } from '../../states/state-params'; import { PactContract, getUrlPath } from '../contracts'; -import { ApplicationParams } from './state-params'; const namespace = 'default'; const app = 'app-to-create'; diff --git a/pact-tests/contracts/application-service/get-application.ts b/pact-tests/contracts/application-service/get-application.ts index 16645c767..2de53baad 100644 --- a/pact-tests/contracts/application-service/get-application.ts +++ b/pact-tests/contracts/application-service/get-application.ts @@ -9,8 +9,8 @@ import { import { ApplicationGroupVersionKind, ApplicationModel } from '../../../src/models/application'; import { ApplicationKind } from '../../../src/types'; import { matchers } from '../../matchers'; +import { ApplicationParams, ComponentsParams } from '../../states/state-params'; import { PactContract, getUrlPath } from '../contracts'; -import { ApplicationParams, ComponentsParams } from './state-params'; export const comp1 = 'gh-component'; diff --git a/pact-tests/get-application.pact.spec.ts b/pact-tests/get-application.pact.spec.ts index afcf8244a..78ee4ad3f 100644 --- a/pact-tests/get-application.pact.spec.ts +++ b/pact-tests/get-application.pact.spec.ts @@ -6,13 +6,13 @@ import { compParams, } from './contracts/application-service/get-application'; import { mockK8sWatchResource } from './contracts/contracts'; +import { ProviderStates, setState } from './states/states'; pactWith({ consumer: 'HACdev', provider: 'HAS' }, (interaction) => { interaction('Getting application', ({ provider, execute }) => { beforeEach(() => { - provider - .given(`Application exists`, appParams) - .given(`Application has components`, compParams) + setState(provider, ProviderStates.appExists, appParams); + setState(provider, ProviderStates.appHasComponent, compParams) .uponReceiving('Get app with its components.') .withRequest(contract.request) .willRespondWith(contract.response); diff --git a/pact-tests/contracts/application-service/state-params.ts b/pact-tests/states/state-params.ts similarity index 100% rename from pact-tests/contracts/application-service/state-params.ts rename to pact-tests/states/state-params.ts diff --git a/pact-tests/states/states.ts b/pact-tests/states/states.ts new file mode 100644 index 000000000..fdb324c2e --- /dev/null +++ b/pact-tests/states/states.ts @@ -0,0 +1,54 @@ +import { inspect } from 'util'; +import { PactV3 } from '@pact-foundation/pact'; +import { JsonMap } from '@pact-foundation/pact/src/common/jsonTypes'; +import { ApplicationParams, ComponentsParams } from './state-params'; + +/** + * This enum contains definitions for all provider states used by hac-dev + */ +export enum ProviderStates { + appExists = 'Application exists', + appNotExists = "Application doesn't exist", + appHasComponent = 'Application has components', +} + +/** + * Mappings for the state definitions and examples of their expected parameters + */ +const stateParams: Record = { + 'Application exists': { appName: 'app', namespace: 'default' } as ApplicationParams, + "Application doesn't exist": { appName: 'app', namespace: 'default' } as ApplicationParams, + 'Application has components': { + components: [{ app: { appName: 'app', namespace: 'default' }, repo: 'url', compName: 'comp' }], + } as ComponentsParams, +}; + +/** + * Declares the use of a provider state for pact tests + * + * @param provider pact provider, comes from pact API + * @param state existing state name from ProviderStates enum + * @param params optional parameters for the state + * @returns provider, to facilitate the fluent pact API + */ +export function setState( + provider: PactV3, + state: ProviderStates, + params?: T, +): PactV3 { + if (!Object.getOwnPropertyNames(stateParams).includes(state)) { + throw new Error(`State "${state}" is not defined in provider states. + \nAvailable states are:\n "${Object.getOwnPropertyNames(stateParams)}"`); + } + if (params) { + const defaultKeys = Object.getOwnPropertyNames(stateParams[state]); + for (const key of defaultKeys) { + if (!Object.getOwnPropertyNames(params).includes(key)) { + throw new Error( + `Invalid state parameters:\n"${inspect(params)}"\n has no property "${key}"`, + ); + } + } + } + return provider.given(state, params); +} diff --git a/pactTests.md b/pactTests.md index 147134924..8b0600afa 100644 --- a/pactTests.md +++ b/pactTests.md @@ -54,7 +54,7 @@ Let's demonstrate that on the `Get application` test. The contract specifies the Now it's the provider's turn to interpret this state. In our example, the provider would have StateHandler defined with the description same as in the consumer and the actual code, that has to be done to fulfill this state. In the code, it can look like this: -``` +```golang pactTypes.StateHandlers{ "App MyApp exists and has component MyComp": createAppAndComponents(myAppNamespace, "MyApp", "MyComp"), } @@ -62,6 +62,42 @@ pactTypes.StateHandlers{ With the state and logic defined, Pact knows what to execute before that particular Pact verification. +### Defining provider states +Pact provides a simple API to define arbitrary states along with any parameters imaginable. However, in order to keep a comprehensive list of all states and parameters in a single place, we have slightly extended this functionality. + +In the `pact-tests/states` folder you will find two files. `state-params.ts` is where we define types/interfaces for different kinds of state parameters, to ensure type safety. + +`states.ts` serves as the single source of truth for provider states of our consumer. As such it contains: + - `ProviderStates` enum: this is the list of state descriptions, when a new state needs to be defined, add it here + - `stateParams` record: this maps entries from `ProviderStates` to their sample parameters. State parameters may be any JSON or `undefined`, but we do prefer them being cast to a more specific type (such as those defined in `state-params.ts`). When adding a new state, make sure to add a sample with parameters here, since it also serves as basic validation of the params at runtime. + - `setState` function: extended version of pact provider state API. We recommend using this function to declare provider states in pact tests, since it will check your state against the existing `ProviderStates` and enforce any new state be added there. + +For example, given we have the following state defined in our `states.ts` file: +```typescript +export enum ProviderStates { + appExists = "Application exists", +} + +const stateParams: Record = { + "Application exists": { appName: 'app', namespace: 'default' } as ApplicationParams, +} +``` + +We can then utilize it in the tests as follows, using the `setState` function: +```typescript +pactWith({ consumer: 'HACdev', provider: 'HAS' }, (interaction) => { + interaction('Getting application', ({ provider, execute }) => { + beforeEach(() => { + setState(provider, ProviderStates.appExists, { appName: 'x', namespace: 'foo' }); + setState(provider, ProviderStates.appExists, { appName: 'y', namespace: 'foo' }) + .uponReceiving('Get app with its components.') + .withRequest(contract.request) + .willRespondWith(contract.response); + }); +``` + +Here we set two states for different applications to exist. Pact is going to combine any declared states together, until it encounters the `withRequest` and `willRespondWith` calls. At that point, the interaction is complete. + ### When does the test run The tables below describe when the tests are running and what is tested/published.