diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index ad2371f3ea..d269124219 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -53,7 +53,11 @@ $: uneditable = $backendUiStore.selectedTable?._id === TableNames.USERS && UNEDITABLE_USER_FIELDS.includes(field.name) - $: invalid = field.type === LINK_TYPE && !field.tableId + $: invalid = + (field.type === LINK_TYPE && !field.tableId) || + Object.keys($backendUiStore.draftTable.schema).some( + key => key === field.name + ) // used to select what different options can be displayed for column type $: canBeSearched = diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ColorPicker.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ColorPicker.svelte index 6235e744f8..c777f79666 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ColorPicker.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ColorPicker.svelte @@ -1,38 +1,42 @@ diff --git a/packages/server/__mocks__/@sendgrid/mail.js b/packages/server/__mocks__/@sendgrid/mail.js new file mode 100644 index 0000000000..e162237ff4 --- /dev/null +++ b/packages/server/__mocks__/@sendgrid/mail.js @@ -0,0 +1,18 @@ +class Email { + constructor() { + this.apiKey = null + } + + setApiKey(apiKey) { + this.apiKey = apiKey + } + + async send(msg) { + if (msg.to === "invalid@test.com") { + throw "Invalid" + } + return msg + } +} + +module.exports = new Email() diff --git a/packages/server/__mocks__/node-fetch.js b/packages/server/__mocks__/node-fetch.js index 1113791ec2..3cc412b1c6 100644 --- a/packages/server/__mocks__/node-fetch.js +++ b/packages/server/__mocks__/node-fetch.js @@ -1,17 +1,35 @@ const fetch = jest.requireActual("node-fetch") module.exports = async (url, opts) => { - // mocked data based on url - if (url.includes("api/apps")) { + function json(body, status = 200) { return { + status, json: async () => { - return { - app1: { - url: "/app1", - }, - } + return body }, } } + + // mocked data based on url + if (url.includes("api/apps")) { + return json({ + app1: { + url: "/app1", + }, + }) + } else if (url.includes("test.com")) { + return json({ + body: opts.body, + url, + method: opts.method, + }) + } else if (url.includes("invalid.com")) { + return json( + { + invalid: true, + }, + 404 + ) + } return fetch(url, opts) } diff --git a/packages/server/package.json b/packages/server/package.json index 0a53aa8f55..6f52f1ac36 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -33,7 +33,7 @@ }, "scripts": { "test": "jest --testPathIgnorePatterns=routes && npm run test:integration", - "test:integration": "jest --runInBand --coverage", + "test:integration": "jest --coverage --detectOpenHandles", "test:watch": "jest --watch", "run:docker": "node src/index", "dev:builder": "cross-env PORT=4001 nodemon src/index.js", @@ -53,11 +53,16 @@ "src/**/*.js", "!**/node_modules/**", "!src/db/views/*.js", - "!src/api/routes/tests/**/*.js", "!src/api/controllers/deploy/**/*.js", - "!src/api/controllers/static/templates/**/*", - "!src/api/controllers/static/selfhost/**/*", - "!src/*.js" + "!src/*.js", + "!src/api/controllers/static/**/*", + "!src/db/dynamoClient.js", + "!src/utilities/usageQuota.js", + "!src/api/routes/tests/**/*", + "!src/tests/**/*", + "!src/automations/tests/**/*", + "!src/utilities/fileProcessor.js", + "!src/utilities/initialiseBudibase.js" ], "coverageReporters": [ "lcov", diff --git a/packages/server/src/api/controllers/table/index.js b/packages/server/src/api/controllers/table/index.js index 93f4ec9f94..4cb1d16146 100644 --- a/packages/server/src/api/controllers/table/index.js +++ b/packages/server/src/api/controllers/table/index.js @@ -65,12 +65,14 @@ exports.save = async function(ctx) { // Don't rename if the name is the same let { _rename } = tableToSave + /* istanbul ignore next */ if (_rename && _rename.old === _rename.updated) { _rename = null delete tableToSave._rename } // rename row fields when table column is renamed + /* istanbul ignore next */ if (_rename && tableToSave.schema[_rename.updated].type === FieldTypes.LINK) { ctx.throw(400, "Cannot rename a linked column.") } else if (_rename && tableToSave.primaryDisplay === _rename.old) { @@ -159,7 +161,7 @@ exports.destroy = async function(ctx) { ctx.eventEmitter && ctx.eventEmitter.emitTable(`table:delete`, appId, tableToDelete) ctx.status = 200 - ctx.message = `Table ${ctx.params.tableId} deleted.` + ctx.body = { message: `Table ${ctx.params.tableId} deleted.` } } exports.validateCSVSchema = async function(ctx) { diff --git a/packages/server/src/api/controllers/table/utils.js b/packages/server/src/api/controllers/table/utils.js index 73e6e60551..66b3651ccf 100644 --- a/packages/server/src/api/controllers/table/utils.js +++ b/packages/server/src/api/controllers/table/utils.js @@ -90,7 +90,8 @@ exports.handleDataImport = async (user, table, dataImport) => { return table } -exports.handleSearchIndexes = async (db, table) => { +exports.handleSearchIndexes = async (appId, table) => { + const db = new CouchDB(appId) // create relevant search indexes if (table.indexes && table.indexes.length > 0) { const currentIndexes = await db.getIndexes() @@ -150,6 +151,9 @@ class TableSaveFunctions { constructor({ db, ctx, oldTable, dataImport }) { this.db = db this.ctx = ctx + if (this.ctx && this.ctx.user) { + this.appId = this.ctx.user.appId + } this.oldTable = oldTable this.dataImport = dataImport // any rows that need updated @@ -178,7 +182,7 @@ class TableSaveFunctions { // after saving async after(table) { - table = await exports.handleSearchIndexes(this.db, table) + table = await exports.handleSearchIndexes(this.appId, table) table = await exports.handleDataImport( this.ctx.user, table, diff --git a/packages/server/src/api/controllers/view/index.js b/packages/server/src/api/controllers/view/index.js index 05dc299754..f482f3f2a6 100644 --- a/packages/server/src/api/controllers/view/index.js +++ b/packages/server/src/api/controllers/view/index.js @@ -29,11 +29,13 @@ const controller = { save: async ctx => { const db = new CouchDB(ctx.user.appId) const { originalName, ...viewToSave } = ctx.request.body - const designDoc = await db.get("_design/database") - const view = viewTemplate(viewToSave) + if (!viewToSave.name) { + ctx.throw(400, "Cannot create view without a name") + } + designDoc.views = { ...designDoc.views, [viewToSave.name]: view, @@ -60,17 +62,16 @@ const controller = { await db.put(table) - ctx.body = table.views[viewToSave.name] - ctx.message = `View ${viewToSave.name} saved successfully.` + ctx.body = { + ...table.views[viewToSave.name], + name: viewToSave.name, + } }, destroy: async ctx => { const db = new CouchDB(ctx.user.appId) const designDoc = await db.get("_design/database") - const viewName = decodeURI(ctx.params.viewName) - const view = designDoc.views[viewName] - delete designDoc.views[viewName] await db.put(designDoc) @@ -80,16 +81,17 @@ const controller = { await db.put(table) ctx.body = view - ctx.message = `View ${ctx.params.viewName} saved successfully.` }, exportView: async ctx => { const db = new CouchDB(ctx.user.appId) const designDoc = await db.get("_design/database") - const viewName = decodeURI(ctx.query.view) const view = designDoc.views[viewName] const format = ctx.query.format + if (!format) { + ctx.throw(400, "Format must be specified, either csv or json") + } if (view) { ctx.params.viewName = viewName @@ -102,6 +104,7 @@ const controller = { } } else { // table all_ view + /* istanbul ignore next */ ctx.params.viewName = viewName } diff --git a/packages/server/src/api/routes/tests/automation.spec.js b/packages/server/src/api/routes/tests/automation.spec.js index 9d11219506..5654c14c17 100644 --- a/packages/server/src/api/routes/tests/automation.spec.js +++ b/packages/server/src/api/routes/tests/automation.spec.js @@ -3,8 +3,8 @@ const { getAllTableRows, clearAllAutomations, } = require("./utilities/TestFunctions") -const { basicAutomation } = require("./utilities/structures") const setup = require("./utilities") +const { basicAutomation } = setup.structures const MAX_RETRIES = 4 diff --git a/packages/server/src/api/routes/tests/datasource.spec.js b/packages/server/src/api/routes/tests/datasource.spec.js index ee1a1c47f5..c1448894b1 100644 --- a/packages/server/src/api/routes/tests/datasource.spec.js +++ b/packages/server/src/api/routes/tests/datasource.spec.js @@ -1,6 +1,6 @@ -let {basicDatasource} = require("./utilities/structures") -let {checkBuilderEndpoint} = require("./utilities/TestFunctions") let setup = require("./utilities") +let { basicDatasource } = setup.structures +let { checkBuilderEndpoint } = require("./utilities/TestFunctions") describe("/datasources", () => { let request = setup.getRequest() diff --git a/packages/server/src/api/routes/tests/layout.spec.js b/packages/server/src/api/routes/tests/layout.spec.js index 6b21554d71..4842b2cc8e 100644 --- a/packages/server/src/api/routes/tests/layout.spec.js +++ b/packages/server/src/api/routes/tests/layout.spec.js @@ -1,6 +1,6 @@ const { checkBuilderEndpoint } = require("./utilities/TestFunctions") const setup = require("./utilities") -const { basicLayout } = require("./utilities/structures") +const { basicLayout } = setup.structures describe("/layouts", () => { let request = setup.getRequest() diff --git a/packages/server/src/api/routes/tests/misc.spec.js b/packages/server/src/api/routes/tests/misc.spec.js index 3d3b6047e2..2957e42d90 100644 --- a/packages/server/src/api/routes/tests/misc.spec.js +++ b/packages/server/src/api/routes/tests/misc.spec.js @@ -1,6 +1,7 @@ const setup = require("./utilities") +const tableUtils = require("../../controllers/table/utils") -describe("/analytics", () => { +describe("run misc tests", () => { let request = setup.getRequest() let config = setup.getConfig() @@ -10,29 +11,44 @@ describe("/analytics", () => { await config.init() }) - describe("isEnabled", () => { - it("check if analytics enabled", async () => { - const res = await request - .get(`/api/analytics`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(typeof res.body.enabled).toEqual("boolean") + describe("/analytics", () => { + it("check if analytics enabled", async () => { + const res = await request + .get(`/api/analytics`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(typeof res.body.enabled).toEqual("boolean") + }) + }) + + describe("/health", () => { + it("should confirm healthy", async () => { + await request.get("/health").expect(200) }) }) -}) -describe("/health", () => { - it("should confirm healthy", async () => { - let config = setup.getConfig() - await config.getRequest().get("/health").expect(200) + describe("/version", () => { + it("should confirm version", async () => { + const res = await request.get("/version").expect(200) + expect(res.text.split(".").length).toEqual(3) + }) }) -}) -describe("/version", () => { - it("should confirm version", async () => { - const config = setup.getConfig() - const res = await config.getRequest().get("/version").expect(200) - expect(res.text.split(".").length).toEqual(3) + describe("test table utilities", () => { + it("should be able to import a CSV", async () => { + const table = await config.createTable() + const dataImport = { + csvString: "a,b,c,d\n1,2,3,4" + } + await tableUtils.handleDataImport({ + appId: config.getAppId(), + userId: "test", + }, table, dataImport) + const rows = await config.getRows() + expect(rows[0].a).toEqual("1") + expect(rows[0].b).toEqual("2") + expect(rows[0].c).toEqual("3") + }) }) }) \ No newline at end of file diff --git a/packages/server/src/api/routes/tests/permissions.spec.js b/packages/server/src/api/routes/tests/permissions.spec.js index b24fac57c0..aab5567881 100644 --- a/packages/server/src/api/routes/tests/permissions.spec.js +++ b/packages/server/src/api/routes/tests/permissions.spec.js @@ -1,6 +1,6 @@ const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles") const setup = require("./utilities") -const { basicRow } = require("./utilities/structures") +const { basicRow } = setup.structures const HIGHER_ROLE_ID = BUILTIN_ROLE_IDS.BASIC const STD_ROLE_ID = BUILTIN_ROLE_IDS.PUBLIC diff --git a/packages/server/src/api/routes/tests/query.spec.js b/packages/server/src/api/routes/tests/query.spec.js index aa0e5428c5..87938c6a37 100644 --- a/packages/server/src/api/routes/tests/query.spec.js +++ b/packages/server/src/api/routes/tests/query.spec.js @@ -1,9 +1,9 @@ // mock out postgres for this jest.mock("pg") -const { checkBuilderEndpoint } = require("./utilities/TestFunctions") -const { basicQuery, basicDatasource } = require("./utilities/structures") const setup = require("./utilities") +const { checkBuilderEndpoint } = require("./utilities/TestFunctions") +const { basicQuery, basicDatasource } = setup.structures describe("/queries", () => { let request = setup.getRequest() diff --git a/packages/server/src/api/routes/tests/role.spec.js b/packages/server/src/api/routes/tests/role.spec.js index 9bb38b295a..062450cf63 100644 --- a/packages/server/src/api/routes/tests/role.spec.js +++ b/packages/server/src/api/routes/tests/role.spec.js @@ -2,8 +2,8 @@ const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles") const { BUILTIN_PERMISSION_IDS, } = require("../../../utilities/security/permissions") -const { basicRole } = require("./utilities/structures") const setup = require("./utilities") +const { basicRole } = setup.structures describe("/roles", () => { let request = setup.getRequest() diff --git a/packages/server/src/api/routes/tests/routing.spec.js b/packages/server/src/api/routes/tests/routing.spec.js index 70d1632bf3..beb1659b2a 100644 --- a/packages/server/src/api/routes/tests/routing.spec.js +++ b/packages/server/src/api/routes/tests/routing.spec.js @@ -1,5 +1,5 @@ const setup = require("./utilities") -const { basicScreen } = require("./utilities/structures") +const { basicScreen } = setup.structures const { checkBuilderEndpoint } = require("./utilities/TestFunctions") const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles") diff --git a/packages/server/src/api/routes/tests/row.spec.js b/packages/server/src/api/routes/tests/row.spec.js index 1442e4eb75..652a17366d 100644 --- a/packages/server/src/api/routes/tests/row.spec.js +++ b/packages/server/src/api/routes/tests/row.spec.js @@ -1,7 +1,6 @@ const { outputProcessing } = require("../../../utilities/rowProcessor") -const env = require("../../../environment") -const { basicRow } = require("./utilities/structures") const setup = require("./utilities") +const { basicRow } = setup.structures describe("/rows", () => { let request = setup.getRequest() @@ -349,7 +348,7 @@ describe("/rows", () => { const view = await config.createView() const row = await config.createRow() const res = await request - .get(`/api/views/${view._id}`) + .get(`/api/views/${view.name}`) .set(config.defaultHeaders()) .expect('Content-Type', /json/) .expect(200) diff --git a/packages/server/src/api/routes/tests/screen.spec.js b/packages/server/src/api/routes/tests/screen.spec.js index ae30afd29c..5533bc5e59 100644 --- a/packages/server/src/api/routes/tests/screen.spec.js +++ b/packages/server/src/api/routes/tests/screen.spec.js @@ -1,6 +1,6 @@ const { checkBuilderEndpoint } = require("./utilities/TestFunctions") const setup = require("./utilities") -const { basicScreen } = require("./utilities/structures") +const { basicScreen } = setup.structures describe("/screens", () => { let request = setup.getRequest() diff --git a/packages/server/src/api/routes/tests/table.spec.js b/packages/server/src/api/routes/tests/table.spec.js index 1a2df624f1..df28eed0c2 100644 --- a/packages/server/src/api/routes/tests/table.spec.js +++ b/packages/server/src/api/routes/tests/table.spec.js @@ -1,5 +1,6 @@ -const { checkBuilderEndpoint } = require("./utilities/TestFunctions") +const { checkBuilderEndpoint, getDB } = require("./utilities/TestFunctions") const setup = require("./utilities") +const { basicTable } = setup.structures describe("/tables", () => { let request = setup.getRequest() @@ -12,25 +13,22 @@ describe("/tables", () => { }) describe("create", () => { - it("returns a success message when the table is successfully created", done => { - request + it("returns a success message when the table is successfully created", async () => { + const res = await request .post(`/api/tables`) - .send({ + .send({ name: "TestTable", key: "name", schema: { - name: { type: "string" } + name: {type: "string"} } }) .set(config.defaultHeaders()) .expect('Content-Type', /json/) .expect(200) - .end(async (err, res) => { - expect(res.res.statusMessage).toEqual("Table TestTable saved successfully.") - expect(res.body.name).toEqual("TestTable") - done() - }) - }) + expect(res.res.statusMessage).toEqual("Table TestTable saved successfully.") + expect(res.body.name).toEqual("TestTable") + }) it("renames all the row fields for a table when a schema key is renamed", async () => { const testTable = await config.createTable() @@ -46,7 +44,7 @@ describe("/tables", () => { const updatedTable = await request .post(`/api/tables`) - .send({ + .send({ _id: testTable._id, _rev: testTable._rev, name: "TestTable", @@ -56,41 +54,40 @@ describe("/tables", () => { updated: "updatedName" }, schema: { - updatedName: { type: "string" } + updatedName: {type: "string"} } }) .set(config.defaultHeaders()) .expect('Content-Type', /json/) .expect(200) + expect(updatedTable.res.statusMessage).toEqual("Table TestTable saved successfully.") + expect(updatedTable.body.name).toEqual("TestTable") - expect(updatedTable.res.statusMessage).toEqual("Table TestTable saved successfully.") - expect(updatedTable.body.name).toEqual("TestTable") + const res = await request + .get(`/api/${testTable._id}/rows/${testRow.body._id}`) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) - const res = await request - .get(`/api/${testTable._id}/rows/${testRow.body._id}`) - .set(config.defaultHeaders()) - .expect('Content-Type', /json/) - .expect(200) + expect(res.body.updatedName).toEqual("test") + expect(res.body.name).toBeUndefined() + }) - expect(res.body.updatedName).toEqual("test") - expect(res.body.name).toBeUndefined() - }) - - it("should apply authorization to endpoint", async () => { - await checkBuilderEndpoint({ - config, - method: "POST", - url: `/api/tables`, - body: { - name: "TestTable", - key: "name", - schema: { - name: { type: "string" } - } + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "POST", + url: `/api/tables`, + body: { + name: "TestTable", + key: "name", + schema: { + name: {type: "string"} } - }) + } }) }) + }) describe("fetch", () => { let testTable @@ -103,28 +100,91 @@ describe("/tables", () => { delete testTable._rev }) - it("returns all the tables for that instance in the response body", done => { - request + it("returns all the tables for that instance in the response body", async () => { + const res = await request .get(`/api/tables`) .set(config.defaultHeaders()) .expect('Content-Type', /json/) .expect(200) - .end(async (_, res) => { - const fetchedTable = res.body[0] - expect(fetchedTable.name).toEqual(testTable.name) - expect(fetchedTable.type).toEqual("table") - done() - }) + const fetchedTable = res.body[0] + expect(fetchedTable.name).toEqual(testTable.name) + expect(fetchedTable.type).toEqual("table") }) it("should apply authorization to endpoint", async () => { - await checkBuilderEndpoint({ - config, - method: "GET", - url: `/api/tables`, - }) + await checkBuilderEndpoint({ + config, + method: "GET", + url: `/api/tables`, }) }) + }) + + describe("indexing", () => { + it("should be able to create a table with indexes", async () => { + const db = getDB(config) + const indexCount = (await db.getIndexes()).total_rows + const table = basicTable() + table.indexes = ["name"] + const res = await request + .post(`/api/tables`) + .send(table) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) + expect(res.body._id).toBeDefined() + expect(res.body._rev).toBeDefined() + expect((await db.getIndexes()).total_rows).toEqual(indexCount + 1) + // update index to see what happens + table.indexes = ["name", "description"] + await request + .post(`/api/tables`) + .send({ + ...table, + _id: res.body._id, + _rev: res.body._rev, + }) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) + // shouldn't have created a new index + expect((await db.getIndexes()).total_rows).toEqual(indexCount + 1) + }) + }) + + describe("updating user table", () => { + it("should add roleId and email field when adjusting user table schema", async () => { + const res = await request + .post(`/api/tables`) + .send({ + ...basicTable(), + _id: "ta_users", + }) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) + expect(res.body.schema.email).toBeDefined() + expect(res.body.schema.roleId).toBeDefined() + }) + }) + + describe("validate csv", () => { + it("should be able to validate a CSV layout", async () => { + const res = await request + .post(`/api/tables/csv/validate`) + .send({ + csvString: "a,b,c,d\n1,2,3,4" + }) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) + expect(res.body.schema).toBeDefined() + expect(res.body.schema.a).toEqual({ + type: "string", + success: true, + }) + }) + }) describe("destroy", () => { let testTable @@ -137,19 +197,16 @@ describe("/tables", () => { delete testTable._rev }) - it("returns a success response when a table is deleted.", async done => { - request + it("returns a success response when a table is deleted.", async () => { + const res = await request .delete(`/api/tables/${testTable._id}/${testTable._rev}`) .set(config.defaultHeaders()) .expect('Content-Type', /json/) .expect(200) - .end(async (_, res) => { - expect(res.res.statusMessage).toEqual(`Table ${testTable._id} deleted.`) - done() - }) - }) + expect(res.body.message).toEqual(`Table ${testTable._id} deleted.`) + }) - it("deletes linked references to the table after deletion", async done => { + it("deletes linked references to the table after deletion", async () => { const linkedTable = await config.createTable({ name: "LinkedTable", type: "table", @@ -171,18 +228,15 @@ describe("/tables", () => { }, }) - request + const res = await request .delete(`/api/tables/${testTable._id}/${testTable._rev}`) .set(config.defaultHeaders()) .expect('Content-Type', /json/) .expect(200) - .end(async (_, res) => { - expect(res.res.statusMessage).toEqual(`Table ${testTable._id} deleted.`) - const dependentTable = await config.getTable(linkedTable._id) - expect(dependentTable.schema.TestTable).not.toBeDefined() - done() - }) - }) + expect(res.body.message).toEqual(`Table ${testTable._id} deleted.`) + const dependentTable = await config.getTable(linkedTable._id) + expect(dependentTable.schema.TestTable).not.toBeDefined() + }) it("should apply authorization to endpoint", async () => { await checkBuilderEndpoint({ @@ -191,6 +245,5 @@ describe("/tables", () => { url: `/api/tables/${testTable._id}/${testTable._rev}`, }) }) - }) }) diff --git a/packages/server/src/api/routes/tests/user.spec.js b/packages/server/src/api/routes/tests/user.spec.js index 5e7ec9e9d4..808f1a2622 100644 --- a/packages/server/src/api/routes/tests/user.spec.js +++ b/packages/server/src/api/routes/tests/user.spec.js @@ -1,7 +1,7 @@ const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles") const { checkPermissionsEndpoint } = require("./utilities/TestFunctions") -const { basicUser } = require("./utilities/structures") const setup = require("./utilities") +const { basicUser } = setup.structures describe("/users", () => { let request = setup.getRequest() diff --git a/packages/server/src/api/routes/tests/utilities/TestFunctions.js b/packages/server/src/api/routes/tests/utilities/TestFunctions.js index 534119d279..313b9e63a8 100644 --- a/packages/server/src/api/routes/tests/utilities/TestFunctions.js +++ b/packages/server/src/api/routes/tests/utilities/TestFunctions.js @@ -1,5 +1,6 @@ const rowController = require("../../../controllers/row") const appController = require("../../../controllers/application") +const CouchDB = require("../../../../db") function Request(appId, params) { this.user = { appId } @@ -77,3 +78,7 @@ exports.checkPermissionsEndpoint = async ({ .set(failHeader) .expect(403) } + +exports.getDB = config => { + return new CouchDB(config.getAppId()) +} diff --git a/packages/server/src/api/routes/tests/utilities/controllers.js b/packages/server/src/api/routes/tests/utilities/controllers.js deleted file mode 100644 index a4eb9ac9de..0000000000 --- a/packages/server/src/api/routes/tests/utilities/controllers.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - table: require("../../../controllers/table"), - row: require("../../../controllers/row"), - role: require("../../../controllers/role"), - perms: require("../../../controllers/permission"), - view: require("../../../controllers/view"), - app: require("../../../controllers/application"), - user: require("../../../controllers/user"), - automation: require("../../../controllers/automation"), - datasource: require("../../../controllers/datasource"), - query: require("../../../controllers/query"), - screen: require("../../../controllers/screen"), - webhook: require("../../../controllers/webhook"), - layout: require("../../../controllers/layout"), -} diff --git a/packages/server/src/api/routes/tests/utilities/index.js b/packages/server/src/api/routes/tests/utilities/index.js index 7126f141e2..ed5c98cc48 100644 --- a/packages/server/src/api/routes/tests/utilities/index.js +++ b/packages/server/src/api/routes/tests/utilities/index.js @@ -1,4 +1,5 @@ -const TestConfig = require("./TestConfiguration") +const TestConfig = require("../../../../tests/utilities/TestConfiguration") +const structures = require("../../../../tests/utilities/structures") const env = require("../../../../environment") exports.delay = ms => new Promise(resolve => setTimeout(resolve, ms)) @@ -51,3 +52,5 @@ exports.switchToCloudForFunction = async func => { throw error } } + +exports.structures = structures diff --git a/packages/server/src/api/routes/tests/view.spec.js b/packages/server/src/api/routes/tests/view.spec.js index a80b09d3a0..3bfbacccbe 100644 --- a/packages/server/src/api/routes/tests/view.spec.js +++ b/packages/server/src/api/routes/tests/view.spec.js @@ -29,9 +29,7 @@ describe("/views", () => { .expect("Content-Type", /json/) .expect(200) - expect(res.res.statusMessage).toEqual( - "View TestView saved successfully." - ) + expect(res.body.tableId).toBe(table._id) }) it("updates the table row with the new view metadata", async () => { @@ -46,10 +44,8 @@ describe("/views", () => { .set(config.defaultHeaders()) .expect("Content-Type", /json/) .expect(200) + expect(res.body.tableId).toBe(table._id) - expect(res.res.statusMessage).toEqual( - "View TestView saved successfully." - ) const updatedTable = await config.getTable(table._id) expect(updatedTable.views).toEqual({ TestView: { @@ -173,4 +169,49 @@ describe("/views", () => { expect(res.body).toMatchSnapshot() }) }) + + describe("destroy", () => { + it("should be able to delete a view", async () => { + const table = await config.createTable() + const view = await config.createView() + const res = await request + .delete(`/api/views/${view.name}`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.map).toBeDefined() + expect(res.body.meta.tableId).toEqual(table._id) + }) + }) + + describe("exportView", () => { + it("should be able to delete a view", async () => { + await config.createTable() + await config.createRow() + const view = await config.createView() + let res = await request + .get(`/api/views/export?view=${view.name}&format=json`) + .set(config.defaultHeaders()) + .expect(200) + let error + try { + const obj = JSON.parse(res.text) + expect(obj.length).toBe(1) + } catch (err) { + error = err + } + expect(error).toBeUndefined() + res = await request + .get(`/api/views/export?view=${view.name}&format=csv`) + .set(config.defaultHeaders()) + .expect(200) + // this shouldn't be JSON + try { + JSON.parse(res.text) + } catch (err) { + error = err + } + expect(error).toBeDefined() + }) + }) }) diff --git a/packages/server/src/api/routes/tests/webhook.spec.js b/packages/server/src/api/routes/tests/webhook.spec.js index 2bf5445a09..7fb7a26fc1 100644 --- a/packages/server/src/api/routes/tests/webhook.spec.js +++ b/packages/server/src/api/routes/tests/webhook.spec.js @@ -1,6 +1,6 @@ const setup = require("./utilities") const { checkBuilderEndpoint } = require("./utilities/TestFunctions") -const { basicWebhook, basicAutomation } = require("./utilities/structures") +const { basicWebhook, basicAutomation } = setup.structures describe("/webhooks", () => { let request = setup.getRequest() diff --git a/packages/server/src/app.js b/packages/server/src/app.js index 15e996cfe6..8bbea00474 100644 --- a/packages/server/src/app.js +++ b/packages/server/src/app.js @@ -9,7 +9,6 @@ const env = require("./environment") const eventEmitter = require("./events") const automations = require("./automations/index") const Sentry = require("@sentry/node") -const selfhost = require("./selfhost") const app = new Koa() @@ -66,11 +65,7 @@ module.exports = server.listen(env.PORT || 0, async () => { console.log(`Budibase running on ${JSON.stringify(server.address())}`) env._set("PORT", server.address().port) eventEmitter.emitPort(env.PORT) - automations.init() - // only init the self hosting DB info in the Pouch, not needed in self hosting prod - if (!env.CLOUD) { - await selfhost.init() - } + await automations.init() }) process.on("uncaughtException", err => { diff --git a/packages/server/src/automations/actions.js b/packages/server/src/automations/actions.js index ea88c2d1d6..ee57f5a109 100644 --- a/packages/server/src/automations/actions.js +++ b/packages/server/src/automations/actions.js @@ -37,10 +37,12 @@ let AUTOMATION_BUCKET = env.AUTOMATION_BUCKET let AUTOMATION_DIRECTORY = env.AUTOMATION_DIRECTORY let MANIFEST = null +/* istanbul ignore next */ function buildBundleName(pkgName, version) { return `${pkgName}@${version}.min.js` } +/* istanbul ignore next */ async function downloadPackage(name, version, bundleName) { await download( `${AUTOMATION_BUCKET}/${name}/${version}/${bundleName}`, @@ -49,6 +51,7 @@ async function downloadPackage(name, version, bundleName) { return require(join(AUTOMATION_DIRECTORY, bundleName)) } +/* istanbul ignore next */ module.exports.getAction = async function(actionName) { if (BUILTIN_ACTIONS[actionName] != null) { return BUILTIN_ACTIONS[actionName] @@ -96,5 +99,6 @@ module.exports.init = async function() { return MANIFEST } +// definitions will have downloaded ones added to it, while builtin won't module.exports.DEFINITIONS = BUILTIN_DEFINITIONS module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS diff --git a/packages/server/src/automations/index.js b/packages/server/src/automations/index.js index a983495fb5..9aba399133 100644 --- a/packages/server/src/automations/index.js +++ b/packages/server/src/automations/index.js @@ -30,23 +30,22 @@ async function updateQuota(automation) { /** * This module is built purely to kick off the worker farm and manage the inputs/outputs */ -module.exports.init = function() { - actions.init().then(() => { - triggers.automationQueue.process(async job => { - try { - if (env.CLOUD && job.data.automation && !env.SELF_HOSTED) { - job.data.automation.apiKey = await updateQuota(job.data.automation) - } - if (env.BUDIBASE_ENVIRONMENT === "PRODUCTION") { - await runWorker(job) - } else { - await singleThread(job) - } - } catch (err) { - console.error( - `${job.data.automation.appId} automation ${job.data.automation._id} was unable to run - ${err}` - ) +module.exports.init = async function() { + await actions.init() + triggers.automationQueue.process(async job => { + try { + if (env.CLOUD && job.data.automation && !env.SELF_HOSTED) { + job.data.automation.apiKey = await updateQuota(job.data.automation) } - }) + if (env.BUDIBASE_ENVIRONMENT === "PRODUCTION") { + await runWorker(job) + } else { + await singleThread(job) + } + } catch (err) { + console.error( + `${job.data.automation.appId} automation ${job.data.automation._id} was unable to run - ${err}` + ) + } }) } diff --git a/packages/server/src/automations/steps/createRow.js b/packages/server/src/automations/steps/createRow.js index aeb75958f6..ef136e1131 100644 --- a/packages/server/src/automations/steps/createRow.js +++ b/packages/server/src/automations/steps/createRow.js @@ -59,15 +59,14 @@ module.exports.definition = { } module.exports.run = async function({ inputs, appId, apiKey, emitter }) { - // TODO: better logging of when actions are missed due to missing parameters if (inputs.row == null || inputs.row.tableId == null) { - return + return { + success: false, + response: { + message: "Invalid inputs", + }, + } } - inputs.row = await automationUtils.cleanUpRow( - appId, - inputs.row.tableId, - inputs.row - ) // have to clean up the row, remove the table from it const ctx = { params: { @@ -81,6 +80,11 @@ module.exports.run = async function({ inputs, appId, apiKey, emitter }) { } try { + inputs.row = await automationUtils.cleanUpRow( + appId, + inputs.row.tableId, + inputs.row + ) if (env.CLOUD) { await usage.update(apiKey, usage.Properties.ROW, 1) } diff --git a/packages/server/src/automations/steps/deleteRow.js b/packages/server/src/automations/steps/deleteRow.js index 8edee38dee..ea4d60a04e 100644 --- a/packages/server/src/automations/steps/deleteRow.js +++ b/packages/server/src/automations/steps/deleteRow.js @@ -51,9 +51,13 @@ module.exports.definition = { } module.exports.run = async function({ inputs, appId, apiKey, emitter }) { - // TODO: better logging of when actions are missed due to missing parameters if (inputs.id == null || inputs.revision == null) { - return + return { + success: false, + response: { + message: "Invalid inputs", + }, + } } let ctx = { params: { diff --git a/packages/server/src/automations/steps/filter.js b/packages/server/src/automations/steps/filter.js index 4286cd44e8..586e424cc4 100644 --- a/packages/server/src/automations/steps/filter.js +++ b/packages/server/src/automations/steps/filter.js @@ -12,6 +12,9 @@ const PrettyLogicConditions = { [LogicConditions.LESS_THAN]: "Less than", } +module.exports.LogicConditions = LogicConditions +module.exports.PrettyLogicConditions = PrettyLogicConditions + module.exports.definition = { name: "Filter", tagline: "{{inputs.field}} {{inputs.condition}} {{inputs.value}}", @@ -64,7 +67,7 @@ module.exports.run = async function filter({ inputs }) { value = Date.parse(value) field = Date.parse(field) } - let success + let success = false if (typeof field !== "object" && typeof value !== "object") { switch (condition) { case LogicConditions.EQUAL: @@ -79,8 +82,6 @@ module.exports.run = async function filter({ inputs }) { case LogicConditions.LESS_THAN: success = field < value break - default: - return } } else { success = false diff --git a/packages/server/src/automations/steps/outgoingWebhook.js b/packages/server/src/automations/steps/outgoingWebhook.js index 817ec424b2..ab8c747c58 100644 --- a/packages/server/src/automations/steps/outgoingWebhook.js +++ b/packages/server/src/automations/steps/outgoingWebhook.js @@ -87,6 +87,7 @@ module.exports.run = async function({ inputs }) { success: response.status === 200, } } catch (err) { + /* istanbul ignore next */ return { success: false, response: err, diff --git a/packages/server/src/automations/steps/updateRow.js b/packages/server/src/automations/steps/updateRow.js index 3b83f961f5..a545662cf8 100644 --- a/packages/server/src/automations/steps/updateRow.js +++ b/packages/server/src/automations/steps/updateRow.js @@ -55,14 +55,14 @@ module.exports.definition = { module.exports.run = async function({ inputs, appId, emitter }) { if (inputs.rowId == null || inputs.row == null) { - return + return { + success: false, + response: { + message: "Invalid inputs", + }, + } } - inputs.row = await automationUtils.cleanUpRowById( - appId, - inputs.rowId, - inputs.row - ) // clear any falsy properties so that they aren't updated for (let propKey of Object.keys(inputs.row)) { if (!inputs.row[propKey] || inputs.row[propKey] === "") { @@ -73,7 +73,7 @@ module.exports.run = async function({ inputs, appId, emitter }) { // have to clean up the row, remove the table from it const ctx = { params: { - id: inputs.rowId, + rowId: inputs.rowId, }, request: { body: inputs.row, @@ -83,6 +83,11 @@ module.exports.run = async function({ inputs, appId, emitter }) { } try { + inputs.row = await automationUtils.cleanUpRowById( + appId, + inputs.rowId, + inputs.row + ) await rowController.patch(ctx) return { row: ctx.body, diff --git a/packages/server/src/automations/tests/automation.spec.js b/packages/server/src/automations/tests/automation.spec.js new file mode 100644 index 0000000000..f4d3b4c865 --- /dev/null +++ b/packages/server/src/automations/tests/automation.spec.js @@ -0,0 +1,152 @@ +const automation = require("../index") +const usageQuota = require("../../utilities/usageQuota") +const thread = require("../thread") +const triggers = require("../triggers") +const { basicAutomation, basicTable } = require("../../tests/utilities/structures") +const { wait } = require("../../utilities") +const env = require("../../environment") +const { makePartial } = require("../../tests/utilities") +const { cleanInputValues } = require("../automationUtils") +const setup = require("./utilities") + +let workerJob + +jest.mock("../../utilities/usageQuota") +usageQuota.getAPIKey.mockReturnValue({ apiKey: "test" }) +jest.mock("../thread") +jest.spyOn(global.console, "error") +jest.mock("worker-farm", () => { + return () => { + const value = jest + .fn() + .mockReturnValueOnce(undefined) + .mockReturnValueOnce("Error") + return (input, callback) => { + workerJob = input + if (callback) { + callback(value()) + } + } + } +}) + +describe("Run through some parts of the automations system", () => { + let config = setup.getConfig() + + beforeEach(async () => { + await automation.init() + await config.init() + }) + + afterAll(setup.afterAll) + + it("should be able to init in builder", async () => { + await triggers.externalTrigger(basicAutomation(), { a: 1 }) + await wait(100) + expect(workerJob).toBeUndefined() + expect(thread).toHaveBeenCalled() + }) + + it("should be able to init in cloud", async () => { + env.CLOUD = true + env.BUDIBASE_ENVIRONMENT = "PRODUCTION" + await triggers.externalTrigger(basicAutomation(), { a: 1 }) + await wait(100) + // haven't added a mock implementation so getAPIKey of usageQuota just returns undefined + expect(usageQuota.update).toHaveBeenCalledWith("test", "automationRuns", 1) + expect(workerJob).toBeDefined() + env.BUDIBASE_ENVIRONMENT = "JEST" + env.CLOUD = false + }) + + it("try error scenario", async () => { + env.CLOUD = true + env.BUDIBASE_ENVIRONMENT = "PRODUCTION" + // the second call will throw an error + await triggers.externalTrigger(basicAutomation(), { a: 1 }) + await wait(100) + expect(console.error).toHaveBeenCalled() + env.BUDIBASE_ENVIRONMENT = "JEST" + env.CLOUD = false + }) + + it("should be able to check triggering row filling", async () => { + const automation = basicAutomation() + let table = basicTable() + table.schema.boolean = { + type: "boolean", + constraints: { + type: "boolean", + }, + } + table.schema.number = { + type: "number", + constraints: { + type: "number", + }, + } + table.schema.datetime = { + type: "datetime", + constraints: { + type: "datetime", + }, + } + table = await config.createTable(table) + automation.definition.trigger.inputs.tableId = table._id + const params = await triggers.fillRowOutput(automation, { appId: config.getAppId() }) + expect(params.row).toBeDefined() + const date = new Date(params.row.datetime) + expect(typeof params.row.name).toBe("string") + expect(typeof params.row.boolean).toBe("boolean") + expect(typeof params.row.number).toBe("number") + expect(date.getFullYear()).toBe(1970) + }) + + it("should check coercion", async () => { + const table = await config.createTable() + const automation = basicAutomation() + automation.definition.trigger.inputs.tableId = table._id + automation.definition.trigger.stepId = "APP" + automation.definition.trigger.inputs.fields = { a: "number" } + await triggers.externalTrigger(automation, { + appId: config.getAppId(), + fields: { + a: "1" + } + }) + await wait(100) + expect(thread).toHaveBeenCalledWith(makePartial({ + data: { + event: { + fields: { + a: 1 + } + } + } + })) + }) + + it("should be able to clean inputs with the utilities", () => { + // can't clean without a schema + let output = cleanInputValues({a: "1"}) + expect(output.a).toBe("1") + output = cleanInputValues({a: "1", b: "true", c: "false", d: 1, e: "help"}, { + properties: { + a: { + type: "number", + }, + b: { + type: "boolean", + }, + c: { + type: "boolean", + } + } + }) + expect(output.a).toBe(1) + expect(output.b).toBe(true) + expect(output.c).toBe(false) + expect(output.d).toBe(1) + expect(output.e).toBe("help") + }) +}) \ No newline at end of file diff --git a/packages/server/src/automations/tests/createRow.spec.js b/packages/server/src/automations/tests/createRow.spec.js new file mode 100644 index 0000000000..0be2803e47 --- /dev/null +++ b/packages/server/src/automations/tests/createRow.spec.js @@ -0,0 +1,57 @@ +const usageQuota = require("../../utilities/usageQuota") +const env = require("../../environment") +const setup = require("./utilities") + +jest.mock("../../utilities/usageQuota") + +describe("test the create row action", () => { + let table, row + let config = setup.getConfig() + + beforeEach(async () => { + await config.init() + table = await config.createTable() + row = { + tableId: table._id, + name: "test", + description: "test", + } + }) + + afterAll(setup.afterAll) + + it("should be able to run the action", async () => { + const res = await setup.runStep(setup.actions.CREATE_ROW.stepId, { + row, + }) + expect(res.id).toBeDefined() + expect(res.revision).toBeDefined() + const gottenRow = await config.getRow(table._id, res.id) + expect(gottenRow.name).toEqual("test") + expect(gottenRow.description).toEqual("test") + }) + + it("should return an error (not throw) when bad info provided", async () => { + const res = await setup.runStep(setup.actions.CREATE_ROW.stepId, { + row: { + tableId: "invalid", + invalid: "invalid", + } + }) + expect(res.success).toEqual(false) + }) + + it("check usage quota attempts", async () => { + env.CLOUD = true + await setup.runStep(setup.actions.CREATE_ROW.stepId, { + row + }) + expect(usageQuota.update).toHaveBeenCalledWith(setup.apiKey, "rows", 1) + env.CLOUD = false + }) + + it("should check invalid inputs return an error", async () => { + const res = await setup.runStep(setup.actions.CREATE_ROW.stepId, {}) + expect(res.success).toEqual(false) + }) +}) diff --git a/packages/server/src/automations/tests/createUser.spec.js b/packages/server/src/automations/tests/createUser.spec.js new file mode 100644 index 0000000000..5f65e260a9 --- /dev/null +++ b/packages/server/src/automations/tests/createUser.spec.js @@ -0,0 +1,43 @@ +const usageQuota = require("../../utilities/usageQuota") +const env = require("../../environment") +const setup = require("./utilities") +const { BUILTIN_ROLE_IDS } = require("../../utilities/security/roles") +const { ViewNames } = require("../../db/utils") + +jest.mock("../../utilities/usageQuota") + +describe("test the create user action", () => { + let config = setup.getConfig() + let user + + beforeEach(async () => { + await config.init() + user = { + email: "test@test.com", + password: "password", + roleId: BUILTIN_ROLE_IDS.POWER + } + }) + + afterAll(setup.afterAll) + + it("should be able to run the action", async () => { + const res = await setup.runStep(setup.actions.CREATE_USER.stepId, user) + expect(res.id).toBeDefined() + expect(res.revision).toBeDefined() + const userDoc = await config.getRow(ViewNames.USERS, res.id) + expect(userDoc.email).toEqual(user.email) + }) + + it("should return an error if no inputs provided", async () => { + const res = await setup.runStep(setup.actions.CREATE_USER.stepId, {}) + expect(res.success).toEqual(false) + }) + + it("check usage quota attempts", async () => { + env.CLOUD = true + await setup.runStep(setup.actions.CREATE_USER.stepId, user) + expect(usageQuota.update).toHaveBeenCalledWith(setup.apiKey, "users", 1) + env.CLOUD = false + }) +}) diff --git a/packages/server/src/automations/tests/delay.spec.js b/packages/server/src/automations/tests/delay.spec.js new file mode 100644 index 0000000000..99046e8171 --- /dev/null +++ b/packages/server/src/automations/tests/delay.spec.js @@ -0,0 +1,12 @@ +const setup = require("./utilities") + +describe("test the delay logic", () => { + it("should be able to run the delay", async () => { + const time = 100 + const before = Date.now() + await setup.runStep(setup.logic.DELAY.stepId, { time: time }) + const now = Date.now() + // divide by two just so that test will always pass as long as there was some sort of delay + expect(now - before).toBeGreaterThanOrEqual(time / 2) + }) +}) \ No newline at end of file diff --git a/packages/server/src/automations/tests/deleteRow.spec.js b/packages/server/src/automations/tests/deleteRow.spec.js new file mode 100644 index 0000000000..0d5ff47ed8 --- /dev/null +++ b/packages/server/src/automations/tests/deleteRow.spec.js @@ -0,0 +1,58 @@ +const usageQuota = require("../../utilities/usageQuota") +const env = require("../../environment") +const setup = require("./utilities") + +jest.mock("../../utilities/usageQuota") + +describe("test the delete row action", () => { + let table, row, inputs + let config = setup.getConfig() + + beforeEach(async () => { + await config.init() + table = await config.createTable() + row = await config.createRow() + inputs = { + tableId: table._id, + id: row._id, + revision: row._rev, + } + }) + + afterAll(setup.afterAll) + + it("should be able to run the action", async () => { + const res = await setup.runStep(setup.actions.DELETE_ROW.stepId, inputs) + expect(res.success).toEqual(true) + expect(res.response).toBeDefined() + expect(res.row._id).toEqual(row._id) + let error + try { + await config.getRow(table._id, res.id) + } catch (err) { + error = err + } + expect(error).toBeDefined() + }) + + it("check usage quota attempts", async () => { + env.CLOUD = true + await setup.runStep(setup.actions.DELETE_ROW.stepId, inputs) + expect(usageQuota.update).toHaveBeenCalledWith(setup.apiKey, "rows", -1) + env.CLOUD = false + }) + + it("should check invalid inputs return an error", async () => { + const res = await setup.runStep(setup.actions.DELETE_ROW.stepId, {}) + expect(res.success).toEqual(false) + }) + + it("should return an error when table doesn't exist", async () => { + const res = await setup.runStep(setup.actions.DELETE_ROW.stepId, { + tableId: "invalid", + id: "invalid", + revision: "invalid", + }) + expect(res.success).toEqual(false) + }) +}) diff --git a/packages/server/src/automations/tests/filter.spec.js b/packages/server/src/automations/tests/filter.spec.js new file mode 100644 index 0000000000..05361f43ed --- /dev/null +++ b/packages/server/src/automations/tests/filter.spec.js @@ -0,0 +1,48 @@ +const setup = require("./utilities") +const { LogicConditions } = require("../steps/filter") + +describe("test the filter logic", () => { + async function checkFilter(field, condition, value, pass = true) { + let res = await setup.runStep(setup.logic.FILTER.stepId, + { field, condition, value } + ) + expect(res.success).toEqual(pass) + } + + it("should be able test equality", async () => { + await checkFilter("hello", LogicConditions.EQUAL, "hello", true) + await checkFilter("hello", LogicConditions.EQUAL, "no", false) + }) + + it("should be able to test greater than", async () => { + await checkFilter(10, LogicConditions.GREATER_THAN, 5, true) + await checkFilter(10, LogicConditions.GREATER_THAN, 15, false) + }) + + it("should be able to test less than", async () => { + await checkFilter(5, LogicConditions.LESS_THAN, 10, true) + await checkFilter(15, LogicConditions.LESS_THAN, 10, false) + }) + + it("should be able to in-equality", async () => { + await checkFilter("hello", LogicConditions.NOT_EQUAL, "no", true) + await checkFilter(10, LogicConditions.NOT_EQUAL, 10, false) + }) + + it("check number coercion", async () => { + await checkFilter("10", LogicConditions.GREATER_THAN, "5", true) + }) + + it("check date coercion", async () => { + await checkFilter( + (new Date()).toISOString(), + LogicConditions.GREATER_THAN, + (new Date(-10000)).toISOString(), + true + ) + }) + + it("check objects always false", async () => { + await checkFilter({}, LogicConditions.EQUAL, {}, false) + }) +}) \ No newline at end of file diff --git a/packages/server/src/automations/tests/outgoingWebhook.spec.js b/packages/server/src/automations/tests/outgoingWebhook.spec.js new file mode 100644 index 0000000000..f1d8d25ba8 --- /dev/null +++ b/packages/server/src/automations/tests/outgoingWebhook.spec.js @@ -0,0 +1,39 @@ +const setup = require("./utilities") +const fetch = require("node-fetch") + +jest.mock("node-fetch") + +describe("test the outgoing webhook action", () => { + let inputs + let config = setup.getConfig() + + beforeEach(async () => { + await config.init() + inputs = { + requestMethod: "POST", + url: "www.test.com", + requestBody: JSON.stringify({ + a: 1, + }), + } + }) + + afterAll(setup.afterAll) + + it("should be able to run the action", async () => { + const res = await setup.runStep(setup.actions.OUTGOING_WEBHOOK.stepId, inputs) + expect(res.success).toEqual(true) + expect(res.response.url).toEqual("http://www.test.com") + expect(res.response.method).toEqual("POST") + expect(res.response.body.a).toEqual(1) + }) + + it("should return an error if something goes wrong in fetch", async () => { + const res = await setup.runStep(setup.actions.OUTGOING_WEBHOOK.stepId, { + requestMethod: "GET", + url: "www.invalid.com" + }) + expect(res.success).toEqual(false) + }) + +}) diff --git a/packages/server/src/automations/tests/sendEmail.spec.js b/packages/server/src/automations/tests/sendEmail.spec.js new file mode 100644 index 0000000000..5f3aabfff8 --- /dev/null +++ b/packages/server/src/automations/tests/sendEmail.spec.js @@ -0,0 +1,36 @@ +const setup = require("./utilities") + +jest.mock("@sendgrid/mail") + +describe("test the send email action", () => { + let inputs + let config = setup.getConfig() + + beforeEach(async () => { + await config.init() + inputs = { + to: "me@test.com", + from: "budibase@test.com", + subject: "Testing", + text: "Email contents", + } + }) + + afterAll(setup.afterAll) + + it("should be able to run the action", async () => { + const res = await setup.runStep(setup.actions.SEND_EMAIL.stepId, inputs) + expect(res.success).toEqual(true) + // the mocked module throws back the input + expect(res.response.to).toEqual("me@test.com") + }) + + it("should return an error if input an invalid email address", async () => { + const res = await setup.runStep(setup.actions.SEND_EMAIL.stepId, { + ...inputs, + to: "invalid@test.com", + }) + expect(res.success).toEqual(false) + }) + +}) diff --git a/packages/server/src/automations/tests/updateRow.spec.js b/packages/server/src/automations/tests/updateRow.spec.js new file mode 100644 index 0000000000..79c998459b --- /dev/null +++ b/packages/server/src/automations/tests/updateRow.spec.js @@ -0,0 +1,45 @@ +const env = require("../../environment") +const setup = require("./utilities") + +describe("test the update row action", () => { + let table, row, inputs + let config = setup.getConfig() + + beforeEach(async () => { + await config.init() + table = await config.createTable() + row = await config.createRow() + inputs = { + rowId: row._id, + row: { + ...row, + name: "Updated name", + // put a falsy option in to be removed + description: "", + } + } + }) + + afterAll(setup.afterAll) + + it("should be able to run the action", async () => { + const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, inputs) + expect(res.success).toEqual(true) + const updatedRow = await config.getRow(table._id, res.id) + expect(updatedRow.name).toEqual("Updated name") + expect(updatedRow.description).not.toEqual("") + }) + + it("should check invalid inputs return an error", async () => { + const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {}) + expect(res.success).toEqual(false) + }) + + it("should return an error when table doesn't exist", async () => { + const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, { + row: { _id: "invalid" }, + rowId: "invalid", + }) + expect(res.success).toEqual(false) + }) +}) diff --git a/packages/server/src/automations/tests/utilities/index.js b/packages/server/src/automations/tests/utilities/index.js new file mode 100644 index 0000000000..ad149d6bde --- /dev/null +++ b/packages/server/src/automations/tests/utilities/index.js @@ -0,0 +1,43 @@ +const TestConfig = require("../../../tests/utilities/TestConfiguration") +const actions = require("../../actions") +const logic = require("../../logic") +const emitter = require("../../../events/index") + +let config + +exports.getConfig = () => { + if (!config) { + config = new TestConfig(false) + } + return config +} + +exports.afterAll = () => { + config.end() +} + +exports.runStep = async function runStep(stepId, inputs) { + let step + if ( + Object.values(exports.actions) + .map(action => action.stepId) + .includes(stepId) + ) { + step = await actions.getAction(stepId) + } else { + step = logic.getLogic(stepId) + } + expect(step).toBeDefined() + return step({ + inputs, + appId: config ? config.getAppId() : null, + // don't really need an API key, mocked out usage quota, not being tested here + apiKey: exports.apiKey, + emitter, + }) +} + +exports.apiKey = "test" + +exports.actions = actions.BUILTIN_DEFINITIONS +exports.logic = logic.BUILTIN_DEFINITIONS diff --git a/packages/server/src/automations/triggers.js b/packages/server/src/automations/triggers.js index 73ce9edeed..7e50e5ee74 100644 --- a/packages/server/src/automations/triggers.js +++ b/packages/server/src/automations/triggers.js @@ -225,6 +225,7 @@ async function queueRelevantRowAutomations(event, eventType) { } emitter.on("row:save", async function(event) { + /* istanbul ignore next */ if (!event || !event.row || !event.row.tableId) { return } @@ -232,6 +233,7 @@ emitter.on("row:save", async function(event) { }) emitter.on("row:update", async function(event) { + /* istanbul ignore next */ if (!event || !event.row || !event.row.tableId) { return } @@ -239,6 +241,7 @@ emitter.on("row:update", async function(event) { }) emitter.on("row:delete", async function(event) { + /* istanbul ignore next */ if (!event || !event.row || !event.row.tableId) { return } @@ -272,6 +275,7 @@ async function fillRowOutput(automation, params) { } params.row = row } catch (err) { + /* istanbul ignore next */ throw "Failed to find table for trigger" } return params @@ -297,6 +301,7 @@ module.exports.externalTrigger = async function(automation, params) { automationQueue.add({ automation, event: params }) } +module.exports.fillRowOutput = fillRowOutput module.exports.automationQueue = automationQueue module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS diff --git a/packages/server/src/db/client.js b/packages/server/src/db/client.js index b5edb1e877..f6dea33a40 100644 --- a/packages/server/src/db/client.js +++ b/packages/server/src/db/client.js @@ -30,6 +30,7 @@ const Pouch = PouchDB.defaults(POUCH_DB_DEFAULTS) allDbs(Pouch) // replicate your local levelDB pouch to a running HTTP compliant couch or pouchdb server. +/* istanbul ignore next */ // eslint-disable-next-line no-unused-vars function replicateLocal() { Pouch.allDbs().then(dbs => { diff --git a/packages/server/src/middleware/tests/usageQuota.spec.js b/packages/server/src/middleware/tests/usageQuota.spec.js index c76acb47d2..395f14c1ed 100644 --- a/packages/server/src/middleware/tests/usageQuota.spec.js +++ b/packages/server/src/middleware/tests/usageQuota.spec.js @@ -3,7 +3,7 @@ const usageQuota = require("../../utilities/usageQuota") const CouchDB = require("../../db") const env = require("../../environment") -jest.mock("../../db"); +jest.mock("../../db") jest.mock("../../utilities/usageQuota") jest.mock("../../environment") diff --git a/packages/server/src/selfhost/README.md b/packages/server/src/selfhost/README.md deleted file mode 100644 index a02743a58c..0000000000 --- a/packages/server/src/selfhost/README.md +++ /dev/null @@ -1,7 +0,0 @@ -### Self hosting -This directory contains utilities that are needed for self hosted platforms to operate. -These will mostly be utilities, necessary to the operation of the server e.g. storing self -hosting specific options and attributes to CouchDB. - -All the internal operations should be exposed through the `index.js` so importing -the self host directory should give you everything you need. \ No newline at end of file diff --git a/packages/server/src/selfhost/index.js b/packages/server/src/selfhost/index.js deleted file mode 100644 index f77d1f0b6c..0000000000 --- a/packages/server/src/selfhost/index.js +++ /dev/null @@ -1,44 +0,0 @@ -const CouchDB = require("../db") -const env = require("../environment") -const newid = require("../db/newid") - -const SELF_HOST_DB = "self-host-db" -const SELF_HOST_DOC = "self-host-info" - -async function createSelfHostDB(db) { - await db.put({ - _id: "_design/database", - views: {}, - }) - const selfHostInfo = { - _id: SELF_HOST_DOC, - apiKeyId: newid(), - } - await db.put(selfHostInfo) - return selfHostInfo -} - -exports.init = async () => { - if (!env.SELF_HOSTED) { - return - } - const db = new CouchDB(SELF_HOST_DB) - try { - await db.get(SELF_HOST_DOC) - } catch (err) { - // failed to retrieve - if (err.status === 404) { - await createSelfHostDB(db) - } - } -} - -exports.getSelfHostInfo = async () => { - const db = new CouchDB(SELF_HOST_DB) - return db.get(SELF_HOST_DOC) -} - -exports.getSelfHostAPIKey = async () => { - const info = await exports.getSelfHostInfo() - return info ? info.apiKeyId : null -} diff --git a/packages/server/src/api/routes/tests/utilities/TestConfiguration.js b/packages/server/src/tests/utilities/TestConfiguration.js similarity index 88% rename from packages/server/src/api/routes/tests/utilities/TestConfiguration.js rename to packages/server/src/tests/utilities/TestConfiguration.js index 0ff742293d..b36b45186a 100644 --- a/packages/server/src/api/routes/tests/utilities/TestConfiguration.js +++ b/packages/server/src/tests/utilities/TestConfiguration.js @@ -1,6 +1,6 @@ -const { BUILTIN_ROLE_IDS } = require("../../../../utilities/security/roles") +const { BUILTIN_ROLE_IDS } = require("../../utilities/security/roles") const jwt = require("jsonwebtoken") -const env = require("../../../../environment") +const env = require("../../environment") const { basicTable, basicRow, @@ -15,18 +15,20 @@ const { const controllers = require("./controllers") const supertest = require("supertest") const fs = require("fs") -const { budibaseAppsDir } = require("../../../../utilities/budibaseDir") +const { budibaseAppsDir } = require("../../utilities/budibaseDir") const { join } = require("path") const EMAIL = "babs@babs.com" const PASSWORD = "babs_password" class TestConfiguration { - constructor() { - env.PORT = 4002 - this.server = require("../../../../app") - // we need the request for logging in, involves cookies, hard to fake - this.request = supertest(this.server) + constructor(openServer = true) { + if (openServer) { + env.PORT = 4002 + this.server = require("../../app") + // we need the request for logging in, involves cookies, hard to fake + this.request = supertest(this.server) + } this.appId = null this.allApps = [] } @@ -61,7 +63,9 @@ class TestConfiguration { } end() { - this.server.close() + if (this.server) { + this.server.close() + } const appDir = budibaseAppsDir() const files = fs.readdirSync(appDir) for (let file of files) { @@ -163,6 +167,17 @@ class TestConfiguration { return this._req(config, { tableId: this.table._id }, controllers.row.save) } + async getRow(tableId, rowId) { + return this._req(null, { tableId, rowId }, controllers.row.find) + } + + async getRows(tableId) { + if (!tableId && this.table) { + tableId = this.table._id + } + return this._req(null, { tableId }, controllers.row.fetchTableRows) + } + async createRole(config = null) { config = config || basicRole() return this._req(config, null, controllers.role.save) @@ -187,6 +202,7 @@ class TestConfiguration { const view = config || { map: "function(doc) { emit(doc[doc.key], doc._id); } ", tableId: this.table._id, + name: "ViewTest", } return this._req(view, null, controllers.view.save) } @@ -285,6 +301,9 @@ class TestConfiguration { } async login(email, password) { + if (!this.request) { + throw "Server has not been opened, cannot login." + } if (!email || !password) { await this.createUser() email = EMAIL diff --git a/packages/server/src/tests/utilities/controllers.js b/packages/server/src/tests/utilities/controllers.js new file mode 100644 index 0000000000..b07754038f --- /dev/null +++ b/packages/server/src/tests/utilities/controllers.js @@ -0,0 +1,15 @@ +module.exports = { + table: require("../../api/controllers/table"), + row: require("../../api/controllers/row"), + role: require("../../api/controllers/role"), + perms: require("../../api/controllers/permission"), + view: require("../../api/controllers/view"), + app: require("../../api/controllers/application"), + user: require("../../api/controllers/user"), + automation: require("../../api/controllers/automation"), + datasource: require("../../api/controllers/datasource"), + query: require("../../api/controllers/query"), + screen: require("../../api/controllers/screen"), + webhook: require("../../api/controllers/webhook"), + layout: require("../../api/controllers/layout"), +} diff --git a/packages/server/src/tests/utilities/index.js b/packages/server/src/tests/utilities/index.js new file mode 100644 index 0000000000..aa8039ce2f --- /dev/null +++ b/packages/server/src/tests/utilities/index.js @@ -0,0 +1,11 @@ +exports.makePartial = obj => { + const newObj = {} + for (let key of Object.keys(obj)) { + if (typeof obj[key] === "object") { + newObj[key] = exports.makePartial(obj[key]) + } else { + newObj[key] = obj[key] + } + } + return expect.objectContaining(newObj) +} diff --git a/packages/server/src/api/routes/tests/utilities/structures.js b/packages/server/src/tests/utilities/structures.js similarity index 86% rename from packages/server/src/api/routes/tests/utilities/structures.js rename to packages/server/src/tests/utilities/structures.js index ff3a239211..e6489f0903 100644 --- a/packages/server/src/api/routes/tests/utilities/structures.js +++ b/packages/server/src/tests/utilities/structures.js @@ -1,9 +1,9 @@ -const { BUILTIN_ROLE_IDS } = require("../../../../utilities/security/roles") +const { BUILTIN_ROLE_IDS } = require("../../utilities/security/roles") const { BUILTIN_PERMISSION_IDS, -} = require("../../../../utilities/security/permissions") -const { createHomeScreen } = require("../../../../constants/screens") -const { EMPTY_LAYOUT } = require("../../../../constants/layouts") +} = require("../../utilities/security/permissions") +const { createHomeScreen } = require("../../constants/screens") +const { EMPTY_LAYOUT } = require("../../constants/layouts") const { cloneDeep } = require("lodash/fp") exports.basicTable = () => { diff --git a/packages/server/src/utilities/createAppPackage.js b/packages/server/src/utilities/createAppPackage.js index a62e8c96df..9500554227 100644 --- a/packages/server/src/utilities/createAppPackage.js +++ b/packages/server/src/utilities/createAppPackage.js @@ -7,6 +7,8 @@ const packageJson = require("../../package.json") const streamPipeline = promisify(stream.pipeline) +// can't really test this due to the downloading nature of it, wouldn't be a great test case +/* istanbul ignore next */ exports.downloadExtractComponentLibraries = async appFolder => { const LIBRARIES = ["standard-components"] diff --git a/packages/server/src/utilities/exceptions.js b/packages/server/src/utilities/exceptions.js deleted file mode 100644 index e02c88eec3..0000000000 --- a/packages/server/src/utilities/exceptions.js +++ /dev/null @@ -1,16 +0,0 @@ -const statusCodes = require("./statusCodes") - -const errorWithStatus = (message, statusCode) => { - const e = new Error(message) - e.statusCode = statusCode - return e -} - -module.exports.unauthorized = message => - errorWithStatus(message, statusCodes.UNAUTHORIZED) - -module.exports.forbidden = message => - errorWithStatus(message, statusCodes.FORBIDDEN) - -module.exports.notfound = message => - errorWithStatus(message, statusCodes.NOT_FOUND) diff --git a/packages/server/src/utilities/routing/index.js b/packages/server/src/utilities/routing/index.js index f4af585dc6..541733dcc4 100644 --- a/packages/server/src/utilities/routing/index.js +++ b/packages/server/src/utilities/routing/index.js @@ -12,6 +12,7 @@ exports.getRoutingInfo = async appId => { return allRouting.rows.map(row => row.value) } catch (err) { // check if the view doesn't exist, it should for all new instances + /* istanbul ignore next */ if (err != null && err.name === "not_found") { await createRoutingView(appId) return exports.getRoutingInfo(appId) diff --git a/packages/server/src/utilities/security/apikey.js b/packages/server/src/utilities/security/apikey.js index c8965cee43..3d5f428bb7 100644 --- a/packages/server/src/utilities/security/apikey.js +++ b/packages/server/src/utilities/security/apikey.js @@ -1,6 +1,5 @@ const { apiKeyTable } = require("../../db/dynamoClient") const env = require("../../environment") -const { getSelfHostAPIKey } = require("../../selfhost") /** * This file purely exists so that we can centralise all logic pertaining to API keys, as their usage differs @@ -8,16 +7,13 @@ const { getSelfHostAPIKey } = require("../../selfhost") */ exports.isAPIKeyValid = async apiKeyId => { - if (env.CLOUD && !env.SELF_HOSTED) { + if (!env.SELF_HOSTED) { let apiKeyInfo = await apiKeyTable.get({ primary: apiKeyId, }) return apiKeyInfo != null - } - if (env.SELF_HOSTED) { - const selfHostKey = await getSelfHostAPIKey() + } else { // if the api key supplied is correct then return structure similar - return apiKeyId === selfHostKey ? { pk: apiKeyId } : null + return apiKeyId === env.HOSTING_KEY } - return false }