Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: list provider states in a separate place #829

Merged
merged 1 commit into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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