From 1a9290b3959239a10c90fe4e0d8f714d3d5f456d Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Fri, 5 Mar 2021 14:13:43 +0000 Subject: [PATCH 01/24] middleware tests --- .../src/middleware/tests/selfhost.spec.js | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/server/src/middleware/tests/selfhost.spec.js diff --git a/packages/server/src/middleware/tests/selfhost.spec.js b/packages/server/src/middleware/tests/selfhost.spec.js new file mode 100644 index 0000000000..0f721bc890 --- /dev/null +++ b/packages/server/src/middleware/tests/selfhost.spec.js @@ -0,0 +1,43 @@ +const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles") +const { checkPermissionsEndpoint } = require("./utilities/TestFunctions") +const { basicUser } = require("./utilities/structures") +const setup = require("./utilities") + +describe("Self host middleware", () => { + let request = setup.getRequest() + let config = setup.getConfig() + + afterAll(setup.afterAll) + + beforeEach(async () => { + await config.init() + }) + + describe("fetch", () => { + it("returns a list of users from an instance db", async () => { + await config.createUser("brenda@brenda.com", "brendas_password") + await config.createUser("pam@pam.com", "pam_password") + const res = await request + .get(`/api/users`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + + expect(res.body.length).toBe(2) + expect(res.body.find(u => u.email === "brenda@brenda.com")).toBeDefined() + expect(res.body.find(u => u.email === "pam@pam.com")).toBeDefined() + }) + + it("should apply authorization to endpoint", async () => { + await config.createUser("brenda@brenda.com", "brendas_password") + await checkPermissionsEndpoint({ + config, + request, + method: "GET", + url: `/api/users`, + passRole: BUILTIN_ROLE_IDS.ADMIN, + failRole: BUILTIN_ROLE_IDS.PUBLIC, + }) + }) + }) +}) From 701a479b2ac26281bdf9a0df48aa44329a80ae93 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 8 Mar 2021 14:49:19 +0000 Subject: [PATCH 02/24] Updating routing test cases. --- .../src/api/routes/tests/routing.spec.js | 85 +++++++++++++++++++ .../tests/utilities/TestConfiguration.js | 16 ++++ .../api/routes/tests/utilities/controllers.js | 1 + .../api/routes/tests/utilities/structures.js | 5 ++ 4 files changed, 107 insertions(+) create mode 100644 packages/server/src/api/routes/tests/routing.spec.js diff --git a/packages/server/src/api/routes/tests/routing.spec.js b/packages/server/src/api/routes/tests/routing.spec.js new file mode 100644 index 0000000000..3b7523f586 --- /dev/null +++ b/packages/server/src/api/routes/tests/routing.spec.js @@ -0,0 +1,85 @@ +const setup = require("./utilities") +const { basicScreen } = require("./utilities/structures") +const { checkBuilderEndpoint } = require("./utilities/TestFunctions") +const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles") + +describe("/routing", () => { + let request = setup.getRequest() + let config = setup.getConfig() + let screen, screen2 + + afterAll(setup.afterAll) + + beforeEach(async () => { + await config.init() + screen = await config.createScreen(basicScreen()) + screen2 = basicScreen() + screen2.routing.roleId = BUILTIN_ROLE_IDS.POWER + screen2 = await config.createScreen(screen2) + }) + + describe("fetch", () => { + it("returns the correct routing for basic user", async () => { + const res = await request + .get(`/api/routing/client`) + .set(await config.roleHeaders("basic@test.com", BUILTIN_ROLE_IDS.BASIC)) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.routes).toBeDefined() + expect(res.body.routes["/"]).toEqual({ + subpaths: { + ["/"]: { + screenId: screen._id, + roleId: screen.routing.roleId + } + } + }) + }) + + it("returns the correct routing for power user", async () => { + const res = await request + .get(`/api/routing/client`) + .set(await config.roleHeaders("basic@test.com", BUILTIN_ROLE_IDS.POWER)) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.routes).toBeDefined() + expect(res.body.routes["/"]).toEqual({ + subpaths: { + ["/"]: { + screenId: screen2._id, + roleId: screen2.routing.roleId + } + } + }) + }) + }) + + describe("fetch all", () => { + it("should fetch all routes for builder", async () => { + const res = await request + .get(`/api/routing`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.routes).toBeDefined() + expect(res.body.routes["/"]).toEqual({ + subpaths: { + ["/"]: { + screens: { + [screen2.routing.roleId]: screen2._id, + [screen.routing.roleId]: screen._id, + } + } + } + }) + }) + + it("make sure it is a builder only endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "GET", + url: `/api/routing`, + }) + }) + }) +}) \ No newline at end of file diff --git a/packages/server/src/api/routes/tests/utilities/TestConfiguration.js b/packages/server/src/api/routes/tests/utilities/TestConfiguration.js index 5f6b1cc267..a124ca607e 100644 --- a/packages/server/src/api/routes/tests/utilities/TestConfiguration.js +++ b/packages/server/src/api/routes/tests/utilities/TestConfiguration.js @@ -8,6 +8,7 @@ const { basicAutomation, basicDatasource, basicQuery, + basicScreen, } = require("./structures") const controllers = require("./controllers") const supertest = require("supertest") @@ -83,6 +84,15 @@ class TestConfiguration { return headers } + async roleHeaders(email = EMAIL, roleId) { + try { + await this.createUser(email, PASSWORD, roleId) + } catch (err) { + // allow errors here + } + return this.login(email, PASSWORD) + } + async createApp(appName) { this.app = await this._req({ name: appName }, null, controllers.app.create) this.appId = this.app._id @@ -208,6 +218,11 @@ class TestConfiguration { return this._req(config, null, controllers.query.save) } + async createScreen(config = null) { + config = config || basicScreen() + return this._req(config, null, controllers.screen.save) + } + async createUser( email = EMAIL, password = PASSWORD, @@ -241,6 +256,7 @@ class TestConfiguration { return { Accept: "application/json", Cookie: result.headers["set-cookie"], + "x-budibase-app-id": this.appId, } } } diff --git a/packages/server/src/api/routes/tests/utilities/controllers.js b/packages/server/src/api/routes/tests/utilities/controllers.js index 541495bec8..e16aa6964c 100644 --- a/packages/server/src/api/routes/tests/utilities/controllers.js +++ b/packages/server/src/api/routes/tests/utilities/controllers.js @@ -9,4 +9,5 @@ module.exports = { automation: require("../../../controllers/automation"), datasource: require("../../../controllers/datasource"), query: require("../../../controllers/query"), + screen: require("../../../controllers/screen"), } diff --git a/packages/server/src/api/routes/tests/utilities/structures.js b/packages/server/src/api/routes/tests/utilities/structures.js index 922228aadf..aec482bb24 100644 --- a/packages/server/src/api/routes/tests/utilities/structures.js +++ b/packages/server/src/api/routes/tests/utilities/structures.js @@ -2,6 +2,7 @@ const { BUILTIN_ROLE_IDS } = require("../../../../utilities/security/roles") const { BUILTIN_PERMISSION_IDS, } = require("../../../../utilities/security/permissions") +const { createHomeScreen } = require("../../../../constants/screens") exports.basicTable = () => { return { @@ -85,3 +86,7 @@ exports.basicUser = role => { roleId: role, } } + +exports.basicScreen = () => { + return createHomeScreen() +} From 1b1ed8a82324d9a90300199fc8df9458814adb3f Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Mon, 8 Mar 2021 15:46:12 +0000 Subject: [PATCH 03/24] self hosted middleware test --- .../src/middleware/tests/TestConfiguration.js | 247 ++++++++++++++++++ .../server/src/middleware/tests/authorized.js | 0 .../src/middleware/tests/selfhost.spec.js | 68 ++--- 3 files changed, 281 insertions(+), 34 deletions(-) create mode 100644 packages/server/src/middleware/tests/TestConfiguration.js create mode 100644 packages/server/src/middleware/tests/authorized.js diff --git a/packages/server/src/middleware/tests/TestConfiguration.js b/packages/server/src/middleware/tests/TestConfiguration.js new file mode 100644 index 0000000000..8092130ce3 --- /dev/null +++ b/packages/server/src/middleware/tests/TestConfiguration.js @@ -0,0 +1,247 @@ +// const { BUILTIN_ROLE_IDS } = require("../../../../utilities/security/roles") +// const jwt = require("jsonwebtoken") +// const env = require("../../../../environment") +// const { +// basicTable, +// basicRow, +// basicRole, +// basicAutomation, +// basicDatasource, +// basicQuery, +// } = require("./structures") +// const controllers = require("./controllers") +// const supertest = require("supertest") + +// 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) +// this.appId = null +// } + +// getRequest() { +// return this.request +// } + +// getAppId() { +// return this.appId +// } + +// async _req(config, params, controlFunc) { +// const request = {} +// // fake cookies, we don't need them +// request.cookies = { set: () => {}, get: () => {} } +// request.config = { jwtSecret: env.JWT_SECRET } +// request.appId = this.appId +// request.user = { appId: this.appId } +// request.request = { +// body: config, +// } +// if (params) { +// request.params = params +// } +// await controlFunc(request) +// return request.body +// } + +// async init(appName = "test_application") { +// return this.createApp(appName) +// } + +// end() { +// this.server.close() +// } + +// defaultHeaders() { +// const builderUser = { +// userId: "BUILDER", +// roleId: BUILTIN_ROLE_IDS.BUILDER, +// } +// const builderToken = jwt.sign(builderUser, env.JWT_SECRET) +// const headers = { +// Accept: "application/json", +// Cookie: [`budibase:builder:local=${builderToken}`], +// } +// if (this.appId) { +// headers["x-budibase-app-id"] = this.appId +// } +// return headers +// } + +// publicHeaders() { +// const headers = { +// Accept: "application/json", +// } +// if (this.appId) { +// headers["x-budibase-app-id"] = this.appId +// } +// return headers +// } + +// async callMiddleware() { +// this.middleware(this.ctx, next) +// return this.app +// } + +// async updateTable(config = null) { +// config = config || basicTable() +// this.table = await this._req(config, null, controllers.table.save) +// return this.table +// } + +// async createTable(config = null) { +// if (config != null && config._id) { +// delete config._id +// } +// return this.updateTable(config) +// } + +// async getTable(tableId = null) { +// tableId = tableId || this.table._id +// return this._req(null, { id: tableId }, controllers.table.find) +// } + +// async createLinkedTable() { +// if (!this.table) { +// throw "Must have created a table first." +// } +// const tableConfig = basicTable() +// tableConfig.primaryDisplay = "name" +// tableConfig.schema.link = { +// type: "link", +// fieldName: "link", +// tableId: this.table._id, +// } +// const linkedTable = await this.createTable(tableConfig) +// this.linkedTable = linkedTable +// return linkedTable +// } + +// async createAttachmentTable() { +// const table = basicTable() +// table.schema.attachment = { +// type: "attachment", +// } +// return this.createTable(table) +// } + +// async createRow(config = null) { +// if (!this.table) { +// throw "Test requires table to be configured." +// } +// config = config || basicRow(this.table._id) +// return this._req(config, { tableId: this.table._id }, controllers.row.save) +// } + +// async createRole(config = null) { +// config = config || basicRole() +// return this._req(config, null, controllers.role.save) +// } + +// async addPermission(roleId, resourceId, level = "read") { +// return this._req( +// null, +// { +// roleId, +// resourceId, +// level, +// }, +// controllers.perms.addPermission +// ) +// } + +// async createView(config) { +// if (!this.table) { +// throw "Test requires table to be configured." +// } +// const view = config || { +// map: "function(doc) { emit(doc[doc.key], doc._id); } ", +// tableId: this.table._id, +// } +// return this._req(view, null, controllers.view.save) +// } + +// async createAutomation(config) { +// config = config || basicAutomation() +// if (config._rev) { +// delete config._rev +// } +// this.automation = ( +// await this._req(config, null, controllers.automation.create) +// ).automation +// return this.automation +// } + +// async getAllAutomations() { +// return this._req(null, null, controllers.automation.fetch) +// } + +// async deleteAutomation(automation = null) { +// automation = automation || this.automation +// if (!automation) { +// return +// } +// return this._req( +// null, +// { id: automation._id, rev: automation._rev }, +// controllers.automation.destroy +// ) +// } + +// async createDatasource(config = null) { +// config = config || basicDatasource() +// this.datasource = await this._req(config, null, controllers.datasource.save) +// return this.datasource +// } + +// async createQuery(config = null) { +// if (!this.datasource && !config) { +// throw "No data source created for query." +// } +// config = config || basicQuery(this.datasource._id) +// return this._req(config, null, controllers.query.save) +// } + +// async createUser( +// email = EMAIL, +// password = PASSWORD, +// roleId = BUILTIN_ROLE_IDS.POWER +// ) { +// return this._req( +// { +// email, +// password, +// roleId, +// }, +// null, +// controllers.user.create +// ) +// } + +// async login(email, password) { +// if (!email || !password) { +// await this.createUser() +// email = EMAIL +// password = PASSWORD +// } +// const result = await this.request +// .post(`/api/authenticate`) +// .set({ +// "x-budibase-app-id": this.appId, +// }) +// .send({ email, password }) + +// // returning necessary request headers +// return { +// Accept: "application/json", +// Cookie: result.headers["set-cookie"], +// } +// } +// } + +// module.exports = TestConfiguration diff --git a/packages/server/src/middleware/tests/authorized.js b/packages/server/src/middleware/tests/authorized.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/server/src/middleware/tests/selfhost.spec.js b/packages/server/src/middleware/tests/selfhost.spec.js index 0f721bc890..9d66f44463 100644 --- a/packages/server/src/middleware/tests/selfhost.spec.js +++ b/packages/server/src/middleware/tests/selfhost.spec.js @@ -1,43 +1,43 @@ -const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles") -const { checkPermissionsEndpoint } = require("./utilities/TestFunctions") -const { basicUser } = require("./utilities/structures") -const setup = require("./utilities") +const selfHostMiddleware = require("../selfhost"); +const env = require("../../environment") +const hosting = require("../../utilities/builder/hosting") +jest.mock("../../environment") +jest.mock("../../utilities/builder/hosting") describe("Self host middleware", () => { - let request = setup.getRequest() - let config = setup.getConfig() + const next = jest.fn() + const throwMock = jest.fn() - afterAll(setup.afterAll) - - beforeEach(async () => { - await config.init() + afterEach(() => { + jest.clearAllMocks() }) - describe("fetch", () => { - it("returns a list of users from an instance db", async () => { - await config.createUser("brenda@brenda.com", "brendas_password") - await config.createUser("pam@pam.com", "pam_password") - const res = await request - .get(`/api/users`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + it("calls next() when CLOUD and SELF_HOSTED env vars are set", async () => { + env.CLOUD = 1 + env.SELF_HOSTED = 1 - expect(res.body.length).toBe(2) - expect(res.body.find(u => u.email === "brenda@brenda.com")).toBeDefined() - expect(res.body.find(u => u.email === "pam@pam.com")).toBeDefined() - }) + await selfHostMiddleware({}, next) + expect(next).toHaveBeenCalled() + }) - it("should apply authorization to endpoint", async () => { - await config.createUser("brenda@brenda.com", "brendas_password") - await checkPermissionsEndpoint({ - config, - request, - method: "GET", - url: `/api/users`, - passRole: BUILTIN_ROLE_IDS.ADMIN, - failRole: BUILTIN_ROLE_IDS.PUBLIC, - }) - }) + it("throws when hostingInfo type is cloud", async () => { + env.CLOUD = 0 + env.SELF_HOSTED = 0 + + hosting.getHostingInfo.mockImplementationOnce(() => ({ type: hosting.HostingTypes.CLOUD })) + + await selfHostMiddleware({ throw: throwMock }, next) + expect(throwMock).toHaveBeenCalledWith(400, "Endpoint unavailable in cloud hosting.") + expect(next).not.toHaveBeenCalled() + }) + + it("calls the self hosting middleware to pass through to next() when the hostingInfo type is self", async () => { + env.CLOUD = 0 + env.SELF_HOSTED = 0 + + hosting.getHostingInfo.mockImplementationOnce(() => ({ type: hosting.HostingTypes.SELF })) + + await selfHostMiddleware({}, next) + expect(next).toHaveBeenCalled() }) }) From 0652133a30e23a35ee5f91dae6c9839448456867 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 8 Mar 2021 15:57:19 +0000 Subject: [PATCH 04/24] Some fixes after testing webhooks as well as adding test cases for all webhook endpoints. --- .../server/src/api/controllers/webhook.js | 10 +- .../tests/utilities/TestConfiguration.js | 9 ++ .../api/routes/tests/utilities/controllers.js | 1 + .../api/routes/tests/utilities/structures.js | 11 ++ .../src/api/routes/tests/webhook.spec.js | 130 ++++++++++++++++++ packages/server/src/middleware/authorized.js | 2 +- 6 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 packages/server/src/api/routes/tests/webhook.spec.js diff --git a/packages/server/src/api/controllers/webhook.js b/packages/server/src/api/controllers/webhook.js index 7a343b1b07..5b76f86190 100644 --- a/packages/server/src/api/controllers/webhook.js +++ b/packages/server/src/api/controllers/webhook.js @@ -43,12 +43,10 @@ exports.save = async ctx => { webhook._id = generateWebhookID() } const response = await db.put(webhook) + webhook._rev = response.rev ctx.body = { message: "Webhook created successfully", - webhook: { - ...webhook, - ...response, - }, + webhook, } } @@ -95,5 +93,7 @@ exports.trigger = async ctx => { }) } ctx.status = 200 - ctx.body = "Webhook trigger fired successfully" + ctx.body = { + message: "Webhook trigger fired successfully", + } } diff --git a/packages/server/src/api/routes/tests/utilities/TestConfiguration.js b/packages/server/src/api/routes/tests/utilities/TestConfiguration.js index a124ca607e..34b8c4fb10 100644 --- a/packages/server/src/api/routes/tests/utilities/TestConfiguration.js +++ b/packages/server/src/api/routes/tests/utilities/TestConfiguration.js @@ -9,6 +9,7 @@ const { basicDatasource, basicQuery, basicScreen, + basicWebhook, } = require("./structures") const controllers = require("./controllers") const supertest = require("supertest") @@ -223,6 +224,14 @@ class TestConfiguration { return this._req(config, null, controllers.screen.save) } + async createWebhook(config = null) { + if (!this.automation) { + throw "Must create an automation before creating webhook." + } + config = config || basicWebhook(this.automation._id) + return (await this._req(config, null, controllers.webhook.save)).webhook + } + async createUser( email = EMAIL, password = PASSWORD, diff --git a/packages/server/src/api/routes/tests/utilities/controllers.js b/packages/server/src/api/routes/tests/utilities/controllers.js index e16aa6964c..d6524bb7f0 100644 --- a/packages/server/src/api/routes/tests/utilities/controllers.js +++ b/packages/server/src/api/routes/tests/utilities/controllers.js @@ -10,4 +10,5 @@ module.exports = { datasource: require("../../../controllers/datasource"), query: require("../../../controllers/query"), screen: require("../../../controllers/screen"), + webhook: require("../../../controllers/webhook"), } diff --git a/packages/server/src/api/routes/tests/utilities/structures.js b/packages/server/src/api/routes/tests/utilities/structures.js index aec482bb24..500ff72044 100644 --- a/packages/server/src/api/routes/tests/utilities/structures.js +++ b/packages/server/src/api/routes/tests/utilities/structures.js @@ -90,3 +90,14 @@ exports.basicUser = role => { exports.basicScreen = () => { return createHomeScreen() } + +exports.basicWebhook = automationId => { + return { + live: true, + name: "webhook", + action: { + type: "automation", + target: automationId, + }, + } +} diff --git a/packages/server/src/api/routes/tests/webhook.spec.js b/packages/server/src/api/routes/tests/webhook.spec.js new file mode 100644 index 0000000000..2bf5445a09 --- /dev/null +++ b/packages/server/src/api/routes/tests/webhook.spec.js @@ -0,0 +1,130 @@ +const setup = require("./utilities") +const { checkBuilderEndpoint } = require("./utilities/TestFunctions") +const { basicWebhook, basicAutomation } = require("./utilities/structures") + +describe("/webhooks", () => { + let request = setup.getRequest() + let config = setup.getConfig() + let webhook + + afterAll(setup.afterAll) + + beforeEach(async () => { + await config.init() + const autoConfig = basicAutomation() + autoConfig.definition.trigger = { + schema: { outputs: { properties: {} } }, + inputs: {}, + } + await config.createAutomation(autoConfig) + webhook = await config.createWebhook() + }) + + describe("create", () => { + it("should create a webhook successfully", async () => { + const automation = await config.createAutomation() + const res = await request + .put(`/api/webhooks`) + .send(basicWebhook(automation._id)) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.webhook).toBeDefined() + expect(typeof res.body.webhook._id).toEqual("string") + expect(typeof res.body.webhook._rev).toEqual("string") + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "PUT", + url: `/api/webhooks`, + }) + }) + }) + + describe("fetch", () => { + it("returns the correct routing for basic user", async () => { + const res = await request + .get(`/api/webhooks`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(Array.isArray(res.body)).toEqual(true) + expect(res.body[0]._id).toEqual(webhook._id) + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "GET", + url: `/api/webhooks`, + }) + }) + }) + + describe("delete", () => { + it("should successfully delete", async () => { + const res = await request + .delete(`/api/webhooks/${webhook._id}/${webhook._rev}`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body).toBeDefined() + expect(res.body.ok).toEqual(true) + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "DELETE", + url: `/api/webhooks/${webhook._id}/${webhook._rev}`, + }) + }) + }) + + describe("build schema", () => { + it("should allow building a schema", async () => { + const res = await request + .post(`/api/webhooks/schema/${config.getAppId()}/${webhook._id}`) + .send({ + a: 1 + }) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body).toBeDefined() + // fetch to see if the schema has been updated + const fetch = await request + .get(`/api/webhooks`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(fetch.body[0]).toBeDefined() + expect(fetch.body[0].bodySchema).toEqual({ + properties: { + a: { type: "integer" } + }, + type: "object", + }) + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "POST", + url: `/api/webhooks/schema/${config.getAppId()}/${webhook._id}`, + }) + }) + }) + + describe("trigger", () => { + it("should allow triggering from public", async () => { + const res = await request + .post(`/api/webhooks/trigger/${config.getAppId()}/${webhook._id}`) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.message).toBeDefined() + }) + }) +}) \ No newline at end of file diff --git a/packages/server/src/middleware/authorized.js b/packages/server/src/middleware/authorized.js index 7eac602f78..1f8b687ba8 100644 --- a/packages/server/src/middleware/authorized.js +++ b/packages/server/src/middleware/authorized.js @@ -13,7 +13,7 @@ const { AuthTypes } = require("../constants") const ADMIN_ROLES = [BUILTIN_ROLE_IDS.ADMIN, BUILTIN_ROLE_IDS.BUILDER] -const LOCAL_PASS = new RegExp(["webhooks/trigger", "webhooks/schema"].join("|")) +const LOCAL_PASS = new RegExp(["webhooks/trigger"].join("|")) function hasResource(ctx) { return ctx.resourceId != null From c5cb40c1cf032c85bfe5ff7ca727401c84cd5691 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 8 Mar 2021 18:03:26 +0000 Subject: [PATCH 05/24] Adding a query find and planning to tackle mocking out the preview and execute functionality. --- .../server/src/api/routes/tests/query.spec.js | 57 ++++++++++++------- .../tests/utilities/TestConfiguration.js | 2 +- .../src/api/routes/tests/utilities/index.js | 19 +++++++ 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/packages/server/src/api/routes/tests/query.spec.js b/packages/server/src/api/routes/tests/query.spec.js index 765baa4426..5867c863dc 100644 --- a/packages/server/src/api/routes/tests/query.spec.js +++ b/packages/server/src/api/routes/tests/query.spec.js @@ -2,14 +2,17 @@ const { checkBuilderEndpoint } = require("./utilities/TestFunctions") const { basicQuery } = require("./utilities/structures") const setup = require("./utilities") + describe("/queries", () => { let request = setup.getRequest() let config = setup.getConfig() + let datasource afterAll(setup.afterAll) beforeEach(async () => { await config.init() + datasource = await config.createDatasource() }) describe("create", () => { @@ -35,16 +38,6 @@ describe("/queries", () => { }) describe("fetch", () => { - let datasource - - beforeEach(async () => { - datasource = await config.createDatasource() - }) - - afterEach(() => { - delete datasource._rev - }) - it("returns all the queries from the server", async () => { const query = await config.createQuery() const res = await request @@ -73,17 +66,33 @@ describe("/queries", () => { }) }) + describe("find", () => { + it("should find a query in builder", async () => { + const query = await config.createQuery() + const res = await request + .get(`/api/queries/${query._id}`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body._id).toEqual(query._id) + }) + + it("should find a query in cloud", async () => { + await setup.switchToCloudForFunction(async () => { + const query = await config.createQuery() + const res = await request + .get(`/api/queries/${query._id}`) + .set(await config.roleHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.fields).toBeUndefined() + expect(res.body.parameters).toBeUndefined() + expect(res.body.schema).toBeUndefined() + }) + }) + }) + describe("destroy", () => { - let datasource - - beforeEach(async () => { - datasource = await config.createDatasource() - }) - - afterEach(() => { - delete datasource._rev - }) - it("deletes a query and returns a success message", async () => { const query = await config.createQuery() @@ -109,4 +118,12 @@ describe("/queries", () => { }) }) }) + + describe("preview", () => { + // TODO: need to mock out an integration with a test one and try this + }) + + describe("execute", () => { + // TODO: need to mock out an integration with a test one and try this + }) }) diff --git a/packages/server/src/api/routes/tests/utilities/TestConfiguration.js b/packages/server/src/api/routes/tests/utilities/TestConfiguration.js index 34b8c4fb10..f92321ddfb 100644 --- a/packages/server/src/api/routes/tests/utilities/TestConfiguration.js +++ b/packages/server/src/api/routes/tests/utilities/TestConfiguration.js @@ -85,7 +85,7 @@ class TestConfiguration { return headers } - async roleHeaders(email = EMAIL, roleId) { + async roleHeaders(email = EMAIL, roleId = BUILTIN_ROLE_IDS.ADMIN) { try { await this.createUser(email, PASSWORD, roleId) } catch (err) { diff --git a/packages/server/src/api/routes/tests/utilities/index.js b/packages/server/src/api/routes/tests/utilities/index.js index 7e9260ce18..3ad415386c 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 env = require("../../../../environment") exports.delay = ms => new Promise(resolve => setTimeout(resolve, ms)) @@ -30,3 +31,21 @@ exports.getConfig = () => { } return config } + +exports.switchToCloudForFunction = async func => { + // self hosted stops any attempts to Dynamo + env.CLOUD = true + env.SELF_HOSTED = true + let error + try { + await func() + } catch (err) { + error = err + } + env.CLOUD = false + env.SELF_HOSTED = false + // don't throw error until after reset + if (error) { + throw error + } +} From 2bf227ab583779bc692bfcd735a3bd545b7d2a82 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 8 Mar 2021 18:18:53 +0000 Subject: [PATCH 06/24] Adding API key tests. --- .../src/api/routes/tests/apikeys.spec.js | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 packages/server/src/api/routes/tests/apikeys.spec.js diff --git a/packages/server/src/api/routes/tests/apikeys.spec.js b/packages/server/src/api/routes/tests/apikeys.spec.js new file mode 100644 index 0000000000..2a99e9e555 --- /dev/null +++ b/packages/server/src/api/routes/tests/apikeys.spec.js @@ -0,0 +1,59 @@ +const setup = require("./utilities") +const { checkBuilderEndpoint } = require("./utilities/TestFunctions") +const { budibaseAppsDir } = require("../../../utilities/budibaseDir") +const fs = require("fs") +const path = require("path") + +describe("/applications", () => { + let request = setup.getRequest() + let config = setup.getConfig() + + afterAll(setup.afterAll) + + beforeEach(async () => { + await config.init() + }) + + describe("fetch", () => { + it("should allow fetching", async () => { + const res = await request + .get(`/api/keys`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body).toBeDefined() + }) + + it("should check authorization for builder", async () => { + await checkBuilderEndpoint({ + config, + method: "GET", + url: `/api/keys`, + }) + }) + }) + + describe("update", () => { + it("should allow updating a value", async () => { + fs.writeFileSync(path.join(budibaseAppsDir(), ".env"), "") + const res = await request + .put(`/api/keys/TEST`) + .send({ + value: "test" + }) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body["TEST"]).toEqual("test") + expect(process.env.TEST_API_KEY).toEqual("test") + }) + + it("should check authorization for builder", async () => { + await checkBuilderEndpoint({ + config, + method: "PUT", + url: `/api/keys/TEST`, + }) + }) + }) +}) \ No newline at end of file From 05efe0506109c4c01f60723ae5f8c8670f55a6c7 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Tue, 9 Mar 2021 11:27:12 +0000 Subject: [PATCH 07/24] tests for authorized middleware --- packages/server/src/middleware/authorized.js | 9 +- .../src/middleware/tests/TestConfiguration.js | 265 ++---------------- .../{authorized.js => authenticated.spec.js} | 0 .../src/middleware/tests/authorized.spec.js | 196 +++++++++++++ .../src/middleware/tests/resourceId.spec.js | 0 .../src/middleware/tests/selfhost.spec.js | 2 +- 6 files changed, 227 insertions(+), 245 deletions(-) rename packages/server/src/middleware/tests/{authorized.js => authenticated.spec.js} (100%) create mode 100644 packages/server/src/middleware/tests/authorized.spec.js create mode 100644 packages/server/src/middleware/tests/resourceId.spec.js diff --git a/packages/server/src/middleware/authorized.js b/packages/server/src/middleware/authorized.js index 7eac602f78..6d646d46fd 100644 --- a/packages/server/src/middleware/authorized.js +++ b/packages/server/src/middleware/authorized.js @@ -24,6 +24,7 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => { if (!env.CLOUD && LOCAL_PASS.test(ctx.request.url)) { return next() } + if (env.CLOUD && ctx.headers["x-api-key"] && ctx.headers["x-instanceid"]) { // api key header passed by external webhook if (await isAPIKeyValid(ctx.headers["x-api-key"])) { @@ -37,14 +38,14 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => { return next() } - ctx.throw(403, "API key invalid") + return ctx.throw(403, "API key invalid") } // don't expose builder endpoints in the cloud if (env.CLOUD && permType === PermissionTypes.BUILDER) return if (!ctx.user) { - ctx.throw(403, "No user info found") + return ctx.throw(403, "No user info found") } const role = ctx.user.role @@ -52,7 +53,7 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => { ctx.appId, role._id ) - const isAdmin = ADMIN_ROLES.indexOf(role._id) !== -1 + const isAdmin = ADMIN_ROLES.includes(role._id) const isAuthed = ctx.auth.authenticated // this may need to change in the future, right now only admins @@ -61,7 +62,7 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => { if (isAdmin && isAuthed) { return next() } else if (permType === PermissionTypes.BUILDER) { - ctx.throw(403, "Not Authorized") + return ctx.throw(403, "Not Authorized") } if ( diff --git a/packages/server/src/middleware/tests/TestConfiguration.js b/packages/server/src/middleware/tests/TestConfiguration.js index 8092130ce3..11d925e8f0 100644 --- a/packages/server/src/middleware/tests/TestConfiguration.js +++ b/packages/server/src/middleware/tests/TestConfiguration.js @@ -1,247 +1,32 @@ -// const { BUILTIN_ROLE_IDS } = require("../../../../utilities/security/roles") -// const jwt = require("jsonwebtoken") -// const env = require("../../../../environment") -// const { -// basicTable, -// basicRow, -// basicRole, -// basicAutomation, -// basicDatasource, -// basicQuery, -// } = require("./structures") -// const controllers = require("./controllers") -// const supertest = require("supertest") +let env = require("../../environment") -// const EMAIL = "babs@babs.com" -// const PASSWORD = "babs_password" +class TestConfiguration { + constructor(middleware) { + // env = config.env || {} + this.middleware = middleware + this.next = jest.fn() + this.throwMock = jest.fn() + } -// 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) -// this.appId = null -// } + callMiddleware(ctx, next) { + return this.middleware(ctx, next) + } -// getRequest() { -// return this.request -// } + clear() { + jest.clearAllMocks() + } -// getAppId() { -// return this.appId -// } + setEnv(config) { + env = config + } -// async _req(config, params, controlFunc) { -// const request = {} -// // fake cookies, we don't need them -// request.cookies = { set: () => {}, get: () => {} } -// request.config = { jwtSecret: env.JWT_SECRET } -// request.appId = this.appId -// request.user = { appId: this.appId } -// request.request = { -// body: config, -// } -// if (params) { -// request.params = params -// } -// await controlFunc(request) -// return request.body -// } + async init() { + // return this.createApp(appName) + } -// async init(appName = "test_application") { -// return this.createApp(appName) -// } + end() { + // this.server.close() + } +} -// end() { -// this.server.close() -// } - -// defaultHeaders() { -// const builderUser = { -// userId: "BUILDER", -// roleId: BUILTIN_ROLE_IDS.BUILDER, -// } -// const builderToken = jwt.sign(builderUser, env.JWT_SECRET) -// const headers = { -// Accept: "application/json", -// Cookie: [`budibase:builder:local=${builderToken}`], -// } -// if (this.appId) { -// headers["x-budibase-app-id"] = this.appId -// } -// return headers -// } - -// publicHeaders() { -// const headers = { -// Accept: "application/json", -// } -// if (this.appId) { -// headers["x-budibase-app-id"] = this.appId -// } -// return headers -// } - -// async callMiddleware() { -// this.middleware(this.ctx, next) -// return this.app -// } - -// async updateTable(config = null) { -// config = config || basicTable() -// this.table = await this._req(config, null, controllers.table.save) -// return this.table -// } - -// async createTable(config = null) { -// if (config != null && config._id) { -// delete config._id -// } -// return this.updateTable(config) -// } - -// async getTable(tableId = null) { -// tableId = tableId || this.table._id -// return this._req(null, { id: tableId }, controllers.table.find) -// } - -// async createLinkedTable() { -// if (!this.table) { -// throw "Must have created a table first." -// } -// const tableConfig = basicTable() -// tableConfig.primaryDisplay = "name" -// tableConfig.schema.link = { -// type: "link", -// fieldName: "link", -// tableId: this.table._id, -// } -// const linkedTable = await this.createTable(tableConfig) -// this.linkedTable = linkedTable -// return linkedTable -// } - -// async createAttachmentTable() { -// const table = basicTable() -// table.schema.attachment = { -// type: "attachment", -// } -// return this.createTable(table) -// } - -// async createRow(config = null) { -// if (!this.table) { -// throw "Test requires table to be configured." -// } -// config = config || basicRow(this.table._id) -// return this._req(config, { tableId: this.table._id }, controllers.row.save) -// } - -// async createRole(config = null) { -// config = config || basicRole() -// return this._req(config, null, controllers.role.save) -// } - -// async addPermission(roleId, resourceId, level = "read") { -// return this._req( -// null, -// { -// roleId, -// resourceId, -// level, -// }, -// controllers.perms.addPermission -// ) -// } - -// async createView(config) { -// if (!this.table) { -// throw "Test requires table to be configured." -// } -// const view = config || { -// map: "function(doc) { emit(doc[doc.key], doc._id); } ", -// tableId: this.table._id, -// } -// return this._req(view, null, controllers.view.save) -// } - -// async createAutomation(config) { -// config = config || basicAutomation() -// if (config._rev) { -// delete config._rev -// } -// this.automation = ( -// await this._req(config, null, controllers.automation.create) -// ).automation -// return this.automation -// } - -// async getAllAutomations() { -// return this._req(null, null, controllers.automation.fetch) -// } - -// async deleteAutomation(automation = null) { -// automation = automation || this.automation -// if (!automation) { -// return -// } -// return this._req( -// null, -// { id: automation._id, rev: automation._rev }, -// controllers.automation.destroy -// ) -// } - -// async createDatasource(config = null) { -// config = config || basicDatasource() -// this.datasource = await this._req(config, null, controllers.datasource.save) -// return this.datasource -// } - -// async createQuery(config = null) { -// if (!this.datasource && !config) { -// throw "No data source created for query." -// } -// config = config || basicQuery(this.datasource._id) -// return this._req(config, null, controllers.query.save) -// } - -// async createUser( -// email = EMAIL, -// password = PASSWORD, -// roleId = BUILTIN_ROLE_IDS.POWER -// ) { -// return this._req( -// { -// email, -// password, -// roleId, -// }, -// null, -// controllers.user.create -// ) -// } - -// async login(email, password) { -// if (!email || !password) { -// await this.createUser() -// email = EMAIL -// password = PASSWORD -// } -// const result = await this.request -// .post(`/api/authenticate`) -// .set({ -// "x-budibase-app-id": this.appId, -// }) -// .send({ email, password }) - -// // returning necessary request headers -// return { -// Accept: "application/json", -// Cookie: result.headers["set-cookie"], -// } -// } -// } - -// module.exports = TestConfiguration +module.exports = TestConfiguration diff --git a/packages/server/src/middleware/tests/authorized.js b/packages/server/src/middleware/tests/authenticated.spec.js similarity index 100% rename from packages/server/src/middleware/tests/authorized.js rename to packages/server/src/middleware/tests/authenticated.spec.js diff --git a/packages/server/src/middleware/tests/authorized.spec.js b/packages/server/src/middleware/tests/authorized.spec.js new file mode 100644 index 0000000000..d3e5e52d2d --- /dev/null +++ b/packages/server/src/middleware/tests/authorized.spec.js @@ -0,0 +1,196 @@ +const authorizedMiddleware = require("../authorized") +const env = require("../../environment") +const apiKey = require("../../utilities/security/apikey") +const { AuthTypes } = require("../../constants") +const { PermissionTypes, PermissionLevels } = require("../../utilities/security/permissions") +const { Test } = require("supertest") +jest.mock("../../environment") +jest.mock("../../utilities/security/apikey") + +class TestConfiguration { + constructor(role) { + this.middleware = authorizedMiddleware(role) + this.next = jest.fn() + this.throw = jest.fn() + this.ctx = { + headers: {}, + request: { + url: "" + }, + auth: {}, + next: this.next, + throw: this.throw + } + } + + executeMiddleware() { + return this.middleware(this.ctx, this.next) + } + + setUser(user) { + this.ctx.user = user + } + + setMiddlewareRequiredPermission(...perms) { + this.middleware = authorizedMiddleware(...perms) + } + + setResourceId(id) { + this.ctx.resourceId = id + } + + setAuthenticated(isAuthed) { + this.ctx.auth = { authenticated: isAuthed } + } + + setRequestUrl(url) { + this.ctx.request.url = url + } + + setCloudEnv(isCloud) { + env.CLOUD = isCloud + } + + setRequestHeaders(headers) { + this.ctx.headers = headers + } + + afterEach() { + jest.clearAllMocks() + } +} + + +describe("Authorization middleware", () => { + const next = jest.fn() + let config + + afterEach(() => { + config.afterEach() + }) + + beforeEach(() => { + config = new TestConfiguration() + }) + + it("passes the middleware for local webhooks", async () => { + config.setRequestUrl("https://something/webhooks/trigger") + await config.executeMiddleware() + expect(config.next).toHaveBeenCalled() + }) + + describe("external web hook call", () => { + let ctx = {} + let middleware + + beforeEach(() => { + config = new TestConfiguration() + config.setCloudEnv(true) + config.setRequestHeaders({ + "x-api-key": "abc123", + "x-instanceid": "instance123", + }) + }) + + it("passes to next() if api key is valid", async () => { + apiKey.isAPIKeyValid.mockResolvedValueOnce(true) + + await config.executeMiddleware() + + expect(config.next).toHaveBeenCalled() + expect(config.ctx.auth).toEqual({ + authenticated: AuthTypes.EXTERNAL, + apiKey: config.ctx.headers["x-api-key"], + }) + expect(config.ctx.user).toEqual({ + appId: config.ctx.headers["x-instanceid"], + }) + }) + + it("throws if api key is invalid", async () => { + apiKey.isAPIKeyValid.mockResolvedValueOnce(false) + + await config.executeMiddleware() + + expect(config.throw).toHaveBeenCalledWith(403, "API key invalid") + }) + }) + + describe("non-webhook call", () => { + let config + + beforeEach(() => { + config = new TestConfiguration() + config.setCloudEnv(true) + config.setAuthenticated(true) + }) + + it("throws when no user data is present in context", async () => { + await config.executeMiddleware() + + expect(config.throw).toHaveBeenCalledWith(403, "No user info found") + }) + + it("passes on to next() middleware if user is an admin", async () => { + config.setUser({ + role: { + _id: "ADMIN", + } + }) + + await config.executeMiddleware() + + expect(config.next).toHaveBeenCalled() + }) + + it("throws if the user has only builder permissions", async () => { + config.setCloudEnv(false) + config.setMiddlewareRequiredPermission(PermissionTypes.BUILDER) + config.setUser({ + role: { + _id: "" + } + }) + await config.executeMiddleware() + + expect(config.throw).toHaveBeenCalledWith(403, "Not Authorized") + }) + + it("passes on to next() middleware if the user has resource permission", async () => { + config.setResourceId(PermissionTypes.QUERY) + config.setUser({ + role: { + _id: "" + } + }) + config.setMiddlewareRequiredPermission(PermissionTypes.QUERY) + + await config.executeMiddleware() + expect(config.next).toHaveBeenCalled() + }) + + it("throws if the user session is not authenticated after permission checks", async () => { + config.setUser({ + role: { + _id: "" + }, + }) + config.setAuthenticated(false) + + await config.executeMiddleware() + expect(config.throw).toHaveBeenCalledWith(403, "Session not authenticated") + }) + + it("throws if the user does not have base permissions to perform the operation", async () => { + config.setUser({ + role: { + _id: "" + }, + }) + config.setMiddlewareRequiredPermission(PermissionTypes.ADMIN, PermissionLevels.BASIC) + + await config.executeMiddleware() + expect(config.throw).toHaveBeenCalledWith(403, "User does not have permission") + }) + }) +}) diff --git a/packages/server/src/middleware/tests/resourceId.spec.js b/packages/server/src/middleware/tests/resourceId.spec.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/server/src/middleware/tests/selfhost.spec.js b/packages/server/src/middleware/tests/selfhost.spec.js index 9d66f44463..3601df89a2 100644 --- a/packages/server/src/middleware/tests/selfhost.spec.js +++ b/packages/server/src/middleware/tests/selfhost.spec.js @@ -1,6 +1,6 @@ const selfHostMiddleware = require("../selfhost"); const env = require("../../environment") -const hosting = require("../../utilities/builder/hosting") +const hosting = require("../../utilities/builder/hosting"); jest.mock("../../environment") jest.mock("../../utilities/builder/hosting") From c4b3a7c88430bbceaac5721774c3796b29c9df6c Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Tue, 9 Mar 2021 11:33:16 +0000 Subject: [PATCH 08/24] refactor selfhost middleware tests to use TestConfiguration --- .../src/middleware/tests/TestConfiguration.js | 32 ---------- .../src/middleware/tests/selfhost.spec.js | 60 ++++++++++++++----- 2 files changed, 46 insertions(+), 46 deletions(-) delete mode 100644 packages/server/src/middleware/tests/TestConfiguration.js diff --git a/packages/server/src/middleware/tests/TestConfiguration.js b/packages/server/src/middleware/tests/TestConfiguration.js deleted file mode 100644 index 11d925e8f0..0000000000 --- a/packages/server/src/middleware/tests/TestConfiguration.js +++ /dev/null @@ -1,32 +0,0 @@ -let env = require("../../environment") - -class TestConfiguration { - constructor(middleware) { - // env = config.env || {} - this.middleware = middleware - this.next = jest.fn() - this.throwMock = jest.fn() - } - - callMiddleware(ctx, next) { - return this.middleware(ctx, next) - } - - clear() { - jest.clearAllMocks() - } - - setEnv(config) { - env = config - } - - async init() { - // return this.createApp(appName) - } - - end() { - // this.server.close() - } -} - -module.exports = TestConfiguration diff --git a/packages/server/src/middleware/tests/selfhost.spec.js b/packages/server/src/middleware/tests/selfhost.spec.js index 3601df89a2..061da17f9c 100644 --- a/packages/server/src/middleware/tests/selfhost.spec.js +++ b/packages/server/src/middleware/tests/selfhost.spec.js @@ -4,40 +4,72 @@ const hosting = require("../../utilities/builder/hosting"); jest.mock("../../environment") jest.mock("../../utilities/builder/hosting") +class TestConfiguration { + constructor() { + this.next = jest.fn() + this.throw = jest.fn() + this.middleware = selfHostMiddleware + + this.ctx = { + next: this.next, + throw: this.throw + } + } + + executeMiddleware() { + return this.middleware(this.ctx, this.next) + } + + setCloudHosted() { + env.CLOUD = 1 + env.SELF_HOSTED = 0 + } + + setSelfHosted() { + env.CLOUD = 0 + env.SELF_HOSTED = 1 + } + + afterEach() { + jest.clearAllMocks() + } +} + describe("Self host middleware", () => { - const next = jest.fn() - const throwMock = jest.fn() + let config + + beforeEach(() => { + config = new TestConfiguration() + }) afterEach(() => { - jest.clearAllMocks() + config.afterEach() }) it("calls next() when CLOUD and SELF_HOSTED env vars are set", async () => { env.CLOUD = 1 env.SELF_HOSTED = 1 - await selfHostMiddleware({}, next) - expect(next).toHaveBeenCalled() + await config.executeMiddleware() + expect(config.next).toHaveBeenCalled() }) it("throws when hostingInfo type is cloud", async () => { - env.CLOUD = 0 - env.SELF_HOSTED = 0 + config.setSelfHosted() hosting.getHostingInfo.mockImplementationOnce(() => ({ type: hosting.HostingTypes.CLOUD })) - await selfHostMiddleware({ throw: throwMock }, next) - expect(throwMock).toHaveBeenCalledWith(400, "Endpoint unavailable in cloud hosting.") - expect(next).not.toHaveBeenCalled() + await config.executeMiddleware() + expect(config.throw).toHaveBeenCalledWith(400, "Endpoint unavailable in cloud hosting.") + expect(config.next).not.toHaveBeenCalled() }) it("calls the self hosting middleware to pass through to next() when the hostingInfo type is self", async () => { - env.CLOUD = 0 - env.SELF_HOSTED = 0 + config.setSelfHosted() hosting.getHostingInfo.mockImplementationOnce(() => ({ type: hosting.HostingTypes.SELF })) - await selfHostMiddleware({}, next) - expect(next).toHaveBeenCalled() + await config.executeMiddleware() + expect(config.next).toHaveBeenCalled() }) }) From 6231c25ed556d1b529bb4036a1a03d77adb00609 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 9 Mar 2021 11:56:32 +0000 Subject: [PATCH 09/24] Updating query test to include mocked preview/execute and adding layout tests. --- packages/server/__mocks__/pg.js | 21 +++++ packages/server/src/api/controllers/layout.js | 2 +- packages/server/src/api/controllers/query.js | 2 - .../src/api/routes/tests/layout.spec.js | 55 ++++++++++++ .../server/src/api/routes/tests/query.spec.js | 87 ++++++++++++++++--- .../tests/utilities/TestConfiguration.js | 6 ++ .../api/routes/tests/utilities/controllers.js | 1 + .../api/routes/tests/utilities/structures.js | 6 ++ 8 files changed, 167 insertions(+), 13 deletions(-) create mode 100644 packages/server/__mocks__/pg.js create mode 100644 packages/server/src/api/routes/tests/layout.spec.js diff --git a/packages/server/__mocks__/pg.js b/packages/server/__mocks__/pg.js new file mode 100644 index 0000000000..2bda8afad0 --- /dev/null +++ b/packages/server/__mocks__/pg.js @@ -0,0 +1,21 @@ +const pg = {} + +// constructor +function Client() {} + +Client.prototype.query = async function() { + return { + rows: [ + { + a: "string", + b: 1, + }, + ], + } +} + +Client.prototype.connect = async function() {} + +pg.Client = Client + +module.exports = pg diff --git a/packages/server/src/api/controllers/layout.js b/packages/server/src/api/controllers/layout.js index de4c647c9f..f270e95bec 100644 --- a/packages/server/src/api/controllers/layout.js +++ b/packages/server/src/api/controllers/layout.js @@ -38,6 +38,6 @@ exports.destroy = async function(ctx) { } await db.remove(layoutId, layoutRev) - ctx.message = "Layout deleted successfully" + ctx.body = { message: "Layout deleted successfully" } ctx.status = 200 } diff --git a/packages/server/src/api/controllers/query.js b/packages/server/src/api/controllers/query.js index 55c2ad14b0..7012219c39 100644 --- a/packages/server/src/api/controllers/query.js +++ b/packages/server/src/api/controllers/query.js @@ -108,7 +108,6 @@ exports.preview = async function(ctx) { if (!Integration) { ctx.throw(400, "Integration type does not exist.") - return } const { fields, parameters, queryVerb } = ctx.request.body @@ -138,7 +137,6 @@ exports.execute = async function(ctx) { if (!Integration) { ctx.throw(400, "Integration type does not exist.") - return } const enrichedQuery = await enrichQueryFields( diff --git a/packages/server/src/api/routes/tests/layout.spec.js b/packages/server/src/api/routes/tests/layout.spec.js new file mode 100644 index 0000000000..4be1c9e18e --- /dev/null +++ b/packages/server/src/api/routes/tests/layout.spec.js @@ -0,0 +1,55 @@ +const { checkBuilderEndpoint } = require("./utilities/TestFunctions") +const setup = require("./utilities") +const { basicLayout } = require("./utilities/structures") + +describe("/queries", () => { + let request = setup.getRequest() + let config = setup.getConfig() + let layout + + afterAll(setup.afterAll) + + beforeEach(async () => { + await config.init() + layout = await config.createLayout() + }) + + describe("save", () => { + it("should be able to create a layout", async () => { + const res = await request + .post(`/api/layouts`) + .send(basicLayout()) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body._rev).toBeDefined() + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "POST", + url: `/api/layouts`, + }) + }) + }) + + describe("destroy", () => { + it("should be able to delete the layout", async () => { + const res = await request + .delete(`/api/layouts/${layout._id}/${layout._rev}`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.message).toBeDefined() + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "DELETE", + url: `/api/layouts/${layout._id}/${layout._rev}`, + }) + }) + }) +}) \ No newline at end of file diff --git a/packages/server/src/api/routes/tests/query.spec.js b/packages/server/src/api/routes/tests/query.spec.js index 5867c863dc..aa0e5428c5 100644 --- a/packages/server/src/api/routes/tests/query.spec.js +++ b/packages/server/src/api/routes/tests/query.spec.js @@ -1,20 +1,32 @@ -const { checkBuilderEndpoint } = require("./utilities/TestFunctions") -const { basicQuery } = require("./utilities/structures") -const setup = require("./utilities") +// mock out postgres for this +jest.mock("pg") +const { checkBuilderEndpoint } = require("./utilities/TestFunctions") +const { basicQuery, basicDatasource } = require("./utilities/structures") +const setup = require("./utilities") describe("/queries", () => { let request = setup.getRequest() let config = setup.getConfig() - let datasource + let datasource, query afterAll(setup.afterAll) beforeEach(async () => { await config.init() datasource = await config.createDatasource() + query = await config.createQuery() }) + async function createInvalidIntegration() { + const datasource = await config.createDatasource({ + ...basicDatasource(), + source: "INVALID_INTEGRATION", + }) + const query = await config.createQuery() + return { datasource, query } + } + describe("create", () => { it("should create a new query", async () => { const { _id } = await config.createDatasource() @@ -39,7 +51,6 @@ describe("/queries", () => { describe("fetch", () => { it("returns all the queries from the server", async () => { - const query = await config.createQuery() const res = await request .get(`/api/queries`) .set(config.defaultHeaders()) @@ -94,8 +105,6 @@ describe("/queries", () => { describe("destroy", () => { it("deletes a query and returns a success message", async () => { - const query = await config.createQuery() - await request .delete(`/api/queries/${query._id}/${query._rev}`) .set(config.defaultHeaders()) @@ -114,16 +123,74 @@ describe("/queries", () => { await checkBuilderEndpoint({ config, method: "DELETE", - url: `/api/datasources/${datasource._id}/${datasource._rev}`, + url: `/api/queries/${config._id}/${config._rev}`, }) }) }) describe("preview", () => { - // TODO: need to mock out an integration with a test one and try this + it("should be able to preview the query", async () => { + const res = await request + .post(`/api/queries/preview`) + .send({ + datasourceId: datasource._id, + parameters: {}, + fields: {}, + queryVerb: "read", + }) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + // these responses come from the mock + expect(res.body.schemaFields).toEqual(["a", "b"]) + expect(res.body.rows.length).toEqual(1) + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "POST", + url: `/api/queries/preview`, + }) + }) + + it("should fail with invalid integration type", async () => { + const { datasource } = await createInvalidIntegration() + await request + .post(`/api/queries/preview`) + .send({ + datasourceId: datasource._id, + parameters: {}, + fields: {}, + queryVerb: "read", + }) + .set(config.defaultHeaders()) + .expect(400) + }) }) describe("execute", () => { - // TODO: need to mock out an integration with a test one and try this + it("should be able to execute the query", async () => { + const res = await request + .post(`/api/queries/${query._id}`) + .send({ + parameters: {}, + }) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.length).toEqual(1) + }) + + it("should fail with invalid integration type", async () => { + const { query } = await createInvalidIntegration() + await request + .post(`/api/queries/${query._id}`) + .send({ + parameters: {}, + }) + .set(config.defaultHeaders()) + .expect(400) + }) }) }) diff --git a/packages/server/src/api/routes/tests/utilities/TestConfiguration.js b/packages/server/src/api/routes/tests/utilities/TestConfiguration.js index f92321ddfb..b72f4f4e5f 100644 --- a/packages/server/src/api/routes/tests/utilities/TestConfiguration.js +++ b/packages/server/src/api/routes/tests/utilities/TestConfiguration.js @@ -9,6 +9,7 @@ const { basicDatasource, basicQuery, basicScreen, + basicLayout, basicWebhook, } = require("./structures") const controllers = require("./controllers") @@ -232,6 +233,11 @@ class TestConfiguration { return (await this._req(config, null, controllers.webhook.save)).webhook } + async createLayout(config = null) { + config = config || basicLayout() + return await this._req(config, null, controllers.layout.save) + } + async createUser( email = EMAIL, password = PASSWORD, diff --git a/packages/server/src/api/routes/tests/utilities/controllers.js b/packages/server/src/api/routes/tests/utilities/controllers.js index d6524bb7f0..a4eb9ac9de 100644 --- a/packages/server/src/api/routes/tests/utilities/controllers.js +++ b/packages/server/src/api/routes/tests/utilities/controllers.js @@ -11,4 +11,5 @@ module.exports = { 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/structures.js b/packages/server/src/api/routes/tests/utilities/structures.js index 500ff72044..ff3a239211 100644 --- a/packages/server/src/api/routes/tests/utilities/structures.js +++ b/packages/server/src/api/routes/tests/utilities/structures.js @@ -3,6 +3,8 @@ const { BUILTIN_PERMISSION_IDS, } = require("../../../../utilities/security/permissions") const { createHomeScreen } = require("../../../../constants/screens") +const { EMPTY_LAYOUT } = require("../../../../constants/layouts") +const { cloneDeep } = require("lodash/fp") exports.basicTable = () => { return { @@ -91,6 +93,10 @@ exports.basicScreen = () => { return createHomeScreen() } +exports.basicLayout = () => { + return cloneDeep(EMPTY_LAYOUT) +} + exports.basicWebhook = automationId => { return { live: true, From c755df9f913170b5b36f61bc5acdb69cfa4713bb Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Tue, 9 Mar 2021 12:39:32 +0000 Subject: [PATCH 10/24] resourceId tests --- packages/server/src/middleware/resourceId.js | 2 + .../src/middleware/tests/resourceId.spec.js | 105 ++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/packages/server/src/middleware/resourceId.js b/packages/server/src/middleware/resourceId.js index 4351901dad..4216131119 100644 --- a/packages/server/src/middleware/resourceId.js +++ b/packages/server/src/middleware/resourceId.js @@ -36,6 +36,8 @@ class ResourceIdGetter { } } +module.exports.ResourceIdGetter = ResourceIdGetter + module.exports.paramResource = main => { return new ResourceIdGetter("params").mainResource(main).build() } diff --git a/packages/server/src/middleware/tests/resourceId.spec.js b/packages/server/src/middleware/tests/resourceId.spec.js index e69de29bb2..35e6e5af50 100644 --- a/packages/server/src/middleware/tests/resourceId.spec.js +++ b/packages/server/src/middleware/tests/resourceId.spec.js @@ -0,0 +1,105 @@ +const { + paramResource, + paramSubResource, + bodyResource, + bodySubResource, + ResourceIdGetter +} = require("../resourceId") + +class TestConfiguration { + constructor(middleware) { + this.middleware = middleware + this.ctx = { + request: {}, + } + this.next = jest.fn() + } + + setParams(params) { + this.ctx.params = params + } + + setBody(body) { + this.ctx.body = body + } + + executeMiddleware() { + return this.middleware(this.ctx, this.next) + } +} + +describe("resourceId middleware", () => { + it("calls next() when there is no request object to parse", () => { + const config = new TestConfiguration(paramResource("main")) + + config.executeMiddleware() + + expect(config.next).toHaveBeenCalled() + expect(config.ctx.resourceId).toBeUndefined() + }) + + it("generates a resourceId middleware for context query parameters", () => { + const config = new TestConfiguration(paramResource("main")) + config.setParams({ + main: "test" + }) + + config.executeMiddleware() + + expect(config.ctx.resourceId).toEqual("test") + }) + + it("generates a resourceId middleware for context query sub parameters", () => { + const config = new TestConfiguration(paramSubResource("main", "sub")) + config.setParams({ + main: "main", + sub: "test" + }) + + config.executeMiddleware() + + expect(config.ctx.resourceId).toEqual("main") + expect(config.ctx.subResourceId).toEqual("test") + }) + + it("generates a resourceId middleware for context request body", () => { + const config = new TestConfiguration(bodyResource("main")) + config.setBody({ + main: "test" + }) + + config.executeMiddleware() + + expect(config.ctx.resourceId).toEqual("test") + }) + + it("generates a resourceId middleware for context request body sub fields", () => { + const config = new TestConfiguration(bodySubResource("main", "sub")) + config.setBody({ + main: "main", + sub: "test" + }) + + config.executeMiddleware() + + expect(config.ctx.resourceId).toEqual("main") + expect(config.ctx.subResourceId).toEqual("test") + }) + + it("parses resourceIds correctly for custom middlewares", () => { + const middleware = new ResourceIdGetter("body") + .mainResource("custom") + .subResource("customSub") + .build() + config = new TestConfiguration(middleware) + config.setBody({ + custom: "test", + customSub: "subtest" + }) + + config.executeMiddleware() + + expect(config.ctx.resourceId).toEqual("test") + expect(config.ctx.subResourceId).toEqual("subtest") + }) +}) \ No newline at end of file From 5dcc4f23c6b6f6adc4ff59de6a4dd62cbbf7498e Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Tue, 9 Mar 2021 15:13:14 +0000 Subject: [PATCH 11/24] usageQuota tests --- .../src/middleware/tests/usageQuota.spec.js | 129 ++++++++++++++++++ packages/server/src/middleware/usageQuota.js | 1 + 2 files changed, 130 insertions(+) create mode 100644 packages/server/src/middleware/tests/usageQuota.spec.js diff --git a/packages/server/src/middleware/tests/usageQuota.spec.js b/packages/server/src/middleware/tests/usageQuota.spec.js new file mode 100644 index 0000000000..c76acb47d2 --- /dev/null +++ b/packages/server/src/middleware/tests/usageQuota.spec.js @@ -0,0 +1,129 @@ +const usageQuotaMiddleware = require("../usageQuota") +const usageQuota = require("../../utilities/usageQuota") +const CouchDB = require("../../db") +const env = require("../../environment") + +jest.mock("../../db"); +jest.mock("../../utilities/usageQuota") +jest.mock("../../environment") + +class TestConfiguration { + constructor() { + this.throw = jest.fn() + this.next = jest.fn() + this.middleware = usageQuotaMiddleware + this.ctx = { + throw: this.throw, + next: this.next, + user: { + appId: "test" + }, + request: { + body: {} + }, + req: { + method: "POST", + url: "/rows" + } + } + } + + executeMiddleware() { + return this.middleware(this.ctx, this.next) + } + + cloudHosted(bool) { + if (bool) { + env.CLOUD = 1 + this.ctx.auth = { apiKey: "test" } + } else { + env.CLOUD = 0 + } + } + + setMethod(method) { + this.ctx.req.method = method + } + + setUrl(url) { + this.ctx.req.url = url + } + + setBody(body) { + this.ctx.request.body = body + } + + setFiles(files) { + this.ctx.request.files = { file: files } + } +} + +describe("usageQuota middleware", () => { + let config + + beforeEach(() => { + config = new TestConfiguration() + }) + + it("skips the middleware if there is no usage property or method", async () => { + await config.executeMiddleware() + expect(config.next).toHaveBeenCalled() + }) + + it("passes through to next middleware if document already exists", async () => { + config.setBody({ + _id: "test" + }) + + CouchDB.mockImplementationOnce(() => ({ + get: async () => true + })) + + await config.executeMiddleware() + + expect(config.next).toHaveBeenCalled() + expect(config.ctx.preExisting).toBe(true) + }) + + it("throws if request has _id, but the document no longer exists", async () => { + config.setBody({ + _id: "123" + }) + + CouchDB.mockImplementationOnce(() => ({ + get: async () => { + throw new Error() + } + })) + + await config.executeMiddleware() + expect(config.throw).toHaveBeenCalledWith(404, `${config.ctx.request.body._id} does not exist`) + }) + + it("calculates and persists the correct usage quota for the relevant action", async () => { + config.setUrl("/rows") + config.cloudHosted(true) + + await config.executeMiddleware() + + expect(usageQuota.update).toHaveBeenCalledWith("test", "rows", 1) + expect(config.next).toHaveBeenCalled() + }) + + it("calculates the correct file size from a file upload call and adds it to quota", async () => { + config.setUrl("/upload") + config.cloudHosted(true) + config.setFiles([ + { + size: 100 + }, + { + size: 10000 + }, + ]) + await config.executeMiddleware() + + expect(usageQuota.update).toHaveBeenCalledWith("test", "storage", 10100) + expect(config.next).toHaveBeenCalled() + }) +}) \ No newline at end of file diff --git a/packages/server/src/middleware/usageQuota.js b/packages/server/src/middleware/usageQuota.js index e980afe678..1b809868be 100644 --- a/packages/server/src/middleware/usageQuota.js +++ b/packages/server/src/middleware/usageQuota.js @@ -43,6 +43,7 @@ module.exports = async (ctx, next) => { return } } + // if running in builder or a self hosted cloud usage quotas should not be executed if (!env.CLOUD || env.SELF_HOSTED) { return next() From 33fa31aae9b68449f47fb62020e18e91844db014 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 9 Mar 2021 16:07:44 +0000 Subject: [PATCH 12/24] Adding test cases for component, templates and hosting as well as updating some existing test cases. --- packages/server/__mocks__/node-fetch.js | 17 + packages/server/package.json | 7 +- .../server/src/api/controllers/templates.js | 2 + packages/server/src/api/routes/hosting.js | 8 +- .../src/api/routes/tests/apikeys.spec.js | 2 +- .../src/api/routes/tests/component.spec.js | 49 ++ .../src/api/routes/tests/hosting.spec.js | 130 ++++++ .../src/api/routes/tests/layout.spec.js | 2 +- .../src/api/routes/tests/permissions.spec.js | 15 + .../src/api/routes/tests/templates.spec.js | 49 ++ packages/server/src/utilities/templates.js | 5 +- packages/server/yarn.lock | 430 +++++++++++++++++- 12 files changed, 691 insertions(+), 25 deletions(-) create mode 100644 packages/server/__mocks__/node-fetch.js create mode 100644 packages/server/src/api/routes/tests/component.spec.js create mode 100644 packages/server/src/api/routes/tests/hosting.spec.js create mode 100644 packages/server/src/api/routes/tests/templates.spec.js diff --git a/packages/server/__mocks__/node-fetch.js b/packages/server/__mocks__/node-fetch.js new file mode 100644 index 0000000000..1113791ec2 --- /dev/null +++ b/packages/server/__mocks__/node-fetch.js @@ -0,0 +1,17 @@ +const fetch = jest.requireActual("node-fetch") + +module.exports = async (url, opts) => { + // mocked data based on url + if (url.includes("api/apps")) { + return { + json: async () => { + return { + app1: { + url: "/app1", + }, + } + }, + } + } + return fetch(url, opts) +} diff --git a/packages/server/package.json b/packages/server/package.json index a23c6f6e89..3fe0d68bf3 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -53,7 +53,11 @@ "src/**/*.js", "!**/node_modules/**", "!src/db/views/*.js", - "!src/api/routes/tests" + "!src/api/routes/tests/**/*.js", + "!src/api/controllers/deploy/**/*.js", + "!src/api/controllers/static/templates/**/*", + "!src/api/controllers/static/selfhost/**/*", + "!src/*.js" ], "coverageReporters": [ "lcov", @@ -122,6 +126,7 @@ "zlib": "1.0.5" }, "devDependencies": { + "@budibase/standard-components": "^0.8.5", "@jest/test-sequencer": "^24.8.0", "cross-env": "^7.0.3", "electron": "10.1.3", diff --git a/packages/server/src/api/controllers/templates.js b/packages/server/src/api/controllers/templates.js index 94243d3c75..c3cfa28706 100644 --- a/packages/server/src/api/controllers/templates.js +++ b/packages/server/src/api/controllers/templates.js @@ -24,6 +24,8 @@ exports.fetch = async function(ctx) { } } +// can't currently test this, have to ignore from coverage +/* istanbul ignore next */ exports.downloadTemplate = async function(ctx) { const { type, name } = ctx.params diff --git a/packages/server/src/api/routes/hosting.js b/packages/server/src/api/routes/hosting.js index 44429dacbf..111968a497 100644 --- a/packages/server/src/api/routes/hosting.js +++ b/packages/server/src/api/routes/hosting.js @@ -11,11 +11,7 @@ router .get("/api/hosting/urls", authorized(BUILDER), controller.fetchUrls) .get("/api/hosting", authorized(BUILDER), controller.fetch) .post("/api/hosting", authorized(BUILDER), controller.save) - .get( - "/api/hosting/apps", - authorized(BUILDER), - selfhost, - controller.getDeployedApps - ) + // this isn't risky, doesn't return anything about apps other than names and URLs + .get("/api/hosting/apps", selfhost, controller.getDeployedApps) module.exports = router diff --git a/packages/server/src/api/routes/tests/apikeys.spec.js b/packages/server/src/api/routes/tests/apikeys.spec.js index 2a99e9e555..a8077a4492 100644 --- a/packages/server/src/api/routes/tests/apikeys.spec.js +++ b/packages/server/src/api/routes/tests/apikeys.spec.js @@ -4,7 +4,7 @@ const { budibaseAppsDir } = require("../../../utilities/budibaseDir") const fs = require("fs") const path = require("path") -describe("/applications", () => { +describe("/api/keys", () => { let request = setup.getRequest() let config = setup.getConfig() diff --git a/packages/server/src/api/routes/tests/component.spec.js b/packages/server/src/api/routes/tests/component.spec.js new file mode 100644 index 0000000000..926efc51e3 --- /dev/null +++ b/packages/server/src/api/routes/tests/component.spec.js @@ -0,0 +1,49 @@ +const { checkBuilderEndpoint } = require("./utilities/TestFunctions") +const setup = require("./utilities") +const fs = require("fs") +const { resolve, join } = require("path") +const { budibaseAppsDir } = require("../../../utilities/budibaseDir") + +describe("/component", () => { + let request = setup.getRequest() + let config = setup.getConfig() + + afterAll(setup.afterAll) + + beforeEach(async () => { + await config.init() + }) + + function mock() { + const manifestFile = "manifest.json" + const appId = config.getAppId() + const libraries = ["@budibase/standard-components"] + for (let library of libraries) { + let appDirectory = resolve(budibaseAppsDir(), appId, "node_modules", library, "package") + fs.mkdirSync(appDirectory, { recursive: true }) + const file = require.resolve(library).split("dist/index.js")[0] + manifestFile + fs.copyFileSync(file, join(appDirectory, manifestFile)) + } + } + + describe("fetch definitions", () => { + it("should be able to fetch definitions", async () => { + // have to "mock" the files required + mock() + const res = await request + .get(`/${config.getAppId()}/components/definitions`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body["@budibase/standard-components/container"]).toBeDefined() + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "GET", + url: `/${config.getAppId()}/components/definitions`, + }) + }) + }) +}) \ No newline at end of file diff --git a/packages/server/src/api/routes/tests/hosting.spec.js b/packages/server/src/api/routes/tests/hosting.spec.js new file mode 100644 index 0000000000..2da5b11778 --- /dev/null +++ b/packages/server/src/api/routes/tests/hosting.spec.js @@ -0,0 +1,130 @@ +// mock out node fetch for this +jest.mock("node-fetch") + +const { checkBuilderEndpoint } = require("./utilities/TestFunctions") +const setup = require("./utilities") + +describe("/hosting", () => { + let request = setup.getRequest() + let config = setup.getConfig() + let app + + afterAll(setup.afterAll) + + beforeEach(async () => { + app = await config.init() + }) + + describe("fetchInfo", () => { + it("should be able to fetch hosting information", async () => { + const res = await request + .get(`/api/hosting/info`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body).toEqual({ types: ["cloud", "self"]}) + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "GET", + url: `/api/hosting/info`, + }) + }) + }) + + describe("fetchUrls", () => { + it("should be able to fetch current app URLs", async () => { + const res = await request + .get(`/api/hosting/urls`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.app).toEqual(`https://${config.getAppId()}.app.budi.live`) + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "GET", + url: `/api/hosting/urls`, + }) + }) + }) + + describe("fetch", () => { + it("should be able to fetch the current hosting information", async () => { + const res = await request + .get(`/api/hosting`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body._id).toBeDefined() + expect(res.body.hostingUrl).toBeDefined() + expect(res.body.type).toEqual("cloud") + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "GET", + url: `/api/hosting`, + }) + }) + }) + + describe("save", () => { + it("should be able to update the hosting information", async () => { + const res = await request + .post(`/api/hosting`) + .send({ + type: "self", + selfHostKey: "budibase", + hostingUrl: "localhost:10000", + useHttps: false, + }) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.ok).toEqual(true) + // make sure URL updated + const urlRes = await request + .get(`/api/hosting/urls`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(urlRes.body.app).toEqual(`http://localhost:10000/app`) + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "POST", + url: `/api/hosting`, + }) + }) + }) + + describe("getDeployedApps", () => { + it("should get apps when in builder", async () => { + const res = await request + .get(`/api/hosting/apps`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.app1).toEqual({url: "/app1"}) + }) + + it("should get apps when in cloud", async () => { + await setup.switchToCloudForFunction(async () => { + const res = await request + .get(`/api/hosting/apps`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.app1).toEqual({url: "/app1"}) + }) + }) + }) +}) \ No newline at end of file diff --git a/packages/server/src/api/routes/tests/layout.spec.js b/packages/server/src/api/routes/tests/layout.spec.js index 4be1c9e18e..6b21554d71 100644 --- a/packages/server/src/api/routes/tests/layout.spec.js +++ b/packages/server/src/api/routes/tests/layout.spec.js @@ -2,7 +2,7 @@ const { checkBuilderEndpoint } = require("./utilities/TestFunctions") const setup = require("./utilities") const { basicLayout } = require("./utilities/structures") -describe("/queries", () => { +describe("/layouts", () => { let request = setup.getRequest() let config = setup.getConfig() let layout diff --git a/packages/server/src/api/routes/tests/permissions.spec.js b/packages/server/src/api/routes/tests/permissions.spec.js index 93e6e29131..b24fac57c0 100644 --- a/packages/server/src/api/routes/tests/permissions.spec.js +++ b/packages/server/src/api/routes/tests/permissions.spec.js @@ -107,4 +107,19 @@ describe("/permission", () => { expect(res.status).toEqual(403) }) }) + + describe("fetch builtins", () => { + it("should be able to fetch builtin definitions", async () => { + const res = await request + .get(`/api/permission/builtin`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(Array.isArray(res.body)).toEqual(true) + const publicPerm = res.body.find(perm => perm._id === "public") + expect(publicPerm).toBeDefined() + expect(publicPerm.permissions).toBeDefined() + expect(publicPerm.name).toBeDefined() + }) + }) }) diff --git a/packages/server/src/api/routes/tests/templates.spec.js b/packages/server/src/api/routes/tests/templates.spec.js new file mode 100644 index 0000000000..f0d26bc7db --- /dev/null +++ b/packages/server/src/api/routes/tests/templates.spec.js @@ -0,0 +1,49 @@ +const setup = require("./utilities") +const { budibaseAppsDir } = require("../../../utilities/budibaseDir") +const fs = require("fs") +const { join } = require("path") + +describe("/templates", () => { + let request = setup.getRequest() + let config = setup.getConfig() + + afterAll(setup.afterAll) + + beforeEach(async () => { + await config.init() + }) + + describe("fetch", () => { + it("should be able to fetch templates", async () => { + const res = await request + .get(`/api/templates`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + // this test is quite light right now, templates aren't heavily utilised yet + expect(Array.isArray(res.body)).toEqual(true) + }) + }) + + describe("export", () => { + it("should be able to export the basic app", async () => { + const res = await request + .post(`/api/templates`) + .send({ + templateName: "test", + }) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.message).toEqual("Created template: test") + const dir = join( + budibaseAppsDir(), + "templates", + "app", + "test", + "db" + ) + expect(fs.existsSync(dir)).toEqual(true) + }) + }) +}) \ No newline at end of file diff --git a/packages/server/src/utilities/templates.js b/packages/server/src/utilities/templates.js index 7383e3115d..c3d89477df 100644 --- a/packages/server/src/utilities/templates.js +++ b/packages/server/src/utilities/templates.js @@ -31,6 +31,8 @@ exports.getLocalTemplates = function() { return templateObj } +// can't really test this, downloading is just not something we should do in a behavioural test +/* istanbul ignore next */ exports.downloadTemplate = async function(type, name) { const dirName = join(budibaseAppsDir(), "templates", type, name) if (env.LOCAL_TEMPLATES) { @@ -67,8 +69,7 @@ exports.performDump = performDump exports.exportTemplateFromApp = async function({ templateName, appId }) { // Copy frontend files const templatesDir = join( - os.homedir(), - ".budibase", + budibaseAppsDir(), "templates", "app", templateName, diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index e1d6e03634..591205393a 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-5.0.3.tgz#bc5b5532ecafd923a61f2fb097e3b108c0106a3f" integrity sha512-GLyWIFBbGvpKPGo55JyRZAo4lVbnBiD52cKlw/0Vt+wnmKvWJkpZvsjVoaIolyBXDeAQKSicRtqFNPem9w0WYA== +"@adobe/spectrum-css-workflow-icons@^1.1.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@adobe/spectrum-css-workflow-icons/-/spectrum-css-workflow-icons-1.2.0.tgz#cda8bbe873ba9317160458858ae979e5393e5550" + integrity sha512-STSQQHvoBM0kf1JrNL3KEt88RklIctaGyGOzwUTnhtTkT1jHLaF4FgxrPDCvr1AT8VOq1nGplKUCeyZ9vdUUmA== + "@azure/ms-rest-azure-env@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@azure/ms-rest-azure-env/-/ms-rest-azure-env-1.1.2.tgz#8505873afd4a1227ec040894a64fdd736b4a101f" @@ -249,12 +254,24 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" -"@budibase/client@^0.8.3": - version "0.8.3" - resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.8.3.tgz#944a745cc82845987cabd48e2ce3a7e58b387865" - integrity sha512-gEOmHlqStsFTtotduRRz9bld2s/066pSwM3CWRuspsz5yycPLMhWKcA3CdfxVlNoR9y7I7IFC9+pfM5STDJAMQ== +"@budibase/bbui@^1.58.13": + version "1.58.13" + resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.13.tgz#59df9c73def2d81c75dcbd2266c52c19db88dbd7" + integrity sha512-Zk6CKXdBfKsTVzA1Xs5++shdSSZLfphVpZuKVbjfzkgtuhyH7ruucexuSHEpFsxjW5rEKgKIBoRFzCK5vPvN0w== dependencies: - "@budibase/string-templates" "^0.8.3" + markdown-it "^12.0.2" + quill "^1.3.7" + sirv-cli "^0.4.6" + svelte-flatpickr "^2.4.0" + svelte-portal "^1.0.0" + turndown "^7.0.0" + +"@budibase/client@^0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.8.5.tgz#31a6bbf8e7ff2a5ab635e8987357c310dcedf555" + integrity sha512-igiHyFpqbYm2EyCy0aUlBlaPibpFa5DtQow1kFBAjUW2cyZdEt84JV4Mei77NueGo7zHcr6/ByF6ycdyeBgXQw== + dependencies: + "@budibase/string-templates" "^0.8.5" deep-equal "^2.0.1" regexparam "^1.3.0" shortid "^2.2.15" @@ -292,10 +309,42 @@ to-gfm-code-block "^0.1.1" year "^0.2.1" -"@budibase/string-templates@^0.8.3": - version "0.8.3" - resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.8.3.tgz#f3b1f31ef914b926fb5285bc701e1200568dc92d" - integrity sha512-X4Z9/1TS5PtO5sF1CDoyp8xSJhXFWIhOldTNBzPeCjAaD+c9Q8gOgcwECWugJh2d05RjiVI6gDbeirT8Q2QMig== +"@budibase/standard-components@^0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@budibase/standard-components/-/standard-components-0.8.5.tgz#4b94653110e4f20a8cb252b6421b620fd5ac31bc" + integrity sha512-wDEuxiu/DyPQYR2zQSt7TdPlAzdjjePitfKDzdIxm/WM7umXDSvLkA39nRzicEXikti34+waS7H96xGNuednVw== + dependencies: + "@adobe/spectrum-css-workflow-icons" "^1.1.0" + "@budibase/bbui" "^1.58.13" + "@budibase/svelte-ag-grid" "^1.0.4" + "@spectrum-css/actionbutton" "^1.0.0-beta.1" + "@spectrum-css/button" "^3.0.0-beta.6" + "@spectrum-css/checkbox" "^3.0.0-beta.6" + "@spectrum-css/fieldlabel" "^3.0.0-beta.7" + "@spectrum-css/icon" "^3.0.0-beta.2" + "@spectrum-css/inputgroup" "^3.0.0-beta.7" + "@spectrum-css/menu" "^3.0.0-beta.5" + "@spectrum-css/page" "^3.0.0-beta.0" + "@spectrum-css/picker" "^1.0.0-beta.3" + "@spectrum-css/popover" "^3.0.0-beta.6" + "@spectrum-css/stepper" "^3.0.0-beta.7" + "@spectrum-css/textfield" "^3.0.0-beta.6" + "@spectrum-css/vars" "^3.0.0-beta.2" + apexcharts "^3.22.1" + flatpickr "^4.6.6" + loadicons "^1.0.0" + lodash.debounce "^4.0.8" + markdown-it "^12.0.2" + quill "^1.3.7" + remixicon "^2.5.0" + svelte-apexcharts "^1.0.2" + svelte-flatpickr "^3.1.0" + turndown "^7.0.0" + +"@budibase/string-templates@^0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.8.5.tgz#ad30e318f7486d4256b1165099fe2bd8004ef472" + integrity sha512-PcpiiDlYJFIVwtFGIRqZQtRl8wbO6yr0/+1Gca0TwR2WhyUyAs/ojO+jLIj97JWh/hE5zKaZW7d4cMOf+BDI/A== dependencies: "@budibase/handlebars-helpers" "^0.11.3" dayjs "^1.10.4" @@ -303,6 +352,13 @@ handlebars-utils "^1.0.6" lodash "^4.17.20" +"@budibase/svelte-ag-grid@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@budibase/svelte-ag-grid/-/svelte-ag-grid-1.0.4.tgz#41cceec4bde2c4aea8b9da8f610fe36055c7709f" + integrity sha512-JZm6qujxnZpqw7Twbegr6se4sHhyWzN0Cibrk5bVBH32hBgzD6dd33fxwrjHKkWFxjys9wRT+cqYgYVlSt9E3w== + dependencies: + ag-grid-community "^24.0.0" + "@cnakazawa/watch@^1.0.3": version "1.0.4" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" @@ -830,6 +886,11 @@ path-to-regexp "^1.1.1" urijs "^1.19.0" +"@polka/url@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-0.5.0.tgz#b21510597fd601e5d7c95008b76bf0d254ebfd31" + integrity sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw== + "@sendgrid/client@^7.1.1": version "7.4.2" resolved "https://registry.yarnpkg.com/@sendgrid/client/-/client-7.4.2.tgz#204a9fbb5dc05a721a5d8cd8930f57f9f8e612b1" @@ -942,6 +1003,73 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== +"@spectrum-css/actionbutton@^1.0.0-beta.1": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@spectrum-css/actionbutton/-/actionbutton-1.0.0.tgz#c2f0939f6d49de0a855f08a9466e3f27105a1747" + integrity sha512-klE5CGJEJXkc4DMLF8W+VPlLZ6SFr4WXI5Tc9NarOtbAc7mqhs2gWA8HpsPT717FWdxRVVt3sSuAydgKC/T0UA== + +"@spectrum-css/button@^3.0.0-beta.6": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@spectrum-css/button/-/button-3.0.0.tgz#eebdd7a05eac9a40f297f802aca3efeb95931e83" + integrity sha512-CUGkuHOhqgfIRYTEceybcW1YsUN61F9BgDhqymhVd1yJFsuh1xkwnmv3IIodukgS+1e3L0JY6ifU86IWX/Dx5w== + +"@spectrum-css/checkbox@^3.0.0-beta.6": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@spectrum-css/checkbox/-/checkbox-3.0.0.tgz#6ed15f433bed31a63818d154960aca044ce62182" + integrity sha512-FpxxftMzuWT8qq3XB4oBQgWglXuCCEGBfgX82EI9VtrJmw9j0Lm/nThMLX353p9awM4GfT3l2LNOneHbNetaRQ== + +"@spectrum-css/fieldlabel@^3.0.0-beta.7": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@spectrum-css/fieldlabel/-/fieldlabel-3.0.0.tgz#01be5e5a7b024516574820962b9a656f7b2e20d4" + integrity sha512-dEOvDEigL9E60kQ9fT6MLyRzPKrPXAKulqDYOYpZaK2bsKrbIvsKb7NcuQynPAOE26FiuqQsp2khv5VqF4KzrA== + +"@spectrum-css/icon@^3.0.0-beta.2": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@spectrum-css/icon/-/icon-3.0.0.tgz#a822c901ca049f487420053dfeaaf71c0850c848" + integrity sha512-0VVx34WECxe+acSZsB+zk8T+AG8YimlCfUothuqLzcUgY6MnBESHJKOEuKKihxnihEm6EJiMc2NYA7+09kPv/A== + +"@spectrum-css/inputgroup@^3.0.0-beta.7": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@spectrum-css/inputgroup/-/inputgroup-3.0.0.tgz#c2790c2c0a4c435ca3fff9ba04f64dd2b252f980" + integrity sha512-dlF8LmMwTa5G6Rl4zUiNCmRv7p2v+88jINnSwZHucgKZL0/HJZBRxjF1neeSfRFrc8R6cemoVXDHRDtZFaVtXQ== + +"@spectrum-css/menu@^3.0.0-beta.5": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@spectrum-css/menu/-/menu-3.0.0.tgz#78153ea60a36c87e9d815ce51dc7f84d6b9b9abd" + integrity sha512-E6L6s1/cwh6hn4yhUHegiJ+Su03Bpa7qP5a6nEccpYePZxPAAN2FjZBWdMOPlGtv1e70vudAsoejli9nVthC2w== + +"@spectrum-css/page@^3.0.0-beta.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@spectrum-css/page/-/page-3.0.0.tgz#159e79fd376e2def1a7a25a8b8a8fcfa94bd79d1" + integrity sha512-4rNpGq99cfNSq/IOQNCiXio5gF/EEfjcSmihHBJlh7/VOB9zE84kMNW1Gux4cGEmdP14U1Zo1ZwnPIVs5ZuPgg== + dependencies: + "@spectrum-css/vars" "^3.0.0" + +"@spectrum-css/picker@^1.0.0-beta.3": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@spectrum-css/picker/-/picker-1.0.0.tgz#5758a128da081becd425b8d9433b24541f12b4b3" + integrity sha512-aSoin2SVYl5W2R3nFp+V/Er6rAJUnwygO4E3g/tfDuImq8p5U3FKZj4sggSqfuD2U1PIwNSwX0D1RdxuGXsnUQ== + +"@spectrum-css/popover@^3.0.0-beta.6": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@spectrum-css/popover/-/popover-3.0.0.tgz#ec1ab86a66cc59bd522d3de2b7febe41e2a9fe46" + integrity sha512-Lr2FZSJbDbDMp3bOlLtvDjOw6AwzRu3g0BbQ7NGK1l5MB06AhnqJX+TPB2iEDTfPdNyaDc5SCp55lBHP3RzHuw== + +"@spectrum-css/stepper@^3.0.0-beta.7": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@spectrum-css/stepper/-/stepper-3.0.0.tgz#ab5af818c86f2bc5050d0caee8b0a1c75201bfaf" + integrity sha512-Gwvb4YLEBy/YtnFQ4aySnlve+pBrgPIm5LSq5IkeyjAKy7ZalQm9IIEkrVERHO1b+vbRZ6DW/aj2zYgzKgGMrA== + +"@spectrum-css/textfield@^3.0.0-beta.6": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@spectrum-css/textfield/-/textfield-3.0.0.tgz#2f2d341b8d2c6f74e074b7e8df4a28307561cbbf" + integrity sha512-ooXiSc5TZuZCFr3wl1JB60nS9FBBkGgqsml7kAS/7bOwRTCUPH7cY80SoaabRL8Z9Clml+K1Pa7I/r+Wphb53g== + +"@spectrum-css/vars@^3.0.0", "@spectrum-css/vars@^3.0.0-beta.2": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-3.0.0.tgz#c3ef4c2f07bd4f0d2734e730233ca81cb18106e7" + integrity sha512-fNXU6qmcCbSiUoWGe/m9A8/THRHbpzwZ+iN8o/27tWIzcQIyZBZgjmV/kIMdF1dHpu5CuWik7mGV1Ex8tlzATg== + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -1226,6 +1354,11 @@ adal-node@^0.1.28: xmldom ">= 0.1.x" xpath.js "~1.1.0" +ag-grid-community@^24.0.0: + version "24.1.0" + resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-24.1.0.tgz#1e3cab51211822e08d56f03a491b7c0deaa398e6" + integrity sha512-pWnWphuDcejZ8ahf6C734EpCx3XQ6dHEZWMWTlCdHNT0mZBLJ4YKCGACX+ttAEtSX2MGM3G13JncvuratUlYag== + agent-base@6: version "6.0.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" @@ -1563,6 +1696,18 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +apexcharts@^3.19.2, apexcharts@^3.22.1: + version "3.25.0" + resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.25.0.tgz#f3f0f9f344f997230f5c7f2918408aa072627496" + integrity sha512-uM7OF+jLL4ba79noYcrMwMgJW8DI+Ff28CCQoGq23g25z8nGSQEoU+u12YWlECA9gBA5tbmdaQhMxjlK+M6B9Q== + dependencies: + svg.draggable.js "^2.2.2" + svg.easing.js "^2.0.0" + svg.filter.js "^2.0.2" + svg.pathmorphing.js "^0.1.3" + svg.resize.js "^1.4.3" + svg.select.js "^3.0.1" + app-builder-bin@3.5.10: version "3.5.10" resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-3.5.10.tgz#4a7f9999fccc0c435b6284ae1366bc76a17c4a7d" @@ -1621,6 +1766,11 @@ argparse@^1.0.10, argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + args@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/args/-/args-5.0.1.tgz#4bf298df90a4799a09521362c579278cc2fdd761" @@ -2356,6 +2506,11 @@ clone-response@1.0.2, clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" +clone@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= + co-body@^5.1.1: version "5.2.0" resolved "https://registry.yarnpkg.com/co-body/-/co-body-5.2.0.tgz#5a0a658c46029131e0e3a306f67647302f71c124" @@ -2489,6 +2644,11 @@ configstore@^5.0.1: write-file-atomic "^3.0.0" xdg-basedir "^4.0.0" +console-clear@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/console-clear/-/console-clear-1.1.1.tgz#995e20cbfbf14dd792b672cde387bd128d674bf7" + integrity sha512-pMD+MVR538ipqkG5JXeOEbKWS5um1H4LUUccUQG68qpeqBYbzYy79Gh55jkd2TtPdRfUaLWdv6LPP//5Zt0aPQ== + content-disposition@^0.5.2, content-disposition@~0.5.2: version "0.5.3" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" @@ -2752,6 +2912,18 @@ decompress@^4.2.1: pify "^2.3.0" strip-dirs "^2.0.0" +deep-equal@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" + integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== + dependencies: + is-arguments "^1.0.4" + is-date-object "^1.0.1" + is-regex "^1.0.4" + object-is "^1.0.1" + object-keys "^1.1.1" + regexp.prototype.flags "^1.2.0" + deep-equal@^2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.5.tgz#55cd2fe326d83f9cbf7261ef0e060b3f724c5cb9" @@ -2944,6 +3116,11 @@ domexception@^1.0.1: dependencies: webidl-conversions "^4.0.2" +domino@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/domino/-/domino-2.1.6.tgz#fe4ace4310526e5e7b9d12c7de01b7f485a57ffe" + integrity sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ== + dot-prop@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -3165,6 +3342,11 @@ ent@^2.2.0: resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== + env-paths@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" @@ -3450,6 +3632,11 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +eventemitter3@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" + integrity sha1-teEHm1n7XhuidxwKmTvgYKWMmbo= + events@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" @@ -3551,7 +3738,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@~3.0.2: +extend@^3.0.0, extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -3621,6 +3808,11 @@ fast-deep-equal@^3.1.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-diff@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154" + integrity sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig== + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -3800,6 +3992,11 @@ flat-cache@^2.0.1: rimraf "2.6.3" write "1.0.3" +flatpickr@^4.5.2, flatpickr@^4.6.6: + version "4.6.9" + resolved "https://registry.yarnpkg.com/flatpickr/-/flatpickr-4.6.9.tgz#9a13383e8a6814bda5d232eae3fcdccb97dc1499" + integrity sha512-F0azNNi8foVWKSF+8X+ZJzz8r9sE1G4hl06RyceIaLvyltKvDl6vqk9Lm/6AUUCi5HWaIjiUbk7UpeE/fOXOpw== + flatstr@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz#c2ba6a08173edbb6c9640e3055b95e287ceb5931" @@ -3988,6 +4185,11 @@ get-object@^0.2.0: is-number "^2.0.2" isobject "^0.2.0" +get-port@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc" + integrity sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw= + get-stream@3.0.0, get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" @@ -4859,7 +5061,7 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-regex@^1.1.1, is-regex@^1.1.2: +is-regex@^1.0.4, is-regex@^1.1.1, is-regex@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== @@ -5693,7 +5895,7 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -kleur@^3.0.3: +kleur@^3.0.0, kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== @@ -6002,6 +6204,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= +linkify-it@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8" + integrity sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ== + dependencies: + uc.micro "^1.0.1" + load-bmfont@^1.3.1, load-bmfont@^1.4.0: version "1.4.1" resolved "https://registry.yarnpkg.com/load-bmfont/-/load-bmfont-1.4.1.tgz#c0f5f4711a1e2ccff725a7b6078087ccfcddd3e9" @@ -6026,6 +6235,16 @@ load-json-file@^4.0.0: pify "^3.0.0" strip-bom "^3.0.0" +loadicons@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/loadicons/-/loadicons-1.0.0.tgz#79fd9b08ef2933988c94068cbd246ef3f21cbd04" + integrity sha512-KSywiudfuOK5sTdhNMM8hwRpMxZ5TbQlU4ZijMxUFwRW7jpxUmb9YJoLIzDn7+xuxeLzCZWBmLJS2JDjDWCpsw== + +local-access@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/local-access/-/local-access-1.1.0.tgz#e007c76ba2ca83d5877ba1a125fc8dfe23ba4798" + integrity sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw== + locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -6280,6 +6499,17 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +markdown-it@^12.0.2: + version "12.0.4" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.0.4.tgz#eec8247d296327eac3ba9746bdeec9cfcc751e33" + integrity sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q== + dependencies: + argparse "^2.0.1" + entities "~2.1.0" + linkify-it "^3.0.1" + mdurl "^1.0.1" + uc.micro "^1.0.5" + matcher@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" @@ -6287,6 +6517,11 @@ matcher@^3.0.0: dependencies: escape-string-regexp "^4.0.0" +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -6396,6 +6631,11 @@ mime@^1.3.4, mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@^2.3.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" + integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== + mime@^2.4.6: version "2.4.6" resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" @@ -6473,6 +6713,11 @@ mri@1.1.4: resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a" integrity sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w== +mri@^1.1.0: + version "1.1.6" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.6.tgz#49952e1044db21dbf90f6cd92bc9c9a777d415a6" + integrity sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -6755,7 +7000,7 @@ object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== -object-is@^1.1.4: +object-is@^1.0.1, object-is@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== @@ -6973,6 +7218,11 @@ pako@^1.0.5: resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== +parchment@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/parchment/-/parchment-1.1.4.tgz#aeded7ab938fe921d4c34bc339ce1168bc2ffde5" + integrity sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -7646,6 +7896,27 @@ quick-format-unescaped@^4.0.1: resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.1.tgz#437a5ea1a0b61deb7605f8ab6a8fd3858dbeb701" integrity sha512-RyYpQ6Q5/drsJyOhrWHYMWTedvjTIat+FTwv0K4yoUxzvekw2aRHMQJLlnvt8UantkZg2++bEzD9EdxXqkWf4A== +quill-delta@^3.6.2: + version "3.6.3" + resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-3.6.3.tgz#b19fd2b89412301c60e1ff213d8d860eac0f1032" + integrity sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg== + dependencies: + deep-equal "^1.0.1" + extend "^3.0.2" + fast-diff "1.1.2" + +quill@^1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/quill/-/quill-1.3.7.tgz#da5b2f3a2c470e932340cdbf3668c9f21f9286e8" + integrity sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g== + dependencies: + clone "^2.1.1" + deep-equal "^1.0.1" + eventemitter3 "^2.0.3" + extend "^3.0.2" + parchment "^1.1.4" + quill-delta "^3.6.2" + raw-body@^2.2.0: version "2.4.1" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" @@ -7822,7 +8093,7 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp.prototype.flags@^1.3.0: +regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== @@ -7869,6 +8140,11 @@ remarkable@^1.6.2, remarkable@^1.7.1: argparse "^1.0.10" autolinker "~0.28.0" +remixicon@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/remixicon/-/remixicon-2.5.0.tgz#b5e245894a1550aa23793f95daceadbf96ad1a41" + integrity sha512-q54ra2QutYDZpuSnFjmeagmEiN9IMo56/zz5dDNitzKD23oFRw77cWo4TsrAdmdkPiEn8mxlrTqxnkujDbEGww== + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -8055,6 +8331,13 @@ rxjs@^6.6.0: dependencies: tslib "^1.9.0" +sade@^1.4.0: + version "1.7.4" + resolved "https://registry.yarnpkg.com/sade/-/sade-1.7.4.tgz#ea681e0c65d248d2095c90578c03ca0bb1b54691" + integrity sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA== + dependencies: + mri "^1.1.0" + safe-buffer@*, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -8287,6 +8570,27 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +sirv-cli@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/sirv-cli/-/sirv-cli-0.4.6.tgz#c28ab20deb3b34637f5a60863dc350f055abca04" + integrity sha512-/Vj85/kBvPL+n9ibgX6FicLE8VjidC1BhlX67PYPBfbBAphzR6i0k0HtU5c2arejfU3uzq8l3SYPCwl1x7z6Ww== + dependencies: + console-clear "^1.1.0" + get-port "^3.2.0" + kleur "^3.0.0" + local-access "^1.0.1" + sade "^1.4.0" + sirv "^0.4.6" + tinydate "^1.0.0" + +sirv@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-0.4.6.tgz#185e44eb93d24009dd183b7494285c5180b81f22" + integrity sha512-rYpOXlNbpHiY4nVXxuDf4mXPvKz1reZGap/LkWp9TvcZ84qD/nPBjjH/6GZsgIjVMbOslnY8YYULAyP8jMn1GQ== + dependencies: + "@polka/url" "^0.5.0" + mime "^2.3.1" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -8749,6 +9053,32 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +svelte-apexcharts@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/svelte-apexcharts/-/svelte-apexcharts-1.0.2.tgz#4e000f8b8f7c901c05658c845457dfc8314d54c1" + integrity sha512-6qlx4rE+XsonZ0FZudfwqOQ34Pq+3wpxgAD75zgEmGoYhYBJcwmikTuTf3o8ZBsZue9U/pAwhNy3ed1Bkq1gmA== + dependencies: + apexcharts "^3.19.2" + +svelte-flatpickr@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-2.4.0.tgz#190871fc3305956c8c8fd3601cd036b8ac71ef49" + integrity sha512-UUC5Te+b0qi4POg7VDwfGh0m5W3Hf64OwkfOTj6FEe/dYZN4cBzpQ82EuuQl0CTbbBAsMkcjJcixV1d2V6EHCQ== + dependencies: + flatpickr "^4.5.2" + +svelte-flatpickr@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.1.0.tgz#ad83588430dbd55196a1a258b8ba27e7f9c1ee37" + integrity sha512-zKyV+ukeVuJ8CW0Ing3T19VSekc4bPkou/5Riutt1yATrLvSsanNqcgqi7Q5IePvIoOF9GJ5OtHvn1qK9Wx9BQ== + dependencies: + flatpickr "^4.5.2" + +svelte-portal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-1.0.0.tgz#36a47c5578b1a4d9b4dc60fa32a904640ec4cdd3" + integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q== + svelte-spa-router@^3.0.5: version "3.1.0" resolved "https://registry.yarnpkg.com/svelte-spa-router/-/svelte-spa-router-3.1.0.tgz#a929f0def7e12c41f32bc356f91685aeadcd75bf" @@ -8761,6 +9091,61 @@ svelte@3.30.0: resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.30.0.tgz#cbde341e96bf34f4ac73c8f14f8a014e03bfb7d6" integrity sha512-z+hdIACb9TROGvJBQWcItMtlr4s0DBUgJss6qWrtFkOoIInkG+iAMo/FJZQFyDBQZc+dul2+TzYSi/tpTT5/Ag== +svg.draggable.js@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz#c514a2f1405efb6f0263e7958f5b68fce50603ba" + integrity sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw== + dependencies: + svg.js "^2.0.1" + +svg.easing.js@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/svg.easing.js/-/svg.easing.js-2.0.0.tgz#8aa9946b0a8e27857a5c40a10eba4091e5691f12" + integrity sha1-iqmUawqOJ4V6XEChDrpAkeVpHxI= + dependencies: + svg.js ">=2.3.x" + +svg.filter.js@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/svg.filter.js/-/svg.filter.js-2.0.2.tgz#91008e151389dd9230779fcbe6e2c9a362d1c203" + integrity sha1-kQCOFROJ3ZIwd5/L5uLJo2LRwgM= + dependencies: + svg.js "^2.2.5" + +svg.js@>=2.3.x, svg.js@^2.0.1, svg.js@^2.2.5, svg.js@^2.4.0, svg.js@^2.6.5: + version "2.7.1" + resolved "https://registry.yarnpkg.com/svg.js/-/svg.js-2.7.1.tgz#eb977ed4737001eab859949b4a398ee1bb79948d" + integrity sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA== + +svg.pathmorphing.js@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz#c25718a1cc7c36e852ecabc380e758ac09bb2b65" + integrity sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww== + dependencies: + svg.js "^2.4.0" + +svg.resize.js@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/svg.resize.js/-/svg.resize.js-1.4.3.tgz#885abd248e0cd205b36b973c4b578b9a36f23332" + integrity sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw== + dependencies: + svg.js "^2.6.5" + svg.select.js "^2.1.2" + +svg.select.js@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-2.1.2.tgz#e41ce13b1acff43a7441f9f8be87a2319c87be73" + integrity sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ== + dependencies: + svg.js "^2.2.5" + +svg.select.js@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-3.0.1.tgz#a4198e359f3825739226415f82176a90ea5cc917" + integrity sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw== + dependencies: + svg.js "^2.6.5" + symbol-tree@^3.2.2: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -8940,6 +9325,11 @@ tinycolor2@^1.4.1: resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803" integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA== +tinydate@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/tinydate/-/tinydate-1.3.0.tgz#e6ca8e5a22b51bb4ea1c3a2a4fd1352dbd4c57fb" + integrity sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -9083,6 +9473,13 @@ tunnel@0.0.6, tunnel@^0.0.6: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== +turndown@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/turndown/-/turndown-7.0.0.tgz#19b2a6a2d1d700387a1e07665414e4af4fec5225" + integrity sha512-G1FfxfR0mUNMeGjszLYl3kxtopC4O9DRRiMlMDDVHvU1jaBkGFg4qxIyjIk2aiKLHyDyZvZyu4qBO2guuYBy3Q== + dependencies: + domino "^2.1.6" + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -9142,6 +9539,11 @@ typeof-article@^0.1.1: dependencies: kind-of "^3.1.0" +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + uglify-js@^3.1.4: version "3.13.0" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.0.tgz#66ed69f7241f33f13531d3d51d5bcebf00df7f69" From d9151cca0ad4122642f87ee4feccc14537fc1ad5 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 9 Mar 2021 16:28:41 +0000 Subject: [PATCH 13/24] Adding test cases for backup and integration. --- .../src/api/routes/tests/backup.spec.js | 32 ++++++++++++ .../src/api/routes/tests/integration.spec.js | 52 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 packages/server/src/api/routes/tests/backup.spec.js create mode 100644 packages/server/src/api/routes/tests/integration.spec.js diff --git a/packages/server/src/api/routes/tests/backup.spec.js b/packages/server/src/api/routes/tests/backup.spec.js new file mode 100644 index 0000000000..d603990294 --- /dev/null +++ b/packages/server/src/api/routes/tests/backup.spec.js @@ -0,0 +1,32 @@ +const { checkBuilderEndpoint } = require("./utilities/TestFunctions") +const setup = require("./utilities") + +describe("/backups", () => { + let request = setup.getRequest() + let config = setup.getConfig() + + afterAll(setup.afterAll) + + beforeEach(async () => { + await config.init() + }) + + describe("exportAppDump", () => { + it("should be able to export app", async () => { + const res = await request + .get(`/api/backups/export?appId=${config.getAppId()}`) + .set(config.defaultHeaders()) + .expect(200) + expect(res.text).toBeDefined() + expect(res.text.includes(`"db_name":"${config.getAppId()}"`)).toEqual(true) + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "GET", + url: `/api/backups/export?appId=${config.getAppId()}`, + }) + }) + }) +}) \ No newline at end of file diff --git a/packages/server/src/api/routes/tests/integration.spec.js b/packages/server/src/api/routes/tests/integration.spec.js new file mode 100644 index 0000000000..528d0d3417 --- /dev/null +++ b/packages/server/src/api/routes/tests/integration.spec.js @@ -0,0 +1,52 @@ +const { checkBuilderEndpoint } = require("./utilities/TestFunctions") +const setup = require("./utilities") + +describe("/integrations", () => { + let request = setup.getRequest() + let config = setup.getConfig() + + afterAll(setup.afterAll) + + beforeEach(async () => { + await config.init() + }) + + describe("fetch", () => { + it("should be able to get all integration definitions", async () => { + const res = await request + .get(`/api/integrations`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.POSTGRES).toBeDefined() + expect(res.body.POSTGRES.friendlyName).toEqual("PostgreSQL") + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "GET", + url: `/api/integrations`, + }) + }) + }) + + describe("find", () => { + it("should be able to get postgres definition", async () => { + const res = await request + .get(`/api/integrations/POSTGRES`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.friendlyName).toEqual("PostgreSQL") + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "GET", + url: `/api/integrations/POSTGRES`, + }) + }) + }) +}) \ No newline at end of file From 4d39cf5405280e039874e2f98cbfe4c75d7ac5b2 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Tue, 9 Mar 2021 17:04:24 +0000 Subject: [PATCH 14/24] authenticated tests --- .../middleware/tests/authenticated.spec.js | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/packages/server/src/middleware/tests/authenticated.spec.js b/packages/server/src/middleware/tests/authenticated.spec.js index e69de29bb2..799fbaf41b 100644 --- a/packages/server/src/middleware/tests/authenticated.spec.js +++ b/packages/server/src/middleware/tests/authenticated.spec.js @@ -0,0 +1,98 @@ +const { AuthTypes } = require("../../constants") +const authenticatedMiddleware = require("../authenticated") + +class TestConfiguration { + constructor(middleware) { + this.middleware = authenticatedMiddleware + this.ctx = { + auth: {}, + request: {}, + cookies: { + set: jest.fn(), + get: jest.fn() + }, + headers: {}, + params: {}, + path: "", + request: { + headers: {} + } + } + this.next = jest.fn() + } + + setHeaders(headers) { + this.ctx.headers = headers + } + + executeMiddleware() { + return this.middleware(this.ctx, this.next) + } +} + +describe("Authenticated middleware", () => { + let config + + beforeEach(() => { + config = new TestConfiguration() + }) + + it("calls next() when on the builder path", async () => { + config.ctx.path = "/_builder" + + await config.executeMiddleware() + + expect(config.next).toHaveBeenCalled() + }) + + it("sets a new cookie when the current cookie does not match the app id from context", async () => { + const appId = "app_123" + config.ctx.cookies.get.mockImplementationOnce(() => "cookieAppId") + config.setHeaders({ + "x-budibase-app-id": appId + }) + + await config.executeMiddleware() + + expect(config.ctx.cookies.set).toHaveBeenCalledWith( + "budibase:currentapp:local", + appId, + expect.any(Object) + ) + + }) + + fit("sets a BUILDER auth type when the x-budibase-type header is not 'client'", async () => { + config.ctx.cookies.get.mockImplementationOnce(() => `budibase:builder:local`) + + await config.executeMiddleware() + + expect(config.ctx.auth.authenticated).toEqual(AuthTypes.BUILDER) + }) + + it("assigns an APP auth type when the user is not in the builder", async () => { + config.setHeaders({ + "x-budibase-type": "client" + }) + config.ctx.cookies.get.mockImplementationOnce(() => `budibase:builder:local`) + + await config.executeMiddleware() + + expect(config.ctx.auth.authenticated).toEqual(AuthTypes.APP) + }) + + it("marks the user as unauthenticated when a token cannot be determined from the users cookie", async () => { + config.executeMiddleware() + expect() + }) + + it("verifies the users JWT token and sets the user information in context when successful", async () => { + config.executeMiddleware() + expect() + }) + + it("clears the cookie when there is an error authenticating in the builder", async () => { + config.executeMiddleware() + expect() + }) +}) \ No newline at end of file From 929db83e99018c7bd14246fbe58ea708eeb51047 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 9 Mar 2021 17:09:18 +0000 Subject: [PATCH 15/24] Upping user test cases to cover all of controller. --- packages/server/src/api/controllers/user.js | 10 ++- .../server/src/api/routes/tests/user.spec.js | 76 +++++++++++++++++-- packages/server/src/api/routes/user.js | 2 +- 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index c100f43d88..3dd28284be 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -52,7 +52,7 @@ exports.create = async function(ctx) { const response = await db.post(user) ctx.status = 200 ctx.message = "User created successfully." - ctx.userId = response._id + ctx.userId = response.id ctx.body = { _rev: response.rev, email, @@ -70,6 +70,9 @@ exports.update = async function(ctx) { const db = new CouchDB(ctx.user.appId) const user = ctx.request.body let dbUser + if (user.email && !user._id) { + user._id = generateUserID(user.email) + } // get user incase password removed if (user._id) { dbUser = await db.get(user._id) @@ -87,14 +90,15 @@ exports.update = async function(ctx) { user._rev = response.rev ctx.status = 200 - ctx.message = `User ${ctx.request.body.email} updated successfully.` ctx.body = response } exports.destroy = async function(ctx) { const database = new CouchDB(ctx.user.appId) await database.destroy(generateUserID(ctx.params.email)) - ctx.message = `User ${ctx.params.email} deleted.` + ctx.body = { + message: `User ${ctx.params.email} deleted.`, + } ctx.status = 200 } diff --git a/packages/server/src/api/routes/tests/user.spec.js b/packages/server/src/api/routes/tests/user.spec.js index 6ec607a093..5e7ec9e9d4 100644 --- a/packages/server/src/api/routes/tests/user.spec.js +++ b/packages/server/src/api/routes/tests/user.spec.js @@ -42,15 +42,19 @@ describe("/users", () => { }) describe("create", () => { + async function create(user, status = 200) { + return request + .post(`/api/users`) + .set(config.defaultHeaders()) + .send(user) + .expect(status) + .expect("Content-Type", /json/) + } + it("returns a success message when a user is successfully created", async () => { const body = basicUser(BUILTIN_ROLE_IDS.POWER) body.email = "bill@budibase.com" - const res = await request - .post(`/api/users`) - .set(config.defaultHeaders()) - .send(body) - .expect(200) - .expect("Content-Type", /json/) + const res = await create(body) expect(res.res.statusMessage).toEqual("User created successfully.") expect(res.body._id).toBeUndefined() @@ -68,5 +72,65 @@ describe("/users", () => { failRole: BUILTIN_ROLE_IDS.PUBLIC, }) }) + + it("should error if no email provided", async () => { + const user = basicUser(BUILTIN_ROLE_IDS.POWER) + delete user.email + await create(user, 400) + }) + + it("should error if no role provided", async () => { + const user = basicUser(null) + await create(user, 400) + }) + + it("should throw error if user exists already", async () => { + await config.createUser("test@test.com") + const user = basicUser(BUILTIN_ROLE_IDS.POWER) + user.email = "test@test.com" + await create(user, 400) + }) + }) + + describe("update", () => { + it("should be able to update the user", async () => { + const user = await config.createUser() + user.roleId = BUILTIN_ROLE_IDS.BASIC + const res = await request + .put(`/api/users`) + .set(config.defaultHeaders()) + .send(user) + .expect(200) + .expect("Content-Type", /json/) + expect(res.body.ok).toEqual(true) + }) + }) + + describe("destroy", () => { + it("should be able to delete the user", async () => { + const email = "test@test.com" + await config.createUser(email) + const res = await request + .delete(`/api/users/${email}`) + .set(config.defaultHeaders()) + .expect(200) + .expect("Content-Type", /json/) + expect(res.body.message).toBeDefined() + }) + }) + + describe("find", () => { + it("should be able to find the user", async () => { + const email = "test@test.com" + await config.createUser(email) + const res = await request + .get(`/api/users/${email}`) + .set(config.defaultHeaders()) + .expect(200) + .expect("Content-Type", /json/) + expect(res.body.email).toEqual(email) + expect(res.body.roleId).toEqual(BUILTIN_ROLE_IDS.POWER) + expect(res.body.tableId).toBeDefined() + }) }) }) diff --git a/packages/server/src/api/routes/user.js b/packages/server/src/api/routes/user.js index 1ad1d2363e..cdaab0cc5b 100644 --- a/packages/server/src/api/routes/user.js +++ b/packages/server/src/api/routes/user.js @@ -21,7 +21,7 @@ router controller.find ) .put( - "/api/users/", + "/api/users", authorized(PermissionTypes.USER, PermissionLevels.WRITE), controller.update ) From ca5ca7add91ed58836a8ce46497634ef785a7392 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Tue, 9 Mar 2021 17:31:52 +0000 Subject: [PATCH 16/24] finish authenticated tests --- .../server/src/middleware/authenticated.js | 2 + .../__snapshots__/authenticated.spec.js.snap | 28 ++++++++++ .../middleware/tests/authenticated.spec.js | 56 ++++++++++++++----- 3 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 packages/server/src/middleware/tests/__snapshots__/authenticated.spec.js.snap diff --git a/packages/server/src/middleware/authenticated.js b/packages/server/src/middleware/authenticated.js index 659baa8f6c..32ed3f63d0 100644 --- a/packages/server/src/middleware/authenticated.js +++ b/packages/server/src/middleware/authenticated.js @@ -31,6 +31,7 @@ module.exports = async (ctx, next) => { token = ctx.cookies.get(getCookieName()) authType = AuthTypes.BUILDER } + if (!token && appId) { token = ctx.cookies.get(getCookieName(appId)) authType = AuthTypes.APP @@ -58,6 +59,7 @@ module.exports = async (ctx, next) => { role: await getRole(appId, jwtPayload.roleId), } } catch (err) { + console.log(err) if (authType === AuthTypes.BUILDER) { clearCookie(ctx) ctx.status = 200 diff --git a/packages/server/src/middleware/tests/__snapshots__/authenticated.spec.js.snap b/packages/server/src/middleware/tests/__snapshots__/authenticated.spec.js.snap new file mode 100644 index 0000000000..1583ecb51f --- /dev/null +++ b/packages/server/src/middleware/tests/__snapshots__/authenticated.spec.js.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Authenticated middleware sets the correct APP auth type information when the user is not in the builder 1`] = ` +Object { + "apiKey": "1234", + "appId": "budibase:app:local", + "role": Role { + "_id": "ADMIN", + "inherits": "POWER", + "name": "Admin", + "permissionId": "admin", + }, + "roleId": "ADMIN", +} +`; + +exports[`Authenticated middleware sets the correct BUILDER auth type information when the x-budibase-type header is not 'client' 1`] = ` +Object { + "apiKey": "1234", + "appId": "budibase:builder:local", + "role": Role { + "_id": "BUILDER", + "name": "Builder", + "permissionId": "admin", + }, + "roleId": "BUILDER", +} +`; diff --git a/packages/server/src/middleware/tests/authenticated.spec.js b/packages/server/src/middleware/tests/authenticated.spec.js index 799fbaf41b..bb124d2f4a 100644 --- a/packages/server/src/middleware/tests/authenticated.spec.js +++ b/packages/server/src/middleware/tests/authenticated.spec.js @@ -1,10 +1,13 @@ const { AuthTypes } = require("../../constants") const authenticatedMiddleware = require("../authenticated") +const jwt = require("jsonwebtoken") +jest.mock("jsonwebtoken") class TestConfiguration { constructor(middleware) { this.middleware = authenticatedMiddleware this.ctx = { + config: {}, auth: {}, request: {}, cookies: { @@ -16,7 +19,8 @@ class TestConfiguration { path: "", request: { headers: {} - } + }, + throw: jest.fn() } this.next = jest.fn() } @@ -28,6 +32,10 @@ class TestConfiguration { executeMiddleware() { return this.middleware(this.ctx, this.next) } + + afterEach() { + jest.resetAllMocks() + } } describe("Authenticated middleware", () => { @@ -37,6 +45,10 @@ describe("Authenticated middleware", () => { config = new TestConfiguration() }) + afterEach(() => { + config.afterEach() + }) + it("calls next() when on the builder path", async () => { config.ctx.path = "/_builder" @@ -47,10 +59,10 @@ describe("Authenticated middleware", () => { it("sets a new cookie when the current cookie does not match the app id from context", async () => { const appId = "app_123" - config.ctx.cookies.get.mockImplementationOnce(() => "cookieAppId") config.setHeaders({ "x-budibase-app-id": appId }) + config.ctx.cookies.get.mockImplementation(() => "cookieAppId") await config.executeMiddleware() @@ -62,37 +74,53 @@ describe("Authenticated middleware", () => { }) - fit("sets a BUILDER auth type when the x-budibase-type header is not 'client'", async () => { - config.ctx.cookies.get.mockImplementationOnce(() => `budibase:builder:local`) + it("sets the correct BUILDER auth type information when the x-budibase-type header is not 'client'", async () => { + config.ctx.cookies.get.mockImplementation(() => "budibase:builder:local") + jwt.verify.mockImplementationOnce(() => ({ + apiKey: "1234", + roleId: "BUILDER" + })) await config.executeMiddleware() expect(config.ctx.auth.authenticated).toEqual(AuthTypes.BUILDER) + expect(config.ctx.user).toMatchSnapshot() }) - it("assigns an APP auth type when the user is not in the builder", async () => { + it("sets the correct APP auth type information when the user is not in the builder", async () => { config.setHeaders({ "x-budibase-type": "client" }) - config.ctx.cookies.get.mockImplementationOnce(() => `budibase:builder:local`) + config.ctx.cookies.get.mockImplementation(() => `budibase:app:local`) + jwt.verify.mockImplementationOnce(() => ({ + apiKey: "1234", + roleId: "ADMIN" + })) await config.executeMiddleware() expect(config.ctx.auth.authenticated).toEqual(AuthTypes.APP) + expect(config.ctx.user).toMatchSnapshot() }) it("marks the user as unauthenticated when a token cannot be determined from the users cookie", async () => { config.executeMiddleware() - expect() - }) - - it("verifies the users JWT token and sets the user information in context when successful", async () => { - config.executeMiddleware() - expect() + expect(config.ctx.auth.authenticated).toBe(false) + expect(config.ctx.user.role).toEqual({ + _id: "PUBLIC", + name: "Public", + permissionId: "public" + }) }) it("clears the cookie when there is an error authenticating in the builder", async () => { - config.executeMiddleware() - expect() + config.ctx.cookies.get.mockImplementation(() => "budibase:builder:local") + jwt.verify.mockImplementationOnce(() => { + throw new Error() + }) + + await config.executeMiddleware() + + expect(config.ctx.cookies.set).toBeCalledWith("budibase:builder:local") }) }) \ No newline at end of file From dd16c84ecd6feb13b5c3489abf5eea1a218d9456 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 10 Mar 2021 11:47:39 +0000 Subject: [PATCH 17/24] Upping automation coverage by adding webhook testing and increasing screen coverage. --- packages/builder/src/analytics.js | 2 +- .../server/src/api/controllers/analytics.js | 4 +- .../server/src/api/controllers/automation.js | 5 ++ packages/server/src/api/controllers/screen.js | 4 +- .../src/api/routes/tests/automation.spec.js | 42 ++++++++++ .../server/src/api/routes/tests/misc.spec.js | 38 +++++++++ .../src/api/routes/tests/screen.spec.js | 77 +++++++++++++++++++ .../server/src/utilities/security/roles.js | 3 +- 8 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 packages/server/src/api/routes/tests/misc.spec.js create mode 100644 packages/server/src/api/routes/tests/screen.spec.js diff --git a/packages/builder/src/analytics.js b/packages/builder/src/analytics.js index df91c16e00..e6a647a08b 100644 --- a/packages/builder/src/analytics.js +++ b/packages/builder/src/analytics.js @@ -16,7 +16,7 @@ async function activate() { // this was an issue as NODE_ENV = 'cypress' on the server, // but 'production' on the client const response = await api.get("/api/analytics") - analyticsEnabled = (await response.json()) === true + analyticsEnabled = (await response.json()).enabled === true } if (!analyticsEnabled) return if (sentryConfigured) Sentry.init({ dsn: process.env.SENTRY_DSN }) diff --git a/packages/server/src/api/controllers/analytics.js b/packages/server/src/api/controllers/analytics.js index 566b4970e9..76e5eb11f1 100644 --- a/packages/server/src/api/controllers/analytics.js +++ b/packages/server/src/api/controllers/analytics.js @@ -1,5 +1,7 @@ const env = require("../../environment") exports.isEnabled = async function(ctx) { - ctx.body = JSON.stringify(env.ENABLE_ANALYTICS === "true") + ctx.body = { + enabled: env.ENABLE_ANALYTICS === "true", + } } diff --git a/packages/server/src/api/controllers/automation.js b/packages/server/src/api/controllers/automation.js index 5fa618654b..df17371f92 100644 --- a/packages/server/src/api/controllers/automation.js +++ b/packages/server/src/api/controllers/automation.js @@ -98,6 +98,11 @@ exports.create = async function(ctx) { let automation = ctx.request.body automation.appId = ctx.user.appId + // call through to update if already exists + if (automation._id && automation._rev) { + return exports.update(ctx) + } + automation._id = generateAutomationID() automation.type = "automation" diff --git a/packages/server/src/api/controllers/screen.js b/packages/server/src/api/controllers/screen.js index f56dae153e..8f9baa8172 100644 --- a/packages/server/src/api/controllers/screen.js +++ b/packages/server/src/api/controllers/screen.js @@ -41,6 +41,8 @@ exports.save = async ctx => { exports.destroy = async ctx => { const db = new CouchDB(ctx.user.appId) await db.remove(ctx.params.screenId, ctx.params.screenRev) - ctx.message = "Screen deleted successfully" + ctx.body = { + message: "Screen deleted successfully", + } ctx.status = 200 } diff --git a/packages/server/src/api/routes/tests/automation.spec.js b/packages/server/src/api/routes/tests/automation.spec.js index 0648bfefa5..588de641b7 100644 --- a/packages/server/src/api/routes/tests/automation.spec.js +++ b/packages/server/src/api/routes/tests/automation.spec.js @@ -109,6 +109,35 @@ describe("/automations", () => { automation = res.body.automation }) + it("should be able to create an automation with a webhook trigger", async () => { + const autoConfig = basicAutomation() + autoConfig.definition.trigger = TRIGGER_DEFINITIONS["WEBHOOK"] + autoConfig.definition.trigger.id = "webhook_trigger_id" + const res = await request + .post(`/api/automations`) + .set(config.defaultHeaders()) + .send(autoConfig) + .expect('Content-Type', /json/) + .expect(200) + const originalAuto = res.body.automation + expect(originalAuto._id).toBeDefined() + expect(originalAuto._rev).toBeDefined() + // try removing the webhook trigger + const newConfig = originalAuto + newConfig.definition.trigger = TRIGGER_DEFINITIONS["ROW_SAVED"] + newConfig.definition.trigger.id = "row_saved_id" + const newRes = await request + .post(`/api/automations`) + .set(config.defaultHeaders()) + .send(newConfig) + .expect('Content-Type', /json/) + .expect(200) + const newAuto = newRes.body.automation + expect(newAuto._id).toEqual(originalAuto._id) + expect(newAuto._rev).toBeDefined() + expect(newAuto._rev).not.toEqual(originalAuto._rev) + }) + it("should apply authorization to endpoint", async () => { await checkBuilderEndpoint({ config, @@ -119,6 +148,19 @@ describe("/automations", () => { }) }) + describe("find", () => { + it("should be able to find the automation", async () => { + const automation = await config.createAutomation() + const res = await request + .get(`/api/automations/${automation._id}`) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) + expect(res.body._id).toEqual(automation._id) + expect(res.body._rev).toEqual(automation._rev) + }) + }) + describe("trigger", () => { it("trigger the automation successfully", async () => { let table = await config.createTable() diff --git a/packages/server/src/api/routes/tests/misc.spec.js b/packages/server/src/api/routes/tests/misc.spec.js new file mode 100644 index 0000000000..3d3b6047e2 --- /dev/null +++ b/packages/server/src/api/routes/tests/misc.spec.js @@ -0,0 +1,38 @@ +const setup = require("./utilities") + +describe("/analytics", () => { + let request = setup.getRequest() + let config = setup.getConfig() + + afterAll(setup.afterAll) + + beforeEach(async () => { + 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("/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 config = setup.getConfig() + const res = await config.getRequest().get("/version").expect(200) + expect(res.text.split(".").length).toEqual(3) + }) +}) \ No newline at end of file diff --git a/packages/server/src/api/routes/tests/screen.spec.js b/packages/server/src/api/routes/tests/screen.spec.js new file mode 100644 index 0000000000..0c2799eca2 --- /dev/null +++ b/packages/server/src/api/routes/tests/screen.spec.js @@ -0,0 +1,77 @@ +const { checkBuilderEndpoint } = require("./utilities/TestFunctions") +const setup = require("./utilities") +const { basicScreen } = require("./utilities/structures") + +describe("/screens", () => { + let request = setup.getRequest() + let config = setup.getConfig() + let screen + + afterAll(setup.afterAll) + + beforeEach(async () => { + await config.init() + screen = await config.createScreen() + }) + + describe("fetch", () => { + it("should be able to create a layout", async () => { + const res = await request + .get(`/api/screens`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.length).toEqual(1) + expect(res.body[0]._id).toEqual(screen._id) + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "GET", + url: `/api/screens`, + }) + }) + }) + + describe("save", () => { + it("should be able to save a screen", async () => { + const screenCfg = basicScreen() + const res = await request + .post(`/api/screens`) + .send(screenCfg) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body._rev).toBeDefined() + expect(res.body.name).toEqual(screenCfg.name) + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "POST", + url: `/api/screens`, + }) + }) + }) + + describe("destroy", () => { + it("should be able to delete the screen", async () => { + const res = await request + .delete(`/api/screens/${screen._id}/${screen._rev}`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.message).toBeDefined() + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "DELETE", + url: `/api/screens/${screen._id}/${screen._rev}`, + }) + }) + }) +}) \ No newline at end of file diff --git a/packages/server/src/utilities/security/roles.js b/packages/server/src/utilities/security/roles.js index 79fd720078..b0cd843591 100644 --- a/packages/server/src/utilities/security/roles.js +++ b/packages/server/src/utilities/security/roles.js @@ -205,7 +205,8 @@ class AccessController { tryingRoleId == null || tryingRoleId === "" || tryingRoleId === userRoleId || - tryingRoleId === BUILTIN_IDS.BUILDER + tryingRoleId === BUILTIN_IDS.BUILDER || + userRoleId === BUILTIN_IDS.BUILDER ) { return true } From 28329d7f6b1f7b532665685af78e51827ac80c7e Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 10 Mar 2021 11:56:52 +0000 Subject: [PATCH 18/24] Fixing issue with datasource find endpoint, removing un-used code and updating data source test. --- .../server/src/api/controllers/datasource.js | 34 ++------ packages/server/src/api/routes/datasource.js | 2 +- .../src/api/routes/tests/datasource.spec.js | 81 +++++++++---------- 3 files changed, 43 insertions(+), 74 deletions(-) diff --git a/packages/server/src/api/controllers/datasource.js b/packages/server/src/api/controllers/datasource.js index 15cea72da7..d73a316adb 100644 --- a/packages/server/src/api/controllers/datasource.js +++ b/packages/server/src/api/controllers/datasource.js @@ -26,35 +26,12 @@ exports.save = async function(ctx) { ...ctx.request.body, } - try { - const response = await db.post(datasource) - datasource._rev = response.rev - - ctx.status = 200 - ctx.message = "Datasource saved successfully." - ctx.body = datasource - } catch (err) { - ctx.throw(err.status, err) - } -} - -exports.update = async function(ctx) { - const db = new CouchDB(ctx.user.appId) - const user = ctx.request.body - const dbUser = await db.get(ctx.request.body._id) - if (user.password) { - user.password = await bcrypt.hash(user.password) - } else { - delete user.password - } - const newData = { ...dbUser, ...user } - - const response = await db.put(newData) - user._rev = response.rev + const response = await db.post(datasource) + datasource._rev = response.rev ctx.status = 200 - ctx.message = `User ${ctx.request.body.email} updated successfully.` - ctx.body = response + ctx.message = "Datasource saved successfully." + ctx.body = datasource } exports.destroy = async function(ctx) { @@ -73,6 +50,5 @@ exports.destroy = async function(ctx) { exports.find = async function(ctx) { const database = new CouchDB(ctx.user.appId) - const datasource = await database.get(ctx.params.datasourceId) - ctx.body = datasource + ctx.body = await database.get(ctx.params.datasourceId) } diff --git a/packages/server/src/api/routes/datasource.js b/packages/server/src/api/routes/datasource.js index 5746ce1839..ee2210704f 100644 --- a/packages/server/src/api/routes/datasource.js +++ b/packages/server/src/api/routes/datasource.js @@ -12,7 +12,7 @@ const router = Router() router .get("/api/datasources", authorized(BUILDER), datasourceController.fetch) .get( - "/api/datasources/:id", + "/api/datasources/:datasourceId", authorized(PermissionTypes.TABLE, PermissionLevels.READ), datasourceController.find ) diff --git a/packages/server/src/api/routes/tests/datasource.spec.js b/packages/server/src/api/routes/tests/datasource.spec.js index 7602b027e1..ee1a1c47f5 100644 --- a/packages/server/src/api/routes/tests/datasource.spec.js +++ b/packages/server/src/api/routes/tests/datasource.spec.js @@ -1,15 +1,17 @@ -let { basicDatasource } = require("./utilities/structures") -let { checkBuilderEndpoint } = require("./utilities/TestFunctions") +let {basicDatasource} = require("./utilities/structures") +let {checkBuilderEndpoint} = require("./utilities/TestFunctions") let setup = require("./utilities") describe("/datasources", () => { let request = setup.getRequest() let config = setup.getConfig() + let datasource afterAll(setup.afterAll) beforeEach(async () => { await config.init() + datasource = await config.createDatasource() }) describe("create", () => { @@ -21,22 +23,12 @@ describe("/datasources", () => { .expect('Content-Type', /json/) .expect(200) - expect(res.res.statusMessage).toEqual("Datasource saved successfully."); - expect(res.body.name).toEqual("Test"); - }) - }); + expect(res.res.statusMessage).toEqual("Datasource saved successfully.") + expect(res.body.name).toEqual("Test") + }) + }) describe("fetch", () => { - let datasource - - beforeEach(async () => { - datasource = await config.createDatasource() - }); - - afterEach(() => { - delete datasource._rev - }); - it("returns all the datasources from the server", async () => { const res = await request .get(`/api/datasources`) @@ -44,36 +36,37 @@ describe("/datasources", () => { .expect('Content-Type', /json/) .expect(200) - const datasources = res.body - expect(datasources).toEqual([ - { - "_id": datasources[0]._id, - "_rev": datasources[0]._rev, - ...basicDatasource() - } - ]); + const datasources = res.body + expect(datasources).toEqual([ + { + "_id": datasources[0]._id, + "_rev": datasources[0]._rev, + ...basicDatasource() + } + ]) }) it("should apply authorization to endpoint", async () => { - await checkBuilderEndpoint({ - config, - method: "GET", - url: `/api/datasources`, - }) + await checkBuilderEndpoint({ + config, + method: "GET", + url: `/api/datasources`, }) - }); + }) + }) + + describe("find", () => { + it("should be able to find a datasource", async () => { + const res = await request + .get(`/api/datasources/${datasource._id}`) + .set(config.defaultHeaders()) + .expect(200) + expect(res.body._rev).toBeDefined() + expect(res.body._id).toEqual(datasource._id) + }) + }) describe("destroy", () => { - let datasource - - beforeEach(async () => { - datasource = await config.createDatasource() - }); - - afterEach(() => { - delete datasource._rev - }); - it("deletes queries for the datasource after deletion and returns a success message", async () => { await config.createQuery() @@ -87,8 +80,8 @@ describe("/datasources", () => { .set(config.defaultHeaders()) .expect('Content-Type', /json/) .expect(200) - - expect(res.body).toEqual([]) + + expect(res.body).toEqual([]) }) it("should apply authorization to endpoint", async () => { @@ -99,5 +92,5 @@ describe("/datasources", () => { }) }) - }); -}); + }) +}) From a48f1c72f24f51f8220a5cc9f50d9944498fe805 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 10 Mar 2021 12:20:07 +0000 Subject: [PATCH 19/24] Adding auth tests. --- packages/server/src/api/controllers/auth.js | 2 + .../server/src/api/routes/tests/auth.spec.js | 106 ++++++++++++++++++ .../tests/utilities/TestConfiguration.js | 20 +++- 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 packages/server/src/api/routes/tests/auth.spec.js diff --git a/packages/server/src/api/controllers/auth.js b/packages/server/src/api/controllers/auth.js index 1cc6db3185..e5c0f9a029 100644 --- a/packages/server/src/api/controllers/auth.js +++ b/packages/server/src/api/controllers/auth.js @@ -46,6 +46,7 @@ exports.authenticate = async ctx => { version: app.version, } // if in cloud add the user api key, unless self hosted + /* istanbul ignore next */ if (env.CLOUD && !env.SELF_HOSTED) { const { apiKey } = await getAPIKey(ctx.user.appId) payload.apiKey = apiKey @@ -70,6 +71,7 @@ exports.authenticate = async ctx => { exports.fetchSelf = async ctx => { const { userId, appId } = ctx.user + /* istanbul ignore next */ if (!userId || !appId) { ctx.body = {} return diff --git a/packages/server/src/api/routes/tests/auth.spec.js b/packages/server/src/api/routes/tests/auth.spec.js new file mode 100644 index 0000000000..0eb0b6d851 --- /dev/null +++ b/packages/server/src/api/routes/tests/auth.spec.js @@ -0,0 +1,106 @@ +const { checkBuilderEndpoint } = require("./utilities/TestFunctions") +const setup = require("./utilities") + +describe("/authenticate", () => { + let request = setup.getRequest() + let config = setup.getConfig() + + afterAll(setup.afterAll) + + beforeEach(async () => { + await config.init() + }) + + describe("authenticate", () => { + it("should be able to create a layout", async () => { + await config.createUser("test@test.com", "p4ssw0rd") + const res = await request + .post(`/api/authenticate`) + .send({ + email: "test@test.com", + password: "p4ssw0rd", + }) + .set(config.publicHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.token).toBeDefined() + expect(res.body.email).toEqual("test@test.com") + expect(res.body.password).toBeUndefined() + }) + + it("should error if no app specified", async () => { + await request + .post(`/api/authenticate`) + .expect(400) + }) + + it("should error if no email specified", async () => { + await request + .post(`/api/authenticate`) + .send({ + password: "test", + }) + .set(config.publicHeaders()) + .expect(400) + }) + + it("should error if no password specified", async () => { + await request + .post(`/api/authenticate`) + .send({ + email: "test", + }) + .set(config.publicHeaders()) + .expect(400) + }) + + it("should error if invalid user specified", async () => { + await request + .post(`/api/authenticate`) + .send({ + email: "test", + password: "test", + }) + .set(config.publicHeaders()) + .expect(401) + }) + + it("should throw same error if wrong password specified", async () => { + await config.createUser("test@test.com", "password") + await request + .post(`/api/authenticate`) + .send({ + email: "test@test.com", + password: "test", + }) + .set(config.publicHeaders()) + .expect(401) + }) + + it("should throw an error for inactive users", async () => { + await config.createUser("test@test.com", "password") + await config.makeUserInactive("test@test.com") + await request + .post(`/api/authenticate`) + .send({ + email: "test@test.com", + password: "password", + }) + .set(config.publicHeaders()) + .expect(401) + }) + }) + + describe("fetch self", () => { + it("should be able to delete the layout", async () => { + await config.createUser("test@test.com", "p4ssw0rd") + const headers = await config.login("test@test.com", "p4ssw0rd") + const res = await request + .get(`/api/self`) + .set(headers) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.email).toEqual("test@test.com") + }) + }) +}) \ No newline at end of file diff --git a/packages/server/src/api/routes/tests/utilities/TestConfiguration.js b/packages/server/src/api/routes/tests/utilities/TestConfiguration.js index b72f4f4e5f..31989894ee 100644 --- a/packages/server/src/api/routes/tests/utilities/TestConfiguration.js +++ b/packages/server/src/api/routes/tests/utilities/TestConfiguration.js @@ -241,7 +241,7 @@ class TestConfiguration { async createUser( email = EMAIL, password = PASSWORD, - roleId = BUILTIN_ROLE_IDS.POWER + roleId = BUILTIN_ROLE_IDS.POWER, ) { return this._req( { @@ -254,6 +254,24 @@ class TestConfiguration { ) } + async makeUserInactive(email) { + const user = await this._req( + null, + { + email, + }, + controllers.user.find + ) + return this._req( + { + ...user, + status: "inactive", + }, + null, + controllers.user.update + ) + } + async login(email, password) { if (!email || !password) { await this.createUser() From a6bde49ad3a7cda31c0bbec8e125b76ebca99365 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 10 Mar 2021 12:56:30 +0000 Subject: [PATCH 20/24] Updating application tests to run a lot deeper, which required updating some other tests to account for creation of empty screens and layouts. --- .../server/src/api/controllers/application.js | 6 ++- .../src/api/routes/tests/application.spec.js | 39 +++++++++++++++++++ .../src/api/routes/tests/automation.spec.js | 2 +- .../src/api/routes/tests/routing.spec.js | 29 +++++++------- .../src/api/routes/tests/screen.spec.js | 4 +- .../tests/utilities/TestConfiguration.js | 2 +- 6 files changed, 61 insertions(+), 21 deletions(-) diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index e899a2dca9..cba3e6455a 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -104,9 +104,10 @@ async function createInstance(template) { await createRoutingView(appId) // replicate the template data to the instance DB + // this is currently very hard to test, downloading and importing template files + /* istanbul ignore next */ if (template) { let dbDumpReadStream - if (template.fileImportPath) { dbDumpReadStream = fs.createReadStream(template.fileImportPath) } else { @@ -181,8 +182,9 @@ exports.create = async function(ctx) { const instanceDb = new CouchDB(appId) await instanceDb.put(newApplication) + const newAppFolder = await createEmptyAppPackage(ctx, newApplication) + /* istanbul ignore next */ if (env.NODE_ENV !== "jest") { - const newAppFolder = await createEmptyAppPackage(ctx, newApplication) await downloadExtractComponentLibraries(newAppFolder) } diff --git a/packages/server/src/api/routes/tests/application.spec.js b/packages/server/src/api/routes/tests/application.spec.js index c146cc63fb..12fcf6f8ea 100644 --- a/packages/server/src/api/routes/tests/application.spec.js +++ b/packages/server/src/api/routes/tests/application.spec.js @@ -58,4 +58,43 @@ describe("/applications", () => { }) }) + describe("fetchAppDefinition", () => { + it("should be able to get an apps definition", async () => { + const res = await request + .get(`/api/applications/${config.getAppId()}/definition`) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) + // should have empty packages + expect(res.body.screens.length).toEqual(2) + expect(res.body.layouts.length).toEqual(2) + }) + }) + + describe("fetchAppPackage", () => { + it("should be able to fetch the app package", async () => { + const res = await request + .get(`/api/applications/${config.getAppId()}/appPackage`) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) + expect(res.body.application).toBeDefined() + expect(res.body.screens.length).toEqual(2) + expect(res.body.layouts.length).toEqual(2) + }) + }) + + describe("update", () => { + it("should be able to fetch the app package", async () => { + const res = await request + .put(`/api/applications/${config.getAppId()}`) + .send({ + name: "TEST_APP" + }) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) + expect(res.body.rev).toBeDefined() + }) + }) }) diff --git a/packages/server/src/api/routes/tests/automation.spec.js b/packages/server/src/api/routes/tests/automation.spec.js index 588de641b7..9d11219506 100644 --- a/packages/server/src/api/routes/tests/automation.spec.js +++ b/packages/server/src/api/routes/tests/automation.spec.js @@ -73,7 +73,7 @@ describe("/automations", () => { .expect('Content-Type', /json/) .expect(200) - expect(Object.keys(res.body.action).length).toEqual(Object.keys(ACTION_DEFINITIONS).length) + expect(Object.keys(res.body.action).length).toBeGreaterThanOrEqual(Object.keys(ACTION_DEFINITIONS).length) expect(Object.keys(res.body.trigger).length).toEqual(Object.keys(TRIGGER_DEFINITIONS).length) expect(Object.keys(res.body.logic).length).toEqual(Object.keys(LOGIC_DEFINITIONS).length) }) diff --git a/packages/server/src/api/routes/tests/routing.spec.js b/packages/server/src/api/routes/tests/routing.spec.js index 3b7523f586..70d1632bf3 100644 --- a/packages/server/src/api/routes/tests/routing.spec.js +++ b/packages/server/src/api/routes/tests/routing.spec.js @@ -3,6 +3,8 @@ const { basicScreen } = require("./utilities/structures") const { checkBuilderEndpoint } = require("./utilities/TestFunctions") const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles") +const route = "/test" + describe("/routing", () => { let request = setup.getRequest() let config = setup.getConfig() @@ -12,9 +14,12 @@ describe("/routing", () => { beforeEach(async () => { await config.init() - screen = await config.createScreen(basicScreen()) + screen = basicScreen() + screen.routing.route = route + screen = await config.createScreen(screen) screen2 = basicScreen() screen2.routing.roleId = BUILTIN_ROLE_IDS.POWER + screen2.routing.route = route screen2 = await config.createScreen(screen2) }) @@ -26,9 +31,9 @@ describe("/routing", () => { .expect("Content-Type", /json/) .expect(200) expect(res.body.routes).toBeDefined() - expect(res.body.routes["/"]).toEqual({ + expect(res.body.routes[route]).toEqual({ subpaths: { - ["/"]: { + [route]: { screenId: screen._id, roleId: screen.routing.roleId } @@ -43,9 +48,9 @@ describe("/routing", () => { .expect("Content-Type", /json/) .expect(200) expect(res.body.routes).toBeDefined() - expect(res.body.routes["/"]).toEqual({ + expect(res.body.routes[route]).toEqual({ subpaths: { - ["/"]: { + [route]: { screenId: screen2._id, roleId: screen2.routing.roleId } @@ -62,16 +67,10 @@ describe("/routing", () => { .expect("Content-Type", /json/) .expect(200) expect(res.body.routes).toBeDefined() - expect(res.body.routes["/"]).toEqual({ - subpaths: { - ["/"]: { - screens: { - [screen2.routing.roleId]: screen2._id, - [screen.routing.roleId]: screen._id, - } - } - } - }) + expect(res.body.routes[route].subpaths[route]).toBeDefined() + const subpath = res.body.routes[route].subpaths[route] + expect(subpath.screens[screen2.routing.roleId]).toEqual(screen2._id) + expect(subpath.screens[screen.routing.roleId]).toEqual(screen._id) }) it("make sure it is a builder only endpoint", async () => { diff --git a/packages/server/src/api/routes/tests/screen.spec.js b/packages/server/src/api/routes/tests/screen.spec.js index 0c2799eca2..ae30afd29c 100644 --- a/packages/server/src/api/routes/tests/screen.spec.js +++ b/packages/server/src/api/routes/tests/screen.spec.js @@ -21,8 +21,8 @@ describe("/screens", () => { .set(config.defaultHeaders()) .expect("Content-Type", /json/) .expect(200) - expect(res.body.length).toEqual(1) - expect(res.body[0]._id).toEqual(screen._id) + expect(res.body.length).toEqual(3) + expect(res.body.some(s => s._id === screen._id)).toEqual(true) }) it("should apply authorization to endpoint", async () => { diff --git a/packages/server/src/api/routes/tests/utilities/TestConfiguration.js b/packages/server/src/api/routes/tests/utilities/TestConfiguration.js index 31989894ee..5e50bd033c 100644 --- a/packages/server/src/api/routes/tests/utilities/TestConfiguration.js +++ b/packages/server/src/api/routes/tests/utilities/TestConfiguration.js @@ -241,7 +241,7 @@ class TestConfiguration { async createUser( email = EMAIL, password = PASSWORD, - roleId = BUILTIN_ROLE_IDS.POWER, + roleId = BUILTIN_ROLE_IDS.POWER ) { return this._req( { From 163d24a7671f777e7303ed308a18867709932f28 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 10 Mar 2021 17:55:42 +0000 Subject: [PATCH 21/24] Updating row tests, reducing console logging during tests for speed and clarity, testing some misc endpoints and updating search functionality to use a starts with operator when working with strings on rows. --- packages/server/package.json | 2 +- .../server/src/api/controllers/apikeys.js | 13 +- packages/server/src/api/controllers/row.js | 25 ++- packages/server/src/api/index.js | 6 +- packages/server/src/api/routes/static.js | 1 + .../src/api/routes/tests/apikeys.spec.js | 2 +- .../server/src/api/routes/tests/cloud.spec.js | 16 ++ .../server/src/api/routes/tests/row.spec.js | 193 +++++++++++++++--- packages/server/src/app.js | 6 +- .../middleware/tests/authenticated.spec.js | 1 - 10 files changed, 204 insertions(+), 61 deletions(-) create mode 100644 packages/server/src/api/routes/tests/cloud.spec.js diff --git a/packages/server/package.json b/packages/server/package.json index 3fe0d68bf3..99763109f3 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 routes --runInBand --coverage", + "test:integration": "jest --runInBand --coverage", "test:watch": "jest --watch", "run:docker": "node src/index", "dev:builder": "cross-env PORT=4001 nodemon src/index.js", diff --git a/packages/server/src/api/controllers/apikeys.js b/packages/server/src/api/controllers/apikeys.js index 1cd69c54df..96754f17cc 100644 --- a/packages/server/src/api/controllers/apikeys.js +++ b/packages/server/src/api/controllers/apikeys.js @@ -3,20 +3,13 @@ const { join } = require("../../utilities/centralPath") const readline = require("readline") const { budibaseAppsDir } = require("../../utilities/budibaseDir") const env = require("../../environment") -const selfhost = require("../../selfhost") const ENV_FILE_PATH = "/.env" exports.fetch = async function(ctx) { ctx.status = 200 - if (env.SELF_HOSTED) { - ctx.body = { - selfhost: await selfhost.getSelfHostAPIKey(), - } - } else { - ctx.body = { - budibase: env.BUDIBASE_API_KEY, - userId: env.USERID_API_KEY, - } + ctx.body = { + budibase: env.BUDIBASE_API_KEY, + userId: env.USERID_API_KEY, } } diff --git a/packages/server/src/api/controllers/row.js b/packages/server/src/api/controllers/row.js index 5d783b7d18..bf985fe55d 100644 --- a/packages/server/src/api/controllers/row.js +++ b/packages/server/src/api/controllers/row.js @@ -224,6 +224,7 @@ exports.fetchView = async function(ctx) { try { table = await db.get(viewInfo.meta.tableId) } catch (err) { + /* istanbul ignore next */ table = { schema: {}, } @@ -255,16 +256,24 @@ exports.fetchView = async function(ctx) { exports.search = async function(ctx) { const appId = ctx.user.appId - const db = new CouchDB(appId) - const { query, pagination: { pageSize = 10, page }, } = ctx.request.body - query.tableId = ctx.params.tableId + // make all strings a starts with operation rather than pure equality + for (const [key, queryVal] of Object.entries(query)) { + if (typeof queryVal === "string") { + query[key] = { + $gt: queryVal, + $lt: `${queryVal}\uffff`, + } + } + } + // pure equality for table + query.tableId = ctx.params.tableId const response = await db.find({ selector: query, limit: pageSize, @@ -324,7 +333,6 @@ exports.destroy = async function(ctx) { const row = await db.get(ctx.params.rowId) if (row.tableId !== ctx.params.tableId) { ctx.throw(400, "Supplied tableId doesn't match the row's tableId") - return } await linkRows.updateLinks({ appId, @@ -376,15 +384,6 @@ exports.fetchEnrichedRow = async function(ctx) { const db = new CouchDB(appId) const tableId = ctx.params.tableId const rowId = ctx.params.rowId - if (appId == null || tableId == null || rowId == null) { - ctx.status = 400 - ctx.body = { - status: 400, - error: - "Cannot handle request, URI params have not been successfully prepared.", - } - return - } // need table to work out where links go in row let [table, row] = await Promise.all([ db.get(tableId), diff --git a/packages/server/src/api/index.js b/packages/server/src/api/index.js index 7b063cb522..7628fa2077 100644 --- a/packages/server/src/api/index.js +++ b/packages/server/src/api/index.js @@ -41,13 +41,15 @@ router.use(async (ctx, next) => { try { await next() } catch (err) { - ctx.log.error(err) ctx.status = err.status || err.statusCode || 500 ctx.body = { message: err.message, status: ctx.status, } - console.trace(err) + if (env.NODE_ENV !== "jest") { + ctx.log.error(err) + console.trace(err) + } } }) diff --git a/packages/server/src/api/routes/static.js b/packages/server/src/api/routes/static.js index 2b0c9b36fc..c812c4d3b1 100644 --- a/packages/server/src/api/routes/static.js +++ b/packages/server/src/api/routes/static.js @@ -8,6 +8,7 @@ const usage = require("../../middleware/usageQuota") const router = Router() +/* istanbul ignore next */ router.param("file", async (file, ctx, next) => { ctx.file = file && file.includes(".") ? file : "index.html" diff --git a/packages/server/src/api/routes/tests/apikeys.spec.js b/packages/server/src/api/routes/tests/apikeys.spec.js index a8077a4492..dbee57c8b0 100644 --- a/packages/server/src/api/routes/tests/apikeys.spec.js +++ b/packages/server/src/api/routes/tests/apikeys.spec.js @@ -35,7 +35,7 @@ describe("/api/keys", () => { describe("update", () => { it("should allow updating a value", async () => { - fs.writeFileSync(path.join(budibaseAppsDir(), ".env"), "") + fs.writeFileSync(path.join(budibaseAppsDir(), ".env"), "TEST_API_KEY=thing") const res = await request .put(`/api/keys/TEST`) .send({ diff --git a/packages/server/src/api/routes/tests/cloud.spec.js b/packages/server/src/api/routes/tests/cloud.spec.js new file mode 100644 index 0000000000..3cb65ed819 --- /dev/null +++ b/packages/server/src/api/routes/tests/cloud.spec.js @@ -0,0 +1,16 @@ +const setup = require("./utilities") + +describe("test things in the Cloud/Self hosted", () => { + describe("test self hosted static page", () => { + it("should be able to load the static page", async () => { + await setup.switchToCloudForFunction(async () => { + let request = setup.getRequest() + let config = setup.getConfig() + await config.init() + const res = await request.get(`/`).expect(200) + expect(res.text.includes("Budibase self hosting️")).toEqual(true) + setup.afterAll() + }) + }) + }) +}) diff --git a/packages/server/src/api/routes/tests/row.spec.js b/packages/server/src/api/routes/tests/row.spec.js index cc50520b77..1442e4eb75 100644 --- a/packages/server/src/api/routes/tests/row.spec.js +++ b/packages/server/src/api/routes/tests/row.spec.js @@ -17,15 +17,15 @@ describe("/rows", () => { row = basicRow(table._id) }) - const loadRow = async id => + const loadRow = async (id, status = 200) => await request .get(`/api/${table._id}/rows/${id}`) .set(config.defaultHeaders()) .expect('Content-Type', /json/) - .expect(200) + .expect(status) - describe("save, load, update, delete", () => { + describe("save, load, update", () => { it("returns a success message when the row is created", async () => { const res = await request .post(`/api/${row.tableId}/rows`) @@ -217,38 +217,152 @@ describe("/rows", () => { expect(savedRow.body.description).toEqual(existing.description) expect(savedRow.body.name).toEqual("Updated Name") - + }) + + it("should throw an error when given improper types", async () => { + const existing = await config.createRow() + await request + .patch(`/api/${table._id}/rows/${existing._id}`) + .send({ + _id: existing._id, + _rev: existing._rev, + tableId: table._id, + name: 1, + }) + .set(config.defaultHeaders()) + .expect(400) + }) + }) + + describe("destroy", () => { + it("should be able to delete a row", async () => { + const createdRow = await config.createRow(row) + const res = await request + .delete(`/api/${table._id}/rows/${createdRow._id}/${createdRow._rev}`) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) + expect(res.body.ok).toEqual(true) + }) + + it("shouldn't allow deleting a row in a table which is different to the one the row was created on", async () => { + const createdRow = await config.createRow(row) + await request + .delete(`/api/wrong_table/rows/${createdRow._id}/${createdRow._rev}`) + .set(config.defaultHeaders()) + .expect(400) }) }) describe("validate", () => { it("should return no errors on valid row", async () => { - const result = await request + const res = await request .post(`/api/${table._id}/rows/validate`) .send({ name: "ivan" }) .set(config.defaultHeaders()) .expect('Content-Type', /json/) .expect(200) - expect(result.body.valid).toBe(true) - expect(Object.keys(result.body.errors)).toEqual([]) + expect(res.body.valid).toBe(true) + expect(Object.keys(res.body.errors)).toEqual([]) }) it("should errors on invalid row", async () => { - const result = await request + const res = await request .post(`/api/${table._id}/rows/validate`) .send({ name: 1 }) .set(config.defaultHeaders()) .expect('Content-Type', /json/) .expect(200) - expect(result.body.valid).toBe(false) - expect(Object.keys(result.body.errors)).toEqual(["name"]) + expect(res.body.valid).toBe(false) + expect(Object.keys(res.body.errors)).toEqual(["name"]) }) }) - describe("enrich row unit test", () => { + describe("bulkDelete", () => { + it("should be able to delete a bulk set of rows", async () => { + const row1 = await config.createRow() + const row2 = await config.createRow() + const res = await request + .post(`/api/${table._id}/rows`) + .send({ + type: "delete", + rows: [ + row1, + row2, + ] + }) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) + expect(res.body.length).toEqual(2) + await loadRow(row1._id, 404) + }) + }) + + describe("search", () => { + it("should run a search on the table", async () => { + const row = await config.createRow() + // add another row that shouldn't be found + await config.createRow({ + ...basicRow(), + name: "Other Contact", + }) + const res = await request + .post(`/api/${table._id}/rows/search`) + .send({ + query: { + name: "Test", + }, + pagination: { pageSize: 25, page: 0 } + }) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) + expect(res.body.length).toEqual(1) + expect(res.body[0]._id).toEqual(row._id) + }) + }) + + describe("fetchView", () => { + it("should be able to fetch tables contents via 'view'", async () => { + const row = await config.createRow() + const res = await request + .get(`/api/views/all_${table._id}`) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) + expect(res.body.length).toEqual(1) + expect(res.body[0]._id).toEqual(row._id) + }) + + it("should throw an error if view doesn't exist", async () => { + await request + .get(`/api/views/derp`) + .set(config.defaultHeaders()) + .expect(400) + }) + + it("should be able to run on a view", async () => { + const view = await config.createView() + const row = await config.createRow() + const res = await request + .get(`/api/views/${view._id}`) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) + expect(res.body.length).toEqual(1) + expect(res.body[0]._id).toEqual(row._id) + }) + }) + + describe("user testing", () => { + + }) + + describe("fetchEnrichedRows", () => { it("should allow enriching some linked rows", async () => { const table = await config.createLinkedTable() const firstRow = await config.createRow({ @@ -262,30 +376,45 @@ describe("/rows", () => { link: [{_id: firstRow._id}], tableId: table._id, }) - const enriched = await outputProcessing(config.getAppId(), table, [secondRow]) - expect(enriched[0].link.length).toBe(1) - expect(enriched[0].link[0]._id).toBe(firstRow._id) - expect(enriched[0].link[0].primaryDisplay).toBe("Test Contact") + + // test basic enrichment + const resBasic = await request + .get(`/api/${table._id}/rows/${secondRow._id}`) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) + expect(resBasic.body.link[0]._id).toBe(firstRow._id) + expect(resBasic.body.link[0].primaryDisplay).toBe("Test Contact") + + // test full enrichment + const resEnriched = await request + .get(`/api/${table._id}/${secondRow._id}/enrich`) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) + expect(resEnriched.body.link.length).toBe(1) + expect(resEnriched.body.link[0]._id).toBe(firstRow._id) + expect(resEnriched.body.link[0].name).toBe("Test Contact") + expect(resEnriched.body.link[0].description).toBe("original description") }) }) - it("should allow enriching attachment rows", async () => { - const table = await config.createAttachmentTable() - const row = await config.createRow({ - name: "test", - description: "test", - attachment: [{ - url: "/test/thing", - }], - tableId: table._id, + describe("attachments", () => { + it("should allow enriching attachment rows", async () => { + const table = await config.createAttachmentTable() + const row = await config.createRow({ + name: "test", + description: "test", + attachment: [{ + url: "/test/thing", + }], + tableId: table._id, + }) + // the environment needs configured for this + await setup.switchToCloudForFunction(async () => { + const enriched = await outputProcessing(config.getAppId(), table, [row]) + expect(enriched[0].attachment[0].url).toBe(`/app-assets/assets/${config.getAppId()}/test/thing`) + }) }) - // the environment needs configured for this - env.CLOUD = 1 - env.SELF_HOSTED = 1 - const enriched = await outputProcessing(config.getAppId(), table, [row]) - expect(enriched[0].attachment[0].url).toBe(`/app-assets/assets/${config.getAppId()}/test/thing`) - // remove env config - env.CLOUD = undefined - env.SELF_HOSTED = undefined }) }) \ No newline at end of file diff --git a/packages/server/src/app.js b/packages/server/src/app.js index 3779890c9d..15e996cfe6 100644 --- a/packages/server/src/app.js +++ b/packages/server/src/app.js @@ -56,7 +56,11 @@ if (electron.app && electron.app.isPackaged) { const server = http.createServer(app.callback()) destroyable(server) -server.on("close", () => console.log("Server Closed")) +server.on("close", () => { + if (env.NODE_ENV !== "jest") { + console.log("Server Closed") + } +}) module.exports = server.listen(env.PORT || 0, async () => { console.log(`Budibase running on ${JSON.stringify(server.address())}`) diff --git a/packages/server/src/middleware/tests/authenticated.spec.js b/packages/server/src/middleware/tests/authenticated.spec.js index bb124d2f4a..fe7e592528 100644 --- a/packages/server/src/middleware/tests/authenticated.spec.js +++ b/packages/server/src/middleware/tests/authenticated.spec.js @@ -9,7 +9,6 @@ class TestConfiguration { this.ctx = { config: {}, auth: {}, - request: {}, cookies: { set: jest.fn(), get: jest.fn() From 68735f1b4f21a1971c6c1c7068cdba9d00deb7d1 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 10 Mar 2021 17:56:16 +0000 Subject: [PATCH 22/24] Formatting. --- .../SetupPanel/AutomationBindingPanel.svelte | 356 +++++++++--------- .../SetupPanel/AutomationBlockSetup.svelte | 6 +- .../automation/SetupPanel/RowSelector.svelte | 4 +- .../design/PropertiesPanel/DesignView.svelte | 28 +- .../EventsEditor/EventEditor.svelte | 30 +- .../PropertiesPanel/SettingsView.svelte | 2 +- .../settings/ThemeEditorDropdown.svelte | 2 +- .../client/src/components/Component.svelte | 2 +- .../standard-components/src/Container.svelte | 52 ++- .../src/grid/Component.svelte | 4 +- 10 files changed, 263 insertions(+), 223 deletions(-) diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBindingPanel.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBindingPanel.svelte index a05d5b9996..bda233f9f2 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBindingPanel.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBindingPanel.svelte @@ -1,190 +1,186 @@
-
- Available bindings - - - - {#each categories as [categoryName, bindings]} - {categoryName} - - {#each bindableProperties.filter(binding => - binding.label.match(searchRgx) - ) as binding} -
addToText(binding)}> - {binding.label} - {binding.type} -
-
- {binding.description || ''} -
-
- {/each} - {/each} - Helpers - - {#each helpers.filter(helper => helper.label.match(searchRgx) || helper.description.match(searchRgx)) as helper} -
addToText(helper)}> - {helper.label} -
-
- {@html helper.description || ''} -
-
{helper.example || ''}
-
- {/each} -
-
-