@gooddata/sdk-e2e-utils 11.60.0-alpha.3 → 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.
Files changed (89) hide show
  1. package/esm/create-test/feature-hub/mock.d.ts +5 -0
  2. package/esm/create-test/feature-hub/mock.d.ts.map +1 -0
  3. package/esm/create-test/feature-hub/mock.js +15 -0
  4. package/esm/create-test/feature-hub/types.d.ts +21 -0
  5. package/esm/create-test/feature-hub/types.d.ts.map +1 -0
  6. package/esm/create-test/feature-hub/types.js +2 -0
  7. package/esm/create-test/goodmock/admin.d.ts +34 -0
  8. package/esm/create-test/goodmock/admin.d.ts.map +1 -0
  9. package/esm/create-test/goodmock/admin.js +99 -0
  10. package/esm/create-test/goodmock/mode.d.ts +14 -0
  11. package/esm/create-test/goodmock/mode.d.ts.map +1 -0
  12. package/esm/create-test/goodmock/mode.js +17 -0
  13. package/esm/create-test/goodmock/playwright.d.ts +5 -0
  14. package/esm/create-test/goodmock/playwright.d.ts.map +1 -0
  15. package/esm/create-test/goodmock/playwright.js +37 -0
  16. package/esm/create-test/goodmock/snapshot/assert-no-leak-patterns.d.ts +9 -0
  17. package/esm/create-test/goodmock/snapshot/assert-no-leak-patterns.d.ts.map +1 -0
  18. package/esm/create-test/goodmock/snapshot/assert-no-leak-patterns.js +20 -0
  19. package/esm/create-test/goodmock/snapshot/assert-no-secret-leaks.d.ts +4 -0
  20. package/esm/create-test/goodmock/snapshot/assert-no-secret-leaks.d.ts.map +1 -0
  21. package/esm/create-test/goodmock/snapshot/assert-no-secret-leaks.js +14 -0
  22. package/esm/create-test/goodmock/snapshot/build-masked-form-pattern.d.ts +2 -0
  23. package/esm/create-test/goodmock/snapshot/build-masked-form-pattern.d.ts.map +1 -0
  24. package/esm/create-test/goodmock/snapshot/build-masked-form-pattern.js +17 -0
  25. package/esm/create-test/goodmock/snapshot/get-effective-secret-mappings.d.ts +3 -0
  26. package/esm/create-test/goodmock/snapshot/get-effective-secret-mappings.d.ts.map +1 -0
  27. package/esm/create-test/goodmock/snapshot/get-effective-secret-mappings.js +2 -0
  28. package/esm/create-test/goodmock/snapshot/replace-secrets.d.ts +10 -0
  29. package/esm/create-test/goodmock/snapshot/replace-secrets.d.ts.map +1 -0
  30. package/esm/create-test/goodmock/snapshot/replace-secrets.js +23 -0
  31. package/esm/create-test/goodmock/snapshot/sanitize-credentials.d.ts +4 -0
  32. package/esm/create-test/goodmock/snapshot/sanitize-credentials.d.ts.map +1 -0
  33. package/esm/create-test/goodmock/snapshot/sanitize-credentials.js +41 -0
  34. package/esm/create-test/goodmock/snapshot/sanitize-workspace-ids.d.ts +4 -0
  35. package/esm/create-test/goodmock/snapshot/sanitize-workspace-ids.d.ts.map +1 -0
  36. package/esm/create-test/goodmock/snapshot/sanitize-workspace-ids.js +21 -0
  37. package/esm/create-test/goodmock/snapshot/snapshot-and-save-recording.d.ts +50 -0
  38. package/esm/create-test/goodmock/snapshot/snapshot-and-save-recording.d.ts.map +1 -0
  39. package/esm/create-test/goodmock/snapshot/snapshot-and-save-recording.js +64 -0
  40. package/esm/create-test/goodmock/snapshot/snapshot-param.d.ts +11 -0
  41. package/esm/create-test/goodmock/snapshot/snapshot-param.d.ts.map +1 -0
  42. package/esm/create-test/goodmock/snapshot/snapshot-param.js +9 -0
  43. package/esm/create-test/goodmock/types.d.ts +60 -0
  44. package/esm/create-test/goodmock/types.d.ts.map +1 -0
  45. package/esm/create-test/goodmock/types.js +2 -0
  46. package/esm/create-test/playwright/call-original-fn.d.ts +17 -0
  47. package/esm/create-test/playwright/call-original-fn.d.ts.map +1 -0
  48. package/esm/create-test/playwright/call-original-fn.js +25 -0
  49. package/esm/create-test/playwright/describe/factory.d.ts +27 -0
  50. package/esm/create-test/playwright/describe/factory.d.ts.map +1 -0
  51. package/esm/create-test/playwright/describe/factory.js +18 -0
  52. package/esm/create-test/playwright/fixtures.d.ts +7 -0
  53. package/esm/create-test/playwright/fixtures.d.ts.map +1 -0
  54. package/esm/create-test/playwright/fixtures.js +2 -0
  55. package/esm/create-test/playwright/playwright-types.d.ts +42 -0
  56. package/esm/create-test/playwright/playwright-types.d.ts.map +1 -0
  57. package/esm/create-test/playwright/playwright-types.js +2 -0
  58. package/esm/create-test/playwright/test/factory.d.ts +7 -0
  59. package/esm/create-test/playwright/test/factory.d.ts.map +1 -0
  60. package/esm/create-test/playwright/test/factory.js +42 -0
  61. package/esm/create-test/playwright/test/types.d.ts +27 -0
  62. package/esm/create-test/playwright/test/types.d.ts.map +1 -0
  63. package/esm/create-test/playwright/test/types.js +2 -0
  64. package/esm/create-test/playwright/top-level-describe/build-suite-callback.d.ts +9 -0
  65. package/esm/create-test/playwright/top-level-describe/build-suite-callback.d.ts.map +1 -0
  66. package/esm/create-test/playwright/top-level-describe/build-suite-callback.js +17 -0
  67. package/esm/create-test/playwright/top-level-describe/factory.d.ts +8 -0
  68. package/esm/create-test/playwright/top-level-describe/factory.d.ts.map +1 -0
  69. package/esm/create-test/playwright/top-level-describe/factory.js +12 -0
  70. package/esm/create-test/playwright/top-level-describe/types.d.ts +17 -0
  71. package/esm/create-test/playwright/top-level-describe/types.d.ts.map +1 -0
  72. package/esm/create-test/playwright/top-level-describe/types.js +2 -0
  73. package/esm/create-test/playwright/types.d.ts +17 -0
  74. package/esm/create-test/playwright/types.d.ts.map +1 -0
  75. package/esm/create-test/playwright/types.js +2 -0
  76. package/esm/create-test/playwright/with-describe-details.d.ts +6 -0
  77. package/esm/create-test/playwright/with-describe-details.d.ts.map +1 -0
  78. package/esm/create-test/playwright/with-describe-details.js +39 -0
  79. package/esm/index.d.ts +14 -3
  80. package/esm/index.d.ts.map +1 -1
  81. package/esm/index.js +8 -3
  82. package/esm/sdk-e2e-utils.d.ts +112 -38
  83. package/package.json +3 -3
  84. package/esm/goodmock.d.ts +0 -138
  85. package/esm/goodmock.d.ts.map +0 -1
  86. package/esm/goodmock.js +0 -284
  87. package/esm/playwright.d.ts +0 -97
  88. package/esm/playwright.d.ts.map +0 -1
  89. 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,2 @@
1
+ // (C) 2026 GoodData Corporation
2
+ export {};
@@ -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,2 @@
1
+ export declare const buildMaskedFormPattern: (escapedSecret: string) => RegExp | undefined;
2
+ //# sourceMappingURL=build-masked-form-pattern.d.ts.map
@@ -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,3 @@
1
+ import { type ISecretMapping } from "../types.js";
2
+ export declare const getEffectiveSecretMappings: (secretMappings: ISecretMapping[] | undefined) => ISecretMapping[];
3
+ //# sourceMappingURL=get-effective-secret-mappings.d.ts.map
@@ -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,2 @@
1
+ // (C) 2026 GoodData Corporation
2
+ export const getEffectiveSecretMappings = (secretMappings) => (secretMappings ?? []).filter(({ secret, placeholder }) => secret && secret !== placeholder);
@@ -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,4 @@
1
+ import { type IGoodmockMapping } from "../types.js";
2
+ /** Sanitize credentials (login cookie, resolveSettings map tokens) in place. */
3
+ export declare function sanitizeCredentials(mappings: IGoodmockMapping[]): void;
4
+ //# sourceMappingURL=sanitize-credentials.d.ts.map
@@ -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
+ }