UNPKG

@metacall/faas

Version:

Reimplementation of MetaCall FaaS platform written in TypeScript.

189 lines (188 loc) 8.85 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const assert_1 = require("assert"); const child_process_1 = require("child_process"); const path_1 = __importDefault(require("path")); // Helper: build the envStringified object the same way deployProcess does. // This is a pure-function extraction of the logic we fixed, so we can unit-test // it without starting an actual metacall process. function buildEnv(env) { const envStringified = { ...process.env }; if (env) { for (const [key, value] of Object.entries(env)) { envStringified[key] = String(value); } } return envStringified; } // Helper: invoke a function that may be sync or async, just as the worker does. async function safeInvoke(fn, args) { try { const result = await fn(...args); return { result }; } catch (err) { return { error: String(err) }; } } // Fix: Environment Variable Injection // Ensures all user-supplied env values are stringified so spawn() never throws. describe('Fix: Environment Variable Injection', function () { it('should cast boolean env values to strings', () => { const input = {}; input['FEATURE_FLAG'] = String(true); const env = buildEnv(input); assert_1.strict.strictEqual(typeof env['FEATURE_FLAG'], 'string'); assert_1.strict.strictEqual(env['FEATURE_FLAG'], 'true'); }); it('should cast numeric env values to strings', () => { const input = {}; input['PORT'] = String(8080); const env = buildEnv(input); assert_1.strict.strictEqual(typeof env['PORT'], 'string'); assert_1.strict.strictEqual(env['PORT'], '8080'); }); it('should preserve string env values unchanged', () => { const input = {}; input['MY_SECRET'] = 'abc123'; const env = buildEnv(input); assert_1.strict.strictEqual(env['MY_SECRET'], 'abc123'); }); it('should merge user env on top of process.env without dropping keys', () => { const input = {}; input['TEST_VAR'] = 'hello'; const env = buildEnv(input); // process.env keys must still be present assert_1.strict.ok('PATH' in env || 'HOME' in env || 'USER' in env); // User key takes priority assert_1.strict.strictEqual(env['TEST_VAR'], 'hello'); }); it('should result in a plain object with only string values (no undefined)', () => { const input = {}; input['A'] = 'x'; input['B'] = String(42); const env = buildEnv(input); for (const v of Object.values(env)) { assert_1.strict.notStrictEqual(v, undefined); assert_1.strict.strictEqual(typeof v, 'string'); } }); }); // Fix: Package Resolution (cwd) // Ensures that a child process spawned with cwd set to the deployment directory // correctly resolves require() calls from that directory. // We test this without metacall by spawning a vanilla node process. describe('Fix: Package Resolution (cwd)', function () { this.timeout(10000); // allow time for child process const testAppDir = path_1.default.resolve(__dirname, '../../test/data/nodejs-base-app'); it('should resolve require() correctly when cwd is the deployment path', done => { const proc = child_process_1.spawn(process.execPath, [ '-e', 'const m = require("./index.js"); process.exit(m && typeof m.isPalindrome === "function" ? 0 : 1)' ], { cwd: testAppDir, stdio: ['pipe', 'pipe', 'pipe'] }); proc.on('exit', code => { assert_1.strict.strictEqual(code, 0, 'Child process should exit 0 when cwd is correct'); done(); }); proc.stderr?.on('data', (d) => { const msg = d.toString(); if (msg.includes('Error:') && !msg.includes('ExperimentalWarning')) { done(new Error(`stderr: ${msg}`)); } }); }); it('should fail to resolve require() when cwd is wrong (proving the fix is necessary)', done => { const wrongCwd = path_1.default.resolve(__dirname, '../../..'); const proc = child_process_1.spawn(process.execPath, ['-e', 'require("./index.js")'], { cwd: wrongCwd, stdio: ['pipe', 'pipe', 'pipe'] }); proc.on('exit', code => { assert_1.strict.notStrictEqual(code, 0, 'Should fail with wrong cwd'); done(); }); }); }); // Fix: Asynchronous Function Execution // Ensures that async functions (returning Promises) are properly awaited so // the IPC message contains the resolved value, not a pending Promise object. describe('Fix: Asynchronous Function Execution', function () { it('should await and return the resolved value of an async function', async () => { const asyncFn = (x) => Promise.resolve(x * 2); const { result, error } = await safeInvoke(asyncFn, [21]); assert_1.strict.strictEqual(error, undefined); assert_1.strict.strictEqual(result, 42); }); it('should NOT return a Promise object for async functions (old bug)', async () => { const asyncFn = () => Promise.resolve('hello'); const { result } = await safeInvoke(asyncFn, []); assert_1.strict.strictEqual(result, 'hello'); }); it('should handle sync functions transparently', async () => { const syncFn = (a, b) => a + b; const { result, error } = await safeInvoke(syncFn, [3, 4]); assert_1.strict.strictEqual(error, undefined); assert_1.strict.strictEqual(result, 7); }); it('should catch and return errors from async functions instead of crashing', async () => { const failingAsync = () => Promise.reject(new Error('intentional failure')); const { result, error } = await safeInvoke(failingAsync, []); assert_1.strict.ok(error !== undefined, 'error should be defined'); assert_1.strict.ok(error?.includes('intentional failure'), `error should include the message, got: ${error ?? ''}`); assert_1.strict.strictEqual(result, undefined); }); it('should catch and return errors from sync functions instead of crashing', async () => { const failingSync = () => { throw new TypeError('sync type error'); }; const { result, error } = await safeInvoke(failingSync, []); assert_1.strict.ok(error !== undefined, 'error should be defined'); assert_1.strict.ok(error?.includes('TypeError'), `error should mention TypeError, got: ${error ?? ''}`); assert_1.strict.strictEqual(result, undefined); }); it('should handle complex async function with multiple awaits', async () => { const complexFn = (input) => new Promise(resolve => setTimeout(() => resolve(input.split('').reverse().join('')), 10)); const { result, error } = await safeInvoke(complexFn, ['madam']); assert_1.strict.strictEqual(error, undefined); assert_1.strict.strictEqual(result, 'madam'); }); it('should correctly execute isPalindrome for sync functions (nodejs-base-app)', async () => { const indexPath = path_1.default.resolve(__dirname, '../../test/data/nodejs-base-app/index.js'); const loaded = (await Promise.resolve().then(() => __importStar(require(indexPath)))); const isPalindrome = loaded.isPalindrome ?? loaded.default?.isPalindrome; assert_1.strict.ok(typeof isPalindrome === 'function', 'isPalindrome must be a function'); const r1 = await safeInvoke(isPalindrome, ['madam']); assert_1.strict.strictEqual(r1.result, true); const r2 = await safeInvoke(isPalindrome, ['world']); assert_1.strict.strictEqual(r2.result, false); }); });