import { helper } from "@grouparoo/spec-helper"; import { specHelper, Connection } from "actionhero"; import { Option, Source, App, GrouparooModel, Property } from "../../../src"; import { SessionCreate } from "../../../src/actions/session"; import { SourceBootstrapUniqueProperty, SourceConnectionApps, SourceConnectionOptions, SourceCreate, SourceDefaultPropertyOptions, SourceDestroy, SourceEdit, SourcePreview, SourcesList, SourceView, } from "../../../src/actions/sources"; import { PropertyDestroy } from "../../../src/actions/properties"; import { ConfigWriter } from "../../../src/modules/configWriter"; describe("actions/sources", () => { helper.grouparooTestServer({ truncate: true, enableTestPlugin: true }); const configSpy = jest.spyOn(ConfigWriter, "run"); let model: GrouparooModel; let app: App; let id: string; let propertyId: string; beforeAll(async () => { await specHelper.runAction("team:initialize", { firstName: "Mario", lastName: "Mario", password: "P@ssw0rd!", email: "mario@example.com", }); model = await helper.factories.model(); app = await helper.factories.app(); await app.update({ name: "test app" }); }); 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( "session:create", connection ); csrfToken = sessionResponse.csrfToken; }); test("an administrator can create a new source from an app", async () => { connection.params = { csrfToken, name: "test source", type: "test-plugin-import", appId: app.id, modelId: model.id, options: { table: "users" }, }; const { error, source } = await specHelper.runAction( "source:create", connection ); expect(error).toBeUndefined(); expect(source.id).toBeTruthy(); expect(source.app.id).toBe(app.id); expect(source.app.name).toBe("test app"); expect(source.state).toBe("draft"); expect(configSpy).toBeCalledTimes(1); id = source.id; }); test("an administrator can list all the sources", async () => { connection.params = { csrfToken, }; const { error, sources, total } = await specHelper.runAction( "sources:list", connection ); expect(error).toBeUndefined(); expect(sources.length).toBe(1); expect(sources[0].app.name).toBe("test app"); expect(total).toBe(1); }); test("a source can be bootstrapped with a property", async () => { connection.params = { csrfToken, id, key: "userId", type: "integer", mappedColumn: "id", }; const { property, error } = await specHelper.runAction( "source:bootstrapUniqueProperty", connection ); expect(error).toBeUndefined(); expect(property.id).toBeTruthy(); propertyId = property.id; }); test("an administrator can list the connection methods and app pairs available for a new connection", async () => { connection.params = { csrfToken, id, }; const { error, connectionApps } = await specHelper.runAction( "sources:connectionApps", connection ); expect(error).toBeUndefined(); expect(connectionApps[0].app.id).toBe(app.id); expect(connectionApps[0].connection.apps).toEqual(["test-plugin-app"]); expect(connectionApps[0].connection.methods).toEqual([ "sourceOptions", "sourcePreview", "propertyOptions", "scheduleOptions", "uniquePropertyBootstrapOptions", "sourceFilters", "importRecords", "recordProperty", "recordProperties", ]); }); describe("options from environment variables", () => { beforeAll(() => { process.env.GROUPAROO_OPTION__SOURCE__TEST_OPTION = "abc123"; }); test("options for a new source will include the names of options included in environment variables", async () => { connection.params = { csrfToken }; const { environmentVariableOptions } = await specHelper.runAction( "sources:connectionApps", connection ); expect(environmentVariableOptions).toEqual(["TEST_OPTION"]); }); afterAll(() => { process.env.GROUPAROO_OPTION__APP__TEST_OPTION = undefined; }); }); test("an administrator can enumerate the source connection options", async () => { connection.params = { csrfToken, id, }; const { error, options } = await specHelper.runAction( "source:connectionOptions", connection ); expect(error).toBeUndefined(); expect(options).toEqual({ tableWithOptions: { options: ["users", "admins", "purchases", "products"], type: "list", }, }); }); test("a source with no options will see an empty preview", async () => { const options = await Option.findAll({ where: { ownerId: id } }); for (const option of options) await option.destroy(); connection.params = { csrfToken, id, }; const { error, preview } = await specHelper.runAction( "source:preview", connection ); expect(error).toBeUndefined(); expect(preview).toEqual([]); }); test("a source can provide options to a preview and column speculation", async () => { connection.params = { csrfToken, id, options: { table: "admins", tableWithOptions: "admins" }, }; const { error, preview, columnSpeculation } = await specHelper.runAction("source:preview", connection); expect(error).toBeUndefined(); expect(preview).toEqual([ { id: 1, fname: "mario", lname: "mario" }, { id: 2, fname: "luigi", lname: "mario" }, { id: 3, fname: "peach", lname: "toadstool" }, ]); expect(columnSpeculation).toEqual({ id: { isUnique: true, type: "integer", suggestedPropertyKey: "id" }, fname: { isUnique: false, type: "string", suggestedPropertyKey: "fname", }, lname: { isUnique: false, type: "string", suggestedPropertyKey: "lname", }, }); }); test("a source can have options set", async () => { connection.params = { csrfToken, id, options: { table: "admins", tableWithOptions: "admins" }, }; const { error, source } = await specHelper.runAction( "source:edit", connection ); expect(error).toBeUndefined(); expect(source.options).toEqual({ table: "admins", tableWithOptions: "admins", }); expect(configSpy).toBeCalledTimes(1); }); test("a source can provide default property options", async () => { connection.params = { csrfToken, id, }; const { error, defaultPropertyOptions } = await specHelper.runAction( "source:defaultPropertyOptions", connection ); expect(error).toBeUndefined(); expect(defaultPropertyOptions).toEqual([ { description: "the column to choose", displayName: undefined, key: "column", options: [ { key: "id", examples: [1, 2, 3] }, { key: "fname", examples: ["mario", "luigi", "peach"] }, { key: "lname", examples: ["mario", "mario", "toadstool"], }, ], required: true, type: "list", }, { description: "how things are combined", displayName: undefined, key: "aggregationMethod", options: [ { key: "exact", default: true }, { key: "count" }, { key: "min" }, { key: "max" }, ], required: false, type: "list", }, { description: "some text you want to set just because", displayName: undefined, key: "arbitraryText", options: [], primary: undefined, required: false, type: "text", }, ]); }); test("a source with options set will return a preview", async () => { connection.params = { csrfToken, id, }; const { error, preview } = await specHelper.runAction( "source:preview", connection ); expect(error).toBeUndefined(); expect(preview).toEqual([ { id: 1, fname: "mario", lname: "mario" }, { id: 2, fname: "luigi", lname: "mario" }, { id: 3, fname: "peach", lname: "toadstool" }, ]); }); test("a source can have mapping set", async () => { connection.params = { csrfToken, id, mapping: { id: "userId" }, }; const { error, source } = await specHelper.runAction( "source:edit", connection ); expect(error).toBeUndefined(); expect(source.mapping).toEqual({ id: "userId" }); expect(configSpy).toBeCalledTimes(1); }); test("a source can be made ready", async () => { connection.params = { csrfToken, id, state: "ready", }; const { error, source } = await specHelper.runAction( "source:edit", connection ); expect(error).toBeUndefined(); expect(source.state).toBe("ready"); }); describe("a source with column(s) that conflict with an existing property", () => { let source: Source; let otherSource: Source; let otherProperty: Property; beforeAll(async () => { source = await Source.findOne(); otherSource = await helper.factories.source(); await otherSource.setOptions({ table: "users" }); await otherSource.setMapping({ id: "userId" }); await otherSource.update({ state: "ready" }); otherProperty = await helper.factories.property( otherSource, { key: "Fname", }, { column: "Fname" } ); }); afterAll(async () => { await otherProperty.destroy(); await otherSource.destroy(); }); test("can provide suggested property keys", async () => { connection.params = { csrfToken, id, options: { table: "admins", tableWithOptions: "admins" }, }; const { error, preview, columnSpeculation } = await specHelper.runAction( "source:preview", connection ); expect(error).toBeUndefined(); expect(preview).toEqual([ { id: 1, fname: "mario", lname: "mario" }, { id: 2, fname: "luigi", lname: "mario" }, { id: 3, fname: "peach", lname: "toadstool" }, ]); expect(columnSpeculation).toEqual({ id: { isUnique: true, type: "integer", suggestedPropertyKey: "id" }, // Check for rename in the case of a conflict fname: { isUnique: false, type: "string", suggestedPropertyKey: "profiles_fname", }, lname: { isUnique: false, type: "string", suggestedPropertyKey: "lname", }, }); }); }); test("an administrator can view a source", async () => { connection.params = { csrfToken, id, }; const { error, source } = await specHelper.runAction( "source:view", connection ); expect(error).toBeUndefined(); expect(source.id).toBeTruthy(); expect(source.app.name).toBe("test app"); }); test("an administrator can destroy a source", async () => { connection.params = { csrfToken, id, mapping: {}, }; const editResponse = await specHelper.runAction( "source:edit", connection ); expect(editResponse.error).toBeUndefined(); expect(configSpy).toBeCalledTimes(1); connection.params = { csrfToken, id: propertyId, }; const deleteRuleResponse = await specHelper.runAction( "property:destroy", connection ); expect(deleteRuleResponse.error).toBeUndefined(); expect(configSpy).toBeCalledTimes(2); connection.params = { csrfToken, id, }; const destroyResponse = await specHelper.runAction( "source:destroy", connection ); expect(destroyResponse.error).toBeUndefined(); expect(destroyResponse.success).toBe(true); const count = await Source.count(); expect(count).toBe(0); expect(configSpy).toBeCalledTimes(3); }); }); });