@inngest/test
Version:
Tooling for testing Inngest functions.
307 lines • 14.2 kB
JavaScript
;
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