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
}