@gooddata/sdk-e2e-utils 11.60.0-alpha.2 → 11.60.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/create-test/feature-hub/mock.d.ts +5 -0
- package/esm/create-test/feature-hub/mock.d.ts.map +1 -0
- package/esm/create-test/feature-hub/mock.js +15 -0
- package/esm/create-test/feature-hub/types.d.ts +21 -0
- package/esm/create-test/feature-hub/types.d.ts.map +1 -0
- package/esm/create-test/feature-hub/types.js +2 -0
- package/esm/create-test/goodmock/admin.d.ts +34 -0
- package/esm/create-test/goodmock/admin.d.ts.map +1 -0
- package/esm/create-test/goodmock/admin.js +99 -0
- package/esm/create-test/goodmock/mode.d.ts +14 -0
- package/esm/create-test/goodmock/mode.d.ts.map +1 -0
- package/esm/create-test/goodmock/mode.js +17 -0
- package/esm/create-test/goodmock/playwright.d.ts +5 -0
- package/esm/create-test/goodmock/playwright.d.ts.map +1 -0
- package/esm/create-test/goodmock/playwright.js +37 -0
- package/esm/create-test/goodmock/snapshot/assert-no-leak-patterns.d.ts +9 -0
- package/esm/create-test/goodmock/snapshot/assert-no-leak-patterns.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/assert-no-leak-patterns.js +20 -0
- package/esm/create-test/goodmock/snapshot/assert-no-secret-leaks.d.ts +4 -0
- package/esm/create-test/goodmock/snapshot/assert-no-secret-leaks.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/assert-no-secret-leaks.js +14 -0
- package/esm/create-test/goodmock/snapshot/build-masked-form-pattern.d.ts +2 -0
- package/esm/create-test/goodmock/snapshot/build-masked-form-pattern.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/build-masked-form-pattern.js +17 -0
- package/esm/create-test/goodmock/snapshot/get-effective-secret-mappings.d.ts +3 -0
- package/esm/create-test/goodmock/snapshot/get-effective-secret-mappings.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/get-effective-secret-mappings.js +2 -0
- package/esm/create-test/goodmock/snapshot/replace-secrets.d.ts +10 -0
- package/esm/create-test/goodmock/snapshot/replace-secrets.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/replace-secrets.js +23 -0
- package/esm/create-test/goodmock/snapshot/sanitize-credentials.d.ts +4 -0
- package/esm/create-test/goodmock/snapshot/sanitize-credentials.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/sanitize-credentials.js +41 -0
- package/esm/create-test/goodmock/snapshot/sanitize-workspace-ids.d.ts +4 -0
- package/esm/create-test/goodmock/snapshot/sanitize-workspace-ids.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/sanitize-workspace-ids.js +21 -0
- package/esm/create-test/goodmock/snapshot/snapshot-and-save-recording.d.ts +50 -0
- package/esm/create-test/goodmock/snapshot/snapshot-and-save-recording.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/snapshot-and-save-recording.js +64 -0
- package/esm/create-test/goodmock/snapshot/snapshot-param.d.ts +11 -0
- package/esm/create-test/goodmock/snapshot/snapshot-param.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/snapshot-param.js +9 -0
- package/esm/create-test/goodmock/types.d.ts +60 -0
- package/esm/create-test/goodmock/types.d.ts.map +1 -0
- package/esm/create-test/goodmock/types.js +2 -0
- package/esm/create-test/playwright/call-original-fn.d.ts +17 -0
- package/esm/create-test/playwright/call-original-fn.d.ts.map +1 -0
- package/esm/create-test/playwright/call-original-fn.js +25 -0
- package/esm/create-test/playwright/describe/factory.d.ts +27 -0
- package/esm/create-test/playwright/describe/factory.d.ts.map +1 -0
- package/esm/create-test/playwright/describe/factory.js +18 -0
- package/esm/create-test/playwright/fixtures.d.ts +7 -0
- package/esm/create-test/playwright/fixtures.d.ts.map +1 -0
- package/esm/create-test/playwright/fixtures.js +2 -0
- package/esm/create-test/playwright/playwright-types.d.ts +42 -0
- package/esm/create-test/playwright/playwright-types.d.ts.map +1 -0
- package/esm/create-test/playwright/playwright-types.js +2 -0
- package/esm/create-test/playwright/test/factory.d.ts +7 -0
- package/esm/create-test/playwright/test/factory.d.ts.map +1 -0
- package/esm/create-test/playwright/test/factory.js +42 -0
- package/esm/create-test/playwright/test/types.d.ts +27 -0
- package/esm/create-test/playwright/test/types.d.ts.map +1 -0
- package/esm/create-test/playwright/test/types.js +2 -0
- package/esm/create-test/playwright/top-level-describe/build-suite-callback.d.ts +9 -0
- package/esm/create-test/playwright/top-level-describe/build-suite-callback.d.ts.map +1 -0
- package/esm/create-test/playwright/top-level-describe/build-suite-callback.js +17 -0
- package/esm/create-test/playwright/top-level-describe/factory.d.ts +8 -0
- package/esm/create-test/playwright/top-level-describe/factory.d.ts.map +1 -0
- package/esm/create-test/playwright/top-level-describe/factory.js +12 -0
- package/esm/create-test/playwright/top-level-describe/types.d.ts +17 -0
- package/esm/create-test/playwright/top-level-describe/types.d.ts.map +1 -0
- package/esm/create-test/playwright/top-level-describe/types.js +2 -0
- package/esm/create-test/playwright/types.d.ts +17 -0
- package/esm/create-test/playwright/types.d.ts.map +1 -0
- package/esm/create-test/playwright/types.js +2 -0
- package/esm/create-test/playwright/with-describe-details.d.ts +6 -0
- package/esm/create-test/playwright/with-describe-details.d.ts.map +1 -0
- package/esm/create-test/playwright/with-describe-details.js +39 -0
- package/esm/index.d.ts +14 -3
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +8 -3
- package/esm/sdk-e2e-utils.d.ts +112 -38
- package/package.json +3 -3
- package/esm/goodmock.d.ts +0 -138
- package/esm/goodmock.d.ts.map +0 -1
- package/esm/goodmock.js +0 -284
- package/esm/playwright.d.ts +0 -97
- package/esm/playwright.d.ts.map +0 -1
- package/esm/playwright.js +0 -189
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Test } from "../playwright/playwright-types.js";
|
|
2
|
+
import type { IFeatureHubEnvironment } from "./types.js";
|
|
3
|
+
/** FeatureHub mock — each test gets a fresh page, so route dies with it. */
|
|
4
|
+
export declare function registerFeatureHubMock(testInstance: Test, featureHubResponse: IFeatureHubEnvironment[]): void;
|
|
5
|
+
//# sourceMappingURL=mock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock.d.ts","sourceRoot":"","sources":["../../../src/create-test/feature-hub/mock.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mCAAmC,CAAC;AAE9D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD,4EAA4E;AAC5E,wBAAgB,sBAAsB,CAClC,YAAY,EAAE,IAAI,EAClB,kBAAkB,EAAE,sBAAsB,EAAE,GAC7C,IAAI,CAYN"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
/** FeatureHub mock — each test gets a fresh page, so route dies with it. */
|
|
3
|
+
export function registerFeatureHubMock(testInstance, featureHubResponse) {
|
|
4
|
+
const body = JSON.stringify(featureHubResponse);
|
|
5
|
+
testInstance.beforeEach(async ({ page }) => {
|
|
6
|
+
await page.route("**/features*", async (route) => {
|
|
7
|
+
await route.fulfill({
|
|
8
|
+
status: 200,
|
|
9
|
+
contentType: "application/json",
|
|
10
|
+
headers: { "access-control-allow-origin": "*" },
|
|
11
|
+
body,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
*/
|
|
4
|
+
export interface IFeatureHubFeature {
|
|
5
|
+
id: string;
|
|
6
|
+
key: string;
|
|
7
|
+
l: boolean;
|
|
8
|
+
version?: number;
|
|
9
|
+
type: string;
|
|
10
|
+
value?: boolean | string | number;
|
|
11
|
+
strategies?: unknown[];
|
|
12
|
+
v?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export interface IFeatureHubEnvironment {
|
|
18
|
+
id: string;
|
|
19
|
+
features: IFeatureHubFeature[];
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/create-test/feature-hub/types.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,CAAC,EAAE,OAAO,CAAC;IACX,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;IAClC,UAAU,CAAC,EAAE,OAAO,EAAE,CAAC;IACvB,CAAC,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,kBAAkB,EAAE,CAAC;CAClC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
* Load goodmock stub mappings from a JSON file on disk.
|
|
4
|
+
*
|
|
5
|
+
* @param host - Goodmock host:port (e.g. "backend-mock:8080")
|
|
6
|
+
* @param mappingFilePath - Absolute path to the mapping JSON file
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadMappings(host: string, mappingFilePath: string): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* @internal
|
|
11
|
+
* Reset goodmock scenario state (sequence counters) without clearing mappings.
|
|
12
|
+
* Must be called between tests so scenario-driven stubs start from the
|
|
13
|
+
* beginning for every test — mirrors the Cypress `resetRecordingsScenarios` task.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resetScenarios(host: string): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* @internal
|
|
18
|
+
* Clear all goodmock stub mappings.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resetMappings(host: string): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* @internal
|
|
23
|
+
* Add a goodmock stub that mocks log requests (POST /gdc/app/projects/.../log).
|
|
24
|
+
* This prevents the app from failing when it tries to send logs.
|
|
25
|
+
*/
|
|
26
|
+
export declare function mockLogRequests(host: string): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* @internal
|
|
29
|
+
* Add a catch-all proxy mapping so goodmock forwards every request to the
|
|
30
|
+
* real backend and records the interactions. Must be called after
|
|
31
|
+
* {@link resetMappings} in recording mode.
|
|
32
|
+
*/
|
|
33
|
+
export declare function startRecording(host: string, backendHost: string): Promise<void>;
|
|
34
|
+
//# sourceMappingURL=admin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../../../src/create-test/goodmock/admin.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBvF;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMhE;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM/D;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBjE;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWrF"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
/**
|
|
4
|
+
* @internal
|
|
5
|
+
* Load goodmock stub mappings from a JSON file on disk.
|
|
6
|
+
*
|
|
7
|
+
* @param host - Goodmock host:port (e.g. "backend-mock:8080")
|
|
8
|
+
* @param mappingFilePath - Absolute path to the mapping JSON file
|
|
9
|
+
*/
|
|
10
|
+
export async function loadMappings(host, mappingFilePath) {
|
|
11
|
+
let mappings;
|
|
12
|
+
try {
|
|
13
|
+
mappings = readFileSync(mappingFilePath, "utf-8");
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
console.warn(`No mapping file found: ${mappingFilePath}, skipping`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
let json;
|
|
20
|
+
try {
|
|
21
|
+
json = JSON.parse(mappings);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
console.error(`Failed to parse mapping file: ${mappingFilePath}`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const response = await fetch(`http://${host}/__admin/mappings/import`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
body: JSON.stringify(json),
|
|
31
|
+
});
|
|
32
|
+
// oxlint-disable-next-line eslint-js/no-console
|
|
33
|
+
console.log(`Goodmock mappings loaded from ${mappingFilePath} (status: ${response.status})`);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* @internal
|
|
37
|
+
* Reset goodmock scenario state (sequence counters) without clearing mappings.
|
|
38
|
+
* Must be called between tests so scenario-driven stubs start from the
|
|
39
|
+
* beginning for every test — mirrors the Cypress `resetRecordingsScenarios` task.
|
|
40
|
+
*/
|
|
41
|
+
export async function resetScenarios(host) {
|
|
42
|
+
const response = await fetch(`http://${host}/__admin/scenarios/reset`, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
});
|
|
45
|
+
// oxlint-disable-next-line eslint-js/no-console
|
|
46
|
+
console.log(`Goodmock scenarios reset (status: ${response.status})`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* @internal
|
|
50
|
+
* Clear all goodmock stub mappings.
|
|
51
|
+
*/
|
|
52
|
+
export async function resetMappings(host) {
|
|
53
|
+
const response = await fetch(`http://${host}/__admin/reset`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
});
|
|
56
|
+
// oxlint-disable-next-line eslint-js/no-console
|
|
57
|
+
console.log(`Goodmock mappings reset (status: ${response.status})`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* @internal
|
|
61
|
+
* Add a goodmock stub that mocks log requests (POST /gdc/app/projects/.../log).
|
|
62
|
+
* This prevents the app from failing when it tries to send logs.
|
|
63
|
+
*/
|
|
64
|
+
export async function mockLogRequests(host) {
|
|
65
|
+
const response = await fetch(`http://${host}/__admin/mappings`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "Content-Type": "application/json" },
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
request: {
|
|
70
|
+
method: "POST",
|
|
71
|
+
urlPattern: "/gdc/app/projects/.*/log",
|
|
72
|
+
},
|
|
73
|
+
response: {
|
|
74
|
+
body: "",
|
|
75
|
+
status: 200,
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
// oxlint-disable-next-line eslint-js/no-console
|
|
80
|
+
console.log(`Goodmock log requests mocked (status: ${response.status})`);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* @internal
|
|
84
|
+
* Add a catch-all proxy mapping so goodmock forwards every request to the
|
|
85
|
+
* real backend and records the interactions. Must be called after
|
|
86
|
+
* {@link resetMappings} in recording mode.
|
|
87
|
+
*/
|
|
88
|
+
export async function startRecording(host, backendHost) {
|
|
89
|
+
const response = await fetch(`http://${host}/__admin/mappings`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "Content-Type": "application/json" },
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
request: { method: "ANY", urlPattern: ".*" },
|
|
94
|
+
response: { proxyBaseUrl: backendHost },
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
// oxlint-disable-next-line eslint-js/no-console
|
|
98
|
+
console.log(`Goodmock recording started, proxying to ${backendHost} (status: ${response.status})`);
|
|
99
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
* Which mode goodmock is running in
|
|
4
|
+
*/
|
|
5
|
+
export declare enum GoodmockMode {
|
|
6
|
+
Replay = "replay",
|
|
7
|
+
Record = "record",
|
|
8
|
+
Proxy = "proxy"
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare function goodmockMode(): GoodmockMode;
|
|
14
|
+
//# sourceMappingURL=mode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mode.d.ts","sourceRoot":"","sources":["../../../src/create-test/goodmock/mode.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,oBAAY,YAAY;IACpB,MAAM,WAAW;IACjB,MAAM,WAAW;IACjB,KAAK,UAAU;CAClB;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,YAAY,CAE3C"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
* Which mode goodmock is running in
|
|
5
|
+
*/
|
|
6
|
+
export var GoodmockMode;
|
|
7
|
+
(function (GoodmockMode) {
|
|
8
|
+
GoodmockMode["Replay"] = "replay";
|
|
9
|
+
GoodmockMode["Record"] = "record";
|
|
10
|
+
GoodmockMode["Proxy"] = "proxy";
|
|
11
|
+
})(GoodmockMode || (GoodmockMode = {}));
|
|
12
|
+
/**
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export function goodmockMode() {
|
|
16
|
+
return process.env["GOODMOCK_MODE"];
|
|
17
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type test } from "@playwright/test";
|
|
2
|
+
import { type IGoodmockOptions } from "./types.js";
|
|
3
|
+
/** Goodmock lifecycle hooks */
|
|
4
|
+
export declare function registerGoodmockHooks(testInstance: typeof test, gm: IGoodmockOptions, specName: string): void;
|
|
5
|
+
//# sourceMappingURL=playwright.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playwright.d.ts","sourceRoot":"","sources":["../../../src/create-test/goodmock/playwright.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAK7C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,+BAA+B;AAC/B,wBAAgB,qBAAqB,CACjC,YAAY,EAAE,OAAO,IAAI,EACzB,EAAE,EAAE,gBAAgB,EACpB,QAAQ,EAAE,MAAM,GACjB,IAAI,CAiCN"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { loadMappings, resetMappings, startRecording } from "./admin.js";
|
|
3
|
+
import { GoodmockMode, goodmockMode as getGoodmockMode } from "./mode.js";
|
|
4
|
+
import { snapshotAndSaveRecording } from "./snapshot/snapshot-and-save-recording.js";
|
|
5
|
+
/** Goodmock lifecycle hooks */
|
|
6
|
+
export function registerGoodmockHooks(testInstance, gm, specName) {
|
|
7
|
+
const goodmockMode = getGoodmockMode();
|
|
8
|
+
if (goodmockMode === GoodmockMode.Proxy) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
testInstance.beforeAll(async () => {
|
|
12
|
+
await resetMappings(gm.host);
|
|
13
|
+
if (goodmockMode === GoodmockMode.Record) {
|
|
14
|
+
await startRecording(gm.host, gm.backendHost);
|
|
15
|
+
}
|
|
16
|
+
else if (goodmockMode === GoodmockMode.Replay) {
|
|
17
|
+
await loadMappings(gm.host, gm.getMappingPath(specName));
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
testInstance.afterAll(async () => {
|
|
21
|
+
if (goodmockMode === GoodmockMode.Record) {
|
|
22
|
+
await snapshotAndSaveRecording(gm.host, gm.getMappingPath(specName), {
|
|
23
|
+
workspaceIdMappings: gm.workspaceIdMappings,
|
|
24
|
+
backendHost: gm.backendHost,
|
|
25
|
+
baseUrl: gm.baseUrl,
|
|
26
|
+
secretMappings: gm.secretMappings,
|
|
27
|
+
leakPatterns: gm.leakPatterns,
|
|
28
|
+
sanitizeMappings: gm.sanitizeMappings,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
await resetMappings(gm.host);
|
|
32
|
+
});
|
|
33
|
+
// TODO: When this is supported in goodmock, uncomment this block
|
|
34
|
+
// testInstance.beforeEach(async () => {
|
|
35
|
+
// await resetScenarios(gm.host);
|
|
36
|
+
// });
|
|
37
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ILeakPattern } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pattern guard: catch credential-shaped values from ANY source (not just the ones the
|
|
4
|
+
* tests typed), e.g. a real key stored in a backend org setting under an unexpected field.
|
|
5
|
+
* Patterns are consumer-supplied — this module has no built-in notion of what a secret
|
|
6
|
+
* looks like for a given domain (see ISnapshotAndSaveRecordingOptions.leakPatterns).
|
|
7
|
+
*/
|
|
8
|
+
export declare function assertNoLeakPatterns(output: string, leakPatterns: ILeakPattern[] | undefined, mappingFilePath: string): void;
|
|
9
|
+
//# sourceMappingURL=assert-no-leak-patterns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assert-no-leak-patterns.d.ts","sourceRoot":"","sources":["../../../../src/create-test/goodmock/snapshot/assert-no-leak-patterns.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAChC,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,YAAY,EAAE,GAAG,SAAS,EACxC,eAAe,EAAE,MAAM,GACxB,IAAI,CAcN"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
/**
|
|
3
|
+
* Pattern guard: catch credential-shaped values from ANY source (not just the ones the
|
|
4
|
+
* tests typed), e.g. a real key stored in a backend org setting under an unexpected field.
|
|
5
|
+
* Patterns are consumer-supplied — this module has no built-in notion of what a secret
|
|
6
|
+
* looks like for a given domain (see ISnapshotAndSaveRecordingOptions.leakPatterns).
|
|
7
|
+
*/
|
|
8
|
+
export function assertNoLeakPatterns(output, leakPatterns, mappingFilePath) {
|
|
9
|
+
for (const { label, pattern } of leakPatterns ?? []) {
|
|
10
|
+
// Reset lastIndex in case pattern carries the g/y flag — exec() on a stateful regex
|
|
11
|
+
// would otherwise resume from wherever a previous call (e.g. a prior recording) left off.
|
|
12
|
+
pattern.lastIndex = 0;
|
|
13
|
+
const match = pattern.exec(output);
|
|
14
|
+
if (match) {
|
|
15
|
+
throw new Error(`Recording for ${mappingFilePath} contains a value shaped like a ${label} ` +
|
|
16
|
+
`("${match[0].slice(0, 8)}…"). Refusing to save the mapping file — extend ` +
|
|
17
|
+
`sanitizeMappings / secretMappings so it gets sanitized.`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ISecretMapping } from "../types.js";
|
|
2
|
+
/** Leak guard: never write a recording that still contains a real secret (full or masked). */
|
|
3
|
+
export declare function assertNoSecretLeaks(output: string, secretMappings: ISecretMapping[] | undefined, mappingFilePath: string): void;
|
|
4
|
+
//# sourceMappingURL=assert-no-secret-leaks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assert-no-secret-leaks.d.ts","sourceRoot":"","sources":["../../../../src/create-test/goodmock/snapshot/assert-no-secret-leaks.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAKlD,8FAA8F;AAC9F,wBAAgB,mBAAmB,CAC/B,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,cAAc,EAAE,GAAG,SAAS,EAC5C,eAAe,EAAE,MAAM,GACxB,IAAI,CAWN"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { buildMaskedFormPattern } from "./build-masked-form-pattern.js";
|
|
3
|
+
import { getEffectiveSecretMappings } from "./get-effective-secret-mappings.js";
|
|
4
|
+
/** Leak guard: never write a recording that still contains a real secret (full or masked). */
|
|
5
|
+
export function assertNoSecretLeaks(output, secretMappings, mappingFilePath) {
|
|
6
|
+
for (const { secret } of getEffectiveSecretMappings(secretMappings)) {
|
|
7
|
+
const escapedSecret = JSON.stringify(secret).slice(1, -1);
|
|
8
|
+
const maskedForm = buildMaskedFormPattern(escapedSecret);
|
|
9
|
+
if (output.includes(escapedSecret) || output.includes(secret) || maskedForm?.test(output)) {
|
|
10
|
+
throw new Error(`Recording for ${mappingFilePath} still contains a secret after sanitization ` +
|
|
11
|
+
`(e.g. in an encoded form). Refusing to save the mapping file.`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-masked-form-pattern.d.ts","sourceRoot":"","sources":["../../../../src/create-test/goodmock/snapshot/build-masked-form-pattern.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,sBAAsB,kBAAmB,MAAM,KAAG,MAAM,GAAG,SAUvE,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3
|
+
// Below this, prefix/suffix would overlap or leave nothing to mask — there's no safe
|
|
4
|
+
// "partial reveal" shape to detect, so such secrets rely on the exact-match replace in replaceSecrets.
|
|
5
|
+
const MIN_MASKABLE_SECRET_LENGTH = 6;
|
|
6
|
+
// Masking keeps a variable-length prefix/suffix of the key around the asterisks; scale how
|
|
7
|
+
// much we match by the secret's own length instead of assuming a fixed provider convention
|
|
8
|
+
// (e.g. OpenAI's "sk-prox***********abcd") so shorter secrets and other providers' masking
|
|
9
|
+
// shapes are still caught.
|
|
10
|
+
export const buildMaskedFormPattern = (escapedSecret) => {
|
|
11
|
+
if (escapedSecret.length < MIN_MASKABLE_SECRET_LENGTH) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const prefixLength = Math.min(7, Math.floor(escapedSecret.length / 3));
|
|
15
|
+
const suffixLength = Math.min(4, Math.floor(escapedSecret.length / 4));
|
|
16
|
+
return new RegExp(`${escapeRegex(escapedSecret.slice(0, prefixLength))}[^"*\\\\]*[*]+[^"*\\\\]*${escapeRegex(escapedSecret.slice(-suffixLength))}`, "g");
|
|
17
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-effective-secret-mappings.d.ts","sourceRoot":"","sources":["../../../../src/create-test/goodmock/snapshot/get-effective-secret-mappings.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,eAAO,MAAM,0BAA0B,mBAAoB,cAAc,EAAE,GAAG,SAAS,KAAG,cAAc,EACR,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ISecretMapping } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Replace secrets typed into the UI / sent by API helpers during recording with their
|
|
4
|
+
* replay-time placeholders. JSON-escape both sides so values containing characters that
|
|
5
|
+
* JSON.stringify escapes (quotes, backslashes) are still found in the serialized output.
|
|
6
|
+
* Provider error messages echo keys in a masked form ("sk-prox***********abcd"), so that
|
|
7
|
+
* shape is rewritten too — it still leaks the ends of a real key.
|
|
8
|
+
*/
|
|
9
|
+
export declare function replaceSecrets(output: string, secretMappings: ISecretMapping[] | undefined): string;
|
|
10
|
+
//# sourceMappingURL=replace-secrets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replace-secrets.d.ts","sourceRoot":"","sources":["../../../../src/create-test/goodmock/snapshot/replace-secrets.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAKlD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,cAAc,EAAE,GAAG,SAAS,GAAG,MAAM,CAYnG"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { buildMaskedFormPattern } from "./build-masked-form-pattern.js";
|
|
3
|
+
import { getEffectiveSecretMappings } from "./get-effective-secret-mappings.js";
|
|
4
|
+
/**
|
|
5
|
+
* Replace secrets typed into the UI / sent by API helpers during recording with their
|
|
6
|
+
* replay-time placeholders. JSON-escape both sides so values containing characters that
|
|
7
|
+
* JSON.stringify escapes (quotes, backslashes) are still found in the serialized output.
|
|
8
|
+
* Provider error messages echo keys in a masked form ("sk-prox***********abcd"), so that
|
|
9
|
+
* shape is rewritten too — it still leaks the ends of a real key.
|
|
10
|
+
*/
|
|
11
|
+
export function replaceSecrets(output, secretMappings) {
|
|
12
|
+
let result = output;
|
|
13
|
+
for (const { secret, placeholder } of getEffectiveSecretMappings(secretMappings)) {
|
|
14
|
+
const escapedSecret = JSON.stringify(secret).slice(1, -1);
|
|
15
|
+
const escapedPlaceholder = JSON.stringify(placeholder).slice(1, -1);
|
|
16
|
+
result = result.replaceAll(escapedSecret, escapedPlaceholder);
|
|
17
|
+
const maskedForm = buildMaskedFormPattern(escapedSecret);
|
|
18
|
+
if (maskedForm) {
|
|
19
|
+
result = result.replace(maskedForm, escapedPlaceholder);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize-credentials.d.ts","sourceRoot":"","sources":["../../../../src/create-test/goodmock/snapshot/sanitize-credentials.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,IAAI,CAyCtE"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
/** Sanitize credentials (login cookie, resolveSettings map tokens) in place. */
|
|
3
|
+
export function sanitizeCredentials(mappings) {
|
|
4
|
+
for (const mapping of mappings) {
|
|
5
|
+
if (mapping?.request?.url === "/gdc/account/login") {
|
|
6
|
+
delete mapping.request.bodyPatterns;
|
|
7
|
+
if (mapping.response.headers) {
|
|
8
|
+
delete mapping.response.headers["Set-Cookie"];
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
// Sanitize secrets from resolveSettings responses. Match both org-level
|
|
12
|
+
// (/api/v1/actions/resolveSettings) and workspace-level variants, via either `url`
|
|
13
|
+
// (no query) or `urlPath` (with query params).
|
|
14
|
+
const requestPath = mapping?.request?.url ?? mapping?.request?.urlPath;
|
|
15
|
+
if (typeof requestPath === "string" && requestPath.includes("/resolveSettings")) {
|
|
16
|
+
try {
|
|
17
|
+
const body = mapping?.response?.jsonBody;
|
|
18
|
+
const settings = Array.isArray(body)
|
|
19
|
+
? body
|
|
20
|
+
: Array.isArray(body?.data)
|
|
21
|
+
? body.data
|
|
22
|
+
: null;
|
|
23
|
+
if (settings) {
|
|
24
|
+
for (const setting of settings) {
|
|
25
|
+
const content = setting?.content;
|
|
26
|
+
if (!content || typeof content !== "object") {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
// Map tokens (agGrid, mapbox) are stored under content.value.
|
|
30
|
+
if (setting?.id === "agGridToken" || setting?.id === "mapboxToken") {
|
|
31
|
+
content["value"] = "";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
console.warn(`sanitizeCredentials – resolveSettings body is not valid JSON for: ${requestPath}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type IGoodmockMapping, type IWorkspaceIdMapping } from "../types.js";
|
|
2
|
+
/** Apply every source/target workspace ID rewrite, in order. */
|
|
3
|
+
export declare function sanitizeWorkspaceIds(mappings: IGoodmockMapping[], workspaceIdMappings: IWorkspaceIdMapping | IWorkspaceIdMapping[] | undefined): IGoodmockMapping[];
|
|
4
|
+
//# sourceMappingURL=sanitize-workspace-ids.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize-workspace-ids.d.ts","sourceRoot":"","sources":["../../../../src/create-test/goodmock/snapshot/sanitize-workspace-ids.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAqB9E,gEAAgE;AAChE,wBAAgB,oBAAoB,CAChC,QAAQ,EAAE,gBAAgB,EAAE,EAC5B,mBAAmB,EAAE,mBAAmB,GAAG,mBAAmB,EAAE,GAAG,SAAS,GAC7E,gBAAgB,EAAE,CAWpB"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
function sanitizeWorkspaceId(mappings, sourceWorkspaceId, targetWorkspaceId) {
|
|
3
|
+
if (sourceWorkspaceId === targetWorkspaceId) {
|
|
4
|
+
return mappings;
|
|
5
|
+
}
|
|
6
|
+
const dataString = JSON.stringify({ mappings });
|
|
7
|
+
const sanitizedDataString = dataString.split(sourceWorkspaceId).join(targetWorkspaceId);
|
|
8
|
+
const sanitizedData = JSON.parse(sanitizedDataString);
|
|
9
|
+
// oxlint-disable-next-line eslint-js/no-console
|
|
10
|
+
console.log(`Sanitized workspaceId in mappings (${sourceWorkspaceId} -> ${targetWorkspaceId})`);
|
|
11
|
+
return sanitizedData.mappings;
|
|
12
|
+
}
|
|
13
|
+
/** Apply every source/target workspace ID rewrite, in order. */
|
|
14
|
+
export function sanitizeWorkspaceIds(mappings, workspaceIdMappings) {
|
|
15
|
+
const mappingsToApply = Array.isArray(workspaceIdMappings)
|
|
16
|
+
? workspaceIdMappings
|
|
17
|
+
: workspaceIdMappings
|
|
18
|
+
? [workspaceIdMappings]
|
|
19
|
+
: [];
|
|
20
|
+
return mappingsToApply.reduce((acc, { sourceWorkspaceId, targetWorkspaceId }) => sanitizeWorkspaceId(acc, sourceWorkspaceId, targetWorkspaceId), mappings);
|
|
21
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type IGoodmockMapping, type ILeakPattern, type ISecretMapping, type IWorkspaceIdMapping } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
export interface ISnapshotAndSaveRecordingOptions {
|
|
6
|
+
/** Source/target workspace ID rewrite(s) applied before mappings are saved. */
|
|
7
|
+
workspaceIdMappings?: IWorkspaceIdMapping | IWorkspaceIdMapping[];
|
|
8
|
+
/**
|
|
9
|
+
* Real backend URL (e.g. "https://example.gooddata.com"). When provided together with
|
|
10
|
+
* baseUrl, all occurrences are replaced in the saved mappings so that recorded responses
|
|
11
|
+
* (e.g. geo tile URLs) resolve correctly during replay.
|
|
12
|
+
*/
|
|
13
|
+
backendHost?: string;
|
|
14
|
+
/** App base URL (e.g. "http://kpi-dashboards-ui:9500") that replaces backendHost references. */
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Secret rewrites. Each secret value is replaced by its placeholder everywhere in the saved
|
|
18
|
+
* mappings (request bodyPatterns included, and masked variants such as "sk-prox***abcd").
|
|
19
|
+
* Saving fails hard if any secret still remains afterwards.
|
|
20
|
+
*/
|
|
21
|
+
secretMappings?: ISecretMapping[];
|
|
22
|
+
/**
|
|
23
|
+
* Credential-shaped patterns that must never appear anywhere in the saved output, regardless
|
|
24
|
+
* of which field they came from. A backstop for secrets `secretMappings` doesn't know about
|
|
25
|
+
* (e.g. a value a backend echoed back that the test never typed). Saving fails hard on a match.
|
|
26
|
+
*/
|
|
27
|
+
leakPatterns?: ILeakPattern[];
|
|
28
|
+
/**
|
|
29
|
+
* Consumer-supplied hook to redact domain-specific secrets from the collected mappings (e.g.
|
|
30
|
+
* blank a `content.apiKey` field on a settings entity) before they're merged and written out.
|
|
31
|
+
* Runs once, right after the built-in credential handling (login cookie, resolveSettings map
|
|
32
|
+
* tokens) and before workspace-id rewriting.
|
|
33
|
+
*/
|
|
34
|
+
sanitizeMappings?: (mappings: IGoodmockMapping[]) => IGoodmockMapping[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* @internal
|
|
38
|
+
* Snapshot goodmock recordings, save the combined mappings to a JSON file
|
|
39
|
+
* on disk, and sanitize any credentials from the output.
|
|
40
|
+
*
|
|
41
|
+
* A single snapshot is taken with repeatsAsScenarios enabled: goodmock turns a
|
|
42
|
+
* request into a scenario chain only when its responses differ across the
|
|
43
|
+
* recording, so stateful sequences are preserved and everything else stays flat.
|
|
44
|
+
*
|
|
45
|
+
* @param host - Goodmock host:port (e.g. "backend-mock:8080")
|
|
46
|
+
* @param mappingFilePath - Absolute path to write the mapping JSON file to
|
|
47
|
+
* @param options - See {@link ISnapshotAndSaveRecordingOptions}.
|
|
48
|
+
*/
|
|
49
|
+
export declare function snapshotAndSaveRecording(host: string, mappingFilePath: string, options?: ISnapshotAndSaveRecordingOptions): Promise<void>;
|
|
50
|
+
//# sourceMappingURL=snapshot-and-save-recording.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-and-save-recording.d.ts","sourceRoot":"","sources":["../../../../src/create-test/goodmock/snapshot/snapshot-and-save-recording.ts"],"names":[],"mappings":"AAKA,OAAO,EACH,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EAC3B,MAAM,aAAa,CAAC;AASrB;;GAEG;AACH,MAAM,WAAW,gCAAgC;IAC7C,+EAA+E;IAC/E,mBAAmB,CAAC,EAAE,mBAAmB,GAAG,mBAAmB,EAAE,CAAC;IAClE;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gGAAgG;IAChG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,cAAc,EAAE,CAAC;IAClC;;;;OAIG;IACH,YAAY,CAAC,EAAE,YAAY,EAAE,CAAC;IAC9B;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,CAAC;CAC3E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,wBAAwB,CAC1C,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,MAAM,EACvB,OAAO,GAAE,gCAAqC,GAC/C,OAAO,CAAC,IAAI,CAAC,CAoDf"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
import { assertNoLeakPatterns } from "./assert-no-leak-patterns.js";
|
|
5
|
+
import { assertNoSecretLeaks } from "./assert-no-secret-leaks.js";
|
|
6
|
+
import { replaceSecrets } from "./replace-secrets.js";
|
|
7
|
+
import { sanitizeCredentials } from "./sanitize-credentials.js";
|
|
8
|
+
import { sanitizeWorkspaceIds } from "./sanitize-workspace-ids.js";
|
|
9
|
+
import { snapshotParams } from "./snapshot-param.js";
|
|
10
|
+
/**
|
|
11
|
+
* @internal
|
|
12
|
+
* Snapshot goodmock recordings, save the combined mappings to a JSON file
|
|
13
|
+
* on disk, and sanitize any credentials from the output.
|
|
14
|
+
*
|
|
15
|
+
* A single snapshot is taken with repeatsAsScenarios enabled: goodmock turns a
|
|
16
|
+
* request into a scenario chain only when its responses differ across the
|
|
17
|
+
* recording, so stateful sequences are preserved and everything else stays flat.
|
|
18
|
+
*
|
|
19
|
+
* @param host - Goodmock host:port (e.g. "backend-mock:8080")
|
|
20
|
+
* @param mappingFilePath - Absolute path to write the mapping JSON file to
|
|
21
|
+
* @param options - See {@link ISnapshotAndSaveRecordingOptions}.
|
|
22
|
+
*/
|
|
23
|
+
export async function snapshotAndSaveRecording(host, mappingFilePath, options = {}) {
|
|
24
|
+
const { workspaceIdMappings, backendHost, baseUrl, secretMappings, leakPatterns, sanitizeMappings } = options;
|
|
25
|
+
// Snapshot everything with scenarios enabled. goodmock only emits a scenario
|
|
26
|
+
// chain when a given request produced *different* responses across the
|
|
27
|
+
// recording; identical repeats collapse to a single mapping. This captures
|
|
28
|
+
// stateful sequences (e.g. a GET whose result changes after a POST) that the
|
|
29
|
+
// old per-URL dedup path silently dropped, while keeping idempotent traffic flat.
|
|
30
|
+
const snapshotRes = await fetch(`http://${host}/__admin/recordings/snapshot`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: { "Content-Type": "application/json" },
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
repeatsAsScenarios: true,
|
|
35
|
+
persist: false,
|
|
36
|
+
...snapshotParams,
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
const snapshotData = (await snapshotRes.json());
|
|
40
|
+
const mappings = [...snapshotData.mappings];
|
|
41
|
+
// An empty snapshot means the recording journal was lost (e.g. a worker crash re-ran the
|
|
42
|
+
// beforeAll reset mid-spec) or the spec made no requests at all. Never overwrite a previous
|
|
43
|
+
// recording with an empty one, and fail the run so the broken recording cannot go unnoticed.
|
|
44
|
+
if (mappings.length === 0) {
|
|
45
|
+
throw new Error(`Recording snapshot for ${mappingFilePath} is empty — keeping the existing mapping ` +
|
|
46
|
+
`file. Investigate (worker crash mid-spec? goodmock proxy misconfigured?) and re-record.`);
|
|
47
|
+
}
|
|
48
|
+
sanitizeCredentials(mappings);
|
|
49
|
+
// Consumer-supplied redaction for secrets this module has no knowledge of (e.g. an LLM
|
|
50
|
+
// provider's API key echoed back on a settings entity).
|
|
51
|
+
const sanitizedByCaller = sanitizeMappings ? sanitizeMappings(mappings) : mappings;
|
|
52
|
+
const sanitizedMappings = sanitizeWorkspaceIds(sanitizedByCaller, workspaceIdMappings);
|
|
53
|
+
let output = JSON.stringify({ mappings: sanitizedMappings }, null, 4) + "\n";
|
|
54
|
+
if (backendHost && baseUrl) {
|
|
55
|
+
output = output.replaceAll(backendHost, baseUrl);
|
|
56
|
+
}
|
|
57
|
+
output = replaceSecrets(output, secretMappings);
|
|
58
|
+
assertNoSecretLeaks(output, secretMappings, mappingFilePath);
|
|
59
|
+
assertNoLeakPatterns(output, leakPatterns, mappingFilePath);
|
|
60
|
+
mkdirSync(dirname(mappingFilePath), { recursive: true });
|
|
61
|
+
writeFileSync(mappingFilePath, output);
|
|
62
|
+
// oxlint-disable-next-line eslint-js/no-console
|
|
63
|
+
console.log(`Recording saved to ${mappingFilePath} (${sanitizedMappings.length} mappings)`);
|
|
64
|
+
}
|