UNPKG

@prismatic-io/spectral

Version:

Utility library for building Prismatic connectors and code-native integrations

549 lines (548 loc) 23.4 kB
"use strict"; /** * 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, };