Skip to content

Commit

Permalink
Merge pull request #829 from jrichter1/pact-states
Browse files Browse the repository at this point in the history
refactor: list provider states in a separate place
  • Loading branch information
openshift-ci[bot] authored Oct 9, 2023
2 parents 256ea5d + 5d4ecb2 commit d2b4bd1
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 8 deletions.
4 changes: 2 additions & 2 deletions pact-tests/application-creation.pact.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
6 changes: 3 additions & 3 deletions pact-tests/get-application.pact.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
File renamed without changes.
54 changes: 54 additions & 0 deletions pact-tests/states/states.ts
Original file line number Diff line number Diff line change
@@ -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<ProviderStates, JsonMap | undefined> = {
'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<T extends JsonMap>(
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);
}
38 changes: 37 additions & 1 deletion pactTests.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,50 @@ 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"),
}
```

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<ProviderStates, JsonMap | undefined> = {
"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.
Expand Down

0 comments on commit d2b4bd1

Please sign in to comment.