UNPKG

@inngest/test

Version:
307 lines 14.2 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InngestTestEngine = void 0; const InngestExecution_1 = require("inngest/components/execution/InngestExecution"); const v1_1 = require("inngest/components/execution/v1"); const errors_1 = require("inngest/helpers/errors"); const promises_1 = require("inngest/helpers/promises"); const ServerTiming_1 = require("inngest/helpers/ServerTiming"); const types_1 = require("inngest/types"); const ulid_1 = require("ulid"); const InngestTestRun_js_1 = require("./InngestTestRun.js"); const util_js_1 = require("./util.js"); /** * A test engine for running Inngest functions in a test environment, providing * the ability to assert inputs, outputs, and step usage, as well as mocking * with support for popular testing libraries. */ class InngestTestEngine { constructor(options) { this.options = options; } /** * Create a new test engine with the given inline options merged with the * existing options. */ clone(inlineOpts) { return new InngestTestEngine(Object.assign(Object.assign({}, this.options), inlineOpts)); } /** * Start a run from the given state and keep executing the function until a * specific checkpoint is reached. * * Is a shortcut for and uses `run.waitFor()`. */ execute( /** * Options and state to start the run with. */ inlineOpts) { return __awaiter(this, void 0, void 0, function* () { const output = yield this.individualExecution(inlineOpts); const resolutionHandler = (output) => { return { ctx: output.ctx, state: output.state, result: output.result.data, }; }; const rejectionHandler = (output) => { if (typeof output === "object" && output !== null && "ctx" in output && "state" in output) { let error = output.result.error; if (!error) { if ("step" in output.result && typeof output.result.step === "object" && output.result.step !== null && "error" in output.result.step && output.result.step.error) { error = output.result.step.error; } else { error = new Error("Function rejected without a visible error; this is a bug"); } } return { ctx: output.ctx, state: output.state, error, }; } throw output; }; if (output.result.type === "function-resolved") { return resolutionHandler(output); } else if (output.result.type === "function-rejected") { return rejectionHandler(output); } else if (output.result.type === "step-ran") { // Any error halts execution until retries are modelled if (output.result.step .error) { return rejectionHandler(output); } } return output.run .waitFor("function-resolved") .then(resolutionHandler) .catch(rejectionHandler); }); } /** * Start a run from the given state and keep executing the function until the * given step has run. */ executeStep( /** * The ID of the step to execute. */ stepId, /** * Options and state to start the run with. */ inlineOpts) { return __awaiter(this, void 0, void 0, function* () { const { run, result: resultaaa } = yield this.individualExecution(Object.assign(Object.assign({}, inlineOpts), { // always overwrite this so it's easier to capture non-runnable steps in // the same flow. disableImmediateExecution: true })); const foundSteps = yield run.waitFor("steps-found", { steps: [{ id: stepId }], }); const hashedStepId = v1_1._internals.hashId(stepId); const step = foundSteps.result.steps.find((step) => step.id === hashedStepId); // never found the step? Unexpected. if (!step) { throw new Error(`Step "${stepId}" not found, but execution was still paused. This is a bug.`); } // if this is not a runnable step, return it now // runnable steps should return void // // some of these ops are nonsensical for the checkpoint we're waiting for, // but we consider them anyway to ensure that this type requires attention // if we add more opcodes const baseRet = { ctx: foundSteps.ctx, state: foundSteps.state, step, }; const opHandlers = { // runnable, so do nothing now [types_1.StepOpCode.StepPlanned]: () => { }, [types_1.StepOpCode.InvokeFunction]: () => baseRet, [types_1.StepOpCode.Sleep]: () => baseRet, [types_1.StepOpCode.StepError]: () => (Object.assign(Object.assign({}, baseRet), { error: step.error })), [types_1.StepOpCode.StepNotFound]: () => baseRet, [types_1.StepOpCode.StepRun]: () => (Object.assign(Object.assign({}, baseRet), { result: step.data })), [types_1.StepOpCode.WaitForEvent]: () => baseRet, [types_1.StepOpCode.Step]: () => (Object.assign(Object.assign({}, baseRet), { result: step.data })), [types_1.StepOpCode.AiGateway]: () => baseRet, }; const result = opHandlers[step.op](); if (result) { return result; } // otherwise, run the step and return the output const runOutput = yield run.waitFor("step-ran", { step: { id: stepId }, }); return { ctx: runOutput.ctx, state: runOutput.state, result: runOutput.result.step.data, error: runOutput.result.step.error, step: runOutput.result.step, }; }); } /** * Start a run from the given state and keep executing the function until a * specific checkpoint is reached. * * Is a shortcut for and uses `run.waitFor()`. * * @TODO This is a duplicate of `execute` and will probably be removed; it's a * very minor convenience method that deals too much with the internals. */ executeAndWaitFor( /** * The checkpoint to wait for. */ checkpoint, /** * Options and state to start the run with. */ inlineOpts) { return __awaiter(this, void 0, void 0, function* () { const { run } = yield this.individualExecution(inlineOpts); return run.waitFor(checkpoint, inlineOpts === null || inlineOpts === void 0 ? void 0 : inlineOpts.subset); }); } /** * Execute the function with the given inline options. */ individualExecution(inlineOpts) { return __awaiter(this, void 0, void 0, function* () { var _a; const options = Object.assign(Object.assign({}, this.options), inlineOpts); const events = (options.events || [(0, util_js_1.createMockEvent)()]).map((event) => { // Make sure every event has some basic mocked data return Object.assign(Object.assign({}, (0, util_js_1.createMockEvent)()), event); }); const steps = (options.steps || []).map((step) => { return Object.assign(Object.assign({}, step), { id: step.idIsHashed ? step.id : v1_1._internals.hashId(step.id) }); }); const stepState = {}; steps.forEach((step) => { const { promise: data, resolve: resolveData } = (0, promises_1.createDeferredPromise)(); const { promise: error, resolve: resolveError } = (0, promises_1.createDeferredPromise)(); const mockHandler = Object.assign(Object.assign({}, step), { data, error, __lazyMockHandler: (state) => __awaiter(this, void 0, void 0, function* () { resolveError(state.error); resolveData(state.data); }) }); stepState[step.id] = mockHandler; }); // Track mock step accesses; if we attempt to get a particular step then // assume we've found it and attempt to lazily run the handler to give us // time to return smarter mocked data based on input and other outputs. // // This gives us the ability for mocks be be async and return dynamic data. const mockStepState = new Proxy(stepState, { get(target, prop) { var _a; if (!(prop in target)) { return undefined; } const mockStep = target[prop]; // kick off the handler if we haven't already (_a = mockStep.__mockResult) !== null && _a !== void 0 ? _a : (mockStep.__mockResult = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { var _a, _b; try { (_a = mockStep.__lazyMockHandler) === null || _a === void 0 ? void 0 : _a.call(mockStep, { // TODO pass it a context then mate data: yield mockStep.handler(), }); } catch (err) { (_b = mockStep.__lazyMockHandler) === null || _b === void 0 ? void 0 : _b.call(mockStep, { error: (0, errors_1.serializeError)(err) }); } finally { resolve(); } }))); return mockStep; }, }); const runId = (0, ulid_1.ulid)(); const execution = options.function["createExecution"]({ version: InngestExecution_1.ExecutionVersion.V1, partialOptions: { runId, data: { runId, attempt: 0, // TODO retries? event: events[0], events, }, reqArgs: [], // TODO allow passing? headers: {}, stepCompletionOrder: steps.map((step) => step.id), stepState: mockStepState, disableImmediateExecution: Boolean(options.disableImmediateExecution), isFailureHandler: false, // TODO need to allow hitting an `onFailure` handler - not dynamically, but choosing it timer: new ServerTiming_1.ServerTiming(), requestedRunStep: options.targetStepId, transformCtx: (_a = this.options.transformCtx) !== null && _a !== void 0 ? _a : util_js_1.mockCtx, }, }); const _b = yield execution.start(), { ctx, ops } = _b, result = __rest(_b, ["ctx", "ops"]); const mockState = yield Object.keys(ops).reduce((acc, stepId) => __awaiter(this, void 0, void 0, function* () { const op = ops[stepId]; if ((op === null || op === void 0 ? void 0 : op.seen) === false || !(op === null || op === void 0 ? void 0 : op.rawArgs) || !(op === null || op === void 0 ? void 0 : op.fulfilled) || !(op === null || op === void 0 ? void 0 : op.promise)) { return acc; } return Object.assign(Object.assign({}, (yield acc)), { [stepId]: op.promise }); }), Promise.resolve({})); InngestTestRun_js_1.InngestTestRun["updateState"](options, result); const run = new InngestTestRun_js_1.InngestTestRun({ testEngine: this.clone(options), }); return { result, ctx: ctx, state: mockState, run, }; }); } } exports.InngestTestEngine = InngestTestEngine; //# sourceMappingURL=InngestTestEngine.js.map