@ondc/automation-mock-runner 1.3.43 → 1.3.45
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/dist/lib/MockRunner.d.ts +11 -2
- package/dist/lib/MockRunner.js +29 -2
- package/dist/lib/configHelper.js +21 -64
- package/dist/lib/constants/function-registry.js +1 -1
- package/dist/lib/helpers/default-helpers-source.d.ts +1 -0
- package/dist/lib/helpers/default-helpers-source.js +221 -0
- package/dist/lib/helpers/default-helpers.d.ts +83 -0
- package/dist/lib/helpers/default-helpers.js +200 -0
- package/dist/lib/helpers/default-helpers.test.d.ts +9 -0
- package/dist/lib/helpers/default-helpers.test.js +265 -0
- package/dist/lib/helpers/index.d.ts +1 -0
- package/dist/lib/helpers/index.js +10 -0
- package/dist/lib/runners/node-runner.d.ts +15 -6
- package/dist/lib/runners/node-runner.js +4 -0
- package/dist/lib/runners/runner-factory.d.ts +3 -2
- package/dist/lib/validators/code-validator.js +3 -1
- package/dist/test/GenerateFetchAndTimeout.test.d.ts +4 -0
- package/dist/test/GenerateFetchAndTimeout.test.js +240 -0
- package/package.json +5 -2
- package/public/node-worker.js +98 -6
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Default helpers available to every `generate()` in a mock step.
|
|
4
|
+
*
|
|
5
|
+
* Authoring rules:
|
|
6
|
+
* 1. Prefer `function` declarations — they hoist, so cross-helper calls
|
|
7
|
+
* work regardless of order.
|
|
8
|
+
* 2. No `require` / `import` inside function bodies. The VM sandbox has no
|
|
9
|
+
* module system. Only sandbox-whitelisted globals (Math, Date, JSON, …)
|
|
10
|
+
* and sibling helpers are in scope.
|
|
11
|
+
* 3. If the helper needs request-scope data, take `sessionData` as an
|
|
12
|
+
* explicit parameter. Free-variable references to `sessionData` do NOT
|
|
13
|
+
* resolve at runtime — helpers run at script scope, `sessionData` is
|
|
14
|
+
* only a parameter of `generate()`.
|
|
15
|
+
* 4. Document every helper with a leading JSDoc block (`@param`, `@returns`).
|
|
16
|
+
*
|
|
17
|
+
* The full file (minus the trailing `module.exports = {...}` block) is
|
|
18
|
+
* embedded verbatim into the sandbox bundle by
|
|
19
|
+
* `scripts/generate-helpers-source.js`, so JSDoc reaches end users unchanged.
|
|
20
|
+
* Re-run `npm run helpers:gen` after editing.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the BPP or BAP subscriber URL from session data.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} sessionData session data (reads `bppUri` or `bapUri`)
|
|
26
|
+
* @param {"bpp"|"bap"|string} type subscriber kind; anything other than "bpp" returns bapUri
|
|
27
|
+
* @returns {string} the subscriber URL
|
|
28
|
+
*/
|
|
29
|
+
function getSubscriberUrl(sessionData, type) {
|
|
30
|
+
if (type === "bpp") {
|
|
31
|
+
return sessionData.bppUri;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
return sessionData.bapUri;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Generate a UUID v4 (RFC 4122, random-based).
|
|
39
|
+
*
|
|
40
|
+
* @returns {string} a new UUID v4, e.g. "550e8400-e29b-41d4-a716-446655440000"
|
|
41
|
+
*/
|
|
42
|
+
function uuidv4() {
|
|
43
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
|
44
|
+
const r = (Math.random() * 16) | 0;
|
|
45
|
+
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
46
|
+
return v.toString(16);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Generate a 6-digit numeric string ID in [100000, 999999].
|
|
51
|
+
*
|
|
52
|
+
* @returns {string} a zero-padded 6-digit numeric string
|
|
53
|
+
*/
|
|
54
|
+
function generate6DigitId() {
|
|
55
|
+
return Math.floor(100000 + Math.random() * 900000).toString();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get the current ISO-8601 UTC timestamp.
|
|
59
|
+
*
|
|
60
|
+
* @returns {string} e.g. "2026-04-23T12:34:56.789Z"
|
|
61
|
+
*/
|
|
62
|
+
function currentTimestamp() {
|
|
63
|
+
return new Date().toISOString();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Convert an ISO 8601 duration string (e.g. "PT1H30M", "P2DT3H") to total seconds.
|
|
67
|
+
*
|
|
68
|
+
* Approximations used: 1 week = 7 days, 1 month ≈ 30.42 days (2628288 sec),
|
|
69
|
+
* 1 year = 365 days. Not calendar-exact.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} duration ISO 8601 duration string
|
|
72
|
+
* @returns {number} total seconds; 0 when the input is unparseable
|
|
73
|
+
*/
|
|
74
|
+
function isoDurToSec(duration) {
|
|
75
|
+
const durRE = /P((\d+)Y)?((\d+)M)?((\d+)W)?((\d+)D)?T?((\d+)H)?((\d+)M)?((\d+)S)?/;
|
|
76
|
+
const s = durRE.exec(duration);
|
|
77
|
+
if (!s)
|
|
78
|
+
return 0;
|
|
79
|
+
return ((Number(s?.[2]) || 0) * 31536000 +
|
|
80
|
+
(Number(s?.[4]) || 0) * 2628288 +
|
|
81
|
+
(Number(s?.[6]) || 0) * 604800 +
|
|
82
|
+
(Number(s?.[8]) || 0) * 86400 +
|
|
83
|
+
(Number(s?.[10]) || 0) * 3600 +
|
|
84
|
+
(Number(s?.[12]) || 0) * 60 +
|
|
85
|
+
(Number(s?.[14]) || 0));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Mutate `payload.context` in place to set the city code from `inputs.city_code`.
|
|
89
|
+
*
|
|
90
|
+
* Version-aware: ONDC v1.x uses flat `context.city`, v2.x uses nested
|
|
91
|
+
* `context.location.city.code`. Falls back to "*" when `city_code` is missing.
|
|
92
|
+
* No-op when `inputs` is falsy.
|
|
93
|
+
*
|
|
94
|
+
* @param {Object} payload payload with a `context` to mutate
|
|
95
|
+
* @param {Object|null|undefined} inputs object with optional `city_code`
|
|
96
|
+
* @returns {string|undefined} "*" when inputs is falsy; otherwise undefined
|
|
97
|
+
*/
|
|
98
|
+
function setCityFromInputs(payload, inputs) {
|
|
99
|
+
if (!inputs)
|
|
100
|
+
return "*";
|
|
101
|
+
const version = payload.context.version || payload.context.core_version || "2.0.0";
|
|
102
|
+
if (version.startsWith("1")) {
|
|
103
|
+
payload.context.city = inputs.city_code ?? "*";
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
payload.context.location.city.code = inputs.city_code ?? "*";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Build a form submission URL from session data.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} domain ONDC domain (e.g. "ONDC:RET10")
|
|
113
|
+
* @param {string} formId form identifier
|
|
114
|
+
* @param {Object} sessionData reads `mockBaseUrl`, `transactionId[0]`, `sessionId`
|
|
115
|
+
* @returns {string} `${baseURL}/forms/${domain}/${formId}/?transaction_id=...&session_id=...`
|
|
116
|
+
*/
|
|
117
|
+
function createFormURL(domain, formId, sessionData) {
|
|
118
|
+
const baseURL = sessionData.mockBaseUrl;
|
|
119
|
+
const transactionId = sessionData.transactionId[0];
|
|
120
|
+
const sessionId = sessionData.sessionId;
|
|
121
|
+
return `${baseURL}/forms/${domain}/${formId}/?transaction_id=${transactionId}&session_id=${sessionId}`;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Generate a consent handler from the Finvu AA Service.
|
|
125
|
+
*
|
|
126
|
+
* Reads the service base URL from `sessionData.finvuUrl` — the installing
|
|
127
|
+
* service MUST include that origin in
|
|
128
|
+
* MockRunner.initSharedRunner({ allowedFetchBaseUrls: [...] })
|
|
129
|
+
* otherwise the sandboxed fetch will be blocked.
|
|
130
|
+
*
|
|
131
|
+
* Times out after 10s via AbortController.
|
|
132
|
+
*
|
|
133
|
+
* @param {Object} sessionData session data; `sessionData.finvuUrl` is required
|
|
134
|
+
* @param {Object} params
|
|
135
|
+
* @param {string} params.custId customer ID (required)
|
|
136
|
+
* @param {string} [params.templateName] defaults to "FINVUDEMO_TESTING"
|
|
137
|
+
* @param {string} [params.consentDescription] defaults to "Gold Loan Account Aggregator Consent"
|
|
138
|
+
* @param {string} [params.redirectUrl] defaults to "https://google.co.in"
|
|
139
|
+
* @returns {Promise<string>} the `consentHandler` returned by the AA service
|
|
140
|
+
* @throws {Error} when `custId` or `sessionData.finvuUrl` is missing, on non-OK
|
|
141
|
+
* response, on missing `consentHandler` in the body, or on 10s timeout
|
|
142
|
+
*/
|
|
143
|
+
async function generateConsentHandler(sessionData, { custId, templateName = "FINVUDEMO_TESTING", consentDescription = "Gold Loan Account Aggregator Consent", redirectUrl = "https://google.co.in", }) {
|
|
144
|
+
if (!custId) {
|
|
145
|
+
throw new Error("custId is required");
|
|
146
|
+
}
|
|
147
|
+
const baseUrl = sessionData && sessionData.finvuUrl;
|
|
148
|
+
if (!baseUrl) {
|
|
149
|
+
throw new Error("sessionData.finvuUrl is required");
|
|
150
|
+
}
|
|
151
|
+
const url = `${baseUrl}/finvu-aa/consent/generate`;
|
|
152
|
+
const payload = {
|
|
153
|
+
custId,
|
|
154
|
+
templateName,
|
|
155
|
+
consentDescription,
|
|
156
|
+
redirectUrl,
|
|
157
|
+
};
|
|
158
|
+
console.log("Calling Finvu AA Service:", url);
|
|
159
|
+
console.log("Consent request payload:", payload);
|
|
160
|
+
const controller = new AbortController();
|
|
161
|
+
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
162
|
+
try {
|
|
163
|
+
const res = await fetch(url, {
|
|
164
|
+
method: "POST",
|
|
165
|
+
headers: {
|
|
166
|
+
"Content-Type": "application/json",
|
|
167
|
+
},
|
|
168
|
+
body: JSON.stringify(payload),
|
|
169
|
+
signal: controller.signal,
|
|
170
|
+
});
|
|
171
|
+
if (!res.ok) {
|
|
172
|
+
const text = await res.text();
|
|
173
|
+
throw new Error(`Request failed: ${res.status} ${text}`);
|
|
174
|
+
}
|
|
175
|
+
const data = await res.json();
|
|
176
|
+
if (!data || !data.consentHandler) {
|
|
177
|
+
throw new Error("Invalid response: consentHandler missing");
|
|
178
|
+
}
|
|
179
|
+
return data.consentHandler;
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
if (err && err.name === "AbortError") {
|
|
183
|
+
throw new Error("Request timed out after 10 seconds");
|
|
184
|
+
}
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
clearTimeout(timeout);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
module.exports = {
|
|
192
|
+
getSubscriberUrl,
|
|
193
|
+
uuidv4,
|
|
194
|
+
generate6DigitId,
|
|
195
|
+
currentTimestamp,
|
|
196
|
+
isoDurToSec,
|
|
197
|
+
setCityFromInputs,
|
|
198
|
+
createFormURL,
|
|
199
|
+
generateConsentHandler,
|
|
200
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the default helper functions + bundle integrity.
|
|
3
|
+
*
|
|
4
|
+
* Unit tests import the functions as a normal CJS module and call them.
|
|
5
|
+
* The bundle test loads DEFAULT_HELPER_LIB into a fresh vm context — this
|
|
6
|
+
* simulates how the string is actually used in the worker sandbox and
|
|
7
|
+
* catches any stringification / hoisting / free-variable regressions.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for the default helper functions + bundle integrity.
|
|
4
|
+
*
|
|
5
|
+
* Unit tests import the functions as a normal CJS module and call them.
|
|
6
|
+
* The bundle test loads DEFAULT_HELPER_LIB into a fresh vm context — this
|
|
7
|
+
* simulates how the string is actually used in the worker sandbox and
|
|
8
|
+
* catches any stringification / hoisting / free-variable regressions.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
const vm = __importStar(require("vm"));
|
|
45
|
+
const default_helpers_1 = require("./default-helpers");
|
|
46
|
+
const index_1 = require("./index");
|
|
47
|
+
describe("default helpers — unit", () => {
|
|
48
|
+
describe("uuidv4", () => {
|
|
49
|
+
it("matches the RFC 4122 v4 shape", () => {
|
|
50
|
+
expect((0, default_helpers_1.uuidv4)()).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
51
|
+
});
|
|
52
|
+
it("produces distinct values", () => {
|
|
53
|
+
expect((0, default_helpers_1.uuidv4)()).not.toBe((0, default_helpers_1.uuidv4)());
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe("generate6DigitId", () => {
|
|
57
|
+
it("returns a 6-digit string in [100000, 999999]", () => {
|
|
58
|
+
for (let i = 0; i < 50; i++) {
|
|
59
|
+
const id = (0, default_helpers_1.generate6DigitId)();
|
|
60
|
+
expect(id).toMatch(/^\d{6}$/);
|
|
61
|
+
const n = parseInt(id, 10);
|
|
62
|
+
expect(n).toBeGreaterThanOrEqual(100000);
|
|
63
|
+
expect(n).toBeLessThanOrEqual(999999);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe("currentTimestamp", () => {
|
|
68
|
+
it("returns a parseable ISO timestamp close to now", () => {
|
|
69
|
+
const before = Date.now();
|
|
70
|
+
const ts = (0, default_helpers_1.currentTimestamp)();
|
|
71
|
+
const after = Date.now();
|
|
72
|
+
const parsed = Date.parse(ts);
|
|
73
|
+
expect(parsed).toBeGreaterThanOrEqual(before);
|
|
74
|
+
expect(parsed).toBeLessThanOrEqual(after);
|
|
75
|
+
expect(ts.endsWith("Z")).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe("isoDurToSec", () => {
|
|
79
|
+
it.each([
|
|
80
|
+
["PT30S", 30],
|
|
81
|
+
["PT5M", 300],
|
|
82
|
+
["PT1H", 3600],
|
|
83
|
+
["P1D", 86400],
|
|
84
|
+
["PT1H30M", 5400],
|
|
85
|
+
["PT0S", 0],
|
|
86
|
+
])("%s → %d sec", (dur, expected) => {
|
|
87
|
+
expect((0, default_helpers_1.isoDurToSec)(dur)).toBe(expected);
|
|
88
|
+
});
|
|
89
|
+
it("returns 0 on unparseable input", () => {
|
|
90
|
+
expect((0, default_helpers_1.isoDurToSec)("not-a-duration")).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe("setCityFromInputs", () => {
|
|
94
|
+
it("writes flat context.city for v1.x", () => {
|
|
95
|
+
const payload = { context: { core_version: "1.2.0", city: "*" } };
|
|
96
|
+
(0, default_helpers_1.setCityFromInputs)(payload, { city_code: "std:080" });
|
|
97
|
+
expect(payload.context.city).toBe("std:080");
|
|
98
|
+
});
|
|
99
|
+
it("writes nested context.location.city.code for v2.x", () => {
|
|
100
|
+
const payload = {
|
|
101
|
+
context: { version: "2.0.0", location: { city: { code: "*" } } },
|
|
102
|
+
};
|
|
103
|
+
(0, default_helpers_1.setCityFromInputs)(payload, { city_code: "std:011" });
|
|
104
|
+
expect(payload.context.location.city.code).toBe("std:011");
|
|
105
|
+
});
|
|
106
|
+
it("falls back to '*' when city_code is missing", () => {
|
|
107
|
+
const payload = {
|
|
108
|
+
context: { version: "2.0.0", location: { city: { code: "foo" } } },
|
|
109
|
+
};
|
|
110
|
+
(0, default_helpers_1.setCityFromInputs)(payload, {});
|
|
111
|
+
expect(payload.context.location.city.code).toBe("*");
|
|
112
|
+
});
|
|
113
|
+
it("no-ops when inputs is falsy", () => {
|
|
114
|
+
const payload = {
|
|
115
|
+
context: { version: "2.0.0", location: { city: { code: "keep" } } },
|
|
116
|
+
};
|
|
117
|
+
(0, default_helpers_1.setCityFromInputs)(payload, null);
|
|
118
|
+
expect(payload.context.location.city.code).toBe("keep");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe("createFormURL", () => {
|
|
122
|
+
it("interpolates domain, formId, transactionId, sessionId", () => {
|
|
123
|
+
const url = (0, default_helpers_1.createFormURL)("ONDC:RET10", "FORM_A", {
|
|
124
|
+
mockBaseUrl: "https://mocks.example.com",
|
|
125
|
+
transactionId: ["txn-xyz"],
|
|
126
|
+
sessionId: "sess-1",
|
|
127
|
+
});
|
|
128
|
+
expect(url).toBe("https://mocks.example.com/forms/ONDC:RET10/FORM_A/?transaction_id=txn-xyz&session_id=sess-1");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe("getSubscriberUrl", () => {
|
|
132
|
+
const sessionData = { bppUri: "https://bpp.example.com", bapUri: "https://bap.example.com" };
|
|
133
|
+
it("returns bppUri when type is 'bpp'", () => {
|
|
134
|
+
expect((0, default_helpers_1.getSubscriberUrl)(sessionData, "bpp")).toBe("https://bpp.example.com");
|
|
135
|
+
});
|
|
136
|
+
it("returns bapUri otherwise", () => {
|
|
137
|
+
expect((0, default_helpers_1.getSubscriberUrl)(sessionData, "bap")).toBe("https://bap.example.com");
|
|
138
|
+
expect((0, default_helpers_1.getSubscriberUrl)(sessionData, "anything-else")).toBe("https://bap.example.com");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
describe("generateConsentHandler", () => {
|
|
142
|
+
function fakeResponse(init) {
|
|
143
|
+
return {
|
|
144
|
+
ok: init.ok,
|
|
145
|
+
status: init.status ?? (init.ok ? 200 : 500),
|
|
146
|
+
json: init.json ?? (async () => ({})),
|
|
147
|
+
text: init.text ?? (async () => ""),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
let fetchSpy;
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
fetchSpy = jest.spyOn(global, "fetch");
|
|
153
|
+
});
|
|
154
|
+
afterEach(() => {
|
|
155
|
+
fetchSpy.mockRestore();
|
|
156
|
+
});
|
|
157
|
+
it("throws when custId is missing", async () => {
|
|
158
|
+
await expect((0, default_helpers_1.generateConsentHandler)({ finvuUrl: "http://finvu" }, {})).rejects.toThrow("custId is required");
|
|
159
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
it("throws when sessionData.finvuUrl is missing", async () => {
|
|
162
|
+
await expect((0, default_helpers_1.generateConsentHandler)({}, { custId: "c1" })).rejects.toThrow("sessionData.finvuUrl is required");
|
|
163
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
it("POSTs JSON to /finvu-aa/consent/generate and returns consentHandler", async () => {
|
|
166
|
+
fetchSpy.mockResolvedValueOnce(fakeResponse({
|
|
167
|
+
ok: true,
|
|
168
|
+
json: async () => ({ consentHandler: "HANDLE-123" }),
|
|
169
|
+
}));
|
|
170
|
+
const result = await (0, default_helpers_1.generateConsentHandler)({ finvuUrl: "http://finvu.local:3002" }, { custId: "cust-42" });
|
|
171
|
+
expect(result).toBe("HANDLE-123");
|
|
172
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
173
|
+
const [url, init] = fetchSpy.mock.calls[0];
|
|
174
|
+
expect(url).toBe("http://finvu.local:3002/finvu-aa/consent/generate");
|
|
175
|
+
expect(init?.method).toBe("POST");
|
|
176
|
+
expect((init?.headers)["Content-Type"]).toBe("application/json");
|
|
177
|
+
const body = JSON.parse(init?.body);
|
|
178
|
+
expect(body).toEqual({
|
|
179
|
+
custId: "cust-42",
|
|
180
|
+
templateName: "FINVUDEMO_TESTING",
|
|
181
|
+
consentDescription: "Gold Loan Account Aggregator Consent",
|
|
182
|
+
redirectUrl: "https://google.co.in",
|
|
183
|
+
});
|
|
184
|
+
expect(init?.signal).toBeDefined();
|
|
185
|
+
});
|
|
186
|
+
it("throws 'Request failed' on non-OK responses", async () => {
|
|
187
|
+
fetchSpy.mockResolvedValueOnce(fakeResponse({
|
|
188
|
+
ok: false,
|
|
189
|
+
status: 503,
|
|
190
|
+
text: async () => "Service Unavailable",
|
|
191
|
+
}));
|
|
192
|
+
await expect((0, default_helpers_1.generateConsentHandler)({ finvuUrl: "http://finvu.local" }, { custId: "c1" })).rejects.toThrow("Request failed: 503 Service Unavailable");
|
|
193
|
+
});
|
|
194
|
+
it("throws when consentHandler is missing from response body", async () => {
|
|
195
|
+
fetchSpy.mockResolvedValueOnce(fakeResponse({
|
|
196
|
+
ok: true,
|
|
197
|
+
json: async () => ({ somethingElse: true }),
|
|
198
|
+
}));
|
|
199
|
+
await expect((0, default_helpers_1.generateConsentHandler)({ finvuUrl: "http://finvu.local" }, { custId: "c1" })).rejects.toThrow("Invalid response: consentHandler missing");
|
|
200
|
+
});
|
|
201
|
+
it("surfaces AbortError as a timeout message", async () => {
|
|
202
|
+
fetchSpy.mockImplementationOnce(async () => {
|
|
203
|
+
const err = new Error("aborted");
|
|
204
|
+
err.name = "AbortError";
|
|
205
|
+
throw err;
|
|
206
|
+
});
|
|
207
|
+
await expect((0, default_helpers_1.generateConsentHandler)({ finvuUrl: "http://finvu.local" }, { custId: "c1" })).rejects.toThrow("Request timed out after 10 seconds");
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
describe("DEFAULT_HELPER_LIB bundle", () => {
|
|
212
|
+
const EXPECTED_NAMES = [
|
|
213
|
+
"getSubscriberUrl",
|
|
214
|
+
"uuidv4",
|
|
215
|
+
"generate6DigitId",
|
|
216
|
+
"currentTimestamp",
|
|
217
|
+
"isoDurToSec",
|
|
218
|
+
"setCityFromInputs",
|
|
219
|
+
"createFormURL",
|
|
220
|
+
"generateConsentHandler",
|
|
221
|
+
];
|
|
222
|
+
function loadBundle() {
|
|
223
|
+
const sandbox = {};
|
|
224
|
+
vm.createContext(sandbox);
|
|
225
|
+
vm.runInContext(index_1.DEFAULT_HELPER_LIB, sandbox);
|
|
226
|
+
return sandbox;
|
|
227
|
+
}
|
|
228
|
+
it("does not leak `module.exports` or `require` into the bundle source", () => {
|
|
229
|
+
// Statement-level `module.exports = ...` (line-anchored) would throw
|
|
230
|
+
// ReferenceError in the sandbox. Mentions inside comments are harmless.
|
|
231
|
+
expect(index_1.DEFAULT_HELPER_LIB).not.toMatch(/^module\.exports\s*=/m);
|
|
232
|
+
expect(index_1.DEFAULT_HELPER_LIB).not.toMatch(/\brequire\s*\(/);
|
|
233
|
+
});
|
|
234
|
+
it("declares every expected helper as a function", () => {
|
|
235
|
+
const sandbox = loadBundle();
|
|
236
|
+
for (const name of EXPECTED_NAMES) {
|
|
237
|
+
expect(typeof sandbox[name]).toBe("function");
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
it("round-trips key helpers when executed in the vm context", () => {
|
|
241
|
+
const sandbox = loadBundle();
|
|
242
|
+
expect(sandbox.uuidv4()).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
243
|
+
expect(sandbox.generate6DigitId()).toMatch(/^\d{6}$/);
|
|
244
|
+
expect(sandbox.isoDurToSec("PT1H")).toBe(3600);
|
|
245
|
+
const payload = {
|
|
246
|
+
context: { version: "2.0.0", location: { city: { code: "*" } } },
|
|
247
|
+
};
|
|
248
|
+
sandbox.setCityFromInputs(payload, { city_code: "std:022" });
|
|
249
|
+
expect(payload.context.location.city.code).toBe("std:022");
|
|
250
|
+
expect(sandbox.getSubscriberUrl({ bppUri: "http://bpp" }, "bpp")).toBe("http://bpp");
|
|
251
|
+
});
|
|
252
|
+
it("preserves in-body docs so playground users see them", () => {
|
|
253
|
+
// Guard against a future refactor silently dropping the in-body comments
|
|
254
|
+
// the way the pre-refactor .toString() pattern did.
|
|
255
|
+
expect(index_1.DEFAULT_HELPER_LIB).toContain("Generate a UUID v4");
|
|
256
|
+
expect(index_1.DEFAULT_HELPER_LIB).toContain("ISO 8601 duration");
|
|
257
|
+
expect(index_1.DEFAULT_HELPER_LIB).toContain("Finvu AA Service");
|
|
258
|
+
});
|
|
259
|
+
it("preserves source fidelity (no bundler lowering / numeric packing)", () => {
|
|
260
|
+
// If a future build step reintroduces fn.toString() or otherwise lets a
|
|
261
|
+
// minifier touch the helper bodies, these canaries break.
|
|
262
|
+
expect(index_1.DEFAULT_HELPER_LIB).toContain("s?.[2]");
|
|
263
|
+
expect(index_1.DEFAULT_HELPER_LIB).toContain("100000 + Math.random() * 900000");
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DEFAULT_HELPER_LIB: string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_HELPER_LIB = void 0;
|
|
4
|
+
const default_helpers_source_1 = require("./default-helpers-source");
|
|
5
|
+
const HEADER = `/*
|
|
6
|
+
Custom helper functions available in all mock generate() functions.
|
|
7
|
+
Source: src/lib/helpers/default-helpers.js — edit there, then run
|
|
8
|
+
\`npm run helpers:gen\` to refresh.
|
|
9
|
+
*/`;
|
|
10
|
+
exports.DEFAULT_HELPER_LIB = HEADER + "\n\n" + default_helpers_source_1.DEFAULT_HELPERS_RAW;
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { FunctionSchema } from "../constants/function-registry";
|
|
2
2
|
import { ExecutionResult } from "../types/execution-results";
|
|
3
3
|
import { BaseCodeRunner } from "./base-runner";
|
|
4
|
+
export interface NodeRunnerOptions {
|
|
5
|
+
maxMemoryMB?: number;
|
|
6
|
+
poolSize?: number;
|
|
7
|
+
maxExecutionsPerWorker?: number;
|
|
8
|
+
maxWorkerAgeMs?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Base URLs that `generate` functions may fetch. Each entry is parsed with
|
|
11
|
+
* `new URL(entry)`; a request is allowed iff its origin matches the entry's
|
|
12
|
+
* origin AND its pathname is a strict segment-prefix of the entry's pathname.
|
|
13
|
+
* Absent / empty → fetch is not injected (blocked).
|
|
14
|
+
*/
|
|
15
|
+
allowedFetchBaseUrls?: string[];
|
|
16
|
+
}
|
|
4
17
|
export declare class NodeRunner implements BaseCodeRunner {
|
|
5
18
|
private pool;
|
|
6
19
|
private waitQueue;
|
|
@@ -11,12 +24,8 @@ export declare class NodeRunner implements BaseCodeRunner {
|
|
|
11
24
|
private readonly poolSize;
|
|
12
25
|
private readonly maxExecutionsPerWorker;
|
|
13
26
|
private readonly maxWorkerAgeMs;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
poolSize?: number;
|
|
17
|
-
maxExecutionsPerWorker?: number;
|
|
18
|
-
maxWorkerAgeMs?: number;
|
|
19
|
-
});
|
|
27
|
+
private readonly allowedFetchBaseUrls;
|
|
28
|
+
constructor(options?: NodeRunnerOptions);
|
|
20
29
|
private createPooledWorker;
|
|
21
30
|
/**
|
|
22
31
|
* Terminate old worker (frees its V8 isolate) and replace with a fresh one.
|
|
@@ -52,6 +52,7 @@ class NodeRunner {
|
|
|
52
52
|
this.maxExecutionsPerWorker =
|
|
53
53
|
options.maxExecutionsPerWorker || MAX_EXECUTIONS_PER_WORKER;
|
|
54
54
|
this.maxWorkerAgeMs = options.maxWorkerAgeMs || MAX_WORKER_AGE_MS;
|
|
55
|
+
this.allowedFetchBaseUrls = options.allowedFetchBaseUrls ?? [];
|
|
55
56
|
// Pre-warm the pool
|
|
56
57
|
for (let i = 0; i < this.poolSize; i++) {
|
|
57
58
|
this.pool.push(this.createPooledWorker());
|
|
@@ -66,6 +67,9 @@ class NodeRunner {
|
|
|
66
67
|
codeRangeSizeMb: 16,
|
|
67
68
|
},
|
|
68
69
|
env: {},
|
|
70
|
+
workerData: {
|
|
71
|
+
allowedFetchBaseUrls: this.allowedFetchBaseUrls,
|
|
72
|
+
},
|
|
69
73
|
});
|
|
70
74
|
const pw = {
|
|
71
75
|
worker,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Worker } from "worker_threads";
|
|
2
2
|
import { BrowserRunner } from "./browser-runner";
|
|
3
|
-
import { NodeRunner } from "./node-runner";
|
|
3
|
+
import { NodeRunner, NodeRunnerOptions } from "./node-runner";
|
|
4
|
+
export type RunnerOptions = NodeRunnerOptions;
|
|
4
5
|
/**
|
|
5
6
|
* Factory for creating workers in both browser and Node.js environments
|
|
6
7
|
*/
|
|
@@ -21,5 +22,5 @@ export declare class RunnerFactory {
|
|
|
21
22
|
/**
|
|
22
23
|
* Creates appropriate runner based on environment
|
|
23
24
|
*/
|
|
24
|
-
static createRunner(options?:
|
|
25
|
+
static createRunner(options?: RunnerOptions, logger?: any): BrowserRunner | NodeRunner;
|
|
25
26
|
}
|
|
@@ -316,7 +316,9 @@ CodeValidator.FORBIDDEN_GLOBALS = [
|
|
|
316
316
|
"SharedWorker",
|
|
317
317
|
"WebSocket",
|
|
318
318
|
"XMLHttpRequest",
|
|
319
|
-
|
|
319
|
+
// `fetch` is allowed past static analysis and gated at runtime in the
|
|
320
|
+
// worker sandbox: injected only for `generate` and only when the
|
|
321
|
+
// configured allowlist matches origin + path prefix.
|
|
320
322
|
];
|
|
321
323
|
CodeValidator.FORBIDDEN_PROPERTIES = [
|
|
322
324
|
"localStorage",
|