@prismatic-io/spectral
Version:
Utility library for building Prismatic connectors and code-native integrations
549 lines (548 loc) • 23.4 kB
JavaScript
;
/**
* This module provides functions to help developers unit
* test custom components prior to publishing them. For
* information on unit testing, check out our docs:
* https://prismatic.io/docs/custom-connectors/unit-testing/
*/
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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createHarness = exports.ComponentTestHarness = exports.invokeFlow = exports.invokeDataSource = exports.invokeTrigger = exports.defaultTriggerPayload = exports.invoke = exports.createMockContextComponents = exports.loggerMock = exports.connectionValue = exports.defaultConnectionValueEnvironmentVariable = exports.createConnection = void 0;
const jest_mock_1 = require("jest-mock");
/**
* Create a test connection to use when testing your custom component locally.
* This builds a `ConnectionValue` from your connection definition and
* test credential values.
*
* @param connectionDef The connection definition (only `key` is used).
* @param values A record of field values for the connection (e.g. `apiKey`, `baseUrl`).
* @param tokenValues Optional OAuth 2.0 token values (e.g. `access_token`, `refresh_token`).
* @param displayName Optional display name for the connection config variable.
* @returns A `ConnectionValue` object suitable for use in test invocations.
* @see {@link https://prismatic.io/docs/custom-connectors/unit-testing/#providing-test-connection-inputs-to-an-action-test | Test Connection Inputs}
* @example
* import { testing } from "@prismatic-io/spectral";
* import { apiKeyConnection } from "./connections";
*
* const testConnection = testing.createConnection(apiKeyConnection, {
* apiKey: "test-api-key-123",
* baseUrl: "https://api.acme.com/v2",
* });
*
* // Use with testing.invoke()
* const { result } = await testing.invoke(myAction, {
* connection: testConnection,
* });
*/
const createConnection = ({ key }, values, tokenValues, displayName) => ({
configVarKey: displayName !== null && displayName !== void 0 ? displayName : "",
key,
fields: values,
token: tokenValues,
});
exports.createConnection = createConnection;
exports.defaultConnectionValueEnvironmentVariable = "PRISMATIC_CONNECTION_VALUE";
/**
* Source a test connection from an environment variable for local testing.
* The environment variable should contain a JSON-serialized connection value.
* Defaults to reading from `PRISMATIC_CONNECTION_VALUE`.
*
* @param envVarKey The name of the environment variable to read. Defaults to `"PRISMATIC_CONNECTION_VALUE"`.
* @returns A `ConnectionValue` parsed from the environment variable.
* @see {@link https://prismatic.io/docs/custom-connectors/unit-testing/#access-connections-for-local-testing | Access Connections for Local Testing}
* @example
* import { testing } from "@prismatic-io/spectral";
*
* // Reads from PRISMATIC_CONNECTION_VALUE env var (default)
* const conn = testing.connectionValue();
*
* // Or specify a custom env var
* const conn = testing.connectionValue("MY_ACME_CONNECTION");
*/
const connectionValue = (envVarKey = exports.defaultConnectionValueEnvironmentVariable) => {
const value = process.env[envVarKey];
if (!value) {
throw new Error("Unable to find connection value.");
}
const result = Object.assign(Object.assign({}, JSON.parse(value)), { key: "" });
return result;
};
exports.connectionValue = connectionValue;
/**
* Pre-built mock of ActionLogger. All log methods (`info`, `warn`, `error`, etc.)
* are Jest spies, so you can assert that your actions log the expected messages.
*
* @returns A mock `ActionLogger` with Jest spy methods.
* @see {@link https://prismatic.io/docs/custom-connectors/unit-testing/#verifying-correct-logging-in-action-tests | Verifying Logging}
* @example
* import { testing } from "@prismatic-io/spectral";
*
* const logger = testing.loggerMock();
* // Pass logger in context, then assert:
* expect(logger.info).toHaveBeenCalledWith("Processing started");
*/
const loggerMock = () => ({
metric: console.log,
trace: (0, jest_mock_1.spyOn)(console, "trace"),
debug: (0, jest_mock_1.spyOn)(console, "debug"),
info: (0, jest_mock_1.spyOn)(console, "info"),
log: (0, jest_mock_1.spyOn)(console, "log"),
warn: (0, jest_mock_1.spyOn)(console, "warn"),
error: (0, jest_mock_1.spyOn)(console, "error"),
});
exports.loggerMock = loggerMock;
function invokeFlowTest(_flowName, _data, _config) {
return __awaiter(this, void 0, void 0, function* () {
return Promise.resolve({});
});
}
/**
* Creates basic component action mocks based on a code-native integration's
* component registry. Each action mock returns the action's `examplePayload`
* by default. Pass overrides in the second argument to customize specific mocks.
*
* @param registry The component registry (or subset) to mock.
* @param mocks Optional overrides for specific component actions.
* @returns An object of mocked component actions, suitable for use in test context.
* @see {@link https://prismatic.io/docs/custom-connectors/unit-testing/ | Unit Testing}
* @example
* import { createMockContextComponents } from "@prismatic-io/spectral/dist/testing";
* import { componentRegistry } from "./componentRegistry";
*
* // Default mocks (uses examplePayload from the manifest)
* const components = createMockContextComponents(componentRegistry);
*
* // With custom overrides
* const components = createMockContextComponents(componentRegistry, {
* actions: {
* slack: {
* postMessage: () => Promise.resolve({ data: { ok: true } }),
* },
* },
* });
*/
const createMockContextComponents = (registry, mocks = { actions: {} }) => {
const components = Object.keys(registry).reduce((accum, componentKey) => {
var _a;
const mockActions = Object.keys(registry[componentKey].actions).reduce((actionAccum, actionKey) => {
actionAccum[actionKey] = (() => {
var _a;
const response = (_a = registry[componentKey].actions[actionKey].examplePayload) !== null && _a !== void 0 ? _a : {
data: null,
};
return Promise.resolve(response);
});
return actionAccum;
}, {});
accum[componentKey] = Object.assign(Object.assign({}, mockActions), ((_a = mocks.actions[componentKey]) !== null && _a !== void 0 ? _a : {}));
return accum;
}, {});
return components;
};
exports.createMockContextComponents = createMockContextComponents;
const createActionContext = (context) => {
return Object.assign({ logger: (0, exports.loggerMock)(), instanceState: {}, crossFlowState: {}, executionState: {}, integrationState: {}, configVars: {}, components: {}, stepId: "mockStepId", executionId: "mockExecutionId", webhookUrls: {
"Flow 1": "https://example.com",
}, webhookApiKeys: {
"Flow 1": ["example-123", "example-456"],
}, invokeUrl: "https://example.com", customer: {
id: "customerId",
name: "Customer 1",
externalId: "1234",
}, instance: {
id: "instanceId",
name: "Instance 1",
}, user: {
id: "userId",
email: "user@example.com",
name: "User 1",
externalId: "1234",
}, integration: {
id: "integrationId",
name: "Integration 1",
versionSequenceId: "1234",
externalVersion: "1.0.0",
}, flow: {
id: "flowId",
name: "Flow 1",
stableId: "flowStableId",
}, startedAt: new Date().toISOString(), invokeFlow: invokeFlowTest, executionFrame: {
invokedByExecutionJWT: "some-jwt",
invokedByExecutionStartedAt: "00-00-0000",
componentActionKey: "my-component-action-key",
executionId: "abc-123",
executionStartedAt: "",
stepName: "some-step",
loopPath: "",
}, debug: {
enabled: false,
timeElapsed: {
mark: (_context, _label) => { },
measure: (_context, _label, _marks) => { },
},
memoryUsage: (_context, _label, _showDetail) => { },
results: {
timeElapsed: { marks: {}, measurements: {} },
memoryUsage: [],
allowedMemory: 1024,
},
}, flowSchemas: {} }, context);
};
const createDataSourceContext = (context) => {
return Object.assign({ logger: (0, exports.loggerMock)(), configVars: {}, customer: {
id: "customerId",
name: "Customer 1",
externalId: "1234",
}, instance: {
id: "instanceId",
name: "Instance 1",
}, user: {
id: "userId",
email: "example@email.com",
externalId: "1234",
name: "Example",
} }, context);
};
/**
* Invokes a custom component action's `perform` function within a test harness.
* Returns both the action result and a mock logger for asserting logging behavior.
*
* @param actionDef The action definition to test (only `perform` is used).
* @param params Input parameter values to pass to the action's `perform` function.
* @param context Optional partial context overrides (e.g. custom `configVars` or `instanceState`).
* @returns An object with `result` (the action's return value) and `loggerMock` (for asserting logs).
* @see {@link https://prismatic.io/docs/custom-connectors/unit-testing/ | Unit Testing}
* @example
* import { testing } from "@prismatic-io/spectral";
* import { myAction } from "./actions";
*
* it("should return items", async () => {
* const { result, loggerMock } = await testing.invoke(myAction, {
* connection: testConnection,
* limit: "10",
* });
*
* expect(result.data).toHaveProperty("items");
* expect(loggerMock.info).toHaveBeenCalled();
* });
*/
const invoke = (_a, params_1, context_1) => __awaiter(void 0, [_a, params_1, context_1], void 0, function* ({ perform }, params, context) {
const realizedContext = createActionContext(context);
const result = yield perform(realizedContext, params);
return {
result,
loggerMock: realizedContext.logger,
};
});
exports.invoke = invoke;
const defaultTriggerPayload = () => {
const payloadData = { foo: "bar" };
const contentType = "application/json";
return {
headers: {
"content-type": contentType,
},
queryParameters: {},
rawBody: {
data: payloadData,
contentType,
},
body: {
data: JSON.stringify(payloadData),
contentType,
},
pathFragment: "",
webhookUrls: {
"Flow 1": "https://example.com",
},
webhookApiKeys: {
"Flow 1": ["example-123", "example-456"],
},
invokeUrl: "https://example.com",
executionId: "executionId",
customer: {
id: "customerId",
name: "Customer 1",
externalId: "1234",
},
instance: {
id: "instanceId",
name: "Instance 1",
},
user: {
id: "userId",
email: "user@example.com",
name: "User 1",
externalId: "1234",
},
integration: {
id: "integrationId",
name: "Integration 1",
versionSequenceId: "1234",
externalVersion: "1.0.0",
},
flow: {
id: "flowId",
name: "Flow 1",
stableId: "flowStableId",
},
startedAt: new Date().toISOString(),
globalDebug: false,
};
};
exports.defaultTriggerPayload = defaultTriggerPayload;
/**
* Invokes a custom component trigger's `perform` function within a test harness.
* Provides a default trigger payload that can be overridden. Returns both the
* trigger result and a mock logger for asserting logging behavior.
*
* @param triggerDef The trigger definition to test (only `perform` is used).
* @param context Optional partial context overrides.
* @param payload Optional partial trigger payload overrides (merged with defaults).
* @param params Optional input parameter values for the trigger.
* @returns An object with `result` (the trigger's return value) and `loggerMock`.
* @see {@link https://prismatic.io/docs/custom-connectors/unit-testing/ | Unit Testing}
* @example
* import { testing } from "@prismatic-io/spectral";
* import { webhookTrigger } from "./triggers";
*
* it("should process the webhook payload", async () => {
* const { result } = await testing.invokeTrigger(
* webhookTrigger,
* undefined, // use default context
* {
* body: { data: JSON.stringify({ event: "created" }) },
* headers: { "x-webhook-secret": "test-secret" },
* },
* { secret: "test-secret" },
* );
*
* expect(result.payload).toBeDefined();
* });
*/
const invokeTrigger = (_a, context_1, payload_1, params_1) => __awaiter(void 0, [_a, context_1, payload_1, params_1], void 0, function* ({ perform }, context, payload, params) {
const realizedContext = createActionContext(context);
const realizedPayload = Object.assign(Object.assign({}, (0, exports.defaultTriggerPayload)()), payload);
const realizedParams = params || {};
const result = yield perform(realizedContext, realizedPayload, realizedParams);
return {
result,
loggerMock: realizedContext.logger,
};
});
exports.invokeTrigger = invokeTrigger;
/**
* Invokes a custom component data source's `perform` function within a test harness.
* Returns the data source result directly.
*
* @param dataSourceDef The data source definition to test (only `perform` is used).
* @param params Input parameter values to pass to the data source's `perform` function.
* @param context Optional partial context overrides.
* @returns The data source result (e.g. a picklist, JSON form, or other data source type).
* @see {@link https://prismatic.io/docs/custom-connectors/unit-testing/ | Unit Testing}
* @example
* import { testing } from "@prismatic-io/spectral";
* import { selectChannel } from "./dataSources";
*
* it("should return a list of channels", async () => {
* const result = await testing.invokeDataSource(selectChannel, {
* connection: testConnection,
* });
*
* expect(result.result).toContainEqual(
* expect.objectContaining({ label: "General" }),
* );
* });
*/
const invokeDataSource = (_a, params_1, context_1) => __awaiter(void 0, [_a, params_1, context_1], void 0, function* ({ perform }, params, context) {
const realizedContext = createDataSourceContext(context);
const result = yield perform(realizedContext, params);
return result;
});
exports.invokeDataSource = invokeDataSource;
const createConfigVars = (values) => {
return Object.entries(values !== null && values !== void 0 ? values : {}).reduce((result, [key, value]) => {
// Connection
if (typeof value === "object" && "fields" in value) {
return Object.assign(Object.assign({}, result), { [key]: Object.assign(Object.assign({}, value), { configVarKey: "" }) });
}
return Object.assign(Object.assign({}, result), { [key]: value });
}, {});
};
/**
* Invokes a code-native integration flow within a test harness. Runs the
* flow's `onTrigger` (if defined) followed by `onExecution`, and returns
* the execution result. Accepts optional config variables, context overrides,
* and a custom trigger payload.
*
* @param flow The flow definition to test.
* @param options Optional config variables, context overrides, and trigger payload.
* @returns An object with `result` (the flow execution return value) and `loggerMock`.
* @see {@link https://prismatic.io/docs/custom-connectors/unit-testing/ | Unit Testing}
* @example
* import { invokeFlow } from "@prismatic-io/spectral/dist/testing";
* import { myFlow } from "./flows";
*
* it("should execute the flow end-to-end", async () => {
* const { result } = await invokeFlow(myFlow, {
* configVars: {
* "Acme API Endpoint": "https://api.acme.com",
* "Acme Connection": {
* fields: { apiKey: "test-key" },
* key: "apiKey",
* },
* },
* payload: {
* body: { data: JSON.stringify({ event: "created" }) },
* },
* });
*
* expect(result.data).toBeDefined();
* });
*/
const invokeFlow = (flow_1, ...args_1) => __awaiter(void 0, [flow_1, ...args_1], void 0, function* (flow, { configVars, context, payload, } = {}) {
const realizedConfigVars = createConfigVars(configVars);
const realizedContext = createActionContext(Object.assign(Object.assign({}, context), { configVars: realizedConfigVars }));
const realizedPayload = Object.assign(Object.assign({}, (0, exports.defaultTriggerPayload)()), payload);
const params = {
onTrigger: { results: realizedPayload },
};
if ("onTrigger" in flow && typeof flow.onTrigger === "function") {
const triggerResult = yield flow.onTrigger(realizedContext, realizedPayload, params);
params.onTrigger = { results: triggerResult === null || triggerResult === void 0 ? void 0 : triggerResult.payload };
}
const result = yield flow.onExecution(realizedContext, params);
return {
result,
loggerMock: realizedContext.logger,
};
});
exports.invokeFlow = invokeFlow;
class ComponentTestHarness {
constructor(component) {
this.component = component;
}
buildParams(inputs, params) {
const defaults = inputs.reduce((result, { key, default: defaultValue }) => (Object.assign(Object.assign({}, result), { [key]: `${defaultValue !== null && defaultValue !== void 0 ? defaultValue : ""}` })), {});
return Object.assign(Object.assign({}, defaults), params);
}
/**
* Source a test connection from an environment variable for local testing. See
* https://prismatic.io/docs/custom-connectors/unit-testing/#access-connections-for-local-testing
*/
connectionValue({ key }) {
const { PRISMATIC_CONNECTION_VALUE: value } = process.env;
if (!value) {
throw new Error("Unable to find connection value.");
}
const result = Object.assign(Object.assign({}, JSON.parse(value)), { key });
return result;
}
/**
* Invoke a trigger by its key within a unit test. See
* https://prismatic.io/docs/custom-connectors/unit-testing/
*/
trigger(key, payload, params, context) {
return __awaiter(this, void 0, void 0, function* () {
const trigger = this.component.triggers[key];
return trigger.perform(
// @ts-expect-error -- Revisit if this should support polling
createActionContext(context), Object.assign(Object.assign({}, (0, exports.defaultTriggerPayload)()), payload), this.buildParams(trigger.inputs, params));
});
}
/**
* Invoke a trigger's onInstanceDeploy function by its key within a unit test. See
* https://prismatic.io/docs/custom-connectors/unit-testing/
*/
triggerOnInstanceDeploy(key, params, context) {
return __awaiter(this, void 0, void 0, function* () {
const trigger = this.component.triggers[key];
if (!trigger.onInstanceDeploy) {
throw new Error("Trigger does not support onInstanceDeploy");
}
return trigger.onInstanceDeploy(createActionContext(context), this.buildParams(trigger.inputs, params));
});
}
/**
* Invoke a trigger's onInstanceDelete function by its key within a unit test. See
* https://prismatic.io/docs/custom-connectors/unit-testing/
*/
triggerOnInstanceDelete(key, params, context) {
return __awaiter(this, void 0, void 0, function* () {
const trigger = this.component.triggers[key];
if (!trigger.onInstanceDelete) {
throw new Error("Trigger does not support onInstanceDelete");
}
return trigger.onInstanceDelete(createActionContext(context), this.buildParams(trigger.inputs, params));
});
}
/**
* Invoke an action by its key within a unit test. See
* https://prismatic.io/docs/custom-connectors/unit-testing/
*/
action(key, params, context) {
return __awaiter(this, void 0, void 0, function* () {
const action = this.component.actions[key];
return action.perform(createActionContext(context), this.buildParams(action.inputs, params));
});
}
/**
* Invoke a data source by its key within a unit test. See
* https://prismatic.io/docs/custom-connectors/unit-testing/
*/
dataSource(key, params, context) {
return __awaiter(this, void 0, void 0, function* () {
const dataSource = this.component.dataSources[key];
return dataSource.perform(createDataSourceContext(context), this.buildParams(dataSource.inputs, params));
});
}
}
exports.ComponentTestHarness = ComponentTestHarness;
/**
* Create a testing harness to test a custom component's actions, triggers
* and data sources by key. The harness automatically provides default input
* values and mock context.
*
* @param component The compiled component object (the result of calling `component()`).
* @returns A `ComponentTestHarness` instance with `.action()`, `.trigger()`, and `.dataSource()` methods.
* @see {@link https://prismatic.io/docs/custom-connectors/unit-testing/ | Unit Testing}
* @example
* import { testing } from "@prismatic-io/spectral";
* import myComponent from ".";
*
* const harness = testing.createHarness(myComponent);
*
* it("should list items", async () => {
* const result = await harness.action("listItems", {
* connection: testConnection,
* limit: "10",
* });
* expect(result.data).toHaveProperty("items");
* });
*
* it("should handle a webhook trigger", async () => {
* const result = await harness.trigger("webhook", {
* body: { data: '{"event":"created"}' },
* });
* expect(result.payload).toBeDefined();
* });
*/
const createHarness = (component) => {
return new ComponentTestHarness(component);
};
exports.createHarness = createHarness;
exports.default = {
loggerMock: exports.loggerMock,
invoke: exports.invoke,
invokeTrigger: exports.invokeTrigger,
createHarness: exports.createHarness,
invokeDataSource: exports.invokeDataSource,
};