@grouparoo/core
Version:
The Grouparoo Core
780 lines (708 loc) • 24.3 kB
text/typescript
import { helper } from "@grouparoo/spec-helper";
import { specHelper, api, Connection } from "actionhero";
import {
Destination,
Group,
GrouparooRecord,
Source,
Run,
App,
Property,
GrouparooModel,
GroupMember,
} from "../../src";
import {
DestinationConnectionApps,
DestinationConnectionOptions,
DestinationCreate,
DestinationDestroy,
DestinationEdit,
DestinationExport,
DestinationExportArrayProperties,
DestinationMappingOptions,
DestinationRecordPreview,
DestinationsList,
DestinationView,
} from "../../src/actions/destinations";
import { SessionCreate } from "../../src/actions/session";
import { ConfigWriter } from "../../src/modules/configWriter";
describe("actions/destinations", () => {
helper.grouparooTestServer({ truncate: true, enableTestPlugin: true });
let app: App;
let id: string;
let model: GrouparooModel;
const configSpy = jest.spyOn(ConfigWriter, "run");
beforeAll(async () => {
await specHelper.runAction("team:initialize", {
firstName: "Mario",
lastName: "Mario",
password: "P@ssw0rd!",
email: "mario@example.com",
});
({ model } = await helper.factories.properties());
await api.resque.queue.connection.redis.flushdb();
});
afterEach(() => {
configSpy.mockClear();
});
describe("administrator signed in", () => {
let connection: Connection;
let csrfToken: string;
beforeAll(async () => {
connection = await specHelper.buildConnection();
connection.params = { email: "mario@example.com", password: "P@ssw0rd!" };
const sessionResponse = await specHelper.runAction<SessionCreate>(
"session:create",
connection
);
csrfToken = sessionResponse.csrfToken;
app = await helper.factories.app();
await app.update({ name: "test app" });
});
test("an administrator can create a new destination from an app", async () => {
connection.params = {
csrfToken,
name: "test destination",
type: "test-plugin-export",
appId: app.id,
modelId: model.id,
syncMode: "sync",
};
const { error, destination } =
await specHelper.runAction<DestinationCreate>(
"destination:create",
connection
);
expect(error).toBeUndefined();
expect(destination.id).toBeTruthy();
expect(destination.app.id).toBe(app.id);
expect(destination.app.name).toBe("test app");
expect(destination.syncMode).toBe("sync");
expect(configSpy).toBeCalledTimes(1);
id = destination.id;
});
test("an administrator can see the combinations of apps and connections available for a new destination", async () => {
connection.params = {
csrfToken,
};
const { error, connectionApps } =
await specHelper.runAction<DestinationConnectionApps>(
"destinations:connectionApps",
connection
);
expect(error).toBeUndefined();
expect(connectionApps.length).toBe(4); // (this one + the app created for the properties ) * export & export-batch
expect(connectionApps[0].connection.name).toBe("test-plugin-export");
});
describe("options from environment variables", () => {
beforeAll(() => {
process.env.GROUPAROO_OPTION__DESTINATION__TEST_OPTION = "abc123";
});
test("options for a new destination will include the names of options included in environment variables", async () => {
connection.params = { csrfToken };
const { environmentVariableOptions } =
await specHelper.runAction<DestinationConnectionApps>(
"destinations:connectionApps",
connection
);
expect(environmentVariableOptions).toEqual(["TEST_OPTION"]);
});
afterAll(() => {
process.env.GROUPAROO_OPTION__APP__TEST_OPTION = undefined;
});
});
test("an administrator can list all the destinations", async () => {
connection.params = {
csrfToken,
};
const { error, destinations, total } =
await specHelper.runAction<DestinationsList>(
"destinations:list",
connection
);
expect(error).toBeUndefined();
expect(destinations.length).toBe(1);
expect(destinations[0].name).toBe("test destination");
expect(destinations[0].app.name).toBe("test app");
expect(total).toBe(1);
expect(configSpy).toBeCalledTimes(0);
});
test("an administrator can view a destination", async () => {
connection.params = {
csrfToken,
id,
};
const { error, destination } =
await specHelper.runAction<DestinationView>(
"destination:view",
connection
);
expect(error).toBeUndefined();
expect(destination.id).toBeTruthy();
expect(destination.name).toBe("test destination");
expect(destination.syncMode).toBe("sync");
expect(destination.app.name).toBe("test app");
expect(configSpy).toBeCalledTimes(0);
});
test("an administrator can see connectionOptions", async () => {
connection.params = {
csrfToken,
id,
};
const { options, error } =
await specHelper.runAction<DestinationConnectionOptions>(
"destination:connectionOptions",
connection
);
expect(error).toBeFalsy();
expect(options).toEqual({
tableWithOptions: {
type: "list",
options: ["users_out", "users", "groups"],
},
});
});
test("an administrator can see mappingOptions", async () => {
connection.params = {
csrfToken,
id,
};
const {
options,
destinationTypeConversions: _destinationTypeConversions,
error,
} = await specHelper.runAction<DestinationMappingOptions>(
"destination:mappingOptions",
connection
);
expect(error).toBeFalsy();
expect(options).toEqual({
labels: {
group: {
singular: "list",
plural: "lists",
},
property: {
singular: "var",
plural: "vars",
},
},
properties: {
required: [{ key: "primary-id", type: "integer" }],
known: [
{ key: "secondary-id", type: "any" },
{ key: "string-property", type: "string" },
],
allowOptionalFromProperties: true,
},
});
expect(_destinationTypeConversions).toEqual({
boolean: ["any", "string", "boolean", "number"],
date: ["any", "float", "integer", "string", "date", "number"],
email: ["any", "string", "email"],
float: ["any", "float", "string", "number"],
integer: ["any", "float", "integer", "string", "number"],
phoneNumber: ["any", "string", "phoneNumber"],
string: [
"any",
"string",
"url",
"email",
"boolean",
"date",
"float",
"integer",
"number",
"phoneNumber",
],
url: ["any", "string", "url"],
});
});
test("an administrator can set the mapping with valid mappings", async () => {
connection.params = {
csrfToken,
id,
mapping: {
"primary-id": "userId",
"something-else": "email",
},
};
const { destination, error } =
await specHelper.runAction<DestinationEdit>(
"destination:edit",
connection
);
expect(error).toBeFalsy();
expect(destination.mapping).toEqual({
"primary-id": "userId",
"something-else": "email",
});
expect(configSpy).toBeCalledTimes(1);
});
test("an administrator cannot set the mapping with invalid mappings", async () => {
connection.params = {
csrfToken,
id,
mapping: {
"something-else": "email",
},
};
const { error } = await specHelper.runAction<DestinationEdit>(
"destination:edit",
connection
);
expect(error.message).toMatch(
/primary-id is a required destination mapping option/
);
});
test("an administrator can see the exportArrayProperties for this destination", async () => {
connection.params = {
csrfToken,
id,
};
const { error, exportArrayProperties } =
await specHelper.runAction<DestinationExportArrayProperties>(
"destination:exportArrayProperties",
connection
);
expect(error).toBeUndefined();
expect(exportArrayProperties).toEqual([]);
});
test("an administrator cannot set a mapping for an array record property if it is not allowed by the exportArrayProperties", async () => {
connection.params = {
csrfToken,
id,
mapping: { "primary-id": "userId", purchases: "purchases" },
};
const { error } = await specHelper.runAction<DestinationEdit>(
"destination:edit",
connection
);
expect(error.message).toMatch(
/purchases is an array record property that .* cannot support/
);
});
test("an administrator can set the sync mode", async () => {
connection.params = {
csrfToken,
id,
syncMode: "enrich",
};
const { destination, error } =
await specHelper.runAction<DestinationEdit>(
"destination:edit",
connection
);
expect(error).toBeFalsy();
expect(destination.syncMode).toBe("enrich");
expect(configSpy).toBeCalledTimes(1);
});
describe("with group", () => {
let group: Group;
let record: GrouparooRecord;
beforeAll(async () => {
record = await helper.factories.record();
await record.addOrUpdateProperties({
userId: [1],
email: ["yoshi@example.com"],
});
group = await helper.factories.group();
await GroupMember.create({ recordId: record.id, groupId: group.id });
});
test("an administrator can add a group to be tracked", async () => {
connection.params = {
csrfToken,
id,
groupId: group.id,
collection: "group",
};
const { destination, newRun, error } =
await specHelper.runAction<DestinationEdit>(
"destination:edit",
connection
);
expect(error).toBeFalsy();
expect(destination.group.id).toBe(group.id);
expect(newRun.creatorId).toBe(group.id);
expect(newRun.state).toBe("running");
expect(configSpy).toBeCalledTimes(1);
});
test("only one destination can be created for each app with the same options and group", async () => {
connection.params = {
csrfToken,
name: "test destination again",
type: "test-plugin-export",
appId: app.id,
modelId: model.id,
syncMode: "sync",
};
const { destination: newDestination } =
await specHelper.runAction<DestinationCreate>(
"destination:create",
connection
);
connection.params = {
csrfToken,
id: newDestination.id,
groupId: group.id,
collection: "group",
};
const { error } = await specHelper.runAction<DestinationEdit>(
"destination:edit",
connection
);
expect(error.message).toMatch(
/destination "test destination" .* is already using this app with the same options/
);
await Destination.destroy({ where: { id: newDestination.id } });
});
test("an administrator can set the destination group memberships", async () => {
const destinationGroupMemberships: Record<string, string> = {};
destinationGroupMemberships[group.id] = "remote-group-tag";
connection.params = {
csrfToken,
id,
destinationGroupMemberships,
};
const { destination, error } =
await specHelper.runAction<DestinationEdit>(
"destination:edit",
connection
);
expect(error).toBeFalsy();
expect(destination.group.id).toBe(group.id);
expect(destination.destinationGroupMemberships).toEqual([
{
groupId: group.id,
groupName: group.name,
remoteKey: "remote-group-tag",
},
]);
expect(configSpy).toBeCalledTimes(1);
});
test("an administrator can get a preview of a record to be exported to a destination, existing mapping & destinationGroupMemberships + no record", async () => {
connection.params = {
csrfToken,
id,
groupId: group.id,
};
const { error, record: _record } =
await specHelper.runAction<DestinationRecordPreview>(
"destination:recordPreview",
connection
);
expect(error).toBeUndefined();
expect(_record.properties["primary-id"].values).toEqual([1]);
expect(_record.properties["something-else"].values).toEqual([
"yoshi@example.com",
]);
expect(_record.groupNames).toEqual(["remote-group-tag"]);
});
test("an administrator can get a preview of a record to be exported to a destination, existing mapping & destinationGroupMemberships + record", async () => {
connection.params = {
csrfToken,
id,
recordId: record.id,
};
const { error, record: _record } =
await specHelper.runAction<DestinationRecordPreview>(
"destination:recordPreview",
connection
);
expect(error).toBeUndefined();
expect(_record.properties["primary-id"].values).toEqual([1]);
expect(_record.properties["something-else"].values).toEqual([
"yoshi@example.com",
]);
expect(_record.groupNames).toEqual(["remote-group-tag"]);
});
test("an administrator can get a preview of a record to be exported to a destination, updated mapping & destinationGroupMemberships", async () => {
const destinationGroupMemberships: Record<string, string> = {};
destinationGroupMemberships[group.id] = "another-group-tag";
connection.params = {
csrfToken,
id,
recordId: record.id,
destinationGroupMemberships,
mapping: {
"primary-id": "userId",
email: "email",
},
};
const { error, record: _record } =
await specHelper.runAction<DestinationRecordPreview>(
"destination:recordPreview",
connection
);
expect(error).toBeUndefined();
expect(_record.properties["primary-id"].values).toEqual([1]);
expect(_record.properties["email"].values).toEqual([
"yoshi@example.com",
]);
expect(_record.groupNames).toEqual(["another-group-tag"]);
});
test("an administrator can get a preview of a record to be exported to a destination, with an un-set optional property", async () => {
connection.params = {
csrfToken,
id,
recordId: record.id,
mapping: {
"primary-id": "userId",
"something-new-null": null,
"something-new-undefined": undefined,
"something-new-string": "",
},
};
const { error, record: _record } =
await specHelper.runAction<DestinationRecordPreview>(
"destination:recordPreview",
connection
);
expect(error).toBeUndefined();
expect(_record.properties["primary-id"].values).toEqual([1]);
expect(_record.properties["something-new-null"]).toBeFalsy();
expect(_record.properties["something-new-undefined"]).toBeFalsy();
expect(_record.properties["something-new-string"]).toBeFalsy();
});
test("destination:recordPreview will not fail if a new record property has just been created or there are missing properties", async () => {
const source = await Source.findOne({ where: { state: "ready" } });
const colorProperty = await Property.create({
key: "color",
type: "string",
sourceId: source.id,
});
await colorProperty.setOptions({ column: "new_rule" });
await colorProperty.update({ state: "ready" });
connection.params = {
csrfToken,
id,
groupId: group.id,
mapping: {
"primary-id": "userId",
color: "color",
},
};
const { error, record: _record } =
await specHelper.runAction<DestinationRecordPreview>(
"destination:recordPreview",
connection
);
expect(error).toBeUndefined();
expect(_record.properties["primary-id"].values).toEqual([1]);
expect(_record.properties["color"].values).toEqual([null]);
await colorProperty.destroy();
});
test("an administrator can view a destination", async () => {
connection.params = {
csrfToken,
id,
};
const { destination } = await specHelper.runAction<DestinationView>(
"destination:view",
connection
);
expect(destination.id).toBe(id);
expect(destination.group.id).toBe(group.id);
expect(configSpy).toBeCalledTimes(0);
});
test("an administrator can track a model", async () => {
connection.params = {
csrfToken,
id,
collection: "model",
};
const { destination, oldRun, newRun, error } =
await specHelper.runAction<DestinationEdit>(
"destination:edit",
connection
);
expect(error).toBeFalsy();
expect(oldRun.creatorId).toBe(group.id);
expect(oldRun.state).toBe("running");
expect(newRun.creatorId).toBe("mod_profiles");
expect(destination.collection).toBe("model");
expect(destination.group).toBe(null);
expect(configSpy).toBeCalledTimes(1);
});
test("an administrator can remove a tracked model", async () => {
connection.params = {
csrfToken,
id,
collection: "none",
};
const {
destination: updatedDestination,
oldRun,
newRun,
error,
} = await specHelper.runAction<DestinationEdit>(
"destination:edit",
connection
);
expect(error).toBeFalsy();
expect(newRun).toBeUndefined();
expect(oldRun.creatorId).toBe("mod_profiles");
expect(oldRun.state).toBe("running");
expect(updatedDestination.group).toBe(null);
expect(updatedDestination.collection).toBe("none");
});
test("update the tracked group", async () => {
connection.params = {
csrfToken,
id,
groupId: group.id,
collection: "group",
};
const { destination: _destination } =
await specHelper.runAction<DestinationEdit>(
"destination:edit",
connection
);
expect(_destination.group.id).toBe(group.id);
const runningRuns = await Run.findAll({
where: { state: "running", creatorType: "group" },
});
expect(runningRuns.length).toBe(1);
expect(runningRuns[0]).toEqual(
expect.objectContaining({
destinationId: id,
creatorId: group.id,
})
);
await runningRuns[0].stop();
});
test("an administrator can trigger an export while updating a destination", async () => {
let runningRuns = await Run.findAll({
where: { state: "running", creatorType: "group" },
});
expect(runningRuns).toHaveLength(0);
connection.params = {
csrfToken,
id,
name: "the test destination",
triggerExport: true,
};
const {
error,
newRun,
oldRun,
destination: destination,
} = await specHelper.runAction<DestinationEdit>(
"destination:edit",
connection
);
expect(error).toBeFalsy();
expect(destination.name).toBe("the test destination");
runningRuns = await Run.findAll({
where: { state: "running", creatorType: "group" },
});
expect(runningRuns.length).toBe(1);
expect(runningRuns[0]).toEqual(
expect.objectContaining({
destinationId: id,
creatorId: destination.group.id,
})
);
expect(newRun.id).toEqual(runningRuns[0].id);
expect(oldRun).toBeUndefined();
await runningRuns[0].stop();
});
test("an administrator will not trigger an export by default when updating a destination", async () => {
connection.params = {
csrfToken,
id,
name: "test destination",
};
const { error, destination: destination } =
await specHelper.runAction<DestinationEdit>(
"destination:edit",
connection
);
expect(error).toBeFalsy();
expect(destination.name).toBe("test destination");
const runningRuns = await Run.findAll({
where: { state: "running", creatorType: "group" },
});
expect(runningRuns.length).toBe(0);
});
test("an administrator can export the members of a destination with a group run", async () => {
connection.params = {
csrfToken,
id,
};
const { success, error } =
await specHelper.runAction<DestinationExport>(
"destination:export",
connection
);
expect(error).toBeFalsy();
expect(success).toBeTruthy();
const { destination } = await specHelper.runAction<DestinationView>(
"destination:view",
connection
);
const runningRuns = await Run.findAll({
where: { state: "running", creatorType: "group" },
});
expect(runningRuns.length).toBe(1);
expect(runningRuns[0]).toEqual(
expect.objectContaining({
destinationId: id,
creatorId: destination.group.id,
})
);
});
});
test("remove the tracked group", async () => {
connection.params = {
csrfToken,
id,
collection: "none",
};
const { destination, error } =
await specHelper.runAction<DestinationEdit>(
"destination:edit",
connection
);
expect(error).toBeUndefined();
expect(destination.group).toBe(null);
expect(configSpy).toBeCalledTimes(1);
});
test("an administrator can destroy a destination (soft)", async () => {
connection.params = {
csrfToken,
force: false,
id,
};
const destroyResponse = await specHelper.runAction<DestinationDestroy>(
"destination:destroy",
connection
);
expect(destroyResponse.error).toBeUndefined();
expect(destroyResponse.success).toBe(true);
const destination = await Destination.scope(null).findOne({
where: { id },
});
expect(destination.state).toBe("deleted");
expect(configSpy).toBeCalledTimes(1);
});
test("an administrator can destroy a destination (force)", async () => {
connection.params = {
csrfToken,
force: true,
id,
};
const destroyResponse = await specHelper.runAction<DestinationDestroy>(
"destination:destroy",
connection
);
expect(destroyResponse.error).toBeUndefined();
expect(destroyResponse.success).toBe(true);
const destination = await Destination.scope(null).findOne({
where: { id },
});
expect(destination).toBeFalsy();
expect(configSpy).toBeCalledTimes(1);
});
});
});