@grouparoo/core
Version:
The Grouparoo Core
1,593 lines (1,439 loc) • 54.3 kB
text/typescript
import { helper } from "@grouparoo/spec-helper";
import { api, specHelper } from "actionhero";
import path from "path";
import {
ApiKey,
App,
Destination,
Group,
GroupMember,
GrouparooRecord,
GroupRule,
Option,
plugin,
Property,
Run,
Schedule,
Setting,
Source,
Team,
TeamMember,
GrouparooModel,
AppRefreshQuery,
} from "../../../src";
import { loadConfigDirectory } from "../../../src/modules/configLoaders";
import { GroupDestroy } from "../../../src/tasks/group/destroy";
describe("modules/codeConfig", () => {
helper.grouparooTestServer({
truncate: true,
enableTestPlugin: true,
resetSettings: true,
});
describe("initial config", () => {
beforeAll(async () => {
await helper.truncate();
// manually run the initializer again after the server has started.
// the test test-app plugin has been loaded
api.codeConfig.allowLockedModelChanges = true;
const { errors, seenIds, deletedIds } = await loadConfigDirectory(
path.join(__dirname, "..", "..", "fixtures", "codeConfig", "initial")
);
expect(errors).toEqual([]);
expect(seenIds).toEqual({
model: ["mod_profiles"],
apikey: ["website_key"],
app: expect.arrayContaining(["data_warehouse"]),
destination: ["test_destination"],
group: ["email_group", "high_value"],
property: expect.arrayContaining([
"user_id",
"email",
"last_name",
"first_name",
]),
schedule: ["users_table_schedule"],
source: ["users_table"],
team: ["admin_team"],
teammember: ["demo"],
record: [],
});
expect(deletedIds).toEqual({
model: [],
apikey: [],
app: [],
destination: [],
group: [],
property: [],
schedule: [],
source: [],
team: [],
teammember: [],
record: [],
});
});
test("settings are updated", async () => {
const setting = await plugin.readSetting("core", "cluster-name");
expect(setting.value).toBe("Test Cluster");
expect(setting.locked).toBe("config:code");
});
test("models are created", async () => {
const models = await GrouparooModel.findAll();
expect(models.length).toBe(1);
expect(models[0].id).toEqual("mod_profiles");
});
test("apps are created", async () => {
const apps = await App.findAll({
order: [["type", "asc"]],
});
expect(apps.length).toBe(1);
expect(apps[0].id).toBe("data_warehouse");
expect(apps[0].name).toBe("Data Warehouse");
expect(apps[0].state).toBe("ready");
expect(apps[0].locked).toBe("config:code");
const options = await apps[0].getOptions();
expect(options).toEqual({ fileId: "test-file-path.db" });
});
test("appRefreshQuery is created", async () => {
const appRefreshQueries = await AppRefreshQuery.findAll();
expect(appRefreshQueries.length).toBe(1);
});
test("appRefreshQuery does not save value on creation", async () => {
const appRefreshQuery = await AppRefreshQuery.findOne();
expect(appRefreshQuery.refreshQuery).toBe("SELECT 'hi' AS name;");
expect(appRefreshQuery.state).toBe("ready");
expect(appRefreshQuery.value).toBeFalsy();
});
test("sources are created", async () => {
const sources = await Source.findAll();
expect(sources.length).toBe(1);
expect(sources[0].id).toBe("users_table");
expect(sources[0].appId).toBe("data_warehouse");
expect(sources[0].name).toBe("Users Table");
expect(sources[0].state).toBe("ready");
expect(sources[0].locked).toBe("config:code");
const options = await sources[0].getOptions();
expect(options).toEqual({ table: "users" });
});
test("the bootstrapped property is created", async () => {
const property = await Property.findOne({
where: { isPrimaryKey: true },
});
expect(property.id).toBe("user_id");
expect(property.key).toBe("userId");
expect(property.type).toBe("integer");
expect(property.unique).toBe(true);
expect(property.state).toBe("ready");
expect(property.locked).toBe("config:code");
});
test("schedules are created", async () => {
const schedules = await Schedule.findAll();
expect(schedules.length).toBe(1);
expect(schedules[0].id).toBe("users_table_schedule");
expect(schedules[0].sourceId).toBe("users_table");
expect(schedules[0].name).toBe("Users Table Schedule");
expect(schedules[0].state).toBe("ready");
expect(schedules[0].incremental).toBe(false);
expect(schedules[0].recurring).toBe(true);
expect(schedules[0].confirmRecords).toBe(true);
expect(schedules[0].recurringFrequency).toBe(900000);
expect(schedules[0].refreshEnabled).toBe(false);
expect(schedules[0].locked).toBe("config:code");
});
test("properties are created", async () => {
const rules = await Property.findAll();
expect(rules.length).toBe(4);
expect(rules.map((r) => r.key).sort()).toEqual([
"email",
"first name",
"last name",
"userId",
]);
expect(rules.map((r) => r.sourceId).sort()).toEqual([
"users_table",
"users_table",
"users_table",
"users_table",
]);
expect(rules.map((r) => r.state).sort()).toEqual([
"ready",
"ready",
"ready",
"ready",
]);
expect(rules.map((r) => r.locked).sort()).toEqual([
"config:code",
"config:code",
"config:code",
"config:code",
]);
const options = await Promise.all(rules.map((r) => r.getOptions()));
expect(options.map((o) => o.column).sort()).toEqual([
"email",
"first_name",
"id",
"last_name",
]);
});
test("groups are created", async () => {
const groups = await Group.findAll({ order: [["id", "asc"]] });
expect(groups.length).toBe(2);
expect(groups[0].id).toBe("email_group");
expect(groups[0].name).toBe("People with Email Addresses");
expect(groups[0].locked).toBe("config:code");
expect(groups[0].state).toBe("updating");
const rules = await groups[0].getRules();
expect(rules).toEqual([
{
key: "userId",
match: "null",
operation: { description: "is not equal to", op: "ne" },
relativeMatchDirection: null,
relativeMatchNumber: null,
relativeMatchUnit: null,
topLevel: false,
type: "integer",
},
{
key: "email",
match: "%@%",
operation: { description: "is like (case sensitive)", op: "like" },
relativeMatchDirection: null,
relativeMatchNumber: null,
relativeMatchUnit: null,
topLevel: false,
type: "email",
},
]);
expect(groups[1].id).toBe("high_value");
expect(groups[1].name).toBe("High Value Individuals");
expect(groups[1].locked).toBe("config:code");
expect(groups[1].state).toBe("updating");
const rules2 = await groups[1].getRules();
expect(rules2).toEqual([
{
key: "userId",
match: "100",
operation: { description: "is greater than", op: "gt" },
relativeMatchDirection: null,
relativeMatchNumber: null,
relativeMatchUnit: null,
topLevel: false,
type: "integer",
},
]);
});
test("destinations are created", async () => {
const destinations = await Destination.findAll();
expect(destinations.length).toBe(1);
expect(destinations[0].id).toBe("test_destination");
expect(destinations[0].appId).toBe("data_warehouse");
expect(destinations[0].name).toBe("Test Destination");
expect(destinations[0].syncMode).toBe("additive");
expect(destinations[0].state).toBe("ready");
expect(destinations[0].locked).toBe("config:code");
const options = await destinations[0].getOptions();
expect(options).toEqual({ table: "output" });
});
test("apiKeys are created", async () => {
const apiKeys = await ApiKey.findAll();
expect(apiKeys.length).toBe(1);
expect(apiKeys[0].id).toBe("website_key");
expect(apiKeys[0].name).toBe("web-api-key");
expect(apiKeys[0].apiKey).toBe("abc123");
expect(apiKeys[0].locked).toBe("config:code");
expect(apiKeys[0].permissionAllRead).toBe(true);
expect(apiKeys[0].permissionAllWrite).toBe(true);
});
test("teams are created", async () => {
const teams = await Team.findAll();
expect(teams.length).toBe(1);
expect(teams[0].id).toBe("admin_team");
expect(teams[0].name).toBe("Admin Team");
expect(teams[0].locked).toBe("config:code");
expect(teams[0].permissionAllRead).toBe(true);
expect(teams[0].permissionAllWrite).toBe(true);
});
test("teamMembers are created", async () => {
const teamMembers = await TeamMember.findAll();
expect(teamMembers.length).toBe(1);
expect(teamMembers[0].email).toEqual("demo@grouparoo.com");
expect(teamMembers[0].firstName).toEqual("Example");
expect(teamMembers[0].lastName).toEqual("Person");
expect(await teamMembers[0].checkPassword("password")).toBe(true);
});
});
describe("changed config", () => {
beforeAll(async () => {
api.codeConfig.allowLockedModelChanges = true;
const { errors, seenIds, deletedIds } = await loadConfigDirectory(
path.join(__dirname, "..", "..", "fixtures", "codeConfig", "changes")
);
expect(errors).toEqual([]);
expect(seenIds).toEqual({
model: ["mod_profiles"],
apikey: ["website_key"],
app: expect.arrayContaining(["data_warehouse"]),
destination: [],
group: ["email_group"],
property: expect.arrayContaining([
"user_id",
"last_name",
"first_name",
"email",
]),
schedule: ["users_table_schedule"],
source: ["users_table"],
team: ["admin_team"],
teammember: ["demo"],
record: [],
});
expect(deletedIds).toEqual({
model: [],
apikey: [],
app: [],
destination: ["test_destination"],
group: ["high_value"],
property: [],
schedule: [],
source: [],
team: [],
teammember: [],
record: [],
});
});
test("settings can be changed", async () => {
const setting = await plugin.readSetting("core", "cluster-name");
expect(setting.value).toBe("Test Cluster!!!");
});
test("changes to an app setting will be updated", async () => {
const apps = await App.findAll();
expect(apps.length).toBe(1);
expect(apps[0].id).toBe("data_warehouse");
expect(apps[0].name).toBe("Data Warehouse");
expect(apps[0].state).toBe("ready");
expect(apps[0].locked).toBe("config:code");
const options = await apps[0].getOptions();
expect(options).toEqual({ fileId: "new-file-path.db" });
});
test("property keys changes will be updated", async () => {
const rules = await Property.findAll();
expect(rules.length).toBe(4);
expect(rules.map((r) => r.key).sort()).toEqual([
"Email",
"First Name",
"Last Name",
"userId",
]);
expect(rules.map((r) => r.sourceId).sort()).toEqual([
"users_table",
"users_table",
"users_table",
"users_table",
]);
expect(rules.map((r) => r.state).sort()).toEqual([
"ready",
"ready",
"ready",
"ready",
]);
expect(rules.map((r) => r.locked).sort()).toEqual([
"config:code",
"config:code",
"config:code",
"config:code",
]);
const options = await Promise.all(rules.map((r) => r.getOptions()));
expect(options.map((o) => o.column).sort()).toEqual([
"email",
"id",
"last_name",
"other_first_name",
]);
});
test("property options will be updated before validating", async () => {
const record = await GrouparooRecord.create({ modelId: "mod_profiles" }); // validations only happen if there's a record
const nameProperty = await Property.findById("first_name");
let options = await nameProperty.getOptions();
expect(options).toEqual({ column: "other_first_name" });
// clear option
await Option.destroy({ where: { ownerId: "first_name", key: "column" } });
delete nameProperty.__options;
options = await nameProperty.getOptions();
expect(options).toEqual({});
// load config again
const { errors } = await loadConfigDirectory(
path.join(__dirname, "..", "..", "fixtures", "codeConfig", "changes")
);
expect(errors.length).toBe(0);
// option should be set
delete nameProperty.__options;
options = await nameProperty.getOptions();
expect(options).toEqual({ column: "other_first_name" });
await record.destroy();
});
test("groups can have changed names and rules", async () => {
const groups = await Group.findAll();
expect(groups.length).toBe(1);
expect(groups[0].id).toBe("email_group");
expect(groups[0].name).toBe("People who have Email Addresses");
expect(groups[0].locked).toBe("config:code");
const rules = await groups[0].getRules();
expect(rules).toEqual([
{
key: "Email",
match: "%@%",
operation: { description: "is like (case sensitive)", op: "like" },
relativeMatchDirection: null,
relativeMatchNumber: null,
relativeMatchUnit: null,
topLevel: false,
type: "email",
},
]);
});
test("a removed destination will be deleted", async () => {
const destinations = await Destination.scope(null).findAll();
expect(destinations.length).toBe(1);
const destination = destinations[0];
expect(destination.id).toBe("test_destination");
expect(destination.state).toEqual("deleted");
});
test("changes to team permissions will be updated", async () => {
const teams = await Team.findAll();
expect(teams.length).toBe(1);
expect(teams[0].id).toBe("admin_team");
expect(teams[0].name).toBe("Admin Team (no write)");
expect(teams[0].locked).toBe("config:code");
expect(teams[0].permissionAllRead).toBe(true);
expect(teams[0].permissionAllWrite).toBe(false);
});
test("a team member password can be changed", async () => {
const teamMembers = await TeamMember.findAll();
expect(teamMembers.length).toBe(1);
expect(teamMembers[0].email).toEqual("demo@grouparoo.com");
expect(teamMembers[0].firstName).toEqual("Example");
expect(teamMembers[0].lastName).toEqual("Person");
expect(await teamMembers[0].checkPassword("new-password")).toBe(true);
});
test("apiKeys can be updated", async () => {
const apiKeys = await ApiKey.findAll();
expect(apiKeys[0].apiKey).toBe("def456");
});
test("an updated refreshQuery will be saved", async () => {
const appRefreshQuery = await AppRefreshQuery.findOne();
expect(appRefreshQuery.refreshQuery).toBe(
"SELECT MAX(stamp) FROM users;"
);
expect(appRefreshQuery.state).toBe("ready");
});
test("an updated refreshQuery will not save a value before a run", async () => {
const appRefreshQuery = await AppRefreshQuery.findOne();
expect(appRefreshQuery.refreshQuery).toBe(
"SELECT MAX(stamp) FROM users;"
);
expect(appRefreshQuery.value).toBeFalsy();
});
});
describe("disabled config", () => {
test("nothing is deleted if config dir is disabled", async () => {
api.codeConfig.allowLockedModelChanges = true;
const { errors, seenIds, deletedIds } = await loadConfigDirectory(false);
expect(errors).toEqual([]);
expect(seenIds).toEqual({});
expect(deletedIds).toEqual({});
const apps = await App.findAll();
expect(apps.length).toBe(1);
const properties = await Property.findAll();
expect(properties.length).toBe(4);
const groups = await Group.findAll();
expect(groups.length).toBe(1);
const teams = await Team.findAll();
expect(teams.length).toBe(1);
const teamMembers = await TeamMember.findAll();
expect(teamMembers.length).toBe(1);
});
});
describe("partially empty config", () => {
beforeAll(async () => {
api.codeConfig.allowLockedModelChanges = true;
const { errors, seenIds, deletedIds } = await loadConfigDirectory(
path.join(
__dirname,
"..",
"..",
"fixtures",
"codeConfig",
"partially-empty"
)
);
expect(errors).toEqual([]);
expect(seenIds).toEqual({
model: ["mod_profiles"],
apikey: [],
app: ["data_warehouse"],
destination: [],
group: [],
property: ["user_id", "email"],
schedule: [],
source: ["users_table"],
team: [],
teammember: [],
record: [],
});
expect(deletedIds).toEqual({
model: [],
apikey: ["website_key"],
app: [],
destination: [],
group: ["email_group"],
property: expect.arrayContaining(["last_name", "first_name"]),
schedule: ["users_table_schedule"],
source: [],
team: ["admin_team"],
teammember: ["demo"],
record: [],
});
});
test("most objects will be deleted with a partially empty config file", async () => {
expect(await GrouparooModel.count()).toBe(1);
expect(await App.count()).toBe(1);
expect(await Source.count()).toBe(1);
expect(await Schedule.count()).toBe(0);
expect(await Destination.count()).toBe(0);
expect(await Property.count()).toBe(4);
expect(await ApiKey.count()).toBe(0);
expect(await Team.count()).toBe(0);
expect(await TeamMember.count()).toBe(0);
});
test("settings remain", async () => {
expect(await Setting.count()).toBeGreaterThan(1);
});
test("a removed group will be deleted", async () => {
const groups = await Group.scope(null).findAll({
order: [["id", "asc"]],
});
expect(groups.length).toBe(2);
expect(groups[0].id).toBe("email_group");
expect(groups[0].state).toBe("deleted");
expect(groups[0].locked).toBe(null);
expect(groups[1].id).toBe("high_value");
expect(groups[1].state).toBe("deleted");
expect(groups[1].locked).toBe(null);
});
test("removed properties will be deleted", async () => {
const properties = await Property.scope(null).findAll({
where: { state: "deleted" },
});
expect(properties.length).toBe(2);
expect(properties.map((p) => p.id).sort()).toEqual([
"first_name",
"last_name",
]);
properties.forEach((prop) => {
expect(prop.state).toBe("deleted");
expect(prop.locked).toBe(null);
});
await specHelper.runTask("property:destroy", {
propertyId: "first_name",
});
await specHelper.runTask("property:destroy", { propertyId: "last_name" });
});
});
describe("empty config", () => {
beforeAll(async () => {
api.codeConfig.allowLockedModelChanges = true;
const { errors, seenIds, deletedIds } = await loadConfigDirectory(
path.join(__dirname, "..", "..", "fixtures", "codeConfig", "empty")
);
expect(errors).toEqual([]);
expect(seenIds).toEqual({
model: [],
apikey: [],
app: [],
destination: [],
group: [],
property: [],
schedule: [],
source: [],
team: [],
teammember: [],
record: [],
});
expect(deletedIds).toEqual({
model: ["mod_profiles"],
apikey: [],
app: ["data_warehouse"],
destination: [],
group: [],
property: ["user_id", "email"],
schedule: [],
source: ["users_table"],
team: [],
teammember: [],
record: [],
});
});
test("a removed app will be deleted", async () => {
const app = await App.scope(null).findOne();
expect(app.state).toBe("deleted");
expect(app.locked).toBeNull();
});
test("all objects will be deleted with an empty config file", async () => {
expect(await GrouparooModel.count()).toBe(0);
expect(await App.count()).toBe(0);
expect(await Source.count()).toBe(0);
expect(await Schedule.count()).toBe(0);
expect(await Destination.count()).toBe(0);
expect(await Group.count()).toBe(0);
expect(await Property.count()).toBe(2);
expect(await ApiKey.count()).toBe(0);
expect(await Team.count()).toBe(0);
expect(await TeamMember.count()).toBe(0);
});
test("removed properties will be deleted", async () => {
const properties = await Property.scope(null).findAll({
order: [["id", "asc"]],
});
expect(properties.length).toBe(2);
expect(properties[0].id).toBe("email");
expect(properties[0].state).toBe("deleted");
expect(properties[0].locked).toBe(null);
expect(properties[1].id).toBe("user_id");
expect(properties[1].state).toBe("deleted");
expect(properties[1].locked).toBe(null);
});
test("a removed source will be deleted", async () => {
const sources = await Source.scope(null).findAll();
expect(sources.length).toBe(1);
expect(sources[0].id).toBe("users_table");
expect(sources[0].state).toBe("deleted");
expect(sources[0].locked).toBeNull();
});
test("a removed model will be deleted", async () => {
const sources = await GrouparooModel.scope(null).findAll();
expect(sources.length).toBe(1);
expect(sources[0].id).toBe("mod_profiles");
expect(sources[0].state).toBe("deleted");
expect(sources[0].locked).toBeNull();
});
test("a removed app will be deleted", async () => {
const app = await App.scope(null).findOne({
where: { id: "data_warehouse" },
});
expect(app.state).toBe("deleted");
expect(app.locked).toBeNull();
});
test("settings remain", async () => {
expect(await Setting.count()).toBeGreaterThan(1);
});
});
describe("bring it all back", () => {
let previousGroupRun: Run;
beforeAll(async () => {
// fake that runs are still being executed for deleted group
const highValue = await Group.scope(null).findOne({
where: { id: "high_value", state: "deleted" },
});
expect(highValue).toBeTruthy();
await highValue.stopPreviousRuns();
const model = await GrouparooModel.scope(null).findOne({
where: { id: "mod_profiles", state: "deleted" },
});
expect(model).toBeTruthy();
await model.update({ state: "ready" });
const record: GrouparooRecord = await helper.factories.record();
await GroupMember.create({
recordId: record.id,
groupId: "high_value",
});
previousGroupRun = await specHelper.runTask<GroupDestroy>(
"group:destroy",
{
groupId: "high_value",
}
);
expect(previousGroupRun).toBeTruthy();
expect(previousGroupRun.state).toBe("running");
// fake that runs are still being executed for deleted destination
const emailGroup = await Group.scope(null).findOne({
where: { id: "email_group", state: "deleted" },
});
expect(emailGroup).toBeTruthy();
await emailGroup.update({ state: "ready", locked: "config:code" });
await emailGroup.stopPreviousRuns();
await GroupMember.create({
recordId: record.id,
groupId: "email_group",
});
const destination = await Destination.scope(null).findOne({
where: { id: "test_destination", state: "deleted" },
});
expect(destination).toBeTruthy();
});
beforeAll(async () => {
api.codeConfig.allowLockedModelChanges = true;
const { errors, seenIds, deletedIds } = await loadConfigDirectory(
path.join(__dirname, "..", "..", "fixtures", "codeConfig", "initial")
);
expect(errors).toEqual([]);
expect(seenIds).toEqual({
model: ["mod_profiles"],
apikey: ["website_key"],
app: expect.arrayContaining(["data_warehouse"]),
destination: ["test_destination"],
group: ["email_group", "high_value"],
property: expect.arrayContaining([
"user_id",
"email",
"last_name",
"first_name",
]),
schedule: ["users_table_schedule"],
source: ["users_table"],
team: ["admin_team"],
teammember: ["demo"],
record: [],
});
expect(deletedIds).toEqual({
model: [],
apikey: [],
app: [],
destination: [],
group: [],
property: [],
schedule: [],
source: [],
team: [],
teammember: [],
record: [],
});
});
test("apps are brought back", async () => {
const apps = await App.findAll({
order: [["type", "asc"]],
});
expect(apps.length).toBe(1);
expect(apps[0].id).toBe("data_warehouse");
expect(apps[0].name).toBe("Data Warehouse");
expect(apps[0].state).toBe("ready");
expect(apps[0].locked).toBe("config:code");
const options = await apps[0].getOptions();
expect(options).toEqual({ fileId: "test-file-path.db" });
});
test("sources are brought back", async () => {
const sources = await Source.findAll();
expect(sources.length).toBe(1);
expect(sources[0].id).toBe("users_table");
expect(sources[0].appId).toBe("data_warehouse");
expect(sources[0].name).toBe("Users Table");
expect(sources[0].state).toBe("ready");
expect(sources[0].locked).toBe("config:code");
const options = await sources[0].getOptions();
expect(options).toEqual({ table: "users" });
});
test("the bootstrapped property is brought back", async () => {
const property = await Property.findOne({
where: { isPrimaryKey: true },
});
expect(property.id).toBe("user_id");
expect(property.key).toBe("userId");
expect(property.type).toBe("integer");
expect(property.unique).toBe(true);
expect(property.state).toBe("ready");
expect(property.locked).toBe("config:code");
});
test("schedules are brought back", async () => {
const schedules = await Schedule.findAll();
expect(schedules.length).toBe(1);
expect(schedules[0].id).toBe("users_table_schedule");
expect(schedules[0].sourceId).toBe("users_table");
expect(schedules[0].name).toBe("Users Table Schedule");
expect(schedules[0].state).toBe("ready");
expect(schedules[0].recurring).toBe(true);
expect(schedules[0].recurringFrequency).toBe(900000);
expect(schedules[0].locked).toBe("config:code");
});
test("properties are brought back", async () => {
const rules = await Property.findAll();
expect(rules.length).toBe(4);
expect(rules.map((r) => r.key).sort()).toEqual([
"email",
"first name",
"last name",
"userId",
]);
expect(rules.map((r) => r.sourceId).sort()).toEqual([
"users_table",
"users_table",
"users_table",
"users_table",
]);
expect(rules.map((r) => r.state).sort()).toEqual([
"ready",
"ready",
"ready",
"ready",
]);
expect(rules.map((r) => r.locked).sort()).toEqual([
"config:code",
"config:code",
"config:code",
"config:code",
]);
const options = await Promise.all(rules.map((r) => r.getOptions()));
expect(options.map((o) => o.column).sort()).toEqual([
"email",
"first_name",
"id",
"last_name",
]);
});
test("groups are brought back", async () => {
const groups = await Group.findAll({ order: [["id", "asc"]] });
expect(groups.length).toBe(2);
expect(groups[0].id).toBe("email_group");
expect(groups[0].name).toBe("People with Email Addresses");
expect(groups[0].locked).toBe("config:code");
expect(groups[0].state).toBe("updating");
const rules = await groups[0].getRules();
expect(rules).toEqual([
{
key: "userId",
match: "null",
operation: { description: "is not equal to", op: "ne" },
relativeMatchDirection: null,
relativeMatchNumber: null,
relativeMatchUnit: null,
topLevel: false,
type: "integer",
},
{
key: "email",
match: "%@%",
operation: { description: "is like (case sensitive)", op: "like" },
relativeMatchDirection: null,
relativeMatchNumber: null,
relativeMatchUnit: null,
topLevel: false,
type: "email",
},
]);
expect(groups[1].id).toBe("high_value");
expect(groups[1].name).toBe("High Value Individuals");
expect(groups[1].locked).toBe("config:code");
expect(groups[1].state).toBe("updating");
const rules2 = await groups[1].getRules();
expect(rules2).toEqual([
{
key: "userId",
match: "100",
operation: { description: "is greater than", op: "gt" },
relativeMatchDirection: null,
relativeMatchNumber: null,
relativeMatchUnit: null,
topLevel: false,
type: "integer",
},
]);
// previous run stopped
await previousGroupRun.reload();
expect(previousGroupRun.state).toBe("stopped");
// new run kicked off
const run = await Run.findOne({
where: {
creatorType: "group",
creatorId: "high_value",
state: "running",
},
});
expect(run).toBeTruthy();
});
test("destinations are brought back", async () => {
const destinations = await Destination.findAll();
expect(destinations.length).toBe(1);
expect(destinations[0].id).toBe("test_destination");
expect(destinations[0].appId).toBe("data_warehouse");
expect(destinations[0].name).toBe("Test Destination");
expect(destinations[0].syncMode).toBe("additive");
expect(destinations[0].state).toBe("ready");
expect(destinations[0].locked).toBe("config:code");
const options = await destinations[0].getOptions();
expect(options).toEqual({ table: "output" });
// new run kicked off
const runs = await Run.findAll({
where: { state: "running", destinationId: destinations[0].id },
});
expect(runs.length).toBe(1);
expect(runs[0].creatorId).toBe("email_group");
});
afterAll(async () => {
await helper.truncate();
});
});
describe("GrouparooRecord columns in group rules", () => {
beforeAll(async () => {
api.codeConfig.allowLockedModelChanges = true;
const { errors, seenIds } = await loadConfigDirectory(
path.join(
__dirname,
"..",
"..",
"fixtures",
"codeConfig",
"top-level-group-rule"
)
);
expect(errors).toEqual([]);
expect(seenIds).toEqual({
model: ["mod_profiles"],
apikey: [],
app: [],
destination: [],
group: ["group_exists", "group_recent"],
property: [],
schedule: [],
source: [],
team: [],
teammember: [],
record: [],
});
});
test("topLevel rules for record columns are created", async () => {
const group = await Group.findById("group_exists");
const rules = await group.getRules();
expect(rules).toEqual([
{
key: "grouparooId",
match: "null",
operation: { description: "is not equal to", op: "ne" },
relativeMatchDirection: null,
relativeMatchNumber: null,
relativeMatchUnit: null,
topLevel: true,
type: "string",
},
]);
});
test("topLevel rules for date record columns are correctly created", async () => {
const group = await Group.findById("group_recent");
const rules = await group.getRules();
expect(rules).toEqual([
{
key: "grouparooCreatedAt",
match: null,
operation: { description: "is after", op: "gt" },
relativeMatchDirection: "subtract",
relativeMatchNumber: 8,
relativeMatchUnit: "days",
topLevel: true,
type: "date",
},
]);
});
afterAll(async () => {
await helper.truncate();
});
});
describe("Dates in group rules", () => {
beforeAll(async () => {
api.codeConfig.allowLockedModelChanges = true;
const { errors, seenIds } = await loadConfigDirectory(
path.join(
__dirname,
"..",
"..",
"fixtures",
"codeConfig",
"group-rule-with-date"
)
);
expect(errors).toEqual([]);
});
test("it parses date only strings to 00:00:00.000UTC", async () => {
const purchaseTimestamp = "1583020800000";
const groupRule = await GroupRule.findOne({
where: { propertyId: "last_purchase_date" },
});
expect(groupRule.match).toBe(purchaseTimestamp);
});
test("It parses datetime strings with timezone", async () => {
const appointmentTimestamp = "1570686480000";
const groupRule = await GroupRule.findOne({
where: { propertyId: "last_appointment_date" },
});
expect(groupRule.match).toBe(appointmentTimestamp);
});
test("It accurately stores relative match rules", async () => {
const groupRules = await GroupRule.findOne({
where: { propertyId: "last_email_date" },
});
expect(groupRules.relativeMatchNumber).toBe(8);
expect(groupRules.relativeMatchUnit).toBe("days");
expect(groupRules.relativeMatchDirection).toBe("subtract");
});
});
describe("cli:config mode", () => {
beforeAll(async () => {
await helper.truncate();
});
describe("sample records", () => {
test("records are not loaded by default", async () => {
api.codeConfig.allowLockedModelChanges = true;
const { errors, seenIds, deletedIds } = await loadConfigDirectory(
path.join(__dirname, "..", "..", "fixtures", "codeConfig", "records")
);
expect(errors).toEqual([]);
expect(seenIds).toEqual({
model: ["mod_profiles"],
apikey: [],
app: ["data_warehouse"],
destination: [],
group: [],
property: expect.arrayContaining(["user_id", "email"]),
schedule: ["users_table_schedule"],
source: ["users_table"],
team: [],
teammember: [],
record: [],
});
expect(deletedIds).toEqual({
model: [],
apikey: [],
app: [],
destination: [],
group: [],
property: [],
schedule: [],
source: [],
team: [],
teammember: [],
record: [],
});
});
test("null record objects should provides a useful error", async () => {
await helper.truncate();
const { errors } = await loadConfigDirectory(
path.join(
__dirname,
"..",
"..",
"fixtures",
"codeConfig",
"error-null-objects"
)
);
expect(errors.length).toEqual(1);
expect(errors[0]).toMatch(
/has 2 null or undefined top-level config objects/
);
expect(errors[0]).toMatch("error-null-objects/config.js");
});
test("records are loaded in cli:config mode", async () => {
await helper.truncate();
process.env.GROUPAROO_RUN_MODE = "cli:config";
api.codeConfig.allowLockedModelChanges = true;
const { errors, seenIds, deletedIds } = await loadConfigDirectory(
path.join(__dirname, "..", "..", "fixtures", "codeConfig", "records")
);
expect(errors).toEqual([]);
expect(seenIds).toEqual({
model: ["mod_profiles"],
apikey: [],
app: ["data_warehouse"],
destination: [],
group: [],
property: expect.arrayContaining(["user_id", "email"]),
schedule: ["users_table_schedule"],
source: ["users_table"],
team: [],
teammember: [],
record: ["record_john", "record_matthew"],
});
expect(deletedIds).toEqual({
model: [],
apikey: [],
app: [],
destination: [],
group: [],
property: [],
schedule: [],
source: [],
team: [],
teammember: [],
record: [],
});
});
test("records are created", async () => {
const records = await GrouparooRecord.findAll({
order: [["id", "asc"]],
});
expect(records.length).toBe(2);
const john = records[0];
expect(john.id).toBe("record_john");
expect(john.state).toBe("pending");
const johnProps = await john.getProperties();
expect(johnProps.userId.state).toBe("ready");
expect(johnProps.userId.values).toEqual([20]);
expect(johnProps.email.state).toBe("pending");
expect(johnProps.email.values).toEqual([null]);
const matthew = records[1];
expect(matthew.id).toBe("record_matthew");
expect(matthew.state).toBe("pending");
const matthewProps = await matthew.getProperties();
expect(matthewProps.userId.state).toBe("ready");
expect(matthewProps.userId.values).toEqual([100]);
expect(matthewProps.email.state).toBe("pending");
expect(matthewProps.email.values).toEqual([null]);
});
afterAll(async () => {
await helper.truncate();
api.codeConfig.allowLockedModelChanges = undefined;
});
});
describe("models are properly locked in cli:config mode", () => {
beforeAll(async () => {
await helper.truncate();
process.env.GROUPAROO_RUN_MODE = "cli:config";
api.codeConfig.allowLockedModelChanges = true;
const { errors, seenIds, deletedIds } = await loadConfigDirectory(
path.join(__dirname, "..", "..", "fixtures", "codeConfig", "initial")
);
expect(errors).toEqual([]);
expect(seenIds).toEqual({
model: ["mod_profiles"],
apikey: ["website_key"],
app: expect.arrayContaining(["data_warehouse"]),
destination: ["test_destination"],
group: ["email_group", "high_value"],
property: expect.arrayContaining([
"user_id",
"email",
"last_name",
"first_name",
]),
schedule: ["users_table_schedule"],
source: ["users_table"],
team: ["admin_team"],
teammember: ["demo"],
record: [],
});
expect(deletedIds).toEqual({
model: [],
apikey: [],
app: [],
destination: [],
group: [],
property: [],
schedule: [],
source: [],
team: [],
teammember: [],
record: [],
});
});
afterAll(async () => {
await helper.truncate();
process.env.GROUPAROO_RUN_MODE = undefined;
});
test('settings are locked with "config:code"', async () => {
const setting = await plugin.readSetting("core", "cluster-name");
expect(setting.value).toBe("Test Cluster");
expect(setting.locked).toBe("config:code");
});
test('models are locked with "config:writer"', async () => {
const models = await GrouparooModel.findAll({});
expect(models.length).toBe(1);
expect(models.map((r) => r.locked)).toEqual(["config:writer"]);
});
test('apps are locked with "config:writer"', async () => {
const apps = await App.findAll({
order: [["type", "asc"]],
});
expect(apps.length).toBe(1);
expect(apps.map((r) => r.locked).sort()).toEqual(["config:writer"]);
});
test('sources are locked with "config:writer"', async () => {
const sources = await Source.findAll();
expect(sources.length).toBe(1);
expect(sources[0].locked).toBe("config:writer");
});
test('schedules are locked with "config:writer"', async () => {
const schedules = await Schedule.findAll();
expect(schedules.length).toBe(1);
expect(schedules[0].locked).toBe("config:writer");
});
test('properties are locked with "config:writer"', async () => {
const rules = await Property.findAll();
expect(rules.length).toBe(4);
expect(rules.map((r) => r.locked).sort()).toEqual([
"config:writer",
"config:writer",
"config:writer",
"config:writer",
]);
});
test('groups are locked with "config:writer"', async () => {
const groups = await Group.findAll({ order: [["id", "asc"]] });
expect(groups.length).toBe(2);
expect(groups.map((g) => g.locked).sort()).toEqual([
"config:writer",
"config:writer",
]);
});
test('destinations are locked with "config:writer"', async () => {
const destinations = await Destination.findAll();
expect(destinations.length).toBe(1);
expect(destinations[0].locked).toBe("config:writer");
});
test('apiKeys are locked with "config:code"', async () => {
const apiKeys = await ApiKey.findAll();
expect(apiKeys.length).toBe(1);
expect(apiKeys[0].locked).toBe("config:code");
});
test('teams are locked with "config:code"', async () => {
const teams = await Team.findAll();
expect(teams.length).toBe(1);
expect(teams[0].locked).toBe("config:code");
});
});
});
describe("duplicate IDs", () => {
test("config with duplicate IDs and the same class will not be applied", async () => {
api.codeConfig.allowLockedModelChanges = true;
const { errors } = await loadConfigDirectory(
path.join(
__dirname,
"..",
"..",
"fixtures",
"codeConfig",
"duplicate-id"
)
);
expect(errors[0]).toEqual(
"Duplicate ID values found for data_warehouse_a of class app"
);
});
test("config with duplicate IDs and differing classes are OK", async () => {
api.codeConfig.allowLockedModelChanges = true;
const { errors } = await loadConfigDirectory(
path.join(__dirname, "..", "..", "fixtures", "codeConfig", "similar-id")
);
expect(errors.length).toEqual(0);
});
});
describe("permissions", () => {
beforeAll(async () => {
await helper.truncate();
});
test("bulk and individual permissions can be loaded", async () => {
api.codeConfig.allowLockedModelChanges = true;
const { errors } = await loadConfigDirectory(
path.join(
__dirname,
"..",
"..",
"fixtures",
"codeConfig",
"permissions"
)
);
const team = await Team.findOne();
const apiKey = await ApiKey.findOne();
expect(errors).toEqual([]);
expect(team.permissionAllRead).toEqual(true);
expect(team.permissionAllWrite).toEqual(false);
const teamPermissions = await team.$get("permissions");
for (const p of teamPermissions) {
expect(p.read).toEqual(true);
expect(p.write).toEqual(false);
}
expect(apiKey.permissionAllRead).toEqual(null);
expect(apiKey.permissionAllWrite).toEqual(null);
const apiKeyPermissions = await apiKey.$get("permissions");
for (const p of apiKeyPermissions) {
if (p.topic === "app") {
expect(p.read).toEqual(true);
expect(p.write).toEqual(true);
} else {
expect(p.read).toEqual(false);
expect(p.write).toEqual(false);
}
}
});
});
describe("errors", () => {
describe("plugin not installed", () => {
beforeAll(async () => {
api.codeConfig.allowLockedModelChanges = true;
});
test("missing plugin", async () => {
const { errors } = await loadConfigDirectory(
path.join(
__dirname,
"..",
"..",
"fixtures",
"codeConfig",
"error-not-installed"
)
);
expect(errors[0]).toMatch(
/Cannot find a \"missing-plugin\" connection available within the installed plugins. Current connections installed are:/
);
});
});
describe("bad ID", () => {
beforeAll(async () => {
api.codeConfig.allowLockedModelChanges = true;
});
test("space in ID", async () => {
const { errors } = await loadConfigDirectory(
path.join(__dirname, "..", "..", "fixtures", "codeConfig", "error-id")
);
expect(errors[0]).toMatch(/invalid id/);
});
});
describe("app", () => {
beforeAll(async () => {
api.codeConfig.allowLockedModelChanges = true;
});
test("missing option", async () => {
const { errors } = await loadConfigDirectory(
path.join(
__dirname,
"..",
"..",
"fixtures",
"codeConfig",
"error-app"
)
);
expect(errors[0]).toMatch(
/fileId is required for a app of type test-plugin-app/
);
});
});
describe("source", () => {
beforeAll(async () => {
api.codeConfig.allowLockedModelChanges = true;
});
test("broken source", async () => {
const { errors } = await loadConfigDirectory(
path.join(
__dirname,
"..",
"..",
"fixtures",
"codeConfig",
"error-source"
)
);
expect(errors[0]).toMatch(/Could not find object with ID: user_id/);
});
});
describe("property", () => {
beforeAll(async () => {
api.codeConfig.allowLockedModelChanges = true;
});
test("broken property", async () => {
const { errors } = await loadConfigDirectory(
path.join(
__dirname,
"..",
"..",
"fixtures",
"codeConfig",
"error-property"
)
);
expect(errors[0]).toMatch(
/Could not find object with ID: missing_source/
);
});
});
describe("group", () => {
beforeAll(async () => {
api.codeConfig.allowLockedModelChanges = true;
});
test("broken group", async () => {
const { errors } = await loadConfigDirectory(
path.join(
__dirname,
"..",
"..",
"fixtures",
"codeConfig",
"error-group"
)
);
expect(errors[0]).toMatch(
/Could not find object with ID: missing_record_property/
);
});
});
describe("broken team member", () => {
beforeAll(async () => {
api.codeConfig.allowLockedModelChanges = true;
});
test("errors will be thrown if the configuration is invalid", async () => {
const { errors } = await loadConfigDirectory(
path.join(
__dirname,
"..",
"..",
"fixtures",
"codeConfig",
"error-teamMember"
)
);
expect(errors[0]).toMatch(/TeamMember.firstName cannot be null/);
});
});
describe("record", () => {
beforeAll(async () => {
process.env.GROUPAROO_RUN_MODE = "cli:config";
api.codeConfig.allowLockedModelChanges = true;
});
test("non unique property", async () => {
const { errors } = await loadConfigDirectory(
path.join(
__dirname,
"..",
"..",
"fixtures",
"codeConfig",
"error-record-non-unique-property"
)
);
expect(errors.length).toBe(1);
expect(errors[0]).toMatch(
/there are no directly mapped record properties provided in/
);
});
test("missing property", async () => {
const { errors } = await loadConfigDirectory(
path.join(
__dirname,
"..",
"..",
"fixtures",
"codeConfig",
"error-record-missing-property"
)
);
expect(errors.length).toBe(1);
expect(errors[0]).toMatch(
/Could not find object with ID: unknown_property_id/
);
});
afterAll(() => {
process.env.GROUPAROO_RUN_MODE = undefined;
});
});
});
describe("errors in a second run", () => {
beforeEach(async () => {
await helper.truncate();
});
test("changing an app'