Skip to content

Commit 61a4f62

Browse files
committed
Throw an error when defining a rule with a premature always-final step
1 parent 1da20d1 commit 61a4f62

File tree

5 files changed

+63
-18
lines changed

5 files changed

+63
-18
lines changed

src/rules/requests/request-rule.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,23 @@ export class RequestRule implements RequestRule {
5151
this.matchers = data.matchers;
5252
this.completionChecker = data.completionChecker;
5353

54-
this.steps = data.steps.map((step) => {
55-
if ('handle' in step) {
56-
return step;
57-
} else {
58-
// We transform the definition into a real step, by creating an instance of the raw step (which is
59-
// a subtype of the definition with the same constructor) and copying the fields across.
60-
return Object.assign(
61-
Object.create(StepLookup[step.type].prototype),
62-
step
54+
this.steps = data.steps.map((stepDefinition, i) => {
55+
const step = 'handle' in stepDefinition
56+
? stepDefinition
57+
: Object.assign(
58+
Object.create(StepLookup[stepDefinition.type].prototype),
59+
stepDefinition
60+
) as RequestStep;
61+
62+
if (StepLookup[step.type].isFinal && i !== data.steps.length - 1) {
63+
throw new Error(
64+
`Cannot create a rule with a final step before the last position ("${
65+
step.explain()
66+
}" in position ${i + 1} of ${data.steps.length})`
6367
);
6468
}
69+
70+
return step;
6571
});
6672
}
6773

src/rules/requests/request-step-definitions.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,9 @@ function validateCustomHeaders(
252252
}
253253

254254
export class SimpleStepDefinition extends Serializable implements RequestStepDefinition {
255+
255256
readonly type = 'simple';
257+
static readonly isFinal = true;
256258

257259
constructor(
258260
public status: number,
@@ -300,7 +302,9 @@ export interface CallbackRequestMessage {
300302
}
301303

302304
export class CallbackStepDefinition extends Serializable implements RequestStepDefinition {
305+
303306
readonly type = 'callback';
307+
static readonly isFinal = true;
304308

305309
constructor(
306310
public callback: (request: CompletedRequest) => MaybePromise<CallbackResponseResult>
@@ -356,7 +360,9 @@ type StreamStepEventMessage =
356360
{ type: 'nil' };
357361

358362
export class StreamStepDefinition extends Serializable implements RequestStepDefinition {
363+
359364
readonly type = 'stream';
365+
static readonly isFinal = true;
360366

361367
constructor(
362368
public status: number,
@@ -416,7 +422,9 @@ export class StreamStepDefinition extends Serializable implements RequestStepDef
416422
}
417423

418424
export class FileStepDefinition extends Serializable implements RequestStepDefinition {
425+
419426
readonly type = 'file';
427+
static readonly isFinal = true;
420428

421429
constructor(
422430
public status: number,
@@ -715,7 +723,9 @@ export interface BeforePassthroughResponseRequest {
715723
export const SERIALIZED_OMIT = "__mockttp__transform__omit__";
716724

717725
export class PassThroughStepDefinition extends Serializable implements RequestStepDefinition {
726+
718727
readonly type = 'passthrough';
728+
static readonly isFinal = true;
719729

720730
public readonly forwarding?: ForwardingOptions;
721731

@@ -980,6 +990,7 @@ export class PassThroughStepDefinition extends Serializable implements RequestSt
980990

981991
export class CloseConnectionStepDefinition extends Serializable implements RequestStepDefinition {
982992
readonly type = 'close-connection';
993+
static readonly isFinal = true;
983994

984995
explain() {
985996
return 'close the connection';
@@ -988,6 +999,7 @@ export class CloseConnectionStepDefinition extends Serializable implements Reque
988999

9891000
export class ResetConnectionStepDefinition extends Serializable implements RequestStepDefinition {
9901001
readonly type = 'reset-connection';
1002+
static readonly isFinal = true;
9911003

9921004
explain() {
9931005
return 'reset the connection';
@@ -996,6 +1008,7 @@ export class ResetConnectionStepDefinition extends Serializable implements Reque
9961008

9971009
export class TimeoutStepDefinition extends Serializable implements RequestStepDefinition {
9981010
readonly type = 'timeout';
1011+
static readonly isFinal = true;
9991012

10001013
explain() {
10011014
return 'time out (never respond)';
@@ -1004,6 +1017,7 @@ export class TimeoutStepDefinition extends Serializable implements RequestStepDe
10041017

10051018
export class JsonRpcResponseStepDefinition extends Serializable implements RequestStepDefinition {
10061019
readonly type = 'json-rpc-response';
1020+
static readonly isFinal = true;
10071021

10081022
constructor(
10091023
public readonly result:

src/rules/websockets/websocket-rule.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,23 @@ export class WebSocketRule implements WebSocketRule {
6464
this.matchers = data.matchers;
6565
this.completionChecker = data.completionChecker;
6666

67-
this.steps = data.steps.map((step) => {
68-
if ('handle' in step) {
69-
return step;
70-
} else {
71-
// We transform the definition into a real step, by creating an instance of the raw step (which is
72-
// a subtype of the definition with the same constructor) and copying the fields across.
73-
return Object.assign(
74-
Object.create(WsStepLookup[step.type].prototype),
75-
step
67+
this.steps = data.steps.map((stepDefinition, i) => {
68+
const step = 'handle' in stepDefinition
69+
? stepDefinition
70+
: Object.assign(
71+
Object.create(WsStepLookup[stepDefinition.type].prototype),
72+
stepDefinition
73+
) as WebSocketStep;
74+
75+
if (WsStepLookup[step.type].isFinal && i !== data.steps.length - 1) {
76+
throw new Error(
77+
`Cannot create a rule with a final step before the last position ("${
78+
step.explain()
79+
}" in position ${i + 1} of ${data.steps.length})`
7680
);
7781
}
82+
83+
return step;
7884
});
7985
}
8086

src/rules/websockets/websocket-step-definitions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ export interface SerializedPassThroughWebSocketData {
5959
}
6060

6161
export class PassThroughWebSocketStepDefinition extends Serializable implements WebSocketStepDefinition {
62+
6263
readonly type = 'ws-passthrough';
64+
static readonly isFinal = true;
6365

6466
// Same lookup configuration as normal request PassThroughStep:
6567
public readonly lookupOptions: PassThroughLookupOptions | undefined;
@@ -144,6 +146,7 @@ export class PassThroughWebSocketStepDefinition extends Serializable implements
144146
export class EchoWebSocketStepDefinition extends Serializable implements WebSocketStepDefinition {
145147

146148
readonly type = 'ws-echo';
149+
static readonly isFinal = true;
147150

148151
explain(): string {
149152
return "echo all websocket messages";
@@ -153,6 +156,7 @@ export class EchoWebSocketStepDefinition extends Serializable implements WebSock
153156
export class ListenWebSocketStepDefinition extends Serializable implements WebSocketStepDefinition {
154157

155158
readonly type = 'ws-listen';
159+
static readonly isFinal = true;
156160

157161
explain(): string {
158162
return "silently accept websocket messages without responding";
@@ -162,6 +166,7 @@ export class ListenWebSocketStepDefinition extends Serializable implements WebSo
162166
export class RejectWebSocketStepDefinition extends Serializable implements WebSocketStepDefinition {
163167

164168
readonly type = 'ws-reject';
169+
static readonly isFinal = true;
165170

166171
constructor(
167172
public readonly statusCode: number,

test/integration/manual-rule-building.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,18 @@ describe("Mockttp rule building", function () {
119119
})
120120
})()).to.be.rejectedWith('Cannot create a rule with no steps');
121121
});
122+
123+
it("should reject rules with non-final final-only steps", async () => {
124+
return expect((async () => { // Funky setup to handle sync & async failure for node & browser
125+
await server.addRequestRules({
126+
matchers: [new matchers.SimplePathMatcher('/endpoint')],
127+
steps: [
128+
new requestSteps.SimpleStepDefinition(200),
129+
new requestSteps.SimpleStepDefinition(200)
130+
]
131+
});
132+
})()).to.be.rejectedWith(
133+
'Cannot create a rule with a final step before the last position ("respond with status 200" in position 1 of 2)'
134+
);
135+
});
122136
});

0 commit comments

Comments
 (0)