@prismatic-io/spectral
Version:
Utility library for building Prismatic connectors and code-native integrations
381 lines (380 loc) • 16.5 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. See
* https://prismatic.io/docs/custom-connectors/unit-testing/#providing-test-connection-inputs-to-an-action-test
*/
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. See
* https://prismatic.io/docs/custom-connectors/unit-testing/#access-connections-for-local-testing
*/
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. Suitable for asserting logs are created as expected. See
* https://prismatic.io/docs/custom-connectors/unit-testing/#verifying-correct-logging-in-action-tests
* for information on testing correct logging behavior in your custom component.
*/
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 mocks based on the CNI's component registry.
* You may pass mock overrides in the second argument, e.g.:
*
* createMockContextComponents(myManifest, {
* actions: {
* myComponentName: {
* myComponentAction: () => Promise.resolve({ data: "my test data "}),
* }
* },
* });
*/
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",
}, 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 specified ActionDefinition perform function using supplied params
* and optional context. Accepts a generic type matching ActionPerformReturn as a convenience
* to avoid extra casting within test methods. Returns an InvokeResult containing both the
* action result and a mock logger for asserting logging.
*/
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",
},
startedAt: new Date().toISOString(),
globalDebug: false,
};
};
exports.defaultTriggerPayload = defaultTriggerPayload;
/**
* Invokes specified TriggerDefinition perform function using supplied params
* and optional context. Accepts a generic type matching TriggerResult as a convenience
* to avoid extra casting within test methods. Returns an InvokeResult containing both the
* trigger result and a mock logger for asserting logging.
*/
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 specified DataSourceDefinition perform function using supplied params.
* Accepts a generic type matching DataSourceResult as a convenience to avoid extra
* casting within test methods. Returns a DataSourceResult.
*/
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 specified Flow of a Code Native Integration using supplied params.
* Runs the Trigger and then the Action function and returns the result of the Action. See
* https://prismatic.io/docs/integrations/triggers/cross-flow/#using-cross-flow-triggers-in-code-native
*/
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(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. See
* https://prismatic.io/docs/custom-connectors/unit-testing/
*/
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,
};