diff --git a/.gitignore b/.gitignore index e1d3e6db0e..654b483288 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,5 @@ stats.html *.tsbuildinfo budibase-component budibase-datasource + +*.iml \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 4838a4fd89..71f0092a59 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,7 +8,7 @@ "editor.defaultFormatter": "vscode.json-language-features" }, "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "vscode.typescript-language-features" }, "debug.javascript.terminalOptions": { "skipFiles": [ @@ -16,4 +16,7 @@ "/**" ] }, + "[typescript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, } diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index f78e2b6b56..9b9dd1ed67 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -78,4 +78,4 @@ "typescript": "4.7.3" }, "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" -} +} \ No newline at end of file diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 51ab101b3c..2377c8ceba 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -1,9 +1,13 @@ function isTest() { - return ( - process.env.NODE_ENV === "jest" || - process.env.NODE_ENV === "cypress" || - process.env.JEST_WORKER_ID != null - ) + return isCypress() || isJest() +} + +function isJest() { + return !!(process.env.NODE_ENV === "jest" || process.env.JEST_WORKER_ID) +} + +function isCypress() { + return process.env.NODE_ENV === "cypress" } function isDev() { @@ -27,6 +31,7 @@ const DefaultBucketName = { const environment = { isTest, + isJest, isDev, JS_BCRYPT: process.env.JS_BCRYPT, JWT_SECRET: process.env.JWT_SECRET, diff --git a/packages/backend-core/tests/utilities/mocks/events.ts b/packages/backend-core/tests/utilities/mocks/events.ts index 415d59019d..40c3706a55 100644 --- a/packages/backend-core/tests/utilities/mocks/events.ts +++ b/packages/backend-core/tests/utilities/mocks/events.ts @@ -117,3 +117,7 @@ jest.spyOn(events.view, "filterDeleted") jest.spyOn(events.view, "calculationCreated") jest.spyOn(events.view, "calculationUpdated") jest.spyOn(events.view, "calculationDeleted") + +jest.spyOn(events.plugin, "init") +jest.spyOn(events.plugin, "imported") +jest.spyOn(events.plugin, "deleted") diff --git a/packages/backend-core/tests/utilities/mocks/index.ts b/packages/backend-core/tests/utilities/mocks/index.ts index e71c739e26..931816be45 100644 --- a/packages/backend-core/tests/utilities/mocks/index.ts +++ b/packages/backend-core/tests/utilities/mocks/index.ts @@ -2,4 +2,5 @@ import "./posthog" import "./events" export * as accounts from "./accounts" export * as date from "./date" +export * as licenses from "./licenses" export { default as fetch } from "./fetch" diff --git a/packages/backend-core/tests/utilities/mocks/licenses.ts b/packages/backend-core/tests/utilities/mocks/licenses.ts new file mode 100644 index 0000000000..0ef5eedb73 --- /dev/null +++ b/packages/backend-core/tests/utilities/mocks/licenses.ts @@ -0,0 +1,83 @@ +import { Feature, License, Quotas } from "@budibase/types" +import _ from "lodash" + +let CLOUD_FREE_LICENSE: License +let TEST_LICENSE: License +let getCachedLicense: any + +// init for the packages other than pro +export function init(proPkg: any) { + initInternal({ + CLOUD_FREE_LICENSE: proPkg.constants.licenses.CLOUD_FREE_LICENSE, + TEST_LICENSE: proPkg.constants.licenses.DEVELOPER_FREE_LICENSE, + getCachedLicense: proPkg.licensing.cache.getCachedLicense, + }) +} + +// init for the pro package +export function initInternal(opts: { + CLOUD_FREE_LICENSE: License + TEST_LICENSE: License + getCachedLicense: any +}) { + CLOUD_FREE_LICENSE = opts.CLOUD_FREE_LICENSE + TEST_LICENSE = opts.TEST_LICENSE + getCachedLicense = opts.getCachedLicense +} + +export interface UseLicenseOpts { + features?: Feature[] + quotas?: Quotas +} + +// LICENSES + +export const useLicense = (license: License, opts?: UseLicenseOpts) => { + if (opts) { + if (opts.features) { + license.features.push(...opts.features) + } + if (opts.quotas) { + license.quotas = opts.quotas + } + } + + getCachedLicense.mockReturnValue(license) + + return license +} + +export const useUnlimited = (opts?: UseLicenseOpts) => { + return useLicense(TEST_LICENSE, opts) +} + +export const useCloudFree = () => { + return useLicense(CLOUD_FREE_LICENSE) +} + +// FEATURES + +const useFeature = (feature: Feature) => { + const license = _.cloneDeep(TEST_LICENSE) + const opts: UseLicenseOpts = { + features: [feature], + } + + return useLicense(license, opts) +} + +export const useBackups = () => { + return useFeature(Feature.APP_BACKUPS) +} + +export const useGroups = () => { + return useFeature(Feature.USER_GROUPS) +} + +// QUOTAS + +export const setAutomationLogsQuota = (value: number) => { + const license = _.cloneDeep(TEST_LICENSE) + license.quotas.constant.automationLogRetentionDays.value = value + return useLicense(license) +} diff --git a/packages/server/__mocks__/node-fetch.ts b/packages/server/__mocks__/node-fetch.ts index c3d6b623dd..44d1e54a32 100644 --- a/packages/server/__mocks__/node-fetch.ts +++ b/packages/server/__mocks__/node-fetch.ts @@ -1,3 +1,4 @@ +import fs from "fs" module FetchMock { const fetch = jest.requireActual("node-fetch") let failCount = 0 @@ -92,6 +93,83 @@ module FetchMock { value: '', }) + } else if ( + url === "https://api.github.com/repos/my-repo/budibase-comment-box" + ) { + return Promise.resolve({ + json: () => { + return { + name: "budibase-comment-box", + releases_url: + "https://api.github.com/repos/my-repo/budibase-comment-box{/id}", + } + }, + }) + } else if ( + url === "https://api.github.com/repos/my-repo/budibase-comment-box/latest" + ) { + return Promise.resolve({ + json: () => { + return { + assets: [ + { + content_type: "application/gzip", + browser_download_url: + "https://github.com/my-repo/budibase-comment-box/releases/download/v1.0.2/comment-box-1.0.2.tar.gz", + }, + ], + } + }, + }) + } else if ( + url === + "https://github.com/my-repo/budibase-comment-box/releases/download/v1.0.2/comment-box-1.0.2.tar.gz" + ) { + return Promise.resolve({ + body: fs.createReadStream( + "src/api/routes/tests/data/comment-box-1.0.2.tar.gz" + ), + ok: true, + }) + } else if (url === "https://www.npmjs.com/package/budibase-component") { + return Promise.resolve({ + status: 200, + json: () => { + return { + name: "budibase-component", + "dist-tags": { + latest: "1.0.0", + }, + versions: { + "1.0.0": { + dist: { + tarball: + "https://registry.npmjs.org/budibase-component/-/budibase-component-1.0.2.tgz", + }, + }, + }, + } + }, + }) + } else if ( + url === + "https://registry.npmjs.org/budibase-component/-/budibase-component-1.0.2.tgz" + ) { + return Promise.resolve({ + body: fs.createReadStream( + "src/api/routes/tests/data/budibase-component-1.0.2.tgz" + ), + ok: true, + }) + } else if ( + url === "https://www.someurl.com/comment-box/comment-box-1.0.2.tar.gz" + ) { + return Promise.resolve({ + body: fs.createReadStream( + "src/api/routes/tests/data/comment-box-1.0.2.tar.gz" + ), + ok: true, + }) } else if (url.includes("failonce.com")) { failCount++ if (failCount === 1) { diff --git a/packages/server/src/api/routes/tests/__snapshots__/datasource.spec.js.snap b/packages/server/src/api/routes/tests/__snapshots__/datasource.spec.ts.snap similarity index 100% rename from packages/server/src/api/routes/tests/__snapshots__/datasource.spec.js.snap rename to packages/server/src/api/routes/tests/__snapshots__/datasource.spec.ts.snap diff --git a/packages/server/src/api/routes/tests/application.spec.js b/packages/server/src/api/routes/tests/application.spec.ts similarity index 83% rename from packages/server/src/api/routes/tests/application.spec.js rename to packages/server/src/api/routes/tests/application.spec.ts index 53b2f0fada..d6a81cd421 100644 --- a/packages/server/src/api/routes/tests/application.spec.js +++ b/packages/server/src/api/routes/tests/application.spec.ts @@ -12,13 +12,11 @@ jest.mock("../../../utilities/redis", () => ({ shutdown: jest.fn(), })) -const { - clearAllApps, - checkBuilderEndpoint, -} = require("./utilities/TestFunctions") -const setup = require("./utilities") -const { AppStatus } = require("../../../db/utils") -const { events } = require("@budibase/backend-core") +import { clearAllApps, checkBuilderEndpoint } from "./utilities/TestFunctions" +import * as setup from "./utilities" +import { AppStatus } from "../../../db/utils" +import { events } from "@budibase/backend-core" +import env from "../../../environment" describe("/applications", () => { let request = setup.getRequest() @@ -234,4 +232,39 @@ describe("/applications", () => { expect(getRes.body.application.updatedAt).toBeDefined() }) }) + + describe("sync", () => { + it("app should sync correctly", async () => { + const res = await request + .post(`/api/applications/${config.getAppId()}/sync`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.message).toEqual("App sync completed successfully.") + }) + + it("app should not sync if production", async () => { + const res = await request + .post(`/api/applications/app_123456/sync`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(400) + expect(res.body.message).toEqual( + "This action cannot be performed for production apps" + ) + }) + + it("app should not sync if sync is disabled", async () => { + env._set("DISABLE_AUTO_PROD_APP_SYNC", true) + const res = await request + .post(`/api/applications/${config.getAppId()}/sync`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.message).toEqual( + "App sync disabled. You can reenable with the DISABLE_AUTO_PROD_APP_SYNC environment variable." + ) + env._set("DISABLE_AUTO_PROD_APP_SYNC", false) + }) + }) }) diff --git a/packages/server/src/api/routes/tests/backup.spec.js b/packages/server/src/api/routes/tests/backup.spec.ts similarity index 61% rename from packages/server/src/api/routes/tests/backup.spec.js rename to packages/server/src/api/routes/tests/backup.spec.ts index 7131aca852..72ccb41598 100644 --- a/packages/server/src/api/routes/tests/backup.spec.js +++ b/packages/server/src/api/routes/tests/backup.spec.ts @@ -8,10 +8,10 @@ jest.mock("@budibase/backend-core", () => { } }) -const { checkBuilderEndpoint } = require("./utilities/TestFunctions") -const setup = require("./utilities") -const { events } = require("@budibase/backend-core") - +import * as setup from "./utilities" +import { events } from "@budibase/backend-core" +import sdk from "../../../sdk" +import { checkBuilderEndpoint } from "./utilities/TestFunctions" describe("/backups", () => { let request = setup.getRequest() let config = setup.getConfig() @@ -30,7 +30,7 @@ describe("/backups", () => { .expect(200) expect(res.text).toBeDefined() expect(res.headers["content-type"]).toEqual("application/gzip") - expect(events.app.exported.mock.calls.length).toBe(1) + expect(events.app.exported).toBeCalledTimes(1) }) it("should apply authorization to endpoint", async () => { @@ -41,4 +41,15 @@ describe("/backups", () => { }) }) }) -}) \ No newline at end of file + + describe("calculateBackupStats", () => { + it("should be able to calculate the backup statistics", async () => { + config.createAutomation() + config.createScreen() + let res = await sdk.backups.calculateBackupStats(config.getAppId()) + expect(res.automations).toEqual(1) + expect(res.datasources).toEqual(1) + expect(res.screens).toEqual(1) + }) + }) +}) diff --git a/packages/server/src/api/routes/tests/cloud.spec.ts b/packages/server/src/api/routes/tests/cloud.spec.ts new file mode 100644 index 0000000000..a1ae2788da --- /dev/null +++ b/packages/server/src/api/routes/tests/cloud.spec.ts @@ -0,0 +1,66 @@ +import { db as dbCore } from "@budibase/backend-core" +import { AppStatus } from "../../../db/utils" + +import * as setup from "./utilities" + +describe("/cloud", () => { + let request = setup.getRequest() + let config = setup.getConfig() + + afterAll(setup.afterAll) + + beforeEach(async () => { + await config.init() + }) + + afterEach(async () => { + // clear all mocks + jest.clearAllMocks() + }) + + describe("import", () => { + it("should be able to import apps", async () => { + // first we need to delete any existing apps on the system so it looks clean otherwise the + // import will not run + await request + .delete( + `/api/applications/${dbCore.getProdAppID( + config.getAppId() + )}?unpublish=true` + ) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + await request + .delete(`/api/applications/${config.getAppId()}`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + + // get a count of apps before the import + const preImportApps = await request + .get(`/api/applications?status=${AppStatus.ALL}`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + + // Perform the import + const res = await request + .post(`/api/cloud/import`) + .attach("importFile", "src/api/routes/tests/data/export-test.tar.gz") + .set(config.defaultHeaders()) + .expect(200) + expect(res.body.message).toEqual("Apps successfully imported.") + + // get a count of apps after the import + const postImportApps = await request + .get(`/api/applications?status=${AppStatus.ALL}`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + + // There are two apps in the file that was imported so check for this + expect(postImportApps.body.length).toEqual(2) + }) + }) +}) diff --git a/packages/server/src/api/routes/tests/data/budibase-component-1.0.1.tgz b/packages/server/src/api/routes/tests/data/budibase-component-1.0.1.tgz new file mode 100644 index 0000000000..8de0015a26 Binary files /dev/null and b/packages/server/src/api/routes/tests/data/budibase-component-1.0.1.tgz differ diff --git a/packages/server/src/api/routes/tests/data/comment-box-1.0.2.tar.gz b/packages/server/src/api/routes/tests/data/comment-box-1.0.2.tar.gz new file mode 100644 index 0000000000..f3b00f4e5d Binary files /dev/null and b/packages/server/src/api/routes/tests/data/comment-box-1.0.2.tar.gz differ diff --git a/packages/server/src/api/routes/tests/data/export-test.tar.gz b/packages/server/src/api/routes/tests/data/export-test.tar.gz new file mode 100644 index 0000000000..add05daba0 Binary files /dev/null and b/packages/server/src/api/routes/tests/data/export-test.tar.gz differ diff --git a/packages/server/src/api/routes/tests/datasource.spec.js b/packages/server/src/api/routes/tests/datasource.spec.ts similarity index 85% rename from packages/server/src/api/routes/tests/datasource.spec.js rename to packages/server/src/api/routes/tests/datasource.spec.ts index 520a17015c..66888023c4 100644 --- a/packages/server/src/api/routes/tests/datasource.spec.js +++ b/packages/server/src/api/routes/tests/datasource.spec.ts @@ -1,16 +1,16 @@ jest.mock("pg") +import * as setup from "./utilities" +import { checkBuilderEndpoint } from "./utilities/TestFunctions" +import { checkCacheForDynamicVariable } from "../../../threads/utils" +import { events } from "@budibase/backend-core" -let setup = require("./utilities") let { basicDatasource } = setup.structures -let { checkBuilderEndpoint } = require("./utilities/TestFunctions") const pg = require("pg") -const { checkCacheForDynamicVariable } = require("../../../threads/utils") -const { events } = require("@budibase/backend-core") describe("/datasources", () => { let request = setup.getRequest() let config = setup.getConfig() - let datasource + let datasource: any afterAll(setup.afterAll) @@ -26,7 +26,7 @@ describe("/datasources", () => { .post(`/api/datasources`) .send(basicDatasource()) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body.datasource.name).toEqual("Test") @@ -42,7 +42,7 @@ describe("/datasources", () => { .put(`/api/datasources/${datasource._id}`) .send(datasource) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body.datasource.name).toEqual("Updated Test") @@ -51,25 +51,34 @@ describe("/datasources", () => { }) describe("dynamic variables", () => { - async function preview(datasource, fields) { + async function preview( + datasource: any, + fields: { path: string; queryString: string } + ) { return config.previewQuery(request, config, datasource, fields) } it("should invalidate changed or removed variables", async () => { const { datasource, query } = await config.dynamicVariableDatasource() // preview once to cache variables - await preview(datasource, { path: "www.test.com", queryString: "test={{ variable3 }}" }) + await preview(datasource, { + path: "www.test.com", + queryString: "test={{ variable3 }}", + }) // check variables in cache - let contents = await checkCacheForDynamicVariable(query._id, "variable3") + let contents = await checkCacheForDynamicVariable( + query._id, + "variable3" + ) expect(contents.rows.length).toEqual(1) - + // update the datasource to remove the variables datasource.config.dynamicVariables = [] const res = await request .put(`/api/datasources/${datasource._id}`) .send(datasource) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body.errors).toBeUndefined() @@ -85,7 +94,7 @@ describe("/datasources", () => { const res = await request .get(`/api/datasources`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) const datasources = res.body @@ -160,7 +169,7 @@ describe("/datasources", () => { const res = await request .get(`/api/datasources`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body.length).toEqual(1) @@ -174,6 +183,5 @@ describe("/datasources", () => { url: `/api/datasources/${datasource._id}/${datasource._rev}`, }) }) - }) }) diff --git a/packages/server/src/api/routes/tests/plugin.spec.ts b/packages/server/src/api/routes/tests/plugin.spec.ts new file mode 100644 index 0000000000..8e59a9392c --- /dev/null +++ b/packages/server/src/api/routes/tests/plugin.spec.ts @@ -0,0 +1,179 @@ +let mockObjectStore = jest.fn().mockImplementation(() => { + return [{ name: "test.js" }] +}) + +let deleteFolder = jest.fn().mockImplementation() +jest.mock("@budibase/backend-core", () => { + const core = jest.requireActual("@budibase/backend-core") + return { + ...core, + objectStore: { + ...core.objectStore, + upload: jest.fn(), + uploadDirectory: mockObjectStore, + deleteFolder: deleteFolder, + }, + } +}) + +import { events } from "@budibase/backend-core" +import * as setup from "./utilities" + +describe("/plugins", () => { + let request = setup.getRequest() + let config = setup.getConfig() + + afterAll(setup.afterAll) + + beforeEach(async () => { + await config.init() + jest.clearAllMocks() + }) + + const createPlugin = async (status?: number) => { + return request + .post(`/api/plugin/upload`) + .attach("file", "src/api/routes/tests/data/comment-box-1.0.2.tar.gz") + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(status ? status : 200) + } + + const getPlugins = async (status?: number) => { + return request + .get(`/api/plugin`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(status ? status : 200) + } + + describe("upload", () => { + it("should be able to upload a plugin", async () => { + let res = await createPlugin() + expect(res.body).toBeDefined() + expect(res.body.plugins).toBeDefined() + expect(res.body.plugins[0]._id).toEqual("plg_comment-box") + expect(events.plugin.imported).toHaveBeenCalledTimes(1) + }) + + it("should not be able to create a plugin if there is an error", async () => { + mockObjectStore.mockImplementationOnce(() => { + throw new Error() + }) + let res = await createPlugin(400) + expect(res.body.message).toEqual("Failed to import plugin: Error") + expect(events.plugin.imported).toHaveBeenCalledTimes(0) + }) + }) + + describe("fetch", () => { + it("should be able to fetch plugins", async () => { + await createPlugin() + const res = await getPlugins() + expect(res.body).toBeDefined() + expect(res.body[0]._id).toEqual("plg_comment-box") + }) + }) + + describe("destroy", () => { + it("should be able to delete a plugin", async () => { + await createPlugin() + const res = await request + .delete(`/api/plugin/plg_comment-box`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body).toBeDefined() + expect(res.body.message).toEqual("Plugin plg_comment-box deleted.") + + const plugins = await getPlugins() + expect(plugins.body).toBeDefined() + expect(plugins.body.length).toEqual(0) + expect(events.plugin.deleted).toHaveBeenCalledTimes(1) + }) + it("should handle an error deleting a plugin", async () => { + deleteFolder.mockImplementationOnce(() => { + throw new Error() + }) + + await createPlugin() + const res = await request + .delete(`/api/plugin/plg_comment-box`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(400) + + expect(res.body.message).toEqual("Failed to delete plugin: Error") + expect(events.plugin.deleted).toHaveBeenCalledTimes(0) + const plugins = await getPlugins() + expect(plugins.body).toBeDefined() + expect(plugins.body.length).toEqual(1) + }) + }) + + describe("github", () => { + const createGithubPlugin = async (status?: number, url?: string) => { + return await request + .post(`/api/plugin`) + .send({ + source: "Github", + url, + githubToken: "token", + }) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(status ? status : 200) + } + it("should be able to create a plugin from github", async () => { + const res = await createGithubPlugin( + 200, + "https://github.com/my-repo/budibase-comment-box.git" + ) + expect(res.body).toBeDefined() + expect(res.body.plugin).toBeDefined() + expect(res.body.plugin._id).toEqual("plg_comment-box") + }) + it("should fail if the url is not from github", async () => { + const res = await createGithubPlugin( + 400, + "https://notgithub.com/my-repo/budibase-comment-box" + ) + expect(res.body.message).toEqual( + "Failed to import plugin: The plugin origin must be from Github" + ) + }) + }) + describe("npm", () => { + it("should be able to create a plugin from npm", async () => { + const res = await request + .post(`/api/plugin`) + .send({ + source: "NPM", + url: "https://www.npmjs.com/package/budibase-component", + }) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body).toBeDefined() + expect(res.body.plugin._id).toEqual("plg_budibase-component") + expect(events.plugin.imported).toHaveBeenCalled() + }) + }) + + describe("url", () => { + it("should be able to create a plugin from a URL", async () => { + const res = await request + .post(`/api/plugin`) + .send({ + source: "URL", + url: "https://www.someurl.com/comment-box/comment-box-1.0.2.tar.gz", + }) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body).toBeDefined() + expect(res.body.plugin._id).toEqual("plg_comment-box") + expect(events.plugin.imported).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/packages/server/src/api/routes/tests/routing.spec.js b/packages/server/src/api/routes/tests/routing.spec.js index 148e5eded1..6fc436ef1c 100644 --- a/packages/server/src/api/routes/tests/routing.spec.js +++ b/packages/server/src/api/routes/tests/routing.spec.js @@ -9,7 +9,6 @@ const route = "/test" // there are checks which are disabled in test env, // these checks need to be enabled for this test - describe("/routing", () => { let request = setup.getRequest() let config = setup.getConfig() diff --git a/packages/server/src/api/routes/tests/table.spec.js b/packages/server/src/api/routes/tests/table.spec.js index 4776878635..b4fd354b9d 100644 --- a/packages/server/src/api/routes/tests/table.spec.js +++ b/packages/server/src/api/routes/tests/table.spec.js @@ -51,7 +51,7 @@ describe("/tables", () => { table.dataImport.schema = table.schema const res = await createTable(table) - + expect(events.table.created).toBeCalledTimes(1) expect(events.table.created).toBeCalledWith(res.body) expect(events.table.imported).toBeCalledTimes(1) @@ -87,6 +87,12 @@ describe("/tables", () => { it("updates all the row fields for a table when a schema key is renamed", async () => { const testTable = await config.createTable() + await config.createView({ + name: "TestView", + field: "Price", + calculation: "stats", + tableId: testTable._id, + }) const testRow = await request .post(`/api/${testTable._id}/rows`) @@ -109,7 +115,7 @@ describe("/tables", () => { updated: "updatedName" }, schema: { - updatedName: {type: "string"} + updatedName: { type: "string" } } }) .set(config.defaultHeaders()) diff --git a/packages/server/src/api/routes/tests/user.spec.js b/packages/server/src/api/routes/tests/user.spec.js index 7e4bf02616..56f1923cb0 100644 --- a/packages/server/src/api/routes/tests/user.spec.js +++ b/packages/server/src/api/routes/tests/user.spec.js @@ -90,4 +90,90 @@ describe("/users", () => { expect(res.body.tableId).toBeDefined() }) }) + describe("setFlag", () => { + it("should throw an error if a flag is not provided", async () => { + await config.createUser() + const res = await request + .post(`/api/users/flags`) + .set(config.defaultHeaders()) + .send({ value: "test" }) + .expect(400) + .expect("Content-Type", /json/) + expect(res.body.message).toEqual("Must supply a 'flag' field in request body.") + + }) + + it("should be able to set a flag on the user", async () => { + await config.createUser() + const res = await request + .post(`/api/users/flags`) + .set(config.defaultHeaders()) + .send({ value: "test", flag: "test" }) + .expect(200) + .expect("Content-Type", /json/) + expect(res.body.message).toEqual("Flag set successfully") + }) + }) + + describe("getFlags", () => { + it("should get flags for a specific user", async () => { + let flagData = { value: "test", flag: "test" } + await config.createUser() + await request + .post(`/api/users/flags`) + .set(config.defaultHeaders()) + .send(flagData) + .expect(200) + .expect("Content-Type", /json/) + + const res = await request + .get(`/api/users/flags`) + .set(config.defaultHeaders()) + .expect(200) + .expect("Content-Type", /json/) + expect(res.body[flagData.value]).toEqual(flagData.flag) + }) + }) + + describe("setFlag", () => { + it("should throw an error if a flag is not provided", async () => { + await config.createUser() + const res = await request + .post(`/api/users/flags`) + .set(config.defaultHeaders()) + .send({ value: "test" }) + .expect(400) + .expect("Content-Type", /json/) + expect(res.body.message).toEqual("Must supply a 'flag' field in request body.") + + }) + + it("should be able to set a flag on the user", async () => { + await config.createUser() + const res = await request + .post(`/api/users/flags`) + .set(config.defaultHeaders()) + .send({ value: "test", flag: "test" }) + .expect(200) + .expect("Content-Type", /json/) + expect(res.body.message).toEqual("Flag set successfully") + }) + }) + + describe("syncUser", () => { + it("should sync the user", async () => { + let user = await config.createUser() + await config.createApp('New App') + let res = await request + .post(`/api/users/metadata/sync/${user._id}`) + .set(config.defaultHeaders()) + .expect(200) + .expect("Content-Type", /json/) + expect(res.body.message).toEqual('User synced.') + }) + }) + + + + }) diff --git a/packages/server/src/api/routes/tests/utilities/index.ts b/packages/server/src/api/routes/tests/utilities/index.ts index 87a373a2c6..519e8a1459 100644 --- a/packages/server/src/api/routes/tests/utilities/index.ts +++ b/packages/server/src/api/routes/tests/utilities/index.ts @@ -63,14 +63,14 @@ export function afterAll() { export function getRequest() { if (!request) { - exports.beforeAll() + beforeAll() } return request } export function getConfig() { if (!config) { - exports.beforeAll() + beforeAll() } return config } diff --git a/packages/server/src/automations/automationUtils.ts b/packages/server/src/automations/automationUtils.ts index 8a75de83dd..2d1cb53bc3 100644 --- a/packages/server/src/automations/automationUtils.ts +++ b/packages/server/src/automations/automationUtils.ts @@ -5,6 +5,7 @@ import { } from "@budibase/string-templates" import sdk from "../sdk" import { Row } from "@budibase/types" +import { LoopStep, LoopStepType, LoopInput } from "../definitions/automations" /** * When values are input to the system generally they will be of type string as this is required for template strings. @@ -123,3 +124,26 @@ export function stringSplit(value: string | string[]) { } return value } + +export function typecastForLooping(loopStep: LoopStep, input: LoopInput) { + if (!input || !input.binding) { + return null + } + try { + switch (loopStep.inputs.option) { + case LoopStepType.ARRAY: + if (typeof input.binding === "string") { + return JSON.parse(input.binding) + } + break + case LoopStepType.STRING: + if (Array.isArray(input.binding)) { + return input.binding.join(",") + } + break + } + } catch (err) { + throw new Error("Unable to cast to correct type") + } + return input.binding +} diff --git a/packages/server/src/automations/tests/bash.spec.js b/packages/server/src/automations/tests/bash.spec.js new file mode 100644 index 0000000000..cf358a089d --- /dev/null +++ b/packages/server/src/automations/tests/bash.spec.js @@ -0,0 +1,34 @@ +const setup = require("./utilities") + +describe("test the bash action", () => { + let config = setup.getConfig() + + beforeEach(async () => { + await config.init() + }) + afterAll(setup.afterAll) + + it("should be able to execute a script", async () => { + + let res = await setup.runStep("EXECUTE_BASH", + inputs = { + code: "echo 'test'" + } + + ) + expect(res.stdout).toEqual("test\n") + expect(res.success).toEqual(true) + }) + + it("should handle a null value", async () => { + + let res = await setup.runStep("EXECUTE_BASH", + inputs = { + code: null + } + + + ) + expect(res.stdout).toEqual("Budibase bash automation failed: Invalid inputs") + }) +}) diff --git a/packages/server/src/automations/tests/discord.spec.js b/packages/server/src/automations/tests/discord.spec.js new file mode 100644 index 0000000000..cb51748a04 --- /dev/null +++ b/packages/server/src/automations/tests/discord.spec.js @@ -0,0 +1,27 @@ +const setup = require("./utilities") +const fetch = require("node-fetch") + +jest.mock("node-fetch") + +describe("test the outgoing webhook action", () => { + let inputs + let config = setup.getConfig() + + beforeEach(async () => { + await config.init() + inputs = { + username: "joe_bloggs", + url: "http://www.test.com", + } + }) + + afterAll(setup.afterAll) + + it("should be able to run the action", async () => { + const res = await setup.runStep(setup.actions.discord.stepId, inputs) + expect(res.response.url).toEqual("http://www.test.com") + expect(res.response.method).toEqual("post") + expect(res.success).toEqual(true) + }) + +}) diff --git a/packages/server/src/automations/tests/executeQuery.spec.js b/packages/server/src/automations/tests/executeQuery.spec.js new file mode 100644 index 0000000000..c9b7629d09 --- /dev/null +++ b/packages/server/src/automations/tests/executeQuery.spec.js @@ -0,0 +1,49 @@ +const setup = require("./utilities") + +describe("test the execute query action", () => { + let datasource + let config = setup.getConfig() + + beforeEach(async () => { + await config.init() + + await config.createDatasource() + query = await config.createQuery() + + }) + + afterAll(setup.afterAll) + + it("should be able to execute a query", async () => { + let res = await setup.runStep(setup.actions.EXECUTE_QUERY.stepId, + inputs = { + query: { queryId: query._id } + } + ) + expect(res.response).toEqual([{ a: 'string', b: 1 }]) + expect(res.success).toEqual(true) + }) + + it("should handle a null query value", async () => { + let res = await setup.runStep(setup.actions.EXECUTE_QUERY.stepId, + inputs = { + query: null + } + ) + expect(res.response.message).toEqual("Invalid inputs") + expect(res.success).toEqual(false) + }) + + + it("should handle an error executing a query", async () => { + let res = await setup.runStep(setup.actions.EXECUTE_QUERY.stepId, + inputs = { + query: { queryId: "wrong_id" } + } + ) + expect(res.response).toEqual('{"status":404,"name":"not_found","message":"missing","reason":"missing"}') + expect(res.success).toEqual(false) + }) + + +}) diff --git a/packages/server/src/automations/tests/executeScript.spec.js b/packages/server/src/automations/tests/executeScript.spec.js new file mode 100644 index 0000000000..d0febe4078 --- /dev/null +++ b/packages/server/src/automations/tests/executeScript.spec.js @@ -0,0 +1,48 @@ +const setup = require("./utilities") + +describe("test the execute script action", () => { + let config = setup.getConfig() + + beforeEach(async () => { + await config.init() + }) + afterAll(setup.afterAll) + + it("should be able to execute a script", async () => { + + let res = await setup.runStep(setup.actions.EXECUTE_SCRIPT.stepId, + inputs = { + code: "return 1 + 1" + } + + ) + expect(res.value).toEqual(2) + expect(res.success).toEqual(true) + }) + + it("should handle a null value", async () => { + + let res = await setup.runStep(setup.actions.EXECUTE_SCRIPT.stepId, + inputs = { + code: null + } + + + ) + expect(res.response.message).toEqual("Invalid inputs") + expect(res.success).toEqual(false) + }) + + it("should be able to handle an error gracefully", async () => { + + let res = await setup.runStep(setup.actions.EXECUTE_SCRIPT.stepId, + inputs = { + code: "return something.map(x => x.name)" + } + + ) + expect(res.response).toEqual("ReferenceError: something is not defined") + expect(res.success).toEqual(false) + }) + +}) diff --git a/packages/server/src/automations/tests/sendSmtpEmail.spec.js b/packages/server/src/automations/tests/sendSmtpEmail.spec.js new file mode 100644 index 0000000000..fcafc329c0 --- /dev/null +++ b/packages/server/src/automations/tests/sendSmtpEmail.spec.js @@ -0,0 +1,71 @@ + +function generateResponse(to, from) { + return { + "success": true, + "response": { + "accepted": [ + to + ], + "envelope": { + "from": from, + "to": [ + to + ] + }, + "message": `Email sent to ${to}.` + } + + } +} + +const mockFetch = jest.fn(() => ({ + headers: { + raw: () => { + return { "content-type": ["application/json"] } + }, + get: () => ["application/json"], + }, + json: jest.fn(() => response), + status: 200, + text: jest.fn(), +})) +jest.mock("node-fetch", () => mockFetch) +const setup = require("./utilities") + + +describe("test the outgoing webhook action", () => { + let inputs + let config = setup.getConfig() + beforeEach(async () => { + await config.init() + }) + + afterAll(setup.afterAll) + + it("should be able to run the action", async () => { + inputs = { + to: "user1@test.com", + from: "admin@test.com", + subject: "hello", + contents: "testing", + } + let resp = generateResponse(inputs.to, inputs.from) + mockFetch.mockImplementationOnce(() => ({ + headers: { + raw: () => { + return { "content-type": ["application/json"] } + }, + get: () => ["application/json"], + }, + json: jest.fn(() => resp), + status: 200, + text: jest.fn(), + })) + const res = await setup.runStep(setup.actions.SEND_EMAIL_SMTP.stepId, inputs) + expect(res.response).toEqual(resp) + expect(res.success).toEqual(true) + + }) + + +}) diff --git a/packages/server/src/automations/tests/serverLog.spec.js b/packages/server/src/automations/tests/serverLog.spec.js new file mode 100644 index 0000000000..550910723a --- /dev/null +++ b/packages/server/src/automations/tests/serverLog.spec.js @@ -0,0 +1,22 @@ +const setup = require("./utilities") + +describe("test the server log action", () => { + let config = setup.getConfig() + + beforeEach(async () => { + await config.init() + inputs = { + text: "log message", + } + }) + afterAll(setup.afterAll) + + it("should be able to log the text", async () => { + + let res = await setup.runStep(setup.actions.SERVER_LOG.stepId, + inputs + ) + expect(res.message).toEqual(`App ${config.getAppId()} - ${inputs.text}`) + expect(res.success).toEqual(true) + }) +}) diff --git a/packages/server/src/automations/tests/utilities/index.ts b/packages/server/src/automations/tests/utilities/index.ts index a18e931bab..3c990a38d0 100644 --- a/packages/server/src/automations/tests/utilities/index.ts +++ b/packages/server/src/automations/tests/utilities/index.ts @@ -31,7 +31,7 @@ export async function runInProd(fn: any) { } } -export async function runStep(stepId: string, inputs: any) { +export async function runStep(stepId: string, inputs: any, stepContext?: any) { async function run() { let step = await getAction(stepId) expect(step).toBeDefined() @@ -39,7 +39,7 @@ export async function runStep(stepId: string, inputs: any) { throw new Error("No step found") } return step({ - context: {}, + context: stepContext || {}, inputs, appId: config ? config.getAppId() : null, // don't really need an API key, mocked out usage quota, not being tested here diff --git a/packages/server/src/automations/tests/zapier.spec.js b/packages/server/src/automations/tests/zapier.spec.js new file mode 100644 index 0000000000..e1fc785152 --- /dev/null +++ b/packages/server/src/automations/tests/zapier.spec.js @@ -0,0 +1,27 @@ +const setup = require("./utilities") +const fetch = require("node-fetch") + +jest.mock("node-fetch") + +describe("test the outgoing webhook action", () => { + let inputs + let config = setup.getConfig() + + beforeEach(async () => { + await config.init() + inputs = { + value1: "test", + url: "http://www.test.com", + } + }) + + afterAll(setup.afterAll) + + it("should be able to run the action", async () => { + const res = await setup.runStep(setup.actions.zapier.stepId, inputs) + expect(res.response.url).toEqual("http://www.test.com") + expect(res.response.method).toEqual("post") + expect(res.success).toEqual(true) + }) + +}) diff --git a/packages/server/src/automations/unitTests/automationUtils.spec.js b/packages/server/src/automations/unitTests/automationUtils.spec.js deleted file mode 100644 index 0992bd6eb2..0000000000 --- a/packages/server/src/automations/unitTests/automationUtils.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -const automationUtils = require("../automationUtils") - -describe("automationUtils", () => { - test("substituteLoopStep should allow multiple loop binding substitutes", () => { - expect(automationUtils.substituteLoopStep( - `{{ loop.currentItem._id }} {{ loop.currentItem._id }} {{ loop.currentItem._id }}`, - "step.2")) - .toBe(`{{ step.2.currentItem._id }} {{ step.2.currentItem._id }} {{ step.2.currentItem._id }}`) - }) - - test("substituteLoopStep should handle not subsituting outside of curly braces", () => { - expect(automationUtils.substituteLoopStep( - `loop {{ loop.currentItem._id }}loop loop{{ loop.currentItem._id }}loop`, - "step.2")) - .toBe(`loop {{ step.2.currentItem._id }}loop loop{{ step.2.currentItem._id }}loop`) - }) -}) \ No newline at end of file diff --git a/packages/server/src/automations/unitTests/automationUtils.spec.ts b/packages/server/src/automations/unitTests/automationUtils.spec.ts new file mode 100644 index 0000000000..b80b2d60be --- /dev/null +++ b/packages/server/src/automations/unitTests/automationUtils.spec.ts @@ -0,0 +1,65 @@ +const automationUtils = require("../automationUtils") + +describe("automationUtils", () => { + describe("substituteLoopStep", () => { + it("should allow multiple loop binding substitutes", () => { + expect( + automationUtils.substituteLoopStep( + `{{ loop.currentItem._id }} {{ loop.currentItem._id }} {{ loop.currentItem._id }}`, + "step.2" + ) + ).toBe( + `{{ step.2.currentItem._id }} {{ step.2.currentItem._id }} {{ step.2.currentItem._id }}` + ) + }) + + it("should handle not subsituting outside of curly braces", () => { + expect( + automationUtils.substituteLoopStep( + `loop {{ loop.currentItem._id }}loop loop{{ loop.currentItem._id }}loop`, + "step.2" + ) + ).toBe( + `loop {{ step.2.currentItem._id }}loop loop{{ step.2.currentItem._id }}loop` + ) + }) + }) + + describe("typeCastForLooping", () => { + it("should parse to correct type", () => { + expect( + automationUtils.typecastForLooping( + { inputs: { option: "Array" } }, + { binding: [1, 2, 3] } + ) + ).toEqual([1, 2, 3]) + expect( + automationUtils.typecastForLooping( + { inputs: { option: "Array" } }, + { binding: "[1, 2, 3]" } + ) + ).toEqual([1, 2, 3]) + expect( + automationUtils.typecastForLooping( + { inputs: { option: "String" } }, + { binding: [1, 2, 3] } + ) + ).toEqual("1,2,3") + }) + it("should handle null values", () => { + // expect it to handle where the binding is null + expect( + automationUtils.typecastForLooping( + { inputs: { option: "Array" } }, + { binding: null } + ) + ).toEqual(null) + expect(() => + automationUtils.typecastForLooping( + { inputs: { option: "Array" } }, + { binding: "test" } + ) + ).toThrow() + }) + }) +}) diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index bbd940150f..2965e530b9 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -1,3 +1,12 @@ +import { mocks } from "@budibase/backend-core/tests" + +// init the licensing mock +import * as pro from "@budibase/pro" +mocks.licenses.init(pro) + +// use unlimited license by default +mocks.licenses.useUnlimited() + import { init as dbInit } from "../../db" dbInit() import env from "../../environment" diff --git a/packages/server/src/threads/automation.ts b/packages/server/src/threads/automation.ts index 8b343cdf8e..315a508da2 100644 --- a/packages/server/src/threads/automation.ts +++ b/packages/server/src/threads/automation.ts @@ -32,31 +32,8 @@ const LOOP_STEP_ID = actions.ACTION_DEFINITIONS.LOOP.stepId const CRON_STEP_ID = triggerDefs.CRON.stepId const STOPPED_STATUS = { success: true, status: AutomationStatus.STOPPED } -function typecastForLooping(loopStep: LoopStep, input: LoopInput) { - if (!input || !input.binding) { - return null - } - try { - switch (loopStep.inputs.option) { - case LoopStepType.ARRAY: - if (typeof input.binding === "string") { - return JSON.parse(input.binding) - } - break - case LoopStepType.STRING: - if (Array.isArray(input.binding)) { - return input.binding.join(",") - } - break - } - } catch (err) { - throw new Error("Unable to cast to correct type") - } - return input.binding -} - function getLoopIterations(loopStep: LoopStep, input: LoopInput) { - const binding = typecastForLooping(loopStep, input) + const binding = automationUtils.typecastForLooping(loopStep, input) if (!loopStep || !binding) { return 1 } @@ -289,7 +266,7 @@ class Orchestrator { let tempOutput = { items: loopSteps, iterations: iterationCount } try { - newInput.binding = typecastForLooping( + newInput.binding = automationUtils.typecastForLooping( loopStep as LoopStep, newInput ) diff --git a/packages/server/src/utilities/rowProcessor/tests/utils.spec.ts b/packages/server/src/utilities/rowProcessor/tests/utils.spec.ts new file mode 100644 index 0000000000..a9ab59c15a --- /dev/null +++ b/packages/server/src/utilities/rowProcessor/tests/utils.spec.ts @@ -0,0 +1,57 @@ +import { fixAutoColumnSubType } from "../utils" +import { AutoFieldDefaultNames, AutoFieldSubTypes } from "../../../constants" + +describe("rowProcessor utility", () => { + describe("fixAutoColumnSubType", () => { + let schema = { + name: "", + type: "link", + subtype: "", // missing subtype + icon: "ri-magic-line", + autocolumn: true, + constraints: { type: "array", presence: false }, + tableId: "ta_users", + fieldName: "test-Updated By", + relationshipType: "many-to-many", + sortable: false, + } + + it("updates the schema with the correct subtype", async () => { + schema.name = AutoFieldDefaultNames.CREATED_BY + expect(fixAutoColumnSubType(schema).subtype).toEqual( + AutoFieldSubTypes.CREATED_BY + ) + schema.subtype = "" + + schema.name = AutoFieldDefaultNames.UPDATED_BY + expect(fixAutoColumnSubType(schema).subtype).toEqual( + AutoFieldSubTypes.UPDATED_BY + ) + schema.subtype = "" + + schema.name = AutoFieldDefaultNames.CREATED_AT + expect(fixAutoColumnSubType(schema).subtype).toEqual( + AutoFieldSubTypes.CREATED_AT + ) + schema.subtype = "" + + schema.name = AutoFieldDefaultNames.UPDATED_AT + expect(fixAutoColumnSubType(schema).subtype).toEqual( + AutoFieldSubTypes.UPDATED_AT + ) + schema.subtype = "" + + schema.name = AutoFieldDefaultNames.AUTO_ID + expect(fixAutoColumnSubType(schema).subtype).toEqual( + AutoFieldSubTypes.AUTO_ID + ) + schema.subtype = "" + }) + + it("returns the column if subtype exists", async () => { + schema.subtype = AutoFieldSubTypes.CREATED_BY + schema.name = AutoFieldDefaultNames.CREATED_AT + expect(fixAutoColumnSubType(schema)).toEqual(schema) + }) + }) +}) diff --git a/packages/server/src/utilities/tests/plugins.spec.ts b/packages/server/src/utilities/tests/plugins.spec.ts new file mode 100644 index 0000000000..4d9b0de449 --- /dev/null +++ b/packages/server/src/utilities/tests/plugins.spec.ts @@ -0,0 +1,23 @@ +import { enrichPluginURLs } from "../plugins" +const env = require("../../environment") +jest.mock("../../environment") + +describe("plugins utility", () => { + let pluginsArray: any = [ + { + name: "test-plugin", + }, + ] + it("enriches the plugins url self-hosted", async () => { + let result = enrichPluginURLs(pluginsArray) + expect(result[0].jsUrl).toEqual("/plugins/test-plugin/plugin.min.js") + }) + + it("enriches the plugins url cloud", async () => { + env.SELF_HOSTED = 0 + let result = enrichPluginURLs(pluginsArray) + expect(result[0].jsUrl).toEqual( + "https://cdn.budi.live/test-plugin/plugin.min.js" + ) + }) +}) diff --git a/packages/worker/jest.config.ts b/packages/worker/jest.config.ts index d8a2d59722..c482dffd38 100644 --- a/packages/worker/jest.config.ts +++ b/packages/worker/jest.config.ts @@ -19,6 +19,8 @@ if (!process.env.CI) { } // add pro sources if they exist if (fs.existsSync("../../../budibase-pro")) { + config.moduleNameMapper["@budibase/pro/(.*)"] = + "/../../../budibase-pro/packages/pro/$1" config.moduleNameMapper["@budibase/pro"] = "/../../../budibase-pro/packages/pro/src" } diff --git a/packages/worker/src/api/routes/global/tests/groups.spec.ts b/packages/worker/src/api/routes/global/tests/groups.spec.ts new file mode 100644 index 0000000000..9e136e58d9 --- /dev/null +++ b/packages/worker/src/api/routes/global/tests/groups.spec.ts @@ -0,0 +1,58 @@ +import { events } from "@budibase/backend-core" +import { structures, TestConfiguration, mocks } from "../../../../tests" + +describe("/api/global/groups", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + beforeEach(async () => { + mocks.licenses.useGroups() + }) + + describe("create", () => { + it("should be able to create a new group", async () => { + const group = structures.groups.UserGroup() + await config.api.groups.saveGroup(group) + expect(events.group.created).toBeCalledTimes(1) + expect(events.group.updated).not.toBeCalled() + expect(events.group.permissionsEdited).not.toBeCalled() + }) + }) + + describe("update", () => { + it("should be able to update a basic group", async () => { + const group = structures.groups.UserGroup() + let oldGroup = await config.api.groups.saveGroup(group) + + let updatedGroup = { + ...oldGroup.body, + ...group, + name: "New Name", + } + await config.api.groups.saveGroup(updatedGroup) + + expect(events.group.updated).toBeCalledTimes(1) + expect(events.group.permissionsEdited).not.toBeCalled() + }) + + describe("destroy", () => { + it("should be able to delete a basic group", async () => { + const group = structures.groups.UserGroup() + let oldGroup = await config.api.groups.saveGroup(group) + await config.api.groups.deleteGroup( + oldGroup.body._id, + oldGroup.body._rev + ) + + expect(events.group.deleted).toBeCalledTimes(1) + }) + }) + }) +}) diff --git a/packages/worker/src/api/routes/global/tests/license.spec.ts b/packages/worker/src/api/routes/global/tests/license.spec.ts index b25b41adb9..be0673729e 100644 --- a/packages/worker/src/api/routes/global/tests/license.spec.ts +++ b/packages/worker/src/api/routes/global/tests/license.spec.ts @@ -1,7 +1,5 @@ import { TestConfiguration } from "../../../../tests" -// TODO - describe("/api/global/license", () => { const config = new TestConfiguration() diff --git a/packages/worker/src/api/routes/global/tests/roles.spec.ts b/packages/worker/src/api/routes/global/tests/roles.spec.ts index 516c3433ab..2289273488 100644 --- a/packages/worker/src/api/routes/global/tests/roles.spec.ts +++ b/packages/worker/src/api/routes/global/tests/roles.spec.ts @@ -1,11 +1,47 @@ -import { TestConfiguration } from "../../../../tests" +import { structures, TestConfiguration } from "../../../../tests" +import { context, db, permissions, roles } from "@budibase/backend-core" +import { Mock } from "jest-mock" -// TODO +jest.mock("@budibase/backend-core", () => { + const core = jest.requireActual("@budibase/backend-core") + return { + ...core, + db: { + ...core.db, + }, + context: { + ...core.context, + getAppDB: jest.fn(), + }, + } +}) + +const appDb = db.getDB("app_test") +const mockAppDB = context.getAppDB as Mock +mockAppDB.mockReturnValue(appDb) + +async function addAppMetadata() { + await appDb.put({ + _id: "app_metadata", + appId: "app_test", + name: "New App", + version: "version", + url: "url", + }) +} describe("/api/global/roles", () => { const config = new TestConfiguration() + const role = new roles.Role( + db.generateRoleID("newRole"), + roles.BUILTIN_ROLE_IDS.BASIC, + permissions.BuiltinPermissionID.READ_ONLY + ) beforeAll(async () => { + console.debug(role) + appDb.put(role) + await addAppMetadata() await config.beforeAll() }) @@ -18,10 +54,35 @@ describe("/api/global/roles", () => { }) describe("GET /api/global/roles", () => { - it("retrieves roles", () => {}) + it("retrieves roles", async () => { + const res = await config.api.roles.get() + expect(res.body).toBeDefined() + expect(res.body["app_test"].roles.length).toEqual(5) + expect(res.body["app_test"].roles.map((r: any) => r._id)).toContain( + role._id + ) + }) }) - describe("GET /api/global/roles/:appId", () => {}) + describe("GET api/global/roles/:appId", () => { + it("finds a role by appId", async () => { + const res = await config.api.roles.find("app_test") + expect(res.body).toBeDefined() + expect(res.body.name).toEqual("New App") + }) + }) - describe("DELETE /api/global/roles/:appId", () => {}) + describe("DELETE /api/global/roles/:appId", () => { + it("removes an app role", async () => { + let user = structures.users.user() + user.roles = { + app_test: "role1", + } + const userResponse = await config.createUser(user) + const res = await config.api.roles.remove("app_test") + const updatedUser = await config.api.users.getUser(userResponse._id!) + expect(updatedUser.body.roles).not.toHaveProperty("app_test") + expect(res.body.message).toEqual("App role removed from all users") + }) + }) }) diff --git a/packages/worker/src/api/routes/global/tests/templates.spec.ts b/packages/worker/src/api/routes/global/tests/templates.spec.ts index d1c296643d..aa0d808d60 100644 --- a/packages/worker/src/api/routes/global/tests/templates.spec.ts +++ b/packages/worker/src/api/routes/global/tests/templates.spec.ts @@ -1,4 +1,17 @@ +import { + addBaseTemplates, + EmailTemplates, + getTemplates, +} from "../../../../constants/templates" +import { + EmailTemplatePurpose, + TemplateMetadata, + TemplateMetadataNames, + TemplateType, +} from "../../../../constants" import { TestConfiguration } from "../../../../tests" +import { join } from "path" +import { readStaticFile } from "../../../../../src/utilities/fileSystem" // TODO @@ -18,18 +31,85 @@ describe("/api/global/template", () => { }) describe("GET /api/global/template/definitions", () => { - it("retrieves definitions", () => {}) + describe("retrieves definitions", () => { + it("checks description definitions", async () => { + let result = await config.api.templates.definitions() + + expect(result.body.info[EmailTemplatePurpose.BASE].description).toEqual( + TemplateMetadata[TemplateType.EMAIL][0].description + ) + expect( + result.body.info[EmailTemplatePurpose.PASSWORD_RECOVERY].description + ).toEqual(TemplateMetadata[TemplateType.EMAIL][1].description) + expect( + result.body.info[EmailTemplatePurpose.WELCOME].description + ).toEqual(TemplateMetadata[TemplateType.EMAIL][2].description) + expect( + result.body.info[EmailTemplatePurpose.INVITATION].description + ).toEqual(TemplateMetadata[TemplateType.EMAIL][3].description) + expect( + result.body.info[EmailTemplatePurpose.CUSTOM].description + ).toEqual(TemplateMetadata[TemplateType.EMAIL][4].description) + }) + + it("checks description bindings", async () => { + let result = await config.api.templates.definitions() + + expect(result.body.bindings[EmailTemplatePurpose.BASE]).toEqual( + TemplateMetadata[TemplateType.EMAIL][0].bindings + ) + expect( + result.body.bindings[EmailTemplatePurpose.PASSWORD_RECOVERY] + ).toEqual(TemplateMetadata[TemplateType.EMAIL][1].bindings) + expect(result.body.bindings[EmailTemplatePurpose.WELCOME]).toEqual( + TemplateMetadata[TemplateType.EMAIL][2].bindings + ) + expect(result.body.bindings[EmailTemplatePurpose.INVITATION]).toEqual( + TemplateMetadata[TemplateType.EMAIL][3].bindings + ) + expect(result.body.bindings[EmailTemplatePurpose.CUSTOM]).toEqual( + TemplateMetadata[TemplateType.EMAIL][4].bindings + ) + }) + }) }) - describe("POST /api/global/template", () => {}) + describe("POST /api/global/template", () => { + it("adds a new template", async () => { + let purpose = "base" + let contents = "Test contents" + let updatedTemplate = { + contents: contents, + purpose: purpose, + type: "email", + } + await config.api.templates.saveTemplate(updatedTemplate) + let res = await config.api.templates.getTemplate() + let newTemplate = res.body.find((t: any) => (t.purpose = purpose)) + expect(newTemplate.contents).toEqual(contents) + }) + }) - describe("GET /api/global/template", () => {}) - - describe("GET /api/global/template/:type", () => {}) - - describe("GET /api/global/template/:ownerId", () => {}) - - describe("GET /api/global/template/:id", () => {}) - - describe("DELETE /api/global/template/:id/:rev", () => {}) + describe("GET /api/global/template", () => { + it("fetches templates", async () => { + let res = await config.api.templates.getTemplate() + expect( + res.body.find((t: any) => t.purpose === EmailTemplatePurpose.BASE) + ).toBeDefined() + expect( + res.body.find((t: any) => t.purpose === EmailTemplatePurpose.CUSTOM) + ).toBeDefined() + expect( + res.body.find((t: any) => t.purpose === EmailTemplatePurpose.INVITATION) + ).toBeDefined() + expect( + res.body.find( + (t: any) => t.purpose === EmailTemplatePurpose.PASSWORD_RECOVERY + ) + ).toBeDefined() + expect( + res.body.find((t: any) => t.purpose === EmailTemplatePurpose.WELCOME) + ).toBeDefined() + }) + }) }) diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index 3b732cb3d9..10c29809b9 100644 --- a/packages/worker/src/api/routes/global/tests/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -116,7 +116,7 @@ describe("/api/global/users", () => { it("should ignore users existing in other tenants", async () => { const user = await config.createUser() - jest.resetAllMocks() + jest.clearAllMocks() await tenancy.doInTenant(TENANT_1, async () => { const response = await config.api.users.bulkCreateUsers([user]) @@ -229,7 +229,7 @@ describe("/api/global/users", () => { it("should not be able to create user that exists in other tenant", async () => { const user = await config.createUser() - jest.resetAllMocks() + jest.clearAllMocks() await tenancy.doInTenant(TENANT_1, async () => { delete user._id diff --git a/packages/worker/src/constants/index.ts b/packages/worker/src/constants/index.ts index 4ed2c99714..dba058eabb 100644 --- a/packages/worker/src/constants/index.ts +++ b/packages/worker/src/constants/index.ts @@ -27,6 +27,14 @@ export enum EmailTemplatePurpose { CUSTOM = "custom", } +export enum TemplateMetadataNames { + BASE = "Base format", + PASSWORD_RECOVERY = "Password recovery", + WELCOME = "User welcome", + INVITATION = "User invitation", + CUSTOM = "Custom", +} + export enum InternalTemplateBinding { PLATFORM_URL = "platformUrl", COMPANY = "company", @@ -93,7 +101,7 @@ export const TemplateBindings = { export const TemplateMetadata = { [TemplateType.EMAIL]: [ { - name: "Base format", + name: TemplateMetadataNames.BASE, description: "This is the base template, all others are based on it. The {{ body }} will be replaced with another email template.", category: "miscellaneous", @@ -110,7 +118,7 @@ export const TemplateMetadata = { ], }, { - name: "Password recovery", + name: TemplateMetadataNames.PASSWORD_RECOVERY, description: "When a user requests a password reset they will receive an email built with this template.", category: "user management", @@ -129,7 +137,7 @@ export const TemplateMetadata = { ], }, { - name: "User welcome", + name: TemplateMetadataNames.WELCOME, description: "When a new user is added they will be sent a welcome email using this template.", category: "user management", @@ -137,7 +145,7 @@ export const TemplateMetadata = { bindings: [], }, { - name: "User invitation", + name: TemplateMetadataNames.INVITATION, description: "When inviting a user via the email on-boarding this template will be used.", category: "user management", @@ -156,7 +164,7 @@ export const TemplateMetadata = { ], }, { - name: "Custom", + name: TemplateMetadataNames.CUSTOM, description: "A custom template, this is currently used for SMTP email actions in automations.", category: "automations", diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index 871b7ec6a6..21b59ba8ed 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -1,4 +1,12 @@ -import "./mocks" +import mocks from "./mocks" + +// init the licensing mock +import * as pro from "@budibase/pro" +mocks.licenses.init(pro) + +// use unlimited license by default +mocks.licenses.useUnlimited() + import * as dbConfig from "../db" dbConfig.init() import env from "../environment" diff --git a/packages/worker/src/tests/api/groups.ts b/packages/worker/src/tests/api/groups.ts new file mode 100644 index 0000000000..4522790d32 --- /dev/null +++ b/packages/worker/src/tests/api/groups.ts @@ -0,0 +1,26 @@ +import { UserGroup } from "@budibase/types" +import TestConfiguration from "../TestConfiguration" +import { TestAPI } from "./base" + +export class GroupsAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + saveGroup = (group: UserGroup) => { + return this.request + .post(`/api/global/groups`) + .send(group) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + } + + deleteGroup = (id: string, rev: string) => { + return this.request + .delete(`/api/global/groups/${id}/${rev}`) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + } +} diff --git a/packages/worker/src/tests/api/index.ts b/packages/worker/src/tests/api/index.ts index bc0271b9c6..0bd0308e2f 100644 --- a/packages/worker/src/tests/api/index.ts +++ b/packages/worker/src/tests/api/index.ts @@ -10,7 +10,10 @@ import { MigrationAPI } from "./migrations" import { StatusAPI } from "./status" import { RestoreAPI } from "./restore" import { TenantAPI } from "./tenants" - +import { GroupsAPI } from "./groups" +import { RolesAPI } from "./roles" +import { TemplatesAPI } from "./templates" +import { LicenseAPI } from "./license" export default class API { accounts: AccountAPI auth: AuthAPI @@ -23,6 +26,10 @@ export default class API { status: StatusAPI restore: RestoreAPI tenants: TenantAPI + groups: GroupsAPI + roles: RolesAPI + templates: TemplatesAPI + license: LicenseAPI constructor(config: TestConfiguration) { this.accounts = new AccountAPI(config) @@ -36,5 +43,9 @@ export default class API { this.status = new StatusAPI(config) this.restore = new RestoreAPI(config) this.tenants = new TenantAPI(config) + this.groups = new GroupsAPI(config) + this.roles = new RolesAPI(config) + this.templates = new TemplatesAPI(config) + this.license = new LicenseAPI(config) } } diff --git a/packages/worker/src/tests/api/license.ts b/packages/worker/src/tests/api/license.ts new file mode 100644 index 0000000000..9d7745a80e --- /dev/null +++ b/packages/worker/src/tests/api/license.ts @@ -0,0 +1,17 @@ +import TestConfiguration from "../TestConfiguration" +import { TestAPI } from "./base" + +export class LicenseAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + activate = async (licenseKey: string) => { + return this.request + .post("/api/global/license/activate") + .send({ licenseKey: licenseKey }) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + } +} diff --git a/packages/worker/src/tests/api/roles.ts b/packages/worker/src/tests/api/roles.ts new file mode 100644 index 0000000000..8e7647583a --- /dev/null +++ b/packages/worker/src/tests/api/roles.ts @@ -0,0 +1,32 @@ +import TestConfiguration from "../TestConfiguration" +import { TestAPI, TestAPIOpts } from "./base" + +export class RolesAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + get = (opts?: TestAPIOpts) => { + return this.request + .get(`/api/global/roles`) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(opts?.status ? opts.status : 200) + } + + find = (appId: string, opts?: TestAPIOpts) => { + return this.request + .get(`/api/global/roles/${appId}`) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(opts?.status ? opts.status : 200) + } + + remove = (appId: string, opts?: TestAPIOpts) => { + return this.request + .delete(`/api/global/roles/${appId}`) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(opts?.status ? opts.status : 200) + } +} diff --git a/packages/worker/src/tests/api/templates.ts b/packages/worker/src/tests/api/templates.ts new file mode 100644 index 0000000000..0c8110394f --- /dev/null +++ b/packages/worker/src/tests/api/templates.ts @@ -0,0 +1,30 @@ +import TestConfiguration from "../TestConfiguration" +import { TestAPI, TestAPIOpts } from "./base" + +export class TemplatesAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + definitions = (opts?: TestAPIOpts) => { + return this.request + .get(`/api/global/template/definitions`) + .set(opts?.headers ? opts.headers : this.config.defaultHeaders()) + .expect(opts?.status ? opts.status : 200) + } + + getTemplate = (opts?: TestAPIOpts) => { + return this.request + .get(`/api/global/template`) + .set(opts?.headers ? opts.headers : this.config.defaultHeaders()) + .expect(opts?.status ? opts.status : 200) + } + + saveTemplate = (data: any, opts?: TestAPIOpts) => { + return this.request + .post(`/api/global/template`) + .send(data) + .set(opts?.headers ? opts.headers : this.config.defaultHeaders()) + .expect(opts?.status ? opts.status : 200) + } +} diff --git a/packages/worker/src/tests/mocks/index.ts b/packages/worker/src/tests/mocks/index.ts index e4b68bbfd4..d11eee7452 100644 --- a/packages/worker/src/tests/mocks/index.ts +++ b/packages/worker/src/tests/mocks/index.ts @@ -1,7 +1,7 @@ const email = require("./email") -import { mocks as coreMocks } from "@budibase/backend-core/tests" +import { mocks } from "@budibase/backend-core/tests" export = { email, - ...coreMocks, + ...mocks, } diff --git a/packages/worker/src/tests/structures/groups.ts b/packages/worker/src/tests/structures/groups.ts index 874d1b6a10..0f7e518895 100644 --- a/packages/worker/src/tests/structures/groups.ts +++ b/packages/worker/src/tests/structures/groups.ts @@ -4,7 +4,7 @@ export const UserGroup = () => { color: "var(--spectrum-global-color-blue-600)", icon: "UserGroup", name: "New group", - roles: {}, + roles: { app_uuid1: "ADMIN", app_uuid2: "POWER" }, users: [], } return group diff --git a/packages/worker/src/tests/structures/users.ts b/packages/worker/src/tests/structures/users.ts index bef9f38586..3348670b7d 100644 --- a/packages/worker/src/tests/structures/users.ts +++ b/packages/worker/src/tests/structures/users.ts @@ -10,7 +10,7 @@ export const user = (userProps?: any): User => { return { email: newEmail(), password: "test", - roles: {}, + roles: { app_test: "admin" }, ...userProps, } } diff --git a/packages/worker/tsconfig.json b/packages/worker/tsconfig.json index 62d167075c..7807de725b 100644 --- a/packages/worker/tsconfig.json +++ b/packages/worker/tsconfig.json @@ -9,7 +9,8 @@ "@budibase/types": ["../types/src"], "@budibase/backend-core": ["../backend-core/src"], "@budibase/backend-core/*": ["../backend-core/*"], - "@budibase/pro": ["../../../budibase-pro/packages/pro/src"] + "@budibase/pro": ["../../../budibase-pro/packages/pro/src"], + "@budibase/pro/*": ["../../../budibase-pro/packages/pro/*"] } }, "ts-node": { @@ -25,7 +26,6 @@ "package.json" ], "exclude": [ - "node_modules", "dist" ] } \ No newline at end of file diff --git a/scripts/link-dependencies.sh b/scripts/link-dependencies.sh index 4485c4924f..d2a501162b 100755 --- a/scripts/link-dependencies.sh +++ b/scripts/link-dependencies.sh @@ -1,25 +1,30 @@ echo "Linking backend-core" cd packages/backend-core +yarn unlink yarn link cd - echo "Linking string-templates" -cd packages/string-templates +cd packages/string-templates +yarn unlink yarn link cd - echo "Linking types" -cd packages/types +cd packages/types +yarn unlink yarn link cd - echo "Linking bbui" -cd packages/bbui +cd packages/bbui +yarn unlink yarn link cd - echo "Linking frontend-core" cd packages/frontend-core +yarn unlink yarn link cd - @@ -30,6 +35,7 @@ if [ -d "../budibase-pro" ]; then cd packages/pro echo "Linking pro" + yarn unlink yarn link echo "Linking backend-core to pro"