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'); + }); + +});