diff --git a/package-lock.json b/package-lock.json
index 1c8518e6..f4507dd5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -80,7 +80,7 @@
"mobx-shallow-undo": "^1.0.0",
"mobx-utils": "^5.1.0",
"mockrtc": "^0.5.0",
- "mockttp": "^4.4.0",
+ "mockttp": "file:../mockttp/mockttp-4.4.2.tgz",
"monaco-editor": "^0.27.0",
"node-forge": "^1.4.0",
"openapi-directory": "^1.3.0",
@@ -2541,9 +2541,9 @@
}
},
"node_modules/@httptoolkit/httpolyglot": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@httptoolkit/httpolyglot/-/httpolyglot-3.0.1.tgz",
- "integrity": "sha512-fU9psZBRc49PfdrxFZ8W19SwVQ4+rrc0EYVxmy7H41y6/elaIu5p68wly4uLVt1DPD0K92MdN65GqP3N1ofZKg==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@httptoolkit/httpolyglot/-/httpolyglot-3.1.0.tgz",
+ "integrity": "sha512-Y+1gebmcMZMjDepn2e+9TBM4D+t3jYYbOwbXL9/PKmJEcaN/sbpv/00skN9KLXs43+BQvHTZc+5J0AnC5+9qDg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
@@ -13213,21 +13213,22 @@
}
},
"node_modules/mockttp": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/mockttp/-/mockttp-4.4.0.tgz",
- "integrity": "sha512-aDZHIFr++a4QJQdZFKEwSJacAJBGVd4OigNM7emhtD5QAo3NvxS1UoSKi9XkSXCWlNwsppVh/ylT4NnSdPCIUg==",
+ "version": "4.4.2",
+ "resolved": "file:../mockttp/mockttp-4.4.2.tgz",
+ "integrity": "sha512-bA1MPjJp4UVjh8j9YuYn/IEPF0Ou9+Hwf1aE9lPomdTM8psRrqlui2U6nHsDuBzW/tR5BKEN3FHl6Or8BYZfzg==",
"license": "Apache-2.0",
"dependencies": {
"@graphql-tools/schema": "^10.0.31",
"@graphql-tools/utils": "^11.0.0",
- "@httptoolkit/httpolyglot": "^3.0.1",
+ "@httptoolkit/httpolyglot": "^3.1.0",
"@httptoolkit/subscriptions-transport-ws": "^0.11.2",
"@httptoolkit/util": "^0.1.7",
"@httptoolkit/websocket-stream": "^6.0.1",
"@peculiar/asn1-cms": "<2.7.0",
"@peculiar/asn1-csr": "<2.7.0",
"@peculiar/asn1-ecc": "<2.7.0",
- "@peculiar/asn1-pkcs8": "^2.5.0 <2.7.0",
+ "@peculiar/asn1-pfx": "<2.7.0",
+ "@peculiar/asn1-pkcs8": "<2.7.0",
"@peculiar/asn1-pkcs9": "<2.7.0",
"@peculiar/asn1-rsa": "<2.7.0",
"@peculiar/asn1-schema": "^2.3.15 <2.7.0",
@@ -13291,6 +13292,80 @@
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
+ "node_modules/mockttp/node_modules/@peculiar/asn1-pfx": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz",
+ "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==",
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-cms": "^2.6.1",
+ "@peculiar/asn1-pkcs8": "^2.6.1",
+ "@peculiar/asn1-rsa": "^2.6.1",
+ "@peculiar/asn1-schema": "^2.6.0",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/mockttp/node_modules/@peculiar/asn1-pfx/node_modules/@peculiar/asn1-cms": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.7.0.tgz",
+ "integrity": "sha512-hew63shtzzvBcSHbhm+cyAmKe6AIfinT9hzEqSPjDC6opTTMKmTkQ0gHuN2KsWlvqiKw1S/fS94fhag/FJkioQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.7.0",
+ "@peculiar/asn1-x509": "^2.7.0",
+ "@peculiar/asn1-x509-attr": "^2.7.0",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/mockttp/node_modules/@peculiar/asn1-pfx/node_modules/@peculiar/asn1-rsa": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.7.0.tgz",
+ "integrity": "sha512-/qvENQrXyTZURjMqSeofHul0JJt2sNSzSwk36pl2olkHbaioMQgrASDZAlHXl0xUlnVbHj0uGgOrBMTb5x2aJQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.7.0",
+ "@peculiar/asn1-x509": "^2.7.0",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/mockttp/node_modules/@peculiar/asn1-pfx/node_modules/@peculiar/asn1-schema": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz",
+ "integrity": "sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/utils": "^2.0.2",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/mockttp/node_modules/@peculiar/asn1-pfx/node_modules/@peculiar/asn1-x509": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.7.0.tgz",
+ "integrity": "sha512-mUn9RRrkGDnG4ALfunDmzyRW5dg+sWCj/pfnCCqEHYbkGxEpvUt6iVJv8Yw1cyp6SWZ26ZE5oSmI5SqEaen15g==",
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.7.0",
+ "@peculiar/utils": "^2.0.2",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/mockttp/node_modules/@peculiar/asn1-pfx/node_modules/@peculiar/asn1-x509-attr": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.7.0.tgz",
+ "integrity": "sha512-NS8e7SOgXipkzUPLF/sce7ukpMpWjhxYsH0n6Y+bHYo4TTxOb95Zv7hqwSuL212mj5YxovjdOKQOgH1As3E94w==",
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.7.0",
+ "@peculiar/asn1-x509": "^2.7.0",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
"node_modules/mockttp/node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -22870,9 +22945,9 @@
}
},
"@httptoolkit/httpolyglot": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@httptoolkit/httpolyglot/-/httpolyglot-3.0.1.tgz",
- "integrity": "sha512-fU9psZBRc49PfdrxFZ8W19SwVQ4+rrc0EYVxmy7H41y6/elaIu5p68wly4uLVt1DPD0K92MdN65GqP3N1ofZKg==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@httptoolkit/httpolyglot/-/httpolyglot-3.1.0.tgz",
+ "integrity": "sha512-Y+1gebmcMZMjDepn2e+9TBM4D+t3jYYbOwbXL9/PKmJEcaN/sbpv/00skN9KLXs43+BQvHTZc+5J0AnC5+9qDg==",
"requires": {
"@types/node": "*"
}
@@ -30846,20 +30921,20 @@
}
},
"mockttp": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/mockttp/-/mockttp-4.4.0.tgz",
- "integrity": "sha512-aDZHIFr++a4QJQdZFKEwSJacAJBGVd4OigNM7emhtD5QAo3NvxS1UoSKi9XkSXCWlNwsppVh/ylT4NnSdPCIUg==",
+ "version": "file:../mockttp/mockttp-4.4.2.tgz",
+ "integrity": "sha512-bA1MPjJp4UVjh8j9YuYn/IEPF0Ou9+Hwf1aE9lPomdTM8psRrqlui2U6nHsDuBzW/tR5BKEN3FHl6Or8BYZfzg==",
"requires": {
"@graphql-tools/schema": "^10.0.31",
"@graphql-tools/utils": "^11.0.0",
- "@httptoolkit/httpolyglot": "^3.0.1",
+ "@httptoolkit/httpolyglot": "^3.1.0",
"@httptoolkit/subscriptions-transport-ws": "^0.11.2",
"@httptoolkit/util": "^0.1.7",
"@httptoolkit/websocket-stream": "^6.0.1",
"@peculiar/asn1-cms": "<2.7.0",
"@peculiar/asn1-csr": "<2.7.0",
"@peculiar/asn1-ecc": "<2.7.0",
- "@peculiar/asn1-pkcs8": "^2.5.0 <2.7.0",
+ "@peculiar/asn1-pfx": "<2.7.0",
+ "@peculiar/asn1-pkcs8": "<2.7.0",
"@peculiar/asn1-pkcs9": "<2.7.0",
"@peculiar/asn1-rsa": "<2.7.0",
"@peculiar/asn1-schema": "^2.3.15 <2.7.0",
@@ -30910,6 +30985,76 @@
"tslib": "^2.4.0"
}
},
+ "@peculiar/asn1-pfx": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz",
+ "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==",
+ "requires": {
+ "@peculiar/asn1-cms": "^2.6.1",
+ "@peculiar/asn1-pkcs8": "^2.6.1",
+ "@peculiar/asn1-rsa": "^2.6.1",
+ "@peculiar/asn1-schema": "^2.6.0",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ },
+ "dependencies": {
+ "@peculiar/asn1-cms": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.7.0.tgz",
+ "integrity": "sha512-hew63shtzzvBcSHbhm+cyAmKe6AIfinT9hzEqSPjDC6opTTMKmTkQ0gHuN2KsWlvqiKw1S/fS94fhag/FJkioQ==",
+ "requires": {
+ "@peculiar/asn1-schema": "^2.7.0",
+ "@peculiar/asn1-x509": "^2.7.0",
+ "@peculiar/asn1-x509-attr": "^2.7.0",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "@peculiar/asn1-rsa": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.7.0.tgz",
+ "integrity": "sha512-/qvENQrXyTZURjMqSeofHul0JJt2sNSzSwk36pl2olkHbaioMQgrASDZAlHXl0xUlnVbHj0uGgOrBMTb5x2aJQ==",
+ "requires": {
+ "@peculiar/asn1-schema": "^2.7.0",
+ "@peculiar/asn1-x509": "^2.7.0",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "@peculiar/asn1-schema": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz",
+ "integrity": "sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg==",
+ "requires": {
+ "@peculiar/utils": "^2.0.2",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "@peculiar/asn1-x509": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.7.0.tgz",
+ "integrity": "sha512-mUn9RRrkGDnG4ALfunDmzyRW5dg+sWCj/pfnCCqEHYbkGxEpvUt6iVJv8Yw1cyp6SWZ26ZE5oSmI5SqEaen15g==",
+ "requires": {
+ "@peculiar/asn1-schema": "^2.7.0",
+ "@peculiar/utils": "^2.0.2",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "@peculiar/asn1-x509-attr": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.7.0.tgz",
+ "integrity": "sha512-NS8e7SOgXipkzUPLF/sce7ukpMpWjhxYsH0n6Y+bHYo4TTxOb95Zv7hqwSuL212mj5YxovjdOKQOgH1As3E94w==",
+ "requires": {
+ "@peculiar/asn1-schema": "^2.7.0",
+ "@peculiar/asn1-x509": "^2.7.0",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ }
+ }
+ },
"accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
diff --git a/package.json b/package.json
index ec60165f..b35f3797 100644
--- a/package.json
+++ b/package.json
@@ -108,7 +108,7 @@
"mobx-shallow-undo": "^1.0.0",
"mobx-utils": "^5.1.0",
"mockrtc": "^0.5.0",
- "mockttp": "^4.4.0",
+ "mockttp": "file:../mockttp/mockttp-4.4.2.tgz",
"monaco-editor": "^0.27.0",
"node-forge": "^1.4.0",
"openapi-directory": "^1.3.0",
diff --git a/src/components/modify/matcher-config.tsx b/src/components/modify/matcher-config.tsx
index f1dec1e4..de02e094 100644
--- a/src/components/modify/matcher-config.tsx
+++ b/src/components/modify/matcher-config.tsx
@@ -162,7 +162,17 @@ export function AdditionalMatcherConfiguration(props:
case 'exact-query-string':
return ;
case 'header':
- return ;
+ return ;
+ case 'header-includes':
+ return ;
case 'raw-body':
return ;
case 'raw-body-includes':
@@ -785,7 +795,12 @@ class ExactQueryMatcherConfig extends MatcherConfig
}
@observer
-class HeaderMatcherConfig extends MatcherConfig {
+class HeaderMatcherConfig<
+ M extends (typeof matchers.HeaderMatcher | typeof matchers.HeaderValueIncludesMatcher)
+> extends MatcherConfig, {
+ matcherClass: M,
+ description: string
+}> {
render() {
const { matcherIndex } = this.props;
@@ -795,7 +810,7 @@ class HeaderMatcherConfig extends MatcherConfig {
return
{ matcherIndex !== undefined &&
- { matcherIndex !== 0 && 'and ' } with headers including
+ { matcherIndex !== 0 && 'and ' } { this.props.description }
}
@@ -821,7 +836,7 @@ class HeaderMatcherConfig extends MatcherConfig {
if (Object.keys(headers).length === 0) {
this.props.onChange();
} else {
- this.props.onChange(new matchers.HeaderMatcher(headers));
+ this.props.onChange(new this.props.matcherClass(headers) as InstanceType);
}
}
}
diff --git a/src/components/modify/step-config.tsx b/src/components/modify/step-config.tsx
index 46def3b1..5dac06b3 100644
--- a/src/components/modify/step-config.tsx
+++ b/src/components/modify/step-config.tsx
@@ -39,7 +39,8 @@ import {
getRulePartKey,
AvailableStepKey,
isHttpCompatibleType,
- MatchReplacePairs
+ MatchReplacePairs,
+ MatchReplaceHeaders
} from '../../model/rules/rules';
import {
StaticResponseStep,
@@ -1293,7 +1294,8 @@ class HeadersTransformConfig ext
private static readonly FIELDS = [
'replaceHeaders',
- 'updateHeaders'
+ 'updateHeaders',
+ 'matchReplaceHeaders'
] as const;
@computed
@@ -1304,8 +1306,8 @@ class HeadersTransformConfig ext
}
@computed
- get headers() {
- if (this.selected === 'none') return {};
+ get headers(): Headers {
+ if (this.selected === 'none' || this.selected === 'matchReplaceHeaders') return {};
return this.props.transform[this.selected] || {};
}
@@ -1315,7 +1317,8 @@ class HeadersTransformConfig ext
selected,
convertResultFromRawHeaders,
onTransformTypeChange,
- setHeadersValue
+ setHeadersValue,
+ setMatchReplaceHeadersValue
} = this;
return
@@ -1327,9 +1330,15 @@ class HeadersTransformConfig ext
+
{
- selected !== 'none' &&
+ selected === 'matchReplaceHeaders'
+ ?
+ : selected !== 'none' &&
ext
@action.bound
setHeadersValue(value: Headers) {
this.clearValues();
- if (this.selected !== 'none') {
+ if (this.selected === 'updateHeaders' || this.selected === 'replaceHeaders') {
this.props.onChange(this.selected)(value);
}
}
+ @action.bound
+ setMatchReplaceHeadersValue(value: MatchReplaceHeaders) {
+ this.clearValues();
+ this.props.onChange('matchReplaceHeaders')(value);
+ }
+
@action.bound
onTransformTypeChange = (event: React.ChangeEvent) => {
const value = event.currentTarget.value as 'none' | typeof HeadersTransformConfig.FIELDS[number];
@@ -1735,6 +1750,45 @@ const validateRegexMatcher = (value: string): true | string => {
}
}
+const HeaderMatchReplaceTransformConfig = (props: {
+ value: MatchReplaceHeaders,
+ onChange: (value: MatchReplaceHeaders) => void
+}) => {
+ // We edit the match/replace config for a single header here: that's by far the
+ // most common case, and keeps the UI simple. The underlying Mockttp transform
+ // does support transforming multiple headers at once, if configured directly.
+ const initialHeaderName = Object.keys(props.value)[0] ?? '';
+
+ const [headerName, setHeaderName] = React.useState(initialHeaderName);
+ const [replacements, setReplacements] = React.useState(
+ props.value[initialHeaderName] ?? []
+ );
+
+ React.useEffect(() => {
+ if (headerName) {
+ props.onChange({ [headerName]: replacements });
+ } else {
+ props.onChange({});
+ }
+ }, [headerName, replacements]);
+
+ return <>
+
+ Header name
+ ) => setHeaderName(e.target.value)}
+ placeholder='The name of the header to modify, e.g. cookie'
+ spellCheck={false}
+ />
+
+
+ >;
+};
+
@observer
class WebhookStepConfig extends StepConfig {
diff --git a/src/model/rules/definitions/http-rule-definitions.ts b/src/model/rules/definitions/http-rule-definitions.ts
index 8c8d59e2..7f7773d9 100644
--- a/src/model/rules/definitions/http-rule-definitions.ts
+++ b/src/model/rules/definitions/http-rule-definitions.ts
@@ -227,6 +227,21 @@ const MatchReplaceSerializer = serializr.list(
)
);
+const MatchReplaceHeadersSerializer = serializr.custom(
+ (headersConfig: { [headerName: string]: Array<[RegExp, string]> }) =>
+ _.mapValues(headersConfig, (pairs) =>
+ pairs.map(([key, value]) =>
+ [{ source: key.source, flags: key.flags }, value]
+ )
+ ),
+ (headersConfig: { [headerName: string]: Array<[{ source: string, flags: string }, string]> }) =>
+ _.mapValues(headersConfig, (pairs) =>
+ pairs.map(([key, value]) =>
+ [new RegExp(key.source, key.flags), value]
+ )
+ )
+);
+
serializr.createModelSchema(TransformingStep, {
uiType: serializeAsTag(() => 'req-res-transformer'),
transformRequest: serializr.object(
@@ -240,6 +255,7 @@ serializr.createModelSchema(TransformingStep, {
matchReplacePath: MatchReplaceSerializer,
matchReplaceQuery: MatchReplaceSerializer,
updateHeaders: serializeWithUndefineds,
+ matchReplaceHeaders: MatchReplaceHeadersSerializer,
updateJsonBody: serializeWithUndefineds,
replaceBody: serializeBuffer,
matchReplaceBody: MatchReplaceSerializer,
@@ -249,6 +265,7 @@ serializr.createModelSchema(TransformingStep, {
transformResponse: serializr.object(
serializr.createSimpleSchema({
updateHeaders: serializeWithUndefineds,
+ matchReplaceHeaders: MatchReplaceHeadersSerializer,
updateJsonBody: serializeWithUndefineds,
replaceBody: serializeBuffer,
matchReplaceBody: serializr.list(
diff --git a/src/model/rules/rule-descriptions.ts b/src/model/rules/rule-descriptions.ts
index 2e452043..5fbd2176 100644
--- a/src/model/rules/rule-descriptions.ts
+++ b/src/model/rules/rule-descriptions.ts
@@ -47,6 +47,8 @@ export function summarizeMatcherClass(key: MatcherClassKey): string {
return "With exact query string";
case 'header':
return "Including headers";
+ case 'header-includes':
+ return "With header values including";
case 'cookie':
return "With cookie";
case 'raw-body':
diff --git a/src/model/rules/rules.ts b/src/model/rules/rules.ts
index e5ede9df..8b47b288 100644
--- a/src/model/rules/rules.ts
+++ b/src/model/rules/rules.ts
@@ -56,7 +56,8 @@ import {
} from './definitions/rtc-rule-definitions';
export type {
- MatchReplacePairs
+ MatchReplacePairs,
+ MatchReplaceHeaders
} from 'mockttp';
/// --- Part-generic logic ---
diff --git a/test/unit/model/header-includes-matcher.spec.ts b/test/unit/model/header-includes-matcher.spec.ts
new file mode 100644
index 00000000..168a09ed
--- /dev/null
+++ b/test/unit/model/header-includes-matcher.spec.ts
@@ -0,0 +1,105 @@
+import { matchers } from 'mockttp';
+
+import { expect } from '../../test-setup';
+
+import {
+ serializeRules,
+ deserializeRules
+} from '../../../src/model/rules/rule-serialization';
+
+const requestWithHeaders = (headers: { [key: string]: string | string[] }) =>
+ ({ headers }) as any;
+
+describe("Header value includes matcher", () => {
+
+ it("matches a substring within a header value", () => {
+ const matcher = new matchers.HeaderValueIncludesMatcher({
+ 'Cookie': 'iamSessionIdExpiryDateInUtc'
+ });
+
+ expect(matcher.matches(requestWithHeaders({
+ 'cookie': 'a=1; iamSessionIdExpiryDateInUtc=2026-06-12T00:00:00Z; b=2'
+ }))).to.equal(true);
+ });
+
+ it("does not match if the header value doesn't contain the text", () => {
+ const matcher = new matchers.HeaderValueIncludesMatcher({
+ 'Cookie': 'iamSessionIdExpiryDateInUtc'
+ });
+
+ expect(matcher.matches(requestWithHeaders({
+ 'cookie': 'a=1; b=2'
+ }))).to.equal(false);
+ });
+
+ it("does not match if the header isn't present at all", () => {
+ const matcher = new matchers.HeaderValueIncludesMatcher({
+ 'Cookie': 'iamSessionIdExpiryDateInUtc'
+ });
+
+ expect(matcher.matches(requestWithHeaders({
+ 'host': 'example.com'
+ }))).to.equal(false);
+ });
+
+ it("matches multi-value headers if any value contains the text", () => {
+ const matcher = new matchers.HeaderValueIncludesMatcher({
+ 'Set-Cookie': 'session'
+ });
+
+ expect(matcher.matches(requestWithHeaders({
+ 'set-cookie': ['other=1', 'session=abc']
+ }))).to.equal(true);
+ });
+
+ it("requires all given headers to match", () => {
+ const matcher = new matchers.HeaderValueIncludesMatcher({
+ 'Cookie': 'mySession',
+ 'User-Agent': 'Chrome'
+ });
+
+ expect(matcher.matches(requestWithHeaders({
+ 'cookie': 'mySession=123',
+ 'user-agent': 'Mozilla/5.0 Chrome/120.0'
+ }))).to.equal(true);
+
+ expect(matcher.matches(requestWithHeaders({
+ 'cookie': 'mySession=123',
+ 'user-agent': 'Mozilla/5.0 Firefox/120.0'
+ }))).to.equal(false);
+ });
+
+ it("survives a rule serialization round-trip", () => {
+ const rules = {
+ id: 'root',
+ title: "Rules",
+ isRoot: true,
+ items: [{
+ id: 'rule-1',
+ type: 'http',
+ activated: true,
+ matchers: [
+ new matchers.HeaderValueIncludesMatcher({ 'Cookie': 'mySession' })
+ ],
+ steps: [{ type: 'simple', status: 200, data: 'mock response' }]
+ }]
+ } as any;
+
+ const restoredRules = deserializeRules(
+ JSON.parse(JSON.stringify(serializeRules(rules))),
+ { rulesStore: {} as any }
+ );
+
+ const restoredMatcher = (restoredRules.items[0] as any).matchers[0];
+
+ expect(restoredMatcher).to.be.instanceOf(matchers.HeaderValueIncludesMatcher);
+ expect(restoredMatcher.headers).to.deep.equal({ 'cookie': 'mySession' });
+ expect(restoredMatcher.matches(requestWithHeaders({
+ 'cookie': 'mySession=123'
+ }))).to.equal(true);
+ expect(restoredMatcher.explain()).to.equal(
+ `with header values including {"cookie":"mySession"}`
+ );
+ });
+
+});
diff --git a/test/unit/model/transform-serialization.spec.ts b/test/unit/model/transform-serialization.spec.ts
new file mode 100644
index 00000000..f5479b88
--- /dev/null
+++ b/test/unit/model/transform-serialization.spec.ts
@@ -0,0 +1,59 @@
+import { expect } from '../../test-setup';
+
+import { TransformingStep } from '../../../src/model/rules/definitions/http-rule-definitions';
+import {
+ serializeRules,
+ deserializeRules
+} from '../../../src/model/rules/rule-serialization';
+
+const fakeRulesStore = { activePassthroughOptions: {} } as any;
+
+describe("Transforming step serialization", () => {
+
+ it("survives a round-trip with a matchReplaceHeaders transform", () => {
+ const step = new TransformingStep(fakeRulesStore, {
+ matchReplaceHeaders: {
+ 'cookie': [
+ [/iamSessionIdExpiryDateInUtc=[^;]+/g, 'iamSessionIdExpiryDateInUtc=123231232']
+ ]
+ }
+ }, {});
+
+ const rules = {
+ id: 'root',
+ title: "Rules",
+ isRoot: true,
+ items: [{
+ id: 'rule-1',
+ type: 'http',
+ activated: true,
+ matchers: [],
+ steps: [step]
+ }]
+ } as any;
+
+ const restoredRules = deserializeRules(
+ JSON.parse(JSON.stringify(serializeRules(rules))),
+ { rulesStore: fakeRulesStore }
+ );
+
+ const restoredStep = (restoredRules.items[0] as any).steps[0];
+
+ expect(restoredStep).to.be.instanceOf(TransformingStep);
+
+ const pairs = restoredStep.transformRequest!.matchReplaceHeaders!['cookie'];
+ expect(pairs).to.have.length(1);
+
+ const [pattern, replacement] = pairs[0];
+ expect(pattern).to.be.instanceOf(RegExp);
+ expect(pattern.source).to.equal('iamSessionIdExpiryDateInUtc=[^;]+');
+ expect(pattern.flags).to.equal('g');
+ expect(replacement).to.equal('iamSessionIdExpiryDateInUtc=123231232');
+
+ // The restored regex actually works as expected:
+ expect(
+ 'a=1; iamSessionIdExpiryDateInUtc=2026-06-12T00:00:00Z; b=2'.replace(pattern, replacement)
+ ).to.equal('a=1; iamSessionIdExpiryDateInUtc=123231232; b=2');
+ });
+
+});