Merge pull request #1288 from Budibase/tests/automation-tests

Tests/automation tests
This commit is contained in:
Michael Drury 2021-03-16 11:42:18 +00:00 committed by GitHub
commit d7890eb7b5
56 changed files with 982 additions and 293 deletions

View File

@ -53,7 +53,11 @@
$: uneditable = $: uneditable =
$backendUiStore.selectedTable?._id === TableNames.USERS && $backendUiStore.selectedTable?._id === TableNames.USERS &&
UNEDITABLE_USER_FIELDS.includes(field.name) 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 // used to select what different options can be displayed for column type
$: canBeSearched = $: canBeSearched =

View File

@ -1,38 +1,42 @@
<script> <script>
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from "svelte"
import Colorpicker from "@budibase/colorpicker" import Colorpicker from "@budibase/colorpicker"
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher()
export let value export let value
const WAIT = 150; const WAIT = 150
function throttle(callback, wait, immediate = false) { function throttle(callback, wait, immediate = false) {
let timeout = null let timeout = null
let initialCall = true let initialCall = true
return function() { return function() {
const callNow = immediate && initialCall const callNow = immediate && initialCall
const next = () => { const next = () => {
callback.apply(this, arguments) callback.apply(this, arguments)
timeout = null timeout = null
} }
if (callNow) { if (callNow) {
initialCall = false initialCall = false
next() next()
} }
if (!timeout) { if (!timeout) {
timeout = setTimeout(next, wait) timeout = setTimeout(next, wait)
} }
} }
} }
const onChange = throttle(e => { const onChange = throttle(
dispatch('change', e.detail) e => {
}, WAIT, true) dispatch("change", e.detail)
},
WAIT,
true
)
</script> </script>
<Colorpicker value={value || '#C4C4C4'} on:change={onChange} /> <Colorpicker value={value || '#C4C4C4'} on:change={onChange} />

View File

@ -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()

View File

@ -1,17 +1,35 @@
const fetch = jest.requireActual("node-fetch") const fetch = jest.requireActual("node-fetch")
module.exports = async (url, opts) => { module.exports = async (url, opts) => {
// mocked data based on url function json(body, status = 200) {
if (url.includes("api/apps")) {
return { return {
status,
json: async () => { json: async () => {
return { return body
app1: {
url: "/app1",
},
}
}, },
} }
} }
// 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) return fetch(url, opts)
} }

View File

@ -33,7 +33,7 @@
}, },
"scripts": { "scripts": {
"test": "jest --testPathIgnorePatterns=routes && npm run test:integration", "test": "jest --testPathIgnorePatterns=routes && npm run test:integration",
"test:integration": "jest --runInBand --coverage", "test:integration": "jest --coverage --detectOpenHandles",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"run:docker": "node src/index", "run:docker": "node src/index",
"dev:builder": "cross-env PORT=4001 nodemon src/index.js", "dev:builder": "cross-env PORT=4001 nodemon src/index.js",
@ -53,11 +53,16 @@
"src/**/*.js", "src/**/*.js",
"!**/node_modules/**", "!**/node_modules/**",
"!src/db/views/*.js", "!src/db/views/*.js",
"!src/api/routes/tests/**/*.js",
"!src/api/controllers/deploy/**/*.js", "!src/api/controllers/deploy/**/*.js",
"!src/api/controllers/static/templates/**/*", "!src/*.js",
"!src/api/controllers/static/selfhost/**/*", "!src/api/controllers/static/**/*",
"!src/*.js" "!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": [ "coverageReporters": [
"lcov", "lcov",

View File

@ -65,12 +65,14 @@ exports.save = async function(ctx) {
// Don't rename if the name is the same // Don't rename if the name is the same
let { _rename } = tableToSave let { _rename } = tableToSave
/* istanbul ignore next */
if (_rename && _rename.old === _rename.updated) { if (_rename && _rename.old === _rename.updated) {
_rename = null _rename = null
delete tableToSave._rename delete tableToSave._rename
} }
// rename row fields when table column is renamed // rename row fields when table column is renamed
/* istanbul ignore next */
if (_rename && tableToSave.schema[_rename.updated].type === FieldTypes.LINK) { if (_rename && tableToSave.schema[_rename.updated].type === FieldTypes.LINK) {
ctx.throw(400, "Cannot rename a linked column.") ctx.throw(400, "Cannot rename a linked column.")
} else if (_rename && tableToSave.primaryDisplay === _rename.old) { } else if (_rename && tableToSave.primaryDisplay === _rename.old) {
@ -159,7 +161,7 @@ exports.destroy = async function(ctx) {
ctx.eventEmitter && ctx.eventEmitter &&
ctx.eventEmitter.emitTable(`table:delete`, appId, tableToDelete) ctx.eventEmitter.emitTable(`table:delete`, appId, tableToDelete)
ctx.status = 200 ctx.status = 200
ctx.message = `Table ${ctx.params.tableId} deleted.` ctx.body = { message: `Table ${ctx.params.tableId} deleted.` }
} }
exports.validateCSVSchema = async function(ctx) { exports.validateCSVSchema = async function(ctx) {

View File

@ -90,7 +90,8 @@ exports.handleDataImport = async (user, table, dataImport) => {
return table return table
} }
exports.handleSearchIndexes = async (db, table) => { exports.handleSearchIndexes = async (appId, table) => {
const db = new CouchDB(appId)
// create relevant search indexes // create relevant search indexes
if (table.indexes && table.indexes.length > 0) { if (table.indexes && table.indexes.length > 0) {
const currentIndexes = await db.getIndexes() const currentIndexes = await db.getIndexes()
@ -150,6 +151,9 @@ class TableSaveFunctions {
constructor({ db, ctx, oldTable, dataImport }) { constructor({ db, ctx, oldTable, dataImport }) {
this.db = db this.db = db
this.ctx = ctx this.ctx = ctx
if (this.ctx && this.ctx.user) {
this.appId = this.ctx.user.appId
}
this.oldTable = oldTable this.oldTable = oldTable
this.dataImport = dataImport this.dataImport = dataImport
// any rows that need updated // any rows that need updated
@ -178,7 +182,7 @@ class TableSaveFunctions {
// after saving // after saving
async after(table) { async after(table) {
table = await exports.handleSearchIndexes(this.db, table) table = await exports.handleSearchIndexes(this.appId, table)
table = await exports.handleDataImport( table = await exports.handleDataImport(
this.ctx.user, this.ctx.user,
table, table,

View File

@ -29,11 +29,13 @@ const controller = {
save: async ctx => { save: async ctx => {
const db = new CouchDB(ctx.user.appId) const db = new CouchDB(ctx.user.appId)
const { originalName, ...viewToSave } = ctx.request.body const { originalName, ...viewToSave } = ctx.request.body
const designDoc = await db.get("_design/database") const designDoc = await db.get("_design/database")
const view = viewTemplate(viewToSave) const view = viewTemplate(viewToSave)
if (!viewToSave.name) {
ctx.throw(400, "Cannot create view without a name")
}
designDoc.views = { designDoc.views = {
...designDoc.views, ...designDoc.views,
[viewToSave.name]: view, [viewToSave.name]: view,
@ -60,17 +62,16 @@ const controller = {
await db.put(table) await db.put(table)
ctx.body = table.views[viewToSave.name] ctx.body = {
ctx.message = `View ${viewToSave.name} saved successfully.` ...table.views[viewToSave.name],
name: viewToSave.name,
}
}, },
destroy: async ctx => { destroy: async ctx => {
const db = new CouchDB(ctx.user.appId) const db = new CouchDB(ctx.user.appId)
const designDoc = await db.get("_design/database") const designDoc = await db.get("_design/database")
const viewName = decodeURI(ctx.params.viewName) const viewName = decodeURI(ctx.params.viewName)
const view = designDoc.views[viewName] const view = designDoc.views[viewName]
delete designDoc.views[viewName] delete designDoc.views[viewName]
await db.put(designDoc) await db.put(designDoc)
@ -80,16 +81,17 @@ const controller = {
await db.put(table) await db.put(table)
ctx.body = view ctx.body = view
ctx.message = `View ${ctx.params.viewName} saved successfully.`
}, },
exportView: async ctx => { exportView: async ctx => {
const db = new CouchDB(ctx.user.appId) const db = new CouchDB(ctx.user.appId)
const designDoc = await db.get("_design/database") const designDoc = await db.get("_design/database")
const viewName = decodeURI(ctx.query.view) const viewName = decodeURI(ctx.query.view)
const view = designDoc.views[viewName] const view = designDoc.views[viewName]
const format = ctx.query.format const format = ctx.query.format
if (!format) {
ctx.throw(400, "Format must be specified, either csv or json")
}
if (view) { if (view) {
ctx.params.viewName = viewName ctx.params.viewName = viewName
@ -102,6 +104,7 @@ const controller = {
} }
} else { } else {
// table all_ view // table all_ view
/* istanbul ignore next */
ctx.params.viewName = viewName ctx.params.viewName = viewName
} }

View File

@ -3,8 +3,8 @@ const {
getAllTableRows, getAllTableRows,
clearAllAutomations, clearAllAutomations,
} = require("./utilities/TestFunctions") } = require("./utilities/TestFunctions")
const { basicAutomation } = require("./utilities/structures")
const setup = require("./utilities") const setup = require("./utilities")
const { basicAutomation } = setup.structures
const MAX_RETRIES = 4 const MAX_RETRIES = 4

View File

@ -1,6 +1,6 @@
let {basicDatasource} = require("./utilities/structures")
let {checkBuilderEndpoint} = require("./utilities/TestFunctions")
let setup = require("./utilities") let setup = require("./utilities")
let { basicDatasource } = setup.structures
let { checkBuilderEndpoint } = require("./utilities/TestFunctions")
describe("/datasources", () => { describe("/datasources", () => {
let request = setup.getRequest() let request = setup.getRequest()

View File

@ -1,6 +1,6 @@
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
const setup = require("./utilities") const setup = require("./utilities")
const { basicLayout } = require("./utilities/structures") const { basicLayout } = setup.structures
describe("/layouts", () => { describe("/layouts", () => {
let request = setup.getRequest() let request = setup.getRequest()

View File

@ -1,6 +1,7 @@
const setup = require("./utilities") const setup = require("./utilities")
const tableUtils = require("../../controllers/table/utils")
describe("/analytics", () => { describe("run misc tests", () => {
let request = setup.getRequest() let request = setup.getRequest()
let config = setup.getConfig() let config = setup.getConfig()
@ -10,29 +11,44 @@ describe("/analytics", () => {
await config.init() await config.init()
}) })
describe("isEnabled", () => { describe("/analytics", () => {
it("check if analytics enabled", async () => { it("check if analytics enabled", async () => {
const res = await request const res = await request
.get(`/api/analytics`) .get(`/api/analytics`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(typeof res.body.enabled).toEqual("boolean") expect(typeof res.body.enabled).toEqual("boolean")
})
})
describe("/health", () => {
it("should confirm healthy", async () => {
await request.get("/health").expect(200)
}) })
}) })
})
describe("/health", () => { describe("/version", () => {
it("should confirm healthy", async () => { it("should confirm version", async () => {
let config = setup.getConfig() const res = await request.get("/version").expect(200)
await config.getRequest().get("/health").expect(200) expect(res.text.split(".").length).toEqual(3)
})
}) })
})
describe("/version", () => { describe("test table utilities", () => {
it("should confirm version", async () => { it("should be able to import a CSV", async () => {
const config = setup.getConfig() const table = await config.createTable()
const res = await config.getRequest().get("/version").expect(200) const dataImport = {
expect(res.text.split(".").length).toEqual(3) 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")
})
}) })
}) })

View File

@ -1,6 +1,6 @@
const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles") const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
const setup = require("./utilities") const setup = require("./utilities")
const { basicRow } = require("./utilities/structures") const { basicRow } = setup.structures
const HIGHER_ROLE_ID = BUILTIN_ROLE_IDS.BASIC const HIGHER_ROLE_ID = BUILTIN_ROLE_IDS.BASIC
const STD_ROLE_ID = BUILTIN_ROLE_IDS.PUBLIC const STD_ROLE_ID = BUILTIN_ROLE_IDS.PUBLIC

View File

@ -1,9 +1,9 @@
// mock out postgres for this // mock out postgres for this
jest.mock("pg") jest.mock("pg")
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
const { basicQuery, basicDatasource } = require("./utilities/structures")
const setup = require("./utilities") const setup = require("./utilities")
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
const { basicQuery, basicDatasource } = setup.structures
describe("/queries", () => { describe("/queries", () => {
let request = setup.getRequest() let request = setup.getRequest()

View File

@ -2,8 +2,8 @@ const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
const { const {
BUILTIN_PERMISSION_IDS, BUILTIN_PERMISSION_IDS,
} = require("../../../utilities/security/permissions") } = require("../../../utilities/security/permissions")
const { basicRole } = require("./utilities/structures")
const setup = require("./utilities") const setup = require("./utilities")
const { basicRole } = setup.structures
describe("/roles", () => { describe("/roles", () => {
let request = setup.getRequest() let request = setup.getRequest()

View File

@ -1,5 +1,5 @@
const setup = require("./utilities") const setup = require("./utilities")
const { basicScreen } = require("./utilities/structures") const { basicScreen } = setup.structures
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles") const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")

View File

@ -1,7 +1,6 @@
const { outputProcessing } = require("../../../utilities/rowProcessor") const { outputProcessing } = require("../../../utilities/rowProcessor")
const env = require("../../../environment")
const { basicRow } = require("./utilities/structures")
const setup = require("./utilities") const setup = require("./utilities")
const { basicRow } = setup.structures
describe("/rows", () => { describe("/rows", () => {
let request = setup.getRequest() let request = setup.getRequest()
@ -349,7 +348,7 @@ describe("/rows", () => {
const view = await config.createView() const view = await config.createView()
const row = await config.createRow() const row = await config.createRow()
const res = await request const res = await request
.get(`/api/views/${view._id}`) .get(`/api/views/${view.name}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)

View File

@ -1,6 +1,6 @@
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
const setup = require("./utilities") const setup = require("./utilities")
const { basicScreen } = require("./utilities/structures") const { basicScreen } = setup.structures
describe("/screens", () => { describe("/screens", () => {
let request = setup.getRequest() let request = setup.getRequest()

View File

@ -1,5 +1,6 @@
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") const { checkBuilderEndpoint, getDB } = require("./utilities/TestFunctions")
const setup = require("./utilities") const setup = require("./utilities")
const { basicTable } = setup.structures
describe("/tables", () => { describe("/tables", () => {
let request = setup.getRequest() let request = setup.getRequest()
@ -12,25 +13,22 @@ describe("/tables", () => {
}) })
describe("create", () => { describe("create", () => {
it("returns a success message when the table is successfully created", done => { it("returns a success message when the table is successfully created", async () => {
request const res = await request
.post(`/api/tables`) .post(`/api/tables`)
.send({ .send({
name: "TestTable", name: "TestTable",
key: "name", key: "name",
schema: { schema: {
name: { type: "string" } name: {type: "string"}
} }
}) })
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
.end(async (err, res) => { expect(res.res.statusMessage).toEqual("Table TestTable saved successfully.")
expect(res.res.statusMessage).toEqual("Table TestTable saved successfully.") expect(res.body.name).toEqual("TestTable")
expect(res.body.name).toEqual("TestTable") })
done()
})
})
it("renames all the row fields for a table when a schema key is renamed", async () => { it("renames all the row fields for a table when a schema key is renamed", async () => {
const testTable = await config.createTable() const testTable = await config.createTable()
@ -46,7 +44,7 @@ describe("/tables", () => {
const updatedTable = await request const updatedTable = await request
.post(`/api/tables`) .post(`/api/tables`)
.send({ .send({
_id: testTable._id, _id: testTable._id,
_rev: testTable._rev, _rev: testTable._rev,
name: "TestTable", name: "TestTable",
@ -56,41 +54,40 @@ describe("/tables", () => {
updated: "updatedName" updated: "updatedName"
}, },
schema: { schema: {
updatedName: { type: "string" } updatedName: {type: "string"}
} }
}) })
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .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.") const res = await request
expect(updatedTable.body.name).toEqual("TestTable") .get(`/api/${testTable._id}/rows/${testRow.body._id}`)
.set(config.defaultHeaders())
.expect('Content-Type', /json/)
.expect(200)
const res = await request expect(res.body.updatedName).toEqual("test")
.get(`/api/${testTable._id}/rows/${testRow.body._id}`) expect(res.body.name).toBeUndefined()
.set(config.defaultHeaders()) })
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.updatedName).toEqual("test") it("should apply authorization to endpoint", async () => {
expect(res.body.name).toBeUndefined() await checkBuilderEndpoint({
}) config,
method: "POST",
it("should apply authorization to endpoint", async () => { url: `/api/tables`,
await checkBuilderEndpoint({ body: {
config, name: "TestTable",
method: "POST", key: "name",
url: `/api/tables`, schema: {
body: { name: {type: "string"}
name: "TestTable",
key: "name",
schema: {
name: { type: "string" }
}
} }
}) }
}) })
}) })
})
describe("fetch", () => { describe("fetch", () => {
let testTable let testTable
@ -103,28 +100,91 @@ describe("/tables", () => {
delete testTable._rev delete testTable._rev
}) })
it("returns all the tables for that instance in the response body", done => { it("returns all the tables for that instance in the response body", async () => {
request const res = await request
.get(`/api/tables`) .get(`/api/tables`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
.end(async (_, res) => { const fetchedTable = res.body[0]
const fetchedTable = res.body[0] expect(fetchedTable.name).toEqual(testTable.name)
expect(fetchedTable.name).toEqual(testTable.name) expect(fetchedTable.type).toEqual("table")
expect(fetchedTable.type).toEqual("table")
done()
})
}) })
it("should apply authorization to endpoint", async () => { it("should apply authorization to endpoint", async () => {
await checkBuilderEndpoint({ await checkBuilderEndpoint({
config, config,
method: "GET", method: "GET",
url: `/api/tables`, 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", () => { describe("destroy", () => {
let testTable let testTable
@ -137,19 +197,16 @@ describe("/tables", () => {
delete testTable._rev delete testTable._rev
}) })
it("returns a success response when a table is deleted.", async done => { it("returns a success response when a table is deleted.", async () => {
request const res = await request
.delete(`/api/tables/${testTable._id}/${testTable._rev}`) .delete(`/api/tables/${testTable._id}/${testTable._rev}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
.end(async (_, res) => { expect(res.body.message).toEqual(`Table ${testTable._id} deleted.`)
expect(res.res.statusMessage).toEqual(`Table ${testTable._id} deleted.`) })
done()
})
})
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({ const linkedTable = await config.createTable({
name: "LinkedTable", name: "LinkedTable",
type: "table", type: "table",
@ -171,18 +228,15 @@ describe("/tables", () => {
}, },
}) })
request const res = await request
.delete(`/api/tables/${testTable._id}/${testTable._rev}`) .delete(`/api/tables/${testTable._id}/${testTable._rev}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
.end(async (_, res) => { expect(res.body.message).toEqual(`Table ${testTable._id} deleted.`)
expect(res.res.statusMessage).toEqual(`Table ${testTable._id} deleted.`) const dependentTable = await config.getTable(linkedTable._id)
const dependentTable = await config.getTable(linkedTable._id) expect(dependentTable.schema.TestTable).not.toBeDefined()
expect(dependentTable.schema.TestTable).not.toBeDefined() })
done()
})
})
it("should apply authorization to endpoint", async () => { it("should apply authorization to endpoint", async () => {
await checkBuilderEndpoint({ await checkBuilderEndpoint({
@ -191,6 +245,5 @@ describe("/tables", () => {
url: `/api/tables/${testTable._id}/${testTable._rev}`, url: `/api/tables/${testTable._id}/${testTable._rev}`,
}) })
}) })
}) })
}) })

View File

@ -1,7 +1,7 @@
const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles") const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
const { checkPermissionsEndpoint } = require("./utilities/TestFunctions") const { checkPermissionsEndpoint } = require("./utilities/TestFunctions")
const { basicUser } = require("./utilities/structures")
const setup = require("./utilities") const setup = require("./utilities")
const { basicUser } = setup.structures
describe("/users", () => { describe("/users", () => {
let request = setup.getRequest() let request = setup.getRequest()

View File

@ -1,5 +1,6 @@
const rowController = require("../../../controllers/row") const rowController = require("../../../controllers/row")
const appController = require("../../../controllers/application") const appController = require("../../../controllers/application")
const CouchDB = require("../../../../db")
function Request(appId, params) { function Request(appId, params) {
this.user = { appId } this.user = { appId }
@ -77,3 +78,7 @@ exports.checkPermissionsEndpoint = async ({
.set(failHeader) .set(failHeader)
.expect(403) .expect(403)
} }
exports.getDB = config => {
return new CouchDB(config.getAppId())
}

View File

@ -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"),
}

View File

@ -1,4 +1,5 @@
const TestConfig = require("./TestConfiguration") const TestConfig = require("../../../../tests/utilities/TestConfiguration")
const structures = require("../../../../tests/utilities/structures")
const env = require("../../../../environment") const env = require("../../../../environment")
exports.delay = ms => new Promise(resolve => setTimeout(resolve, ms)) exports.delay = ms => new Promise(resolve => setTimeout(resolve, ms))
@ -51,3 +52,5 @@ exports.switchToCloudForFunction = async func => {
throw error throw error
} }
} }
exports.structures = structures

View File

@ -29,9 +29,7 @@ describe("/views", () => {
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.res.statusMessage).toEqual( expect(res.body.tableId).toBe(table._id)
"View TestView saved successfully."
)
}) })
it("updates the table row with the new view metadata", async () => { it("updates the table row with the new view metadata", async () => {
@ -46,10 +44,8 @@ describe("/views", () => {
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .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) const updatedTable = await config.getTable(table._id)
expect(updatedTable.views).toEqual({ expect(updatedTable.views).toEqual({
TestView: { TestView: {
@ -173,4 +169,49 @@ describe("/views", () => {
expect(res.body).toMatchSnapshot() 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()
})
})
}) })

View File

@ -1,6 +1,6 @@
const setup = require("./utilities") const setup = require("./utilities")
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
const { basicWebhook, basicAutomation } = require("./utilities/structures") const { basicWebhook, basicAutomation } = setup.structures
describe("/webhooks", () => { describe("/webhooks", () => {
let request = setup.getRequest() let request = setup.getRequest()

View File

@ -9,7 +9,6 @@ const env = require("./environment")
const eventEmitter = require("./events") const eventEmitter = require("./events")
const automations = require("./automations/index") const automations = require("./automations/index")
const Sentry = require("@sentry/node") const Sentry = require("@sentry/node")
const selfhost = require("./selfhost")
const app = new Koa() 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())}`) console.log(`Budibase running on ${JSON.stringify(server.address())}`)
env._set("PORT", server.address().port) env._set("PORT", server.address().port)
eventEmitter.emitPort(env.PORT) eventEmitter.emitPort(env.PORT)
automations.init() await automations.init()
// only init the self hosting DB info in the Pouch, not needed in self hosting prod
if (!env.CLOUD) {
await selfhost.init()
}
}) })
process.on("uncaughtException", err => { process.on("uncaughtException", err => {

View File

@ -37,10 +37,12 @@ let AUTOMATION_BUCKET = env.AUTOMATION_BUCKET
let AUTOMATION_DIRECTORY = env.AUTOMATION_DIRECTORY let AUTOMATION_DIRECTORY = env.AUTOMATION_DIRECTORY
let MANIFEST = null let MANIFEST = null
/* istanbul ignore next */
function buildBundleName(pkgName, version) { function buildBundleName(pkgName, version) {
return `${pkgName}@${version}.min.js` return `${pkgName}@${version}.min.js`
} }
/* istanbul ignore next */
async function downloadPackage(name, version, bundleName) { async function downloadPackage(name, version, bundleName) {
await download( await download(
`${AUTOMATION_BUCKET}/${name}/${version}/${bundleName}`, `${AUTOMATION_BUCKET}/${name}/${version}/${bundleName}`,
@ -49,6 +51,7 @@ async function downloadPackage(name, version, bundleName) {
return require(join(AUTOMATION_DIRECTORY, bundleName)) return require(join(AUTOMATION_DIRECTORY, bundleName))
} }
/* istanbul ignore next */
module.exports.getAction = async function(actionName) { module.exports.getAction = async function(actionName) {
if (BUILTIN_ACTIONS[actionName] != null) { if (BUILTIN_ACTIONS[actionName] != null) {
return BUILTIN_ACTIONS[actionName] return BUILTIN_ACTIONS[actionName]
@ -96,5 +99,6 @@ module.exports.init = async function() {
return MANIFEST return MANIFEST
} }
// definitions will have downloaded ones added to it, while builtin won't
module.exports.DEFINITIONS = BUILTIN_DEFINITIONS module.exports.DEFINITIONS = BUILTIN_DEFINITIONS
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS

View File

@ -30,23 +30,22 @@ async function updateQuota(automation) {
/** /**
* This module is built purely to kick off the worker farm and manage the inputs/outputs * This module is built purely to kick off the worker farm and manage the inputs/outputs
*/ */
module.exports.init = function() { module.exports.init = async function() {
actions.init().then(() => { await actions.init()
triggers.automationQueue.process(async job => { triggers.automationQueue.process(async job => {
try { try {
if (env.CLOUD && job.data.automation && !env.SELF_HOSTED) { if (env.CLOUD && job.data.automation && !env.SELF_HOSTED) {
job.data.automation.apiKey = await updateQuota(job.data.automation) 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}`
)
} }
}) 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}`
)
}
}) })
} }

View File

@ -59,15 +59,14 @@ module.exports.definition = {
} }
module.exports.run = async function({ inputs, appId, apiKey, emitter }) { 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) { 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 // have to clean up the row, remove the table from it
const ctx = { const ctx = {
params: { params: {
@ -81,6 +80,11 @@ module.exports.run = async function({ inputs, appId, apiKey, emitter }) {
} }
try { try {
inputs.row = await automationUtils.cleanUpRow(
appId,
inputs.row.tableId,
inputs.row
)
if (env.CLOUD) { if (env.CLOUD) {
await usage.update(apiKey, usage.Properties.ROW, 1) await usage.update(apiKey, usage.Properties.ROW, 1)
} }

View File

@ -51,9 +51,13 @@ module.exports.definition = {
} }
module.exports.run = async function({ inputs, appId, apiKey, emitter }) { 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) { if (inputs.id == null || inputs.revision == null) {
return return {
success: false,
response: {
message: "Invalid inputs",
},
}
} }
let ctx = { let ctx = {
params: { params: {

View File

@ -12,6 +12,9 @@ const PrettyLogicConditions = {
[LogicConditions.LESS_THAN]: "Less than", [LogicConditions.LESS_THAN]: "Less than",
} }
module.exports.LogicConditions = LogicConditions
module.exports.PrettyLogicConditions = PrettyLogicConditions
module.exports.definition = { module.exports.definition = {
name: "Filter", name: "Filter",
tagline: "{{inputs.field}} {{inputs.condition}} {{inputs.value}}", tagline: "{{inputs.field}} {{inputs.condition}} {{inputs.value}}",
@ -64,7 +67,7 @@ module.exports.run = async function filter({ inputs }) {
value = Date.parse(value) value = Date.parse(value)
field = Date.parse(field) field = Date.parse(field)
} }
let success let success = false
if (typeof field !== "object" && typeof value !== "object") { if (typeof field !== "object" && typeof value !== "object") {
switch (condition) { switch (condition) {
case LogicConditions.EQUAL: case LogicConditions.EQUAL:
@ -79,8 +82,6 @@ module.exports.run = async function filter({ inputs }) {
case LogicConditions.LESS_THAN: case LogicConditions.LESS_THAN:
success = field < value success = field < value
break break
default:
return
} }
} else { } else {
success = false success = false

View File

@ -87,6 +87,7 @@ module.exports.run = async function({ inputs }) {
success: response.status === 200, success: response.status === 200,
} }
} catch (err) { } catch (err) {
/* istanbul ignore next */
return { return {
success: false, success: false,
response: err, response: err,

View File

@ -55,14 +55,14 @@ module.exports.definition = {
module.exports.run = async function({ inputs, appId, emitter }) { module.exports.run = async function({ inputs, appId, emitter }) {
if (inputs.rowId == null || inputs.row == null) { 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 // clear any falsy properties so that they aren't updated
for (let propKey of Object.keys(inputs.row)) { for (let propKey of Object.keys(inputs.row)) {
if (!inputs.row[propKey] || inputs.row[propKey] === "") { 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 // have to clean up the row, remove the table from it
const ctx = { const ctx = {
params: { params: {
id: inputs.rowId, rowId: inputs.rowId,
}, },
request: { request: {
body: inputs.row, body: inputs.row,
@ -83,6 +83,11 @@ module.exports.run = async function({ inputs, appId, emitter }) {
} }
try { try {
inputs.row = await automationUtils.cleanUpRowById(
appId,
inputs.rowId,
inputs.row
)
await rowController.patch(ctx) await rowController.patch(ctx)
return { return {
row: ctx.body, row: ctx.body,

View File

@ -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")
})
})

View File

@ -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)
})
})

View File

@ -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
})
})

View File

@ -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)
})
})

View File

@ -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)
})
})

View File

@ -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)
})
})

View File

@ -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)
})
})

View File

@ -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)
})
})

View File

@ -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)
})
})

View File

@ -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

View File

@ -225,6 +225,7 @@ async function queueRelevantRowAutomations(event, eventType) {
} }
emitter.on("row:save", async function(event) { emitter.on("row:save", async function(event) {
/* istanbul ignore next */
if (!event || !event.row || !event.row.tableId) { if (!event || !event.row || !event.row.tableId) {
return return
} }
@ -232,6 +233,7 @@ emitter.on("row:save", async function(event) {
}) })
emitter.on("row:update", async function(event) { emitter.on("row:update", async function(event) {
/* istanbul ignore next */
if (!event || !event.row || !event.row.tableId) { if (!event || !event.row || !event.row.tableId) {
return return
} }
@ -239,6 +241,7 @@ emitter.on("row:update", async function(event) {
}) })
emitter.on("row:delete", async function(event) { emitter.on("row:delete", async function(event) {
/* istanbul ignore next */
if (!event || !event.row || !event.row.tableId) { if (!event || !event.row || !event.row.tableId) {
return return
} }
@ -272,6 +275,7 @@ async function fillRowOutput(automation, params) {
} }
params.row = row params.row = row
} catch (err) { } catch (err) {
/* istanbul ignore next */
throw "Failed to find table for trigger" throw "Failed to find table for trigger"
} }
return params return params
@ -297,6 +301,7 @@ module.exports.externalTrigger = async function(automation, params) {
automationQueue.add({ automation, event: params }) automationQueue.add({ automation, event: params })
} }
module.exports.fillRowOutput = fillRowOutput
module.exports.automationQueue = automationQueue module.exports.automationQueue = automationQueue
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS

View File

@ -30,6 +30,7 @@ const Pouch = PouchDB.defaults(POUCH_DB_DEFAULTS)
allDbs(Pouch) allDbs(Pouch)
// replicate your local levelDB pouch to a running HTTP compliant couch or pouchdb server. // replicate your local levelDB pouch to a running HTTP compliant couch or pouchdb server.
/* istanbul ignore next */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
function replicateLocal() { function replicateLocal() {
Pouch.allDbs().then(dbs => { Pouch.allDbs().then(dbs => {

View File

@ -3,7 +3,7 @@ const usageQuota = require("../../utilities/usageQuota")
const CouchDB = require("../../db") const CouchDB = require("../../db")
const env = require("../../environment") const env = require("../../environment")
jest.mock("../../db"); jest.mock("../../db")
jest.mock("../../utilities/usageQuota") jest.mock("../../utilities/usageQuota")
jest.mock("../../environment") jest.mock("../../environment")

View File

@ -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.

View File

@ -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
}

View File

@ -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 jwt = require("jsonwebtoken")
const env = require("../../../../environment") const env = require("../../environment")
const { const {
basicTable, basicTable,
basicRow, basicRow,
@ -15,18 +15,20 @@ const {
const controllers = require("./controllers") const controllers = require("./controllers")
const supertest = require("supertest") const supertest = require("supertest")
const fs = require("fs") const fs = require("fs")
const { budibaseAppsDir } = require("../../../../utilities/budibaseDir") const { budibaseAppsDir } = require("../../utilities/budibaseDir")
const { join } = require("path") const { join } = require("path")
const EMAIL = "babs@babs.com" const EMAIL = "babs@babs.com"
const PASSWORD = "babs_password" const PASSWORD = "babs_password"
class TestConfiguration { class TestConfiguration {
constructor() { constructor(openServer = true) {
env.PORT = 4002 if (openServer) {
this.server = require("../../../../app") env.PORT = 4002
// we need the request for logging in, involves cookies, hard to fake this.server = require("../../app")
this.request = supertest(this.server) // we need the request for logging in, involves cookies, hard to fake
this.request = supertest(this.server)
}
this.appId = null this.appId = null
this.allApps = [] this.allApps = []
} }
@ -61,7 +63,9 @@ class TestConfiguration {
} }
end() { end() {
this.server.close() if (this.server) {
this.server.close()
}
const appDir = budibaseAppsDir() const appDir = budibaseAppsDir()
const files = fs.readdirSync(appDir) const files = fs.readdirSync(appDir)
for (let file of files) { for (let file of files) {
@ -163,6 +167,17 @@ class TestConfiguration {
return this._req(config, { tableId: this.table._id }, controllers.row.save) 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) { async createRole(config = null) {
config = config || basicRole() config = config || basicRole()
return this._req(config, null, controllers.role.save) return this._req(config, null, controllers.role.save)
@ -187,6 +202,7 @@ class TestConfiguration {
const view = config || { const view = config || {
map: "function(doc) { emit(doc[doc.key], doc._id); } ", map: "function(doc) { emit(doc[doc.key], doc._id); } ",
tableId: this.table._id, tableId: this.table._id,
name: "ViewTest",
} }
return this._req(view, null, controllers.view.save) return this._req(view, null, controllers.view.save)
} }
@ -285,6 +301,9 @@ class TestConfiguration {
} }
async login(email, password) { async login(email, password) {
if (!this.request) {
throw "Server has not been opened, cannot login."
}
if (!email || !password) { if (!email || !password) {
await this.createUser() await this.createUser()
email = EMAIL email = EMAIL

View File

@ -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"),
}

View File

@ -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)
}

View File

@ -1,9 +1,9 @@
const { BUILTIN_ROLE_IDS } = require("../../../../utilities/security/roles") const { BUILTIN_ROLE_IDS } = require("../../utilities/security/roles")
const { const {
BUILTIN_PERMISSION_IDS, BUILTIN_PERMISSION_IDS,
} = require("../../../../utilities/security/permissions") } = require("../../utilities/security/permissions")
const { createHomeScreen } = require("../../../../constants/screens") const { createHomeScreen } = require("../../constants/screens")
const { EMPTY_LAYOUT } = require("../../../../constants/layouts") const { EMPTY_LAYOUT } = require("../../constants/layouts")
const { cloneDeep } = require("lodash/fp") const { cloneDeep } = require("lodash/fp")
exports.basicTable = () => { exports.basicTable = () => {

View File

@ -7,6 +7,8 @@ const packageJson = require("../../package.json")
const streamPipeline = promisify(stream.pipeline) 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 => { exports.downloadExtractComponentLibraries = async appFolder => {
const LIBRARIES = ["standard-components"] const LIBRARIES = ["standard-components"]

View File

@ -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)

View File

@ -12,6 +12,7 @@ exports.getRoutingInfo = async appId => {
return allRouting.rows.map(row => row.value) return allRouting.rows.map(row => row.value)
} catch (err) { } catch (err) {
// check if the view doesn't exist, it should for all new instances // check if the view doesn't exist, it should for all new instances
/* istanbul ignore next */
if (err != null && err.name === "not_found") { if (err != null && err.name === "not_found") {
await createRoutingView(appId) await createRoutingView(appId)
return exports.getRoutingInfo(appId) return exports.getRoutingInfo(appId)

View File

@ -1,6 +1,5 @@
const { apiKeyTable } = require("../../db/dynamoClient") const { apiKeyTable } = require("../../db/dynamoClient")
const env = require("../../environment") 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 * 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 => { exports.isAPIKeyValid = async apiKeyId => {
if (env.CLOUD && !env.SELF_HOSTED) { if (!env.SELF_HOSTED) {
let apiKeyInfo = await apiKeyTable.get({ let apiKeyInfo = await apiKeyTable.get({
primary: apiKeyId, primary: apiKeyId,
}) })
return apiKeyInfo != null return apiKeyInfo != null
} } else {
if (env.SELF_HOSTED) {
const selfHostKey = await getSelfHostAPIKey()
// if the api key supplied is correct then return structure similar // if the api key supplied is correct then return structure similar
return apiKeyId === selfHostKey ? { pk: apiKeyId } : null return apiKeyId === env.HOSTING_KEY
} }
return false
} }