Skip to content

Commit b238fd0

Browse files
authored
fix: allow hooks to run only once per interaction (#1243)
Current behaviour will run the beforeEach and afterEach hooks multiple times if there are several provider states defined in an interaction. This is due to the way the hook are run on every call to "/_pactSetup" via the proxy. This change will ensure each of those hooks is run only once per interaction, regardless of how many provider states are defined in that interaction.
1 parent 7bf180f commit b238fd0

File tree

3 files changed

+179
-27
lines changed

3 files changed

+179
-27
lines changed

src/dsl/verifier/proxy/hooks.spec.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { expect } from 'chai';
2+
import { stub } from 'sinon';
3+
import { RequestHandler } from 'express';
4+
5+
import {
6+
registerHookStateTracking,
7+
registerBeforeHook,
8+
registerAfterHook,
9+
HooksState,
10+
} from './hooks';
11+
12+
// This mimics the proxy setup (src/dsl/verifier/proxy/proxy.ts), whereby the
13+
// state handling middleware is run regardless of whether a hook is registered
14+
// or not.
15+
const doRequest = async (
16+
action: string,
17+
hooksState: HooksState,
18+
hookHandler?: RequestHandler
19+
) => {
20+
const hooksStateHandler = registerHookStateTracking(hooksState);
21+
const hookRequestHandler = hookHandler || ((req, res, next) => next());
22+
23+
const request: any = {
24+
body: {
25+
action,
26+
},
27+
};
28+
29+
return new Promise((resolve) => {
30+
hooksStateHandler(request, null as any, () => {
31+
hookRequestHandler(request, null as any, resolve);
32+
});
33+
});
34+
};
35+
36+
describe('Verifier', () => {
37+
describe('#registerBeforeHook', () => {
38+
describe('when the state setup routine is called multiple times before the next teardown', () => {
39+
it('it executes the beforeEach hook only once', async () => {
40+
const hooksState: HooksState = { setupCounter: 0 };
41+
const hook = stub().resolves();
42+
const hookHandler = registerBeforeHook(hook, hooksState);
43+
44+
await doRequest('setup', hooksState, hookHandler);
45+
await doRequest('setup', hooksState, hookHandler);
46+
await doRequest('teardown', hooksState);
47+
await doRequest('teardown', hooksState);
48+
49+
expect(hook).to.be.calledOnce;
50+
});
51+
});
52+
});
53+
54+
describe('#registerAfterHook', () => {
55+
describe('when the state teardown routine is called multiple times before the next setup', () => {
56+
it('it executes the afterEach hook only once', async () => {
57+
const hooksState: HooksState = { setupCounter: 0 };
58+
const hook = stub().resolves();
59+
const hookHandler = registerAfterHook(hook, hooksState);
60+
61+
await doRequest('setup', hooksState);
62+
await doRequest('setup', hooksState);
63+
await doRequest('teardown', hooksState, hookHandler);
64+
await doRequest('teardown', hooksState, hookHandler);
65+
66+
expect(hook).to.be.calledOnce;
67+
});
68+
});
69+
});
70+
71+
describe('#registerBeforeHook and #registerAfterHook', () => {
72+
describe('when the state teardown routine is called multiple times before the next setup', () => {
73+
it('it executes the beforeEach and afterEach hooks only once', async () => {
74+
const hooksState: HooksState = { setupCounter: 0 };
75+
const beforeHook = stub().resolves();
76+
const afterHook = stub().resolves();
77+
const beforeHookHandler = registerBeforeHook(beforeHook, hooksState);
78+
const afterHookHandler = registerAfterHook(afterHook, hooksState);
79+
80+
await doRequest('setup', hooksState, beforeHookHandler);
81+
await doRequest('setup', hooksState, beforeHookHandler);
82+
await doRequest('teardown', hooksState, afterHookHandler);
83+
await doRequest('teardown', hooksState, afterHookHandler);
84+
85+
expect(beforeHook).to.be.calledOnce;
86+
expect(afterHook).to.be.calledOnce;
87+
});
88+
});
89+
90+
describe('when multiple interactions are executed', () => {
91+
it('it executes the beforeEach and afterEach hooks once for each interaction', async () => {
92+
const hooksState: HooksState = { setupCounter: 0 };
93+
const beforeHook = stub().resolves();
94+
const afterHook = stub().resolves();
95+
const beforeHookHandler = registerBeforeHook(beforeHook, hooksState);
96+
const afterHookHandler = registerAfterHook(afterHook, hooksState);
97+
98+
// Interaction 1 (two "given" states)
99+
await doRequest('setup', hooksState, beforeHookHandler);
100+
await doRequest('setup', hooksState, beforeHookHandler);
101+
await doRequest('teardown', hooksState, afterHookHandler);
102+
await doRequest('teardown', hooksState, afterHookHandler);
103+
104+
// Interaction 2 (one "given" state)
105+
await doRequest('setup', hooksState, beforeHookHandler);
106+
await doRequest('teardown', hooksState, afterHookHandler);
107+
108+
// Interaction 3 (three "given" states)
109+
await doRequest('setup', hooksState, beforeHookHandler);
110+
await doRequest('setup', hooksState, beforeHookHandler);
111+
await doRequest('setup', hooksState, beforeHookHandler);
112+
await doRequest('teardown', hooksState, afterHookHandler);
113+
await doRequest('teardown', hooksState, afterHookHandler);
114+
await doRequest('teardown', hooksState, afterHookHandler);
115+
116+
expect(beforeHook).to.be.calledThrice;
117+
expect(afterHook).to.be.calledThrice;
118+
});
119+
});
120+
});
121+
});

src/dsl/verifier/proxy/hooks.ts

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,39 @@
1-
import express from 'express';
1+
/* eslint-disable no-param-reassign */
2+
/**
3+
* These handlers assume that the number of "setup" and "teardown" requests to
4+
* `/_pactSetup` are always sequential and balanced, i.e. if 3 "setup" actions
5+
* are received prior to an interaction being executed, then 3 "teardown"
6+
* actions will be received after that interaction has ended.
7+
*/
8+
import { RequestHandler } from 'express';
29

310
import logger from '../../../common/logger';
4-
import { ProxyOptions } from './types';
11+
import { Hook } from './types';
512

6-
export const registerBeforeHook = (
7-
app: express.Express,
8-
config: ProxyOptions,
9-
stateSetupPath: string
10-
): void => {
11-
if (config.beforeEach) logger.trace("registered 'beforeEach' hook");
12-
app.use(async (req, res, next) => {
13-
if (req.path === stateSetupPath && config.beforeEach) {
13+
export type HooksState = {
14+
setupCounter: number;
15+
};
16+
17+
export const registerHookStateTracking =
18+
(hooksState: HooksState): RequestHandler =>
19+
async ({ body }, res, next) => {
20+
if (body?.action === 'setup') hooksState.setupCounter += 1;
21+
if (body?.action === 'teardown') hooksState.setupCounter -= 1;
22+
23+
logger.debug(
24+
`hooks state counter is ${hooksState.setupCounter} after receiving "${body?.action}" action`
25+
);
26+
27+
next();
28+
};
29+
30+
export const registerBeforeHook =
31+
(beforeEach: Hook, hooksState: HooksState): RequestHandler =>
32+
async ({ body }, res, next) => {
33+
if (body?.action === 'setup' && hooksState.setupCounter === 1) {
1434
logger.debug("executing 'beforeEach' hook");
1535
try {
16-
await config.beforeEach();
36+
await beforeEach();
1737
next();
1838
} catch (e) {
1939
logger.error(`error executing 'beforeEach' hook: ${e.message}`);
@@ -23,20 +43,15 @@ export const registerBeforeHook = (
2343
} else {
2444
next();
2545
}
26-
});
27-
};
46+
};
2847

29-
export const registerAfterHook = (
30-
app: express.Express,
31-
config: ProxyOptions,
32-
stateSetupPath: string
33-
): void => {
34-
if (config.afterEach) logger.trace("registered 'afterEach' hook");
35-
app.use(async (req, res, next) => {
36-
if (req.path !== stateSetupPath && config.afterEach) {
48+
export const registerAfterHook =
49+
(afterEach: Hook, hooksState: HooksState): RequestHandler =>
50+
async ({ body }, res, next) => {
51+
if (body?.action === 'teardown' && hooksState.setupCounter === 0) {
3752
logger.debug("executing 'afterEach' hook");
3853
try {
39-
await config.afterEach();
54+
await afterEach();
4055
next();
4156
} catch (e) {
4257
logger.error(`error executing 'afterEach' hook: ${e.message}`);
@@ -46,5 +61,4 @@ export const registerAfterHook = (
4661
} else {
4762
next();
4863
}
49-
});
50-
};
64+
};

src/dsl/verifier/proxy/proxy.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import * as http from 'http';
66
import { ProxyOptions } from './types';
77
import logger from '../../../common/logger';
88
import { createProxyStateHandler } from './stateHandler/stateHandler';
9-
import { registerAfterHook, registerBeforeHook } from './hooks';
9+
import {
10+
registerHookStateTracking,
11+
registerAfterHook,
12+
registerBeforeHook,
13+
HooksState,
14+
} from './hooks';
1015
import { createRequestTracer, createResponseTracer } from './tracer';
1116
import { createProxyMessageHandler } from './messages';
1217
import { toServerOptions } from './proxyRequest';
@@ -43,8 +48,20 @@ export const createProxy = (
4348
);
4449
app.use(bodyParser.urlencoded({ extended: true }));
4550
app.use('/*', bodyParser.raw({ type: '*/*' }));
46-
registerBeforeHook(app, config, stateSetupPath);
47-
registerAfterHook(app, config, stateSetupPath);
51+
52+
// Hooks
53+
const hooksState: HooksState = {
54+
setupCounter: 0,
55+
};
56+
app.use(stateSetupPath, registerHookStateTracking(hooksState));
57+
if (config.beforeEach) {
58+
logger.trace("registered 'beforeEach' hook");
59+
app.use(stateSetupPath, registerBeforeHook(config.beforeEach, hooksState));
60+
}
61+
if (config.afterEach) {
62+
logger.trace("registered 'afterEach' hook");
63+
app.use(stateSetupPath, registerAfterHook(config.afterEach, hooksState));
64+
}
4865

4966
// Trace req/res logging
5067
if (config.logLevel === 'debug' || config.logLevel === 'trace') {

0 commit comments

Comments
 (0)