diff --git a/packages/client/src/components/app/forms/RelationshipField.svelte b/packages/client/src/components/app/forms/RelationshipField.svelte index 54171309de..c40041c7d6 100644 --- a/packages/client/src/components/app/forms/RelationshipField.svelte +++ b/packages/client/src/components/app/forms/RelationshipField.svelte @@ -1,18 +1,15 @@ {#if fieldState} option.primaryDisplay} getOptionValue={option => option._id} + {options} {placeholder} + {autocomplete} bind:searchTerm - loading={$fetch.loading} bind:open + on:change={handleChange} + on:loadMore={() => fetch?.nextPage()} /> {/if} diff --git a/packages/frontend-core/src/fetch/GroupUserFetch.ts b/packages/frontend-core/src/fetch/GroupUserFetch.ts index 5b07e9decb..ca28a4dfa1 100644 --- a/packages/frontend-core/src/fetch/GroupUserFetch.ts +++ b/packages/frontend-core/src/fetch/GroupUserFetch.ts @@ -4,7 +4,7 @@ import { GroupUserDatasource, InternalTable } from "@budibase/types" interface GroupUserQuery { groupId: string - emailSearch: string + emailSearch?: string } interface GroupUserDefinition { diff --git a/packages/frontend-core/src/fetch/UserFetch.ts b/packages/frontend-core/src/fetch/UserFetch.ts index e72ed8997d..995071b587 100644 --- a/packages/frontend-core/src/fetch/UserFetch.ts +++ b/packages/frontend-core/src/fetch/UserFetch.ts @@ -9,8 +9,8 @@ import { } from "@budibase/types" interface UserFetchQuery { - appId: string - paginated: boolean + appId?: string + paginated?: boolean } interface UserDefinition { diff --git a/packages/server/package.json b/packages/server/package.json index 9a70ecba9c..38dd746c4f 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -156,6 +156,7 @@ "@types/pouchdb": "6.4.2", "@types/server-destroy": "1.0.1", "@types/supertest": "2.0.14", + "@types/swagger-jsdoc": "^6.0.4", "@types/tar": "6.1.5", "@types/tmp": "0.2.6", "@types/uuid": "8.3.4", diff --git a/packages/server/specs/generate.ts b/packages/server/specs/generate.ts index 8f6376195f..1176f187a7 100644 --- a/packages/server/specs/generate.ts +++ b/packages/server/specs/generate.ts @@ -4,11 +4,11 @@ import { examples, schemas } from "./resources" import * as parameters from "./parameters" import * as security from "./security" -const swaggerJsdoc = require("swagger-jsdoc") +import swaggerJsdoc from "swagger-jsdoc" const VARIABLES = {} -const options = { +const opts: swaggerJsdoc.Options = { definition: { openapi: "3.0.0", info: { @@ -58,30 +58,27 @@ const options = { } function writeFile(output: any, filename: string) { - try { - const path = join(__dirname, filename) - let spec = output - if (filename.endsWith("json")) { - spec = JSON.stringify(output, null, 2) - } - // input the static variables - for (let [key, replacement] of Object.entries(VARIABLES)) { - spec = spec.replace(new RegExp(`{${key}}`, "g"), replacement) - } - writeFileSync(path, spec) - console.log(`Wrote spec to ${path}`) - return path - } catch (err) { - console.error("Error writing spec file", err) + const path = join(__dirname, filename) + let spec = output + if (filename.endsWith("json")) { + spec = JSON.stringify(output, null, 2) } + // input the static variables + for (let [key, replacement] of Object.entries(VARIABLES)) { + spec = spec.replace(new RegExp(`{${key}}`, "g"), replacement) + } + writeFileSync(path, spec) + console.log(`Wrote spec to ${path}`) + return path +} + +export function spec() { + return swaggerJsdoc({ ...opts, format: ".json" }) } export function run() { - const outputJSON = swaggerJsdoc(options) - options.format = ".yaml" - const outputYAML = swaggerJsdoc(options) - writeFile(outputJSON, "openapi.json") - return writeFile(outputYAML, "openapi.yaml") + writeFile(swaggerJsdoc({ ...opts, format: ".json" }), "openapi.json") + return writeFile(swaggerJsdoc({ ...opts, format: ".yaml" }), "openapi.yaml") } if (require.main === module) { diff --git a/packages/server/specs/resources/error.ts b/packages/server/specs/resources/error.ts new file mode 100644 index 0000000000..0d42963b73 --- /dev/null +++ b/packages/server/specs/resources/error.ts @@ -0,0 +1,21 @@ +import { object } from "./utils" +import Resource from "./utils/Resource" + +const errorSchema = object({ + status: { + type: "number", + description: "The HTTP status code of the error.", + }, + message: { + type: "string", + description: "A descriptive message about the error.", + }, +}) + +export default new Resource() + .setExamples({ + error: {}, + }) + .setSchemas({ + error: errorSchema, + }) diff --git a/packages/server/src/api/controllers/integration.ts b/packages/server/src/api/controllers/integration.ts index f26f8dcc52..555bf72e53 100644 --- a/packages/server/src/api/controllers/integration.ts +++ b/packages/server/src/api/controllers/integration.ts @@ -9,6 +9,7 @@ import { const DISABLED_EXTERNAL_INTEGRATIONS = [ SourceName.AIRTABLE, SourceName.BUDIBASE, + SourceName.ARANGODB, ] export async function fetch(ctx: UserCtx) { diff --git a/packages/server/src/api/controllers/public/users.ts b/packages/server/src/api/controllers/public/users.ts index 4265c7ac22..c0cd3248a2 100644 --- a/packages/server/src/api/controllers/public/users.ts +++ b/packages/server/src/api/controllers/public/users.ts @@ -48,7 +48,7 @@ function getUser(ctx: UserCtx, userId?: string) { if (userId) { ctx.params = { userId } } else if (!ctx.params?.userId) { - throw "No user ID provided for getting" + throw new Error("No user ID provided for getting") } return readGlobalUser(ctx) } diff --git a/packages/server/src/api/routes/public/index.ts b/packages/server/src/api/routes/public/index.ts index 531192811c..8e4533f156 100644 --- a/packages/server/src/api/routes/public/index.ts +++ b/packages/server/src/api/routes/public/index.ts @@ -12,6 +12,7 @@ import { paramResource, paramSubResource } from "../../../middleware/resourceId" import { PermissionLevel, PermissionType } from "@budibase/types" import { CtxFn } from "./utils/Endpoint" import mapperMiddleware from "./middleware/mapper" +import testErrorHandling from "./middleware/testErrorHandling" import env from "../../../environment" import { middleware, redis } from "@budibase/backend-core" import { SelectableDatabase } from "@budibase/backend-core/src/redis/utils" @@ -144,6 +145,10 @@ function applyRoutes( // add the output mapper middleware addMiddleware(endpoints.read, mapperMiddleware, { output: true }) addMiddleware(endpoints.write, mapperMiddleware, { output: true }) + if (env.isTest()) { + addMiddleware(endpoints.read, testErrorHandling()) + addMiddleware(endpoints.write, testErrorHandling()) + } addToRouter(endpoints.read) addToRouter(endpoints.write) } diff --git a/packages/server/src/api/routes/public/middleware/testErrorHandling.ts b/packages/server/src/api/routes/public/middleware/testErrorHandling.ts new file mode 100644 index 0000000000..b22acae8a8 --- /dev/null +++ b/packages/server/src/api/routes/public/middleware/testErrorHandling.ts @@ -0,0 +1,28 @@ +import { Ctx } from "@budibase/types" +import environment from "../../../../environment" + +export default () => { + if (!environment.isTest()) { + throw new Error("This middleware is only for testing") + } + + return async (ctx: Ctx, next: any) => { + try { + await next() + } catch (err: any) { + if (!ctx.headers["x-budibase-include-stacktrace"]) { + throw err + } + + const status = err.status || err.statusCode || 500 + + let error = err + while (error.cause) { + error = error.cause + } + + ctx.status = status + ctx.body = { status, message: error.message, stack: error.stack } + } + } +} diff --git a/packages/server/src/api/routes/public/tests/compare.spec.ts b/packages/server/src/api/routes/public/tests/compare.spec.ts index ccf248f360..b4f5f20e2c 100644 --- a/packages/server/src/api/routes/public/tests/compare.spec.ts +++ b/packages/server/src/api/routes/public/tests/compare.spec.ts @@ -2,184 +2,174 @@ import jestOpenAPI from "jest-openapi" import { run as generateSchema } from "../../../../../specs/generate" import * as setup from "../../tests/utilities" import { generateMakeRequest } from "./utils" -import { Table, App, Row, User } from "@budibase/types" +import { Table, App, Row } from "@budibase/types" +import nock from "nock" +import environment from "../../../../environment" const yamlPath = generateSchema() jestOpenAPI(yamlPath!) -let config = setup.getConfig() -let apiKey: string, table: Table, app: App, makeRequest: any +describe("compare", () => { + let config = setup.getConfig() + let apiKey: string, table: Table, app: App, makeRequest: any -beforeAll(async () => { - app = await config.init() - table = await config.upsertTable() - apiKey = await config.generateApiKey() - makeRequest = generateMakeRequest(apiKey) -}) - -afterAll(setup.afterAll) - -describe("check the applications endpoints", () => { - it("should allow retrieving applications through search", async () => { - const res = await makeRequest("post", "/applications/search") - expect(res).toSatisfyApiSpec() - }) - - it("should allow creating an application", async () => { - const res = await makeRequest( - "post", - "/applications", - { - name: "new App", - }, - null - ) - expect(res).toSatisfyApiSpec() - }) - - it("should allow updating an application", async () => { - const app = config.getApp() - const appId = config.getAppId() - const res = await makeRequest( - "put", - `/applications/${appId}`, - { - ...app, - name: "updated app name", - }, - appId - ) - expect(res).toSatisfyApiSpec() - }) - - it("should allow retrieving an application", async () => { - const res = await makeRequest("get", `/applications/${config.getAppId()}`) - expect(res).toSatisfyApiSpec() - }) - - it("should allow deleting an application", async () => { - const res = await makeRequest( - "delete", - `/applications/${config.getAppId()}` - ) - expect(res).toSatisfyApiSpec() - }) -}) - -describe("check the tables endpoints", () => { - it("should allow retrieving tables through search", async () => { - await config.createApp("new app 1") + beforeAll(async () => { + app = await config.init() table = await config.upsertTable() - const res = await makeRequest("post", "/tables/search") - expect(res).toSatisfyApiSpec() + apiKey = await config.generateApiKey() + makeRequest = generateMakeRequest(apiKey) }) - it("should allow creating a table", async () => { - const res = await makeRequest("post", "/tables", { - name: "table name", - primaryDisplay: "column1", - schema: { - column1: { - type: "string", - constraints: {}, + afterAll(setup.afterAll) + + beforeEach(() => { + nock.cleanAll() + }) + + describe("check the applications endpoints", () => { + it("should allow retrieving applications through search", async () => { + const res = await makeRequest("post", "/applications/search") + expect(res).toSatisfyApiSpec() + }) + + it("should allow creating an application", async () => { + const res = await makeRequest( + "post", + "/applications", + { + name: "new App", }, - }, + null + ) + expect(res).toSatisfyApiSpec() }) - expect(res).toSatisfyApiSpec() - }) - it("should allow updating a table", async () => { - const updated = { ...table, _rev: undefined, name: "new name" } - const res = await makeRequest("put", `/tables/${table._id}`, updated) - expect(res).toSatisfyApiSpec() - }) - - it("should allow retrieving a table", async () => { - const res = await makeRequest("get", `/tables/${table._id}`) - expect(res).toSatisfyApiSpec() - }) - - it("should allow deleting a table", async () => { - const res = await makeRequest("delete", `/tables/${table._id}`) - expect(res).toSatisfyApiSpec() - }) -}) - -describe("check the rows endpoints", () => { - let row: Row - it("should allow retrieving rows through search", async () => { - table = await config.upsertTable() - const res = await makeRequest("post", `/tables/${table._id}/rows/search`, { - query: {}, + it("should allow updating an application", async () => { + const app = config.getApp() + const appId = config.getAppId() + const res = await makeRequest( + "put", + `/applications/${appId}`, + { + ...app, + name: "updated app name", + }, + appId + ) + expect(res).toSatisfyApiSpec() }) - expect(res).toSatisfyApiSpec() - }) - it("should allow creating a row", async () => { - const res = await makeRequest("post", `/tables/${table._id}/rows`, { - name: "test row", + it("should allow retrieving an application", async () => { + const res = await makeRequest("get", `/applications/${config.getAppId()}`) + expect(res).toSatisfyApiSpec() + }) + + it("should allow deleting an application", async () => { + nock(environment.WORKER_URL!) + .delete(`/api/global/roles/${config.getProdAppId()}`) + .reply(200, {}) + + const res = await makeRequest( + "delete", + `/applications/${config.getAppId()}` + ) + expect(res).toSatisfyApiSpec() }) - expect(res).toSatisfyApiSpec() - row = res.body.data }) - it("should allow updating a row", async () => { - const res = await makeRequest( - "put", - `/tables/${table._id}/rows/${row._id}`, - { - name: "test row updated", - } - ) - expect(res).toSatisfyApiSpec() + describe("check the tables endpoints", () => { + it("should allow retrieving tables through search", async () => { + await config.createApp("new app 1") + table = await config.upsertTable() + const res = await makeRequest("post", "/tables/search") + expect(res).toSatisfyApiSpec() + }) + + it("should allow creating a table", async () => { + const res = await makeRequest("post", "/tables", { + name: "table name", + primaryDisplay: "column1", + schema: { + column1: { + type: "string", + constraints: {}, + }, + }, + }) + expect(res).toSatisfyApiSpec() + }) + + it("should allow updating a table", async () => { + const updated = { ...table, _rev: undefined, name: "new name" } + const res = await makeRequest("put", `/tables/${table._id}`, updated) + expect(res).toSatisfyApiSpec() + }) + + it("should allow retrieving a table", async () => { + const res = await makeRequest("get", `/tables/${table._id}`) + expect(res).toSatisfyApiSpec() + }) + + it("should allow deleting a table", async () => { + const res = await makeRequest("delete", `/tables/${table._id}`) + expect(res).toSatisfyApiSpec() + }) }) - it("should allow retrieving a row", async () => { - const res = await makeRequest("get", `/tables/${table._id}/rows/${row._id}`) - expect(res).toSatisfyApiSpec() + describe("check the rows endpoints", () => { + let row: Row + it("should allow retrieving rows through search", async () => { + table = await config.upsertTable() + const res = await makeRequest( + "post", + `/tables/${table._id}/rows/search`, + { + query: {}, + } + ) + expect(res).toSatisfyApiSpec() + }) + + it("should allow creating a row", async () => { + const res = await makeRequest("post", `/tables/${table._id}/rows`, { + name: "test row", + }) + expect(res).toSatisfyApiSpec() + row = res.body.data + }) + + it("should allow updating a row", async () => { + const res = await makeRequest( + "put", + `/tables/${table._id}/rows/${row._id}`, + { + name: "test row updated", + } + ) + expect(res).toSatisfyApiSpec() + }) + + it("should allow retrieving a row", async () => { + const res = await makeRequest( + "get", + `/tables/${table._id}/rows/${row._id}` + ) + expect(res).toSatisfyApiSpec() + }) + + it("should allow deleting a row", async () => { + const res = await makeRequest( + "delete", + `/tables/${table._id}/rows/${row._id}` + ) + expect(res).toSatisfyApiSpec() + }) }) - it("should allow deleting a row", async () => { - const res = await makeRequest( - "delete", - `/tables/${table._id}/rows/${row._id}` - ) - expect(res).toSatisfyApiSpec() - }) -}) - -describe("check the users endpoints", () => { - let user: User - it("should allow retrieving users through search", async () => { - user = await config.createUser() - const res = await makeRequest("post", "/users/search") - expect(res).toSatisfyApiSpec() - }) - - it("should allow creating a user", async () => { - const res = await makeRequest("post", "/users") - expect(res).toSatisfyApiSpec() - }) - - it("should allow updating a user", async () => { - const res = await makeRequest("put", `/users/${user._id}`) - expect(res).toSatisfyApiSpec() - }) - - it("should allow retrieving a user", async () => { - const res = await makeRequest("get", `/users/${user._id}`) - expect(res).toSatisfyApiSpec() - }) - - it("should allow deleting a user", async () => { - const res = await makeRequest("delete", `/users/${user._id}`) - expect(res).toSatisfyApiSpec() - }) -}) - -describe("check the queries endpoints", () => { - it("should allow retrieving queries through search", async () => { - const res = await makeRequest("post", "/queries/search") - expect(res).toSatisfyApiSpec() + describe("check the queries endpoints", () => { + it("should allow retrieving queries through search", async () => { + const res = await makeRequest("post", "/queries/search") + expect(res).toSatisfyApiSpec() + }) }) }) diff --git a/packages/server/src/api/routes/public/tests/users.spec.ts b/packages/server/src/api/routes/public/tests/users.spec.ts index 4ca9ff8104..bb3e4b6fba 100644 --- a/packages/server/src/api/routes/public/tests/users.spec.ts +++ b/packages/server/src/api/routes/public/tests/users.spec.ts @@ -1,132 +1,143 @@ import * as setup from "../../tests/utilities" -import { generateMakeRequest, MakeRequestResponse } from "./utils" import { User } from "@budibase/types" -import { mocks } from "@budibase/backend-core/tests" +import { generator, mocks } from "@budibase/backend-core/tests" +import nock from "nock" +import TestConfiguration from "../../../../tests/utilities/TestConfiguration" +import { mockWorkerUserAPI } from "./utils" -import * as workerRequests from "../../../../utilities/workerRequests" +describe("public users API", () => { + const config = new TestConfiguration() + let globalUser: User -const mockedWorkerReq = jest.mocked(workerRequests) - -let config = setup.getConfig() -let apiKey: string, globalUser: User, makeRequest: MakeRequestResponse - -beforeAll(async () => { - await config.init() - globalUser = await config.globalUser() - apiKey = await config.generateApiKey(globalUser._id) - makeRequest = generateMakeRequest(apiKey) - mockedWorkerReq.readGlobalUser.mockImplementation(() => - Promise.resolve(globalUser) - ) -}) - -afterAll(setup.afterAll) - -function base() { - return { - tenantId: config.getTenantId(), - firstName: "Test", - lastName: "Test", - } -} - -function updateMock() { - mockedWorkerReq.readGlobalUser.mockImplementation(ctx => ctx.request.body) -} - -describe("check user endpoints", () => { - it("should not allow a user to update their own roles", async () => { - const res = await makeRequest("put", `/users/${globalUser._id}`, { - ...globalUser, - roles: { - app_1: "ADMIN", - }, - }) - expect( - mockedWorkerReq.saveGlobalUser.mock.lastCall?.[0].body.data.roles["app_1"] - ).toBeUndefined() - expect(res.status).toBe(200) - expect(res.body.data.roles["app_1"]).toBeUndefined() + beforeAll(async () => { + await config.init() }) - it("should not allow a user to delete themselves", async () => { - const res = await makeRequest("delete", `/users/${globalUser._id}`) - expect(res.status).toBe(405) - expect(mockedWorkerReq.deleteGlobalUser.mock.lastCall).toBeUndefined() - }) -}) - -describe("no user role update in free", () => { - beforeAll(() => { - updateMock() - }) - - it("should not allow 'roles' to be updated", async () => { - const res = await makeRequest("post", "/users", { - ...base(), - roles: { app_a: "BASIC" }, - }) - expect(res.status).toBe(200) - expect(res.body.data.roles["app_a"]).toBeUndefined() - expect(res.body.message).toBeDefined() - }) - - it("should not allow 'admin' to be updated", async () => { - const res = await makeRequest("post", "/users", { - ...base(), - admin: { global: true }, - }) - expect(res.status).toBe(200) - expect(res.body.data.admin).toBeUndefined() - expect(res.body.message).toBeDefined() - }) - - it("should not allow 'builder' to be updated", async () => { - const res = await makeRequest("post", "/users", { - ...base(), - builder: { global: true }, - }) - expect(res.status).toBe(200) - expect(res.body.data.builder).toBeUndefined() - expect(res.body.message).toBeDefined() - }) -}) - -describe("no user role update in business", () => { - beforeAll(() => { - updateMock() - mocks.licenses.useExpandedPublicApi() - }) - - it("should allow 'roles' to be updated", async () => { - const res = await makeRequest("post", "/users", { - ...base(), - roles: { app_a: "BASIC" }, - }) - expect(res.status).toBe(200) - expect(res.body.data.roles["app_a"]).toBe("BASIC") - expect(res.body.message).toBeUndefined() - }) - - it("should allow 'admin' to be updated", async () => { - mocks.licenses.useExpandedPublicApi() - const res = await makeRequest("post", "/users", { - ...base(), - admin: { global: true }, - }) - expect(res.status).toBe(200) - expect(res.body.data.admin.global).toBe(true) - expect(res.body.message).toBeUndefined() - }) - - it("should allow 'builder' to be updated", async () => { - mocks.licenses.useExpandedPublicApi() - const res = await makeRequest("post", "/users", { - ...base(), - builder: { global: true }, - }) - expect(res.status).toBe(200) - expect(res.body.data.builder.global).toBe(true) - expect(res.body.message).toBeUndefined() + afterAll(setup.afterAll) + + beforeEach(async () => { + globalUser = await config.globalUser() + + nock.cleanAll() + mockWorkerUserAPI(globalUser) + }) + + describe("read", () => { + it("should allow a user to read themselves", async () => { + const user = await config.api.user.find(globalUser._id!) + expect(user._id).toBe(globalUser._id) + }) + + it("should allow a user to read another user", async () => { + const otherUser = await config.api.public.user.create({ + email: generator.email({ domain: "example.com" }), + roles: {}, + }) + const user = await config.withUser(globalUser, () => + config.api.public.user.find(otherUser._id!) + ) + expect(user._id).toBe(otherUser._id) + }) + }) + + describe("create", () => { + it("can successfully create a new user", async () => { + const email = generator.email({ domain: "example.com" }) + const newUser = await config.api.public.user.create({ + email, + roles: {}, + }) + expect(newUser.email).toBe(email) + expect(newUser._id).toBeDefined() + }) + + describe("role creation on free tier", () => { + it("should not allow 'roles' to be updated", async () => { + const newUser = await config.api.public.user.create({ + email: generator.email({ domain: "example.com" }), + roles: { app_a: "BASIC" }, + }) + expect(newUser.roles["app_a"]).toBeUndefined() + }) + + it("should not allow 'admin' to be updated", async () => { + const newUser = await config.api.public.user.create({ + email: generator.email({ domain: "example.com" }), + roles: {}, + admin: { global: true }, + }) + expect(newUser.admin).toBeUndefined() + }) + + it("should not allow 'builder' to be updated", async () => { + const newUser = await config.api.public.user.create({ + email: generator.email({ domain: "example.com" }), + roles: {}, + builder: { global: true }, + }) + expect(newUser.builder).toBeUndefined() + }) + }) + + describe("role creation on business tier", () => { + beforeAll(() => { + mocks.licenses.useExpandedPublicApi() + }) + + it("should allow 'roles' to be updated", async () => { + const newUser = await config.api.public.user.create({ + email: generator.email({ domain: "example.com" }), + roles: { app_a: "BASIC" }, + }) + expect(newUser.roles["app_a"]).toBe("BASIC") + }) + + it("should allow 'admin' to be updated", async () => { + const newUser = await config.api.public.user.create({ + email: generator.email({ domain: "example.com" }), + roles: {}, + admin: { global: true }, + }) + expect(newUser.admin?.global).toBe(true) + }) + + it("should allow 'builder' to be updated", async () => { + const newUser = await config.api.public.user.create({ + email: generator.email({ domain: "example.com" }), + roles: {}, + builder: { global: true }, + }) + expect(newUser.builder?.global).toBe(true) + }) + }) + }) + + describe("update", () => { + it("can update a user", async () => { + const updatedUser = await config.api.public.user.update({ + ...globalUser, + email: `updated-${globalUser.email}`, + }) + expect(updatedUser.email).toBe(`updated-${globalUser.email}`) + }) + + it("should not allow a user to update their own roles", async () => { + await config.withUser(globalUser, () => + config.api.public.user.update({ + ...globalUser, + roles: { app_1: "ADMIN" }, + }) + ) + const updatedUser = await config.api.user.find(globalUser._id!) + expect(updatedUser.roles?.app_1).toBeUndefined() + }) + }) + + describe("delete", () => { + it("should not allow a user to delete themselves", async () => { + await config.withUser(globalUser, () => + config.api.public.user.destroy(globalUser._id!, { status: 405 }) + ) + }) }) }) diff --git a/packages/server/src/api/routes/public/tests/utils.ts b/packages/server/src/api/routes/public/tests/utils.ts index 4fb048a540..4985c0db58 100644 --- a/packages/server/src/api/routes/public/tests/utils.ts +++ b/packages/server/src/api/routes/public/tests/utils.ts @@ -1,6 +1,10 @@ import * as setup from "../../tests/utilities" import { checkSlashesInUrl } from "../../../../utilities" import supertest from "supertest" +import { User } from "@budibase/types" +import environment from "../../../../environment" +import nock from "nock" +import { generator } from "@budibase/backend-core/tests" export type HttpMethod = "post" | "get" | "put" | "delete" | "patch" @@ -91,3 +95,43 @@ export function generateMakeRequestWithFormData( return res } } + +export function mockWorkerUserAPI(...seedUsers: User[]) { + const users: Record = { + ...seedUsers.reduce((acc, user) => { + acc[user._id!] = user + return acc + }, {} as Record), + } + + nock(environment.WORKER_URL!) + .get(new RegExp(`/api/global/users/.*`)) + .reply(200, (uri, body) => { + const id = uri.split("/").pop() + return users[id!] + }) + .persist() + + nock(environment.WORKER_URL!) + .post(`/api/global/users`) + .reply(200, (uri, body) => { + const newUser = body as User + if (!newUser._id) { + newUser._id = `us_${generator.guid()}` + } + users[newUser._id!] = newUser + return newUser + }) + .persist() + + nock(environment.WORKER_URL!) + .put(new RegExp(`/api/global/users/.*`)) + .reply(200, (uri, body) => { + const id = uri.split("/").pop()! + const updatedUser = body as User + const existingUser = users[id] || {} + users[id] = { ...existingUser, ...updatedUser } + return users[id] + }) + .persist() +} diff --git a/packages/server/src/api/routes/tests/environmentVariables.spec.ts b/packages/server/src/api/routes/tests/environmentVariables.spec.ts index beb6012c9c..c663e2e26e 100644 --- a/packages/server/src/api/routes/tests/environmentVariables.spec.ts +++ b/packages/server/src/api/routes/tests/environmentVariables.spec.ts @@ -1,153 +1,106 @@ -const pg = require("pg") - -jest.mock("pg", () => { - return { - Client: jest.fn().mockImplementation(() => ({ - connect: jest.fn(), - query: jest.fn().mockImplementation(() => ({ rows: [] })), - end: jest.fn().mockImplementation((fn: any) => fn()), - })), - queryMock: jest.fn().mockImplementation(() => {}), - on: jest.fn(), - } -}) -import * as setup from "./utilities" +import { structures } from "./utilities" import { mocks } from "@budibase/backend-core/tests" -import { env, events } from "@budibase/backend-core" -import { QueryPreview } from "@budibase/types" +import { setEnv } from "@budibase/backend-core" +import { Datasource } from "@budibase/types" +import TestConfiguration from "../../../tests/utilities/TestConfiguration" +import { + DatabaseName, + datasourceDescribe, +} from "../../../integrations/tests/utils" -const structures = setup.structures +const describes = datasourceDescribe({ only: [DatabaseName.POSTGRES] }) -env._set("ENCRYPTION_KEY", "budibase") -mocks.licenses.useEnvironmentVariables() +if (describes.length > 0) { + describe.each(describes)("/api/env/variables", ({ dsProvider }) => { + const config = new TestConfiguration() -describe("/api/env/variables", () => { - let request = setup.getRequest() - let config = setup.getConfig() + let rawDatasource: Datasource + let restoreEnv: () => void - afterAll(setup.afterAll) + beforeAll(async () => { + await config.init() + restoreEnv = setEnv({ ENCRYPTION_KEY: "budibase" }) + mocks.licenses.useEnvironmentVariables() - beforeAll(async () => { - await config.init() - }) + const ds = await dsProvider() + rawDatasource = ds.rawDatasource! + }) - it("should be able check the status of env var API", async () => { - const res = await request - .get(`/api/env/variables/status`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + afterAll(() => { + restoreEnv() + }) - expect(res.body.encryptionKeyAvailable).toEqual(true) - }) - - it("should be able to create an environment variable", async () => { - await request - .post(`/api/env/variables`) - .send(structures.basicEnvironmentVariable("test", "test")) - .set(config.defaultHeaders()) - .expect(200) - }) - - it("should be able to fetch the 'test' variable name", async () => { - const res = await request - .get(`/api/env/variables`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(res.body.variables.length).toEqual(1) - expect(res.body.variables[0]).toEqual("test") - }) - - it("should be able to update the environment variable 'test'", async () => { - const varName = "test" - await request - .patch(`/api/env/variables/${varName}`) - .send(structures.basicEnvironmentVariable("test", "test1")) - .set(config.defaultHeaders()) - .expect(200) - }) - - it("should be able to delete the environment variable 'test'", async () => { - const varName = "test" - await request - .delete(`/api/env/variables/${varName}`) - .set(config.defaultHeaders()) - .expect(200) - }) - - it("should create a datasource (using the environment variable) and query", async () => { - const datasourceBase = structures.basicDatasource() - await request - .post(`/api/env/variables`) - .send(structures.basicEnvironmentVariable("test", "test")) - .set(config.defaultHeaders()) - - datasourceBase.datasource.config = { - password: "{{ env.test }}", - } - const response = await request - .post(`/api/datasources`) - .send(datasourceBase) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(response.body.datasource._id).toBeDefined() - - const response2 = await request - .post(`/api/queries`) - .send(structures.basicQuery(response.body.datasource._id)) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(response2.body._id).toBeDefined() - }) - - it("should run a query preview and check the mocked results", async () => { - const datasourceBase = structures.basicDatasource() - await request - .post(`/api/env/variables`) - .send(structures.basicEnvironmentVariable("test", "test")) - .set(config.defaultHeaders()) - - datasourceBase.datasource.config = { - password: "{{ env.test }}", - } - const response = await request - .post(`/api/datasources`) - .send(datasourceBase) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(response.body.datasource._id).toBeDefined() - - const queryPreview: QueryPreview = { - datasourceId: response.body.datasource._id, - parameters: [], - fields: {}, - queryVerb: "read", - name: response.body.datasource.name, - transformer: null, - schema: {}, - readable: true, - } - const res = await request - .post(`/api/queries/preview`) - .send(queryPreview) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(res.body.rows.length).toEqual(0) - expect(events.query.previewed).toHaveBeenCalledTimes(1) - // API doesn't include config in response - delete response.body.datasource.config - expect(events.query.previewed).toHaveBeenCalledWith( - response.body.datasource, - { - ...queryPreview, - nullDefaultSupport: true, + beforeEach(async () => { + const { variables } = await config.api.environment.fetch() + for (const variable of variables) { + await config.api.environment.destroy(variable) } - ) - expect(pg.Client).toHaveBeenCalledWith({ password: "test", ssl: undefined }) + + await config.api.environment.create({ + name: "test", + production: rawDatasource.config!.password, + development: rawDatasource.config!.password, + }) + }) + + it("should be able check the status of env var API", async () => { + const { encryptionKeyAvailable } = await config.api.environment.status() + expect(encryptionKeyAvailable).toEqual(true) + }) + + it("should be able to fetch the 'test' variable name", async () => { + const { variables } = await config.api.environment.fetch() + expect(variables.length).toEqual(1) + expect(variables[0]).toEqual("test") + }) + + it("should be able to update the environment variable 'test'", async () => { + await config.api.environment.update("test", { + production: "test1", + development: "test1", + }) + }) + + it("should be able to delete the environment variable 'test'", async () => { + await config.api.environment.destroy("test") + }) + + it("should create a datasource (using the environment variable) and query", async () => { + const datasource = await config.api.datasource.create({ + ...structures.basicDatasource().datasource, + config: { + ...rawDatasource.config, + password: "{{ env.test }}", + }, + }) + + const query = await config.api.query.save({ + ...structures.basicQuery(datasource._id!), + fields: { sql: "SELECT 1" }, + }) + expect(query._id).toBeDefined() + }) + + it("should run a query preview and check the mocked results", async () => { + const datasource = await config.api.datasource.create({ + ...structures.basicDatasource().datasource, + config: { + ...rawDatasource.config, + password: "{{ env.test }}", + }, + }) + + const query = await config.api.query.save({ + ...structures.basicQuery(datasource._id!), + fields: { sql: "SELECT 1 as id" }, + }) + + const { rows } = await config.api.query.preview({ + ...query, + queryId: query._id!, + }) + + expect(rows).toEqual([{ id: 1 }]) + }) }) -}) +} diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index caa651f3bb..e115297ee9 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -6,6 +6,7 @@ import { docIds, MAX_VALID_DATE, MIN_VALID_DATE, + setEnv, SQLITE_DESIGN_DOC_ID, utils, withEnv as withCoreEnv, @@ -43,19 +44,7 @@ import { generator, structures, mocks } from "@budibase/backend-core/tests" import { DEFAULT_EMPLOYEE_TABLE_SCHEMA } from "../../../db/defaultData/datasource_bb_default" import { generateRowIdField } from "../../../integrations/utils" import { cloneDeep } from "lodash/fp" - -jest.mock("@budibase/pro", () => ({ - ...jest.requireActual("@budibase/pro"), - ai: { - LargeLanguageModel: { - forCurrentTenant: async () => ({ - llm: {}, - run: jest.fn(() => `Mock LLM Response`), - buildPromptFromAIOperation: jest.fn(), - }), - }, - }, -})) +import { mockChatGPTResponse } from "../../../tests/utilities/mocks/openai" const descriptions = datasourceDescribe({ plus: true }) @@ -1896,11 +1885,15 @@ if (descriptions.length) { !isInMemory && describe("AI Column", () => { const UNEXISTING_AI_COLUMN = "Real LLM Response" + let envCleanup: () => void beforeAll(async () => { mocks.licenses.useBudibaseAI() mocks.licenses.useAICustomConfigs() + envCleanup = setEnv({ OPENAI_API_KEY: "mock" }) + mockChatGPTResponse("Mock LLM Response") + tableOrViewId = await createTableOrView({ product: { name: "product", type: FieldType.STRING }, ai: { @@ -1917,6 +1910,10 @@ if (descriptions.length) { ]) }) + afterAll(() => { + envCleanup() + }) + describe("equal", () => { it("successfully finds rows based on AI column", async () => { await expectQuery({ diff --git a/packages/server/src/api/routes/tests/utilities/index.ts b/packages/server/src/api/routes/tests/utilities/index.ts index dcb8ccd6c0..944a56d7ba 100644 --- a/packages/server/src/api/routes/tests/utilities/index.ts +++ b/packages/server/src/api/routes/tests/utilities/index.ts @@ -3,44 +3,6 @@ import supertest from "supertest" export * as structures from "../../../../tests/utilities/structures" -function user() { - return { - _id: "user", - _rev: "rev", - createdAt: Date.now(), - email: "test@example.com", - roles: {}, - tenantId: "default", - status: "active", - } -} - -jest.mock("../../../../utilities/workerRequests", () => ({ - getGlobalUsers: jest.fn(() => { - return { - _id: "us_uuid1", - } - }), - getGlobalSelf: jest.fn(() => { - return { - _id: "us_uuid1", - } - }), - allGlobalUsers: jest.fn(() => { - return [user()] - }), - readGlobalUser: jest.fn(() => { - return user() - }), - saveGlobalUser: jest.fn(() => { - return { _id: "user", _rev: "rev" } - }), - deleteGlobalUser: jest.fn(() => { - return { message: "deleted user" } - }), - removeAppFromUserRoles: jest.fn(), -})) - export function delay(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)) } diff --git a/packages/server/src/automations/tests/steps/bash.spec.ts b/packages/server/src/automations/tests/steps/bash.spec.ts index a2172c3578..79ddf67a75 100644 --- a/packages/server/src/automations/tests/steps/bash.spec.ts +++ b/packages/server/src/automations/tests/steps/bash.spec.ts @@ -16,6 +16,7 @@ describe("Execute Bash Automations", () => { name: "test row", description: "test description", }) + await config.api.automation.deleteAll() }) afterAll(() => { diff --git a/packages/server/src/automations/tests/steps/createRow.spec.ts b/packages/server/src/automations/tests/steps/createRow.spec.ts index 01ce227f36..6da142434a 100644 --- a/packages/server/src/automations/tests/steps/createRow.spec.ts +++ b/packages/server/src/automations/tests/steps/createRow.spec.ts @@ -33,6 +33,7 @@ describe("test the create row action", () => { name: "test", description: "test", } + await config.api.automation.deleteAll() }) afterAll(() => { diff --git a/packages/server/src/automations/tests/steps/delay.spec.ts b/packages/server/src/automations/tests/steps/delay.spec.ts index 173beccbda..03f6a528a6 100644 --- a/packages/server/src/automations/tests/steps/delay.spec.ts +++ b/packages/server/src/automations/tests/steps/delay.spec.ts @@ -6,6 +6,7 @@ describe("test the delay logic", () => { beforeAll(async () => { await config.init() + await config.api.automation.deleteAll() }) afterAll(() => { diff --git a/packages/server/src/automations/tests/steps/deleteRow.spec.ts b/packages/server/src/automations/tests/steps/deleteRow.spec.ts index 8c141f82da..5ec5e6035e 100644 --- a/packages/server/src/automations/tests/steps/deleteRow.spec.ts +++ b/packages/server/src/automations/tests/steps/deleteRow.spec.ts @@ -13,6 +13,7 @@ describe("test the delete row action", () => { await config.init() table = await config.api.table.save(basicTable()) row = await config.api.row.save(table._id!, {}) + await config.api.automation.deleteAll() }) afterAll(() => { diff --git a/packages/server/src/automations/tests/steps/discord.spec.ts b/packages/server/src/automations/tests/steps/discord.spec.ts index 9618a0c994..8e2af09721 100644 --- a/packages/server/src/automations/tests/steps/discord.spec.ts +++ b/packages/server/src/automations/tests/steps/discord.spec.ts @@ -7,6 +7,7 @@ describe("test the outgoing webhook action", () => { beforeAll(async () => { await config.init() + await config.api.automation.deleteAll() }) afterAll(() => { diff --git a/packages/server/src/automations/tests/steps/executeQuery.spec.ts b/packages/server/src/automations/tests/steps/executeQuery.spec.ts index a51d335902..d65c30c789 100644 --- a/packages/server/src/automations/tests/steps/executeQuery.spec.ts +++ b/packages/server/src/automations/tests/steps/executeQuery.spec.ts @@ -26,6 +26,7 @@ if (descriptions.length) { const ds = await dsProvider() datasource = ds.datasource! client = ds.client! + await config.api.automation.deleteAll() }) beforeEach(async () => { diff --git a/packages/server/src/automations/tests/steps/executeScript.spec.ts b/packages/server/src/automations/tests/steps/executeScript.spec.ts index 117c2341ba..ea5b8dc6b2 100644 --- a/packages/server/src/automations/tests/steps/executeScript.spec.ts +++ b/packages/server/src/automations/tests/steps/executeScript.spec.ts @@ -13,6 +13,7 @@ describe("Execute Script Automations", () => { await config.init() table = await config.api.table.save(basicTable()) await config.api.row.save(table._id!, {}) + await config.api.automation.deleteAll() }) afterAll(() => { diff --git a/packages/server/src/automations/tests/steps/filter.spec.ts b/packages/server/src/automations/tests/steps/filter.spec.ts index da1f6e4702..ba7fb3e1b7 100644 --- a/packages/server/src/automations/tests/steps/filter.spec.ts +++ b/packages/server/src/automations/tests/steps/filter.spec.ts @@ -26,6 +26,7 @@ describe("test the filter logic", () => { beforeAll(async () => { await config.init() + await config.api.automation.deleteAll() }) afterAll(() => { diff --git a/packages/server/src/automations/tests/steps/loop.spec.ts b/packages/server/src/automations/tests/steps/loop.spec.ts index 2bdf33b253..34fc175c71 100644 --- a/packages/server/src/automations/tests/steps/loop.spec.ts +++ b/packages/server/src/automations/tests/steps/loop.spec.ts @@ -22,10 +22,7 @@ describe("Attempt to run a basic loop automation", () => { }) beforeEach(async () => { - const { automations } = await config.api.automation.fetch() - for (const automation of automations) { - await config.api.automation.delete(automation) - } + await config.api.automation.deleteAll() table = await config.api.table.save(basicTable()) await config.api.row.save(table._id!, {}) diff --git a/packages/server/src/automations/tests/steps/make.spec.ts b/packages/server/src/automations/tests/steps/make.spec.ts index bbc0c3791a..3f473560ed 100644 --- a/packages/server/src/automations/tests/steps/make.spec.ts +++ b/packages/server/src/automations/tests/steps/make.spec.ts @@ -7,6 +7,7 @@ describe("test the outgoing webhook action", () => { beforeAll(async () => { await config.init() + await config.api.automation.deleteAll() }) afterAll(() => { diff --git a/packages/server/src/automations/tests/steps/n8n.spec.ts b/packages/server/src/automations/tests/steps/n8n.spec.ts index 4ee3123d98..754015baa1 100644 --- a/packages/server/src/automations/tests/steps/n8n.spec.ts +++ b/packages/server/src/automations/tests/steps/n8n.spec.ts @@ -8,6 +8,7 @@ describe("test the outgoing webhook action", () => { beforeAll(async () => { await config.init() + await config.api.automation.deleteAll() }) afterAll(() => { diff --git a/packages/server/src/automations/tests/steps/openai.spec.ts b/packages/server/src/automations/tests/steps/openai.spec.ts index d5f002571d..a06c633e5e 100644 --- a/packages/server/src/automations/tests/steps/openai.spec.ts +++ b/packages/server/src/automations/tests/steps/openai.spec.ts @@ -16,6 +16,7 @@ describe("test the openai action", () => { beforeAll(async () => { await config.init() + await config.api.automation.deleteAll() }) beforeEach(() => { diff --git a/packages/server/src/automations/tests/steps/outgoingWebhook.spec.ts b/packages/server/src/automations/tests/steps/outgoingWebhook.spec.ts index 85ccfb8eac..a62bb4721a 100644 --- a/packages/server/src/automations/tests/steps/outgoingWebhook.spec.ts +++ b/packages/server/src/automations/tests/steps/outgoingWebhook.spec.ts @@ -8,6 +8,7 @@ describe("test the outgoing webhook action", () => { beforeAll(async () => { await config.init() + await config.api.automation.deleteAll() }) afterAll(() => { diff --git a/packages/server/src/automations/tests/steps/queryRows.spec.ts b/packages/server/src/automations/tests/steps/queryRows.spec.ts index f6d756e770..66b445a69b 100644 --- a/packages/server/src/automations/tests/steps/queryRows.spec.ts +++ b/packages/server/src/automations/tests/steps/queryRows.spec.ts @@ -21,6 +21,7 @@ describe("Test a query step automation", () => { } await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row) + await config.api.automation.deleteAll() }) afterAll(() => { diff --git a/packages/server/src/automations/tests/steps/sendSmtpEmail.spec.ts b/packages/server/src/automations/tests/steps/sendSmtpEmail.spec.ts index 7aff612a97..821d7a1326 100644 --- a/packages/server/src/automations/tests/steps/sendSmtpEmail.spec.ts +++ b/packages/server/src/automations/tests/steps/sendSmtpEmail.spec.ts @@ -28,6 +28,7 @@ describe("test the outgoing webhook action", () => { beforeAll(async () => { await config.init() + await config.api.automation.deleteAll() }) afterAll(() => { diff --git a/packages/server/src/automations/tests/steps/serverLog.spec.ts b/packages/server/src/automations/tests/steps/serverLog.spec.ts index 82f097d0da..4ddcdf4d9a 100644 --- a/packages/server/src/automations/tests/steps/serverLog.spec.ts +++ b/packages/server/src/automations/tests/steps/serverLog.spec.ts @@ -6,6 +6,7 @@ describe("test the server log action", () => { beforeAll(async () => { await config.init() + await config.api.automation.deleteAll() }) afterAll(() => { diff --git a/packages/server/src/automations/tests/steps/triggerAutomationRun.spec.ts b/packages/server/src/automations/tests/steps/triggerAutomationRun.spec.ts index 8d4a29c2b6..44aee65e61 100644 --- a/packages/server/src/automations/tests/steps/triggerAutomationRun.spec.ts +++ b/packages/server/src/automations/tests/steps/triggerAutomationRun.spec.ts @@ -9,6 +9,7 @@ describe("Test triggering an automation from another automation", () => { beforeAll(async () => { await automation.init() await config.init() + await config.api.automation.deleteAll() }) afterAll(async () => { diff --git a/packages/server/src/automations/tests/steps/updateRow.spec.ts b/packages/server/src/automations/tests/steps/updateRow.spec.ts index a2f1825099..833ff99bfa 100644 --- a/packages/server/src/automations/tests/steps/updateRow.spec.ts +++ b/packages/server/src/automations/tests/steps/updateRow.spec.ts @@ -23,6 +23,7 @@ describe("test the update row action", () => { await config.init() table = await config.createTable() row = await config.createRow() + await config.api.automation.deleteAll() }) afterAll(() => { diff --git a/packages/server/src/automations/tests/steps/zapier.spec.ts b/packages/server/src/automations/tests/steps/zapier.spec.ts index e6b5417563..436738388f 100644 --- a/packages/server/src/automations/tests/steps/zapier.spec.ts +++ b/packages/server/src/automations/tests/steps/zapier.spec.ts @@ -7,6 +7,7 @@ describe("test the outgoing webhook action", () => { beforeAll(async () => { await config.init() + await config.api.automation.deleteAll() }) afterAll(() => { diff --git a/packages/server/src/automations/tests/triggers/appAction.spec.ts b/packages/server/src/automations/tests/triggers/appAction.spec.ts index 2247868c44..97c34b35dc 100644 --- a/packages/server/src/automations/tests/triggers/appAction.spec.ts +++ b/packages/server/src/automations/tests/triggers/appAction.spec.ts @@ -9,6 +9,8 @@ describe("app action trigger", () => { beforeAll(async () => { await config.init() + await config.api.automation.deleteAll() + automation = await createAutomationBuilder(config) .onAppAction() .serverLog({ diff --git a/packages/server/src/automations/tests/triggers/cron.spec.ts b/packages/server/src/automations/tests/triggers/cron.spec.ts index 8db9cb425e..ae6652033e 100644 --- a/packages/server/src/automations/tests/triggers/cron.spec.ts +++ b/packages/server/src/automations/tests/triggers/cron.spec.ts @@ -16,6 +16,7 @@ describe("cron trigger", () => { beforeAll(async () => { await config.init() + await config.api.automation.deleteAll() }) afterAll(() => { diff --git a/packages/server/src/automations/tests/triggers/rowDeleted.spec.ts b/packages/server/src/automations/tests/triggers/rowDeleted.spec.ts index df6b28b31d..a93e0f8683 100644 --- a/packages/server/src/automations/tests/triggers/rowDeleted.spec.ts +++ b/packages/server/src/automations/tests/triggers/rowDeleted.spec.ts @@ -11,6 +11,7 @@ describe("row deleted trigger", () => { beforeAll(async () => { await config.init() + await config.api.automation.deleteAll() table = await config.api.table.save(basicTable()) automation = await createAutomationBuilder(config) .onRowDeleted({ tableId: table._id! }) diff --git a/packages/server/src/automations/tests/triggers/rowSaved.spec.ts b/packages/server/src/automations/tests/triggers/rowSaved.spec.ts index 874abb8872..573a8dd392 100644 --- a/packages/server/src/automations/tests/triggers/rowSaved.spec.ts +++ b/packages/server/src/automations/tests/triggers/rowSaved.spec.ts @@ -11,6 +11,7 @@ describe("row saved trigger", () => { beforeAll(async () => { await config.init() + await config.api.automation.deleteAll() table = await config.api.table.save(basicTable()) automation = await createAutomationBuilder(config) .onRowSaved({ tableId: table._id! }) diff --git a/packages/server/src/automations/tests/triggers/rowUpdated.spec.ts b/packages/server/src/automations/tests/triggers/rowUpdated.spec.ts index 672335f65a..bb896636a6 100644 --- a/packages/server/src/automations/tests/triggers/rowUpdated.spec.ts +++ b/packages/server/src/automations/tests/triggers/rowUpdated.spec.ts @@ -11,6 +11,7 @@ describe("row updated trigger", () => { beforeAll(async () => { await config.init() + await config.api.automation.deleteAll() table = await config.api.table.save(basicTable()) automation = await createAutomationBuilder(config) .onRowUpdated({ tableId: table._id! }) diff --git a/packages/server/src/automations/tests/triggers/webhook.spec.ts b/packages/server/src/automations/tests/triggers/webhook.spec.ts index 9649846830..77d63a7ffa 100644 --- a/packages/server/src/automations/tests/triggers/webhook.spec.ts +++ b/packages/server/src/automations/tests/triggers/webhook.spec.ts @@ -37,6 +37,7 @@ describe("Webhook trigger test", () => { beforeEach(async () => { await config.init() + await config.api.automation.deleteAll() table = await config.createTable() }) diff --git a/packages/server/src/integrations/arangodb.ts b/packages/server/src/integrations/arangodb.ts index 0127d25632..76a5fb1d69 100644 --- a/packages/server/src/integrations/arangodb.ts +++ b/packages/server/src/integrations/arangodb.ts @@ -9,6 +9,11 @@ import { import { Database, aql } from "arangojs" +/** + * @deprecated 3rd March 2025 + * datasource disabled - this datasource is marked for deprecation and removal + */ + interface ArangodbConfig { url: string username: string diff --git a/packages/server/src/integrations/index.ts b/packages/server/src/integrations/index.ts index de700d631d..de05e300e6 100644 --- a/packages/server/src/integrations/index.ts +++ b/packages/server/src/integrations/index.ts @@ -33,15 +33,17 @@ const DEFINITIONS: Record = { [SourceName.COUCHDB]: couchdb.schema, [SourceName.SQL_SERVER]: sqlServer.schema, [SourceName.S3]: s3.schema, - [SourceName.AIRTABLE]: airtable.schema, [SourceName.MYSQL]: mysql.schema, - [SourceName.ARANGODB]: arangodb.schema, [SourceName.REST]: rest.schema, [SourceName.FIRESTORE]: firebase.schema, [SourceName.GOOGLE_SHEETS]: googlesheets.schema, [SourceName.REDIS]: redis.schema, [SourceName.SNOWFLAKE]: snowflake.schema, [SourceName.ORACLE]: oracle.schema, + /* deprecated - not available through UI */ + [SourceName.ARANGODB]: arangodb.schema, + [SourceName.AIRTABLE]: airtable.schema, + /* un-used */ [SourceName.BUDIBASE]: undefined, } @@ -56,15 +58,17 @@ const INTEGRATIONS: Record = [SourceName.COUCHDB]: couchdb.integration, [SourceName.SQL_SERVER]: sqlServer.integration, [SourceName.S3]: s3.integration, - [SourceName.AIRTABLE]: airtable.integration, [SourceName.MYSQL]: mysql.integration, - [SourceName.ARANGODB]: arangodb.integration, [SourceName.REST]: rest.integration, [SourceName.FIRESTORE]: firebase.integration, [SourceName.GOOGLE_SHEETS]: googlesheets.integration, [SourceName.REDIS]: redis.integration, [SourceName.SNOWFLAKE]: snowflake.integration, [SourceName.ORACLE]: oracle.integration, + /* deprecated - not available through UI */ + [SourceName.ARANGODB]: arangodb.integration, + [SourceName.AIRTABLE]: airtable.integration, + /* un-used */ [SourceName.BUDIBASE]: undefined, } diff --git a/packages/server/src/integrations/tests/airtable.spec.ts b/packages/server/src/integrations/tests/airtable.spec.ts deleted file mode 100644 index 367e31e8a0..0000000000 --- a/packages/server/src/integrations/tests/airtable.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { default as AirtableIntegration } from "../airtable" - -jest.mock("airtable") - -class TestConfiguration { - integration: any - client: any - - constructor(config: any = {}) { - this.integration = new AirtableIntegration.integration(config) - this.client = { - create: jest.fn(), - select: jest.fn(() => ({ - firstPage: jest.fn(() => []), - })), - update: jest.fn(), - destroy: jest.fn(), - } - this.integration.client = () => this.client - } -} - -describe("Airtable Integration", () => { - let config: any - - beforeEach(() => { - config = new TestConfiguration() - }) - - it("calls the create method with the correct params", async () => { - await config.integration.create({ - table: "test", - json: {}, - }) - expect(config.client.create).toHaveBeenCalledWith([ - { - fields: {}, - }, - ]) - }) - - it("calls the read method with the correct params", async () => { - await config.integration.read({ - table: "test", - view: "Grid view", - }) - expect(config.client.select).toHaveBeenCalledWith({ - maxRecords: 10, - view: "Grid view", - }) - }) - - it("calls the update method with the correct params", async () => { - await config.integration.update({ - table: "table", - id: "123", - json: { - name: "test", - }, - }) - expect(config.client.update).toHaveBeenCalledWith([ - { - id: "123", - fields: { name: "test" }, - }, - ]) - }) - - it("calls the delete method with the correct params", async () => { - const ids = [1, 2, 3, 4] - await config.integration.delete({ - ids, - }) - expect(config.client.destroy).toHaveBeenCalledWith(ids) - }) -}) diff --git a/packages/server/src/integrations/tests/arangodb.spec.ts b/packages/server/src/integrations/tests/arangodb.spec.ts deleted file mode 100644 index 6ac242d8db..0000000000 --- a/packages/server/src/integrations/tests/arangodb.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { default as ArangoDBIntegration } from "../arangodb" - -jest.mock("arangojs") - -class TestConfiguration { - integration: any - - constructor(config: any = {}) { - this.integration = new ArangoDBIntegration.integration(config) - } -} - -describe("ArangoDB Integration", () => { - let config: any - - beforeEach(() => { - config = new TestConfiguration() - }) - - it("calls the create method with the correct params", async () => { - const body = { - json: "Hello", - } - - await config.integration.create(body) - expect(config.integration.client.query).toHaveBeenCalledWith( - `INSERT Hello INTO collection RETURN NEW` - ) - }) - - it("calls the read method with the correct params", async () => { - const query = { - sql: `test`, - } - await config.integration.read(query) - expect(config.integration.client.query).toHaveBeenCalledWith(query.sql) - }) -}) diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index edb397169d..219a428f05 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -67,6 +67,7 @@ import { View, Webhook, WithRequired, + DevInfo, } from "@budibase/types" import API from "./api" @@ -248,7 +249,7 @@ export default class TestConfiguration { } } - async withUser(user: User, f: () => Promise) { + async withUser(user: User, f: () => Promise): Promise { const oldUser = this.user this.user = user try { @@ -469,7 +470,10 @@ export default class TestConfiguration { } } - defaultHeaders(extras = {}, prodApp = false) { + defaultHeaders( + extras: Record = {}, + prodApp = false + ) { const tenantId = this.getTenantId() const user = this.getUser() const authObj: AuthToken = { @@ -498,10 +502,13 @@ export default class TestConfiguration { } } - publicHeaders({ prodApp = true } = {}) { + publicHeaders({ + prodApp = true, + extras = {}, + }: { prodApp?: boolean; extras?: Record } = {}) { const appId = prodApp ? this.prodAppId : this.appId - const headers: any = { + const headers: Record = { Accept: "application/json", Cookie: "", } @@ -514,6 +521,7 @@ export default class TestConfiguration { return { ...headers, ...this.temporaryHeaders, + ...extras, } } @@ -577,17 +585,17 @@ export default class TestConfiguration { } const db = tenancy.getTenantDB(this.getTenantId()) const id = dbCore.generateDevInfoID(userId) - let devInfo: any - try { - devInfo = await db.get(id) - } catch (err) { - devInfo = { _id: id, userId } + const devInfo = await db.tryGet(id) + if (devInfo && devInfo.apiKey) { + return devInfo.apiKey } - devInfo.apiKey = encryption.encrypt( + + const apiKey = encryption.encrypt( `${this.getTenantId()}${dbCore.SEPARATOR}${newid()}` ) - await db.put(devInfo) - return devInfo.apiKey + const newDevInfo: DevInfo = { _id: id, userId, apiKey } + await db.put(newDevInfo) + return apiKey } // APP diff --git a/packages/server/src/tests/utilities/api/automation.ts b/packages/server/src/tests/utilities/api/automation.ts index c4438560ae..6bb4c9760c 100644 --- a/packages/server/src/tests/utilities/api/automation.ts +++ b/packages/server/src/tests/utilities/api/automation.ts @@ -133,4 +133,11 @@ export class AutomationAPI extends TestAPI { } ) } + + deleteAll = async (expectations?: Expectations): Promise => { + const { automations } = await this.fetch() + await Promise.all( + automations.map(automation => this.delete(automation, expectations)) + ) + } } diff --git a/packages/server/src/tests/utilities/api/base.ts b/packages/server/src/tests/utilities/api/base.ts index 39ac5cefc0..9b47cfb820 100644 --- a/packages/server/src/tests/utilities/api/base.ts +++ b/packages/server/src/tests/utilities/api/base.ts @@ -1,8 +1,12 @@ +import jestOpenAPI from "jest-openapi" +import { spec } from "../../../../specs/generate" import TestConfiguration from "../TestConfiguration" import request, { SuperTest, Test, Response } from "supertest" import { ReadStream } from "fs" import { getServer } from "../../../app" +jestOpenAPI(spec() as any) + type Headers = Record type Method = "get" | "post" | "put" | "patch" | "delete" @@ -46,6 +50,7 @@ export interface RequestOpts { export abstract class TestAPI { config: TestConfiguration request: SuperTest + prefix = "" constructor(config: TestConfiguration) { this.config = config @@ -53,26 +58,26 @@ export abstract class TestAPI { } protected _get = async (url: string, opts?: RequestOpts): Promise => { - return await this._request("get", url, opts) + return await this._request("get", `${this.prefix}${url}`, opts) } protected _post = async (url: string, opts?: RequestOpts): Promise => { - return await this._request("post", url, opts) + return await this._request("post", `${this.prefix}${url}`, opts) } protected _put = async (url: string, opts?: RequestOpts): Promise => { - return await this._request("put", url, opts) + return await this._request("put", `${this.prefix}${url}`, opts) } protected _patch = async (url: string, opts?: RequestOpts): Promise => { - return await this._request("patch", url, opts) + return await this._request("patch", `${this.prefix}${url}`, opts) } protected _delete = async ( url: string, opts?: RequestOpts ): Promise => { - return await this._request("delete", url, opts) + return await this._request("delete", `${this.prefix}${url}`, opts) } protected _requestRaw = async ( @@ -88,7 +93,6 @@ export abstract class TestAPI { fields = {}, files = {}, expectations, - publicUser = false, } = opts || {} const { status = 200 } = expectations || {} const expectHeaders = expectations?.headers || {} @@ -97,7 +101,7 @@ export abstract class TestAPI { expectHeaders["Content-Type"] = /^application\/json/ } - let queryParams = [] + let queryParams: string[] = [] for (const [key, value] of Object.entries(query)) { if (value) { queryParams.push(`${key}=${value}`) @@ -107,18 +111,10 @@ export abstract class TestAPI { url += `?${queryParams.join("&")}` } - const headersFn = publicUser - ? (_extras = {}) => - this.config.publicHeaders.bind(this.config)({ - prodApp: opts?.useProdApp, - }) - : (extras = {}) => - this.config.defaultHeaders.bind(this.config)(extras, opts?.useProdApp) - const app = getServer() let req = request(app)[method](url) req = req.set( - headersFn({ + await this.getHeaders(opts, { "x-budibase-include-stacktrace": "true", }) ) @@ -167,10 +163,18 @@ export abstract class TestAPI { } } - protected _checkResponse = ( - response: Response, - expectations?: Expectations - ) => { + protected async getHeaders( + opts?: RequestOpts, + extras?: Record + ): Promise> { + if (opts?.publicUser) { + return this.config.publicHeaders({ prodApp: opts?.useProdApp, extras }) + } else { + return this.config.defaultHeaders(extras, opts?.useProdApp) + } + } + + protected _checkResponse(response: Response, expectations?: Expectations) { const { status = 200 } = expectations || {} if (response.status !== status) { @@ -236,3 +240,34 @@ export abstract class TestAPI { ).body } } + +export abstract class PublicAPI extends TestAPI { + prefix = "/api/public/v1" + + protected async getHeaders( + opts?: RequestOpts, + extras?: Record + ): Promise> { + const apiKey = await this.config.generateApiKey() + + const headers: Record = { + Accept: "application/json", + Host: this.config.tenantHost(), + "x-budibase-api-key": apiKey, + "x-budibase-app-id": this.config.getAppId(), + ...extras, + } + + return headers + } + + protected _checkResponse(response: Response, expectations?: Expectations) { + const checked = super._checkResponse(response, expectations) + if (checked.status >= 200 && checked.status < 300) { + // We don't seem to have documented our errors yet, so for the time being + // we'll only do the schema check for successful responses. + expect(checked).toSatisfyApiSpec() + } + return checked + } +} diff --git a/packages/server/src/tests/utilities/api/environment.ts b/packages/server/src/tests/utilities/api/environment.ts new file mode 100644 index 0000000000..152336b316 --- /dev/null +++ b/packages/server/src/tests/utilities/api/environment.ts @@ -0,0 +1,51 @@ +import { Expectations, TestAPI } from "./base" +import { + CreateEnvironmentVariableRequest, + CreateEnvironmentVariableResponse, + GetEnvironmentVariablesResponse, + StatusEnvironmentVariableResponse, + UpdateEnvironmentVariableRequest, +} from "@budibase/types" + +export class EnvironmentAPI extends TestAPI { + create = async ( + body: CreateEnvironmentVariableRequest, + expectations?: Expectations + ) => { + return await this._post( + `/api/env/variables`, + { body, expectations } + ) + } + + status = async (expectations?: Expectations) => { + return await this._get( + `/api/env/variables/status`, + { expectations } + ) + } + + fetch = async (expectations?: Expectations) => { + return await this._get( + `/api/env/variables`, + { expectations } + ) + } + + update = async ( + varName: string, + body: UpdateEnvironmentVariableRequest, + expectations?: Expectations + ) => { + return await this._patch(`/api/env/variables/${varName}`, { + body, + expectations, + }) + } + + destroy = async (varName: string, expectations?: Expectations) => { + return await this._delete(`/api/env/variables/${varName}`, { + expectations, + }) + } +} diff --git a/packages/server/src/tests/utilities/api/index.ts b/packages/server/src/tests/utilities/api/index.ts index 2fdf726b6c..4c96f36b43 100644 --- a/packages/server/src/tests/utilities/api/index.ts +++ b/packages/server/src/tests/utilities/api/index.ts @@ -17,6 +17,8 @@ import { RowActionAPI } from "./rowAction" import { AutomationAPI } from "./automation" import { PluginAPI } from "./plugin" import { WebhookAPI } from "./webhook" +import { EnvironmentAPI } from "./environment" +import { UserPublicAPI } from "./public/user" export default class API { application: ApplicationAPI @@ -24,6 +26,7 @@ export default class API { automation: AutomationAPI backup: BackupAPI datasource: DatasourceAPI + environment: EnvironmentAPI legacyView: LegacyViewAPI permission: PermissionAPI plugin: PluginAPI @@ -38,12 +41,17 @@ export default class API { viewV2: ViewV2API webhook: WebhookAPI + public: { + user: UserPublicAPI + } + constructor(config: TestConfiguration) { this.application = new ApplicationAPI(config) this.attachment = new AttachmentAPI(config) this.automation = new AutomationAPI(config) this.backup = new BackupAPI(config) this.datasource = new DatasourceAPI(config) + this.environment = new EnvironmentAPI(config) this.legacyView = new LegacyViewAPI(config) this.permission = new PermissionAPI(config) this.plugin = new PluginAPI(config) @@ -57,5 +65,8 @@ export default class API { this.user = new UserAPI(config) this.viewV2 = new ViewV2API(config) this.webhook = new WebhookAPI(config) + this.public = { + user: new UserPublicAPI(config), + } } } diff --git a/packages/server/src/tests/utilities/api/public/user.ts b/packages/server/src/tests/utilities/api/public/user.ts new file mode 100644 index 0000000000..4f5fccc740 --- /dev/null +++ b/packages/server/src/tests/utilities/api/public/user.ts @@ -0,0 +1,34 @@ +import { UnsavedUser, User } from "@budibase/types" +import { Expectations, PublicAPI } from "../base" + +export class UserPublicAPI extends PublicAPI { + find = async (id: string, expectations?: Expectations): Promise => { + const response = await this._get<{ data: User }>(`/users/${id}`, { + expectations, + }) + return response.data + } + + update = async (user: User, expectations?: Expectations): Promise => { + const response = await this._put<{ data: User }>(`/users/${user._id}`, { + body: user, + expectations, + }) + return response.data + } + + destroy = async (id: string, expectations?: Expectations): Promise => { + return await this._delete(`/users/${id}`, { expectations }) + } + + create = async ( + user: UnsavedUser, + expectations?: Expectations + ): Promise => { + const response = await this._post<{ data: User }>("/users", { + body: user, + expectations, + }) + return response.data + } +} diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts index 38d60e1c11..1679334cab 100644 --- a/packages/server/src/tests/utilities/structures.ts +++ b/packages/server/src/tests/utilities/structures.ts @@ -37,6 +37,7 @@ import { DeepPartial, FilterCondition, AutomationTriggerResult, + CreateEnvironmentVariableRequest, } from "@budibase/types" import { LoopInput } from "../../definitions/automations" import { merge } from "lodash" @@ -574,7 +575,7 @@ export function basicEnvironmentVariable( name: string, prod: string, dev?: string -) { +): CreateEnvironmentVariableRequest { return { name, production: prod, diff --git a/yarn.lock b/yarn.lock index 279c4067a4..5173e81408 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2796,9 +2796,9 @@ through2 "^2.0.0" "@budibase/pro@npm:@budibase/pro@latest": - version "3.4.20" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.4.20.tgz#0d855d6ed8fe92fd178c74a8963d879cc124b034" - integrity sha512-hUteGvhMOKjBo0fluxcqNs7d4x8OU5W8Oqqrm7eIS9Ohe7ala2iWNCcrj+x+S9CavIm6s7JZZnAewa2Maiz2zQ== + version "3.4.22" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.4.22.tgz#943f23cb7056041bc1f433ee60b3d093145e7a4a" + integrity sha512-Du3iZsmRLopfoi2SvxQyY1P2Su3Nw0WbITOrKmZFsVLjZ9MzzTZs0Ph/SJHzrfJpM7rn9+8788BLSf3Z3l9KcQ== dependencies: "@anthropic-ai/sdk" "^0.27.3" "@budibase/backend-core" "*" @@ -7142,6 +7142,11 @@ dependencies: "@types/superagent" "*" +"@types/swagger-jsdoc@^6.0.4": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz#bb4f60f3a5f103818e022f2e29ff8935113fb83d" + integrity sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ== + "@types/tar-fs@2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-2.0.1.tgz#6391dcad1b03dea2d79fac07371585ab54472bb1"