diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index 98e24e0996..69f0fd64ea 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -222,9 +222,12 @@ export class DatabaseImpl implements Database { } async getMultiple( - ids: string[], + ids?: string[], opts?: { allowMissing?: boolean; excludeDocs?: boolean } ): Promise { + if (!ids || ids.length === 0) { + return [] + } // get unique ids = [...new Set(ids)] const includeDocs = !opts?.excludeDocs @@ -249,7 +252,7 @@ export class DatabaseImpl implements Database { if (!opts?.allowMissing && someMissing) { const missing = response.rows.filter(row => rowUnavailable(row)) const missingIds = missing.map(row => row.key).join(", ") - throw new Error(`Unable to get documents: ${missingIds}`) + throw new Error(`Unable to get bulk documents: ${missingIds}`) } return rows.map(row => (includeDocs ? row.doc! : row.value)) } diff --git a/packages/backend-core/src/db/instrumentation.ts b/packages/backend-core/src/db/instrumentation.ts index 0c0056d6ed..68c3694672 100644 --- a/packages/backend-core/src/db/instrumentation.ts +++ b/packages/backend-core/src/db/instrumentation.ts @@ -52,13 +52,13 @@ export class DDInstrumentedDatabase implements Database { } getMultiple( - ids: string[], + ids?: string[], opts?: { allowMissing?: boolean | undefined } | undefined ): Promise { return tracer.trace("db.getMultiple", async span => { span.addTags({ db_name: this.name, - num_docs: ids.length, + num_docs: ids?.length || 0, allow_missing: opts?.allowMissing, }) const docs = await this.db.getMultiple(ids, opts) diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index 2b15338925..677feed678 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -26,8 +26,9 @@ import { import { getAccountHolderFromUsers, isAdmin, - isCreator, + creatorsInList, validateUniqueUser, + isCreatorAsync, } from "./utils" import { getFirstPlatformUser, @@ -261,8 +262,16 @@ export class UserDB { } const change = dbUser ? 0 : 1 // no change if there is existing user - const creatorsChange = - (await isCreator(dbUser)) !== (await isCreator(user)) ? 1 : 0 + + let creatorsChange = 0 + if (dbUser) { + const [isDbUserCreator, isUserCreator] = await creatorsInList([ + dbUser, + user, + ]) + creatorsChange = isDbUserCreator !== isUserCreator ? 1 : 0 + } + return UserDB.quotas.addUsers(change, creatorsChange, async () => { if (!opts.isAccountHolder) { await validateUniqueUser(email, tenantId) @@ -353,7 +362,7 @@ export class UserDB { } newUser.userGroups = groups || [] newUsers.push(newUser) - if (await isCreator(newUser)) { + if (await isCreatorAsync(newUser)) { newCreators.push(newUser) } } @@ -453,10 +462,8 @@ export class UserDB { })) const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete) - const creatorsEval = await Promise.all(usersToDelete.map(isCreator)) - const creatorsToDeleteCount = creatorsEval.filter( - creator => !!creator - ).length + const creatorsEval = await creatorsInList(usersToDelete) + const creatorsToDeleteCount = creatorsEval.filter(creator => creator).length const ssoUsersToDelete: AnyDocument[] = [] for (let user of usersToDelete) { @@ -533,7 +540,7 @@ export class UserDB { await db.remove(userId, dbUser._rev!) - const creatorsToDelete = (await isCreator(dbUser)) ? 1 : 0 + const creatorsToDelete = (await isCreatorAsync(dbUser)) ? 1 : 0 await UserDB.quotas.removeUsers(1, creatorsToDelete) await eventHelpers.handleDeleteEvents(dbUser) await cache.user.invalidateUser(userId) diff --git a/packages/backend-core/src/users/test/utils.spec.ts b/packages/backend-core/src/users/test/utils.spec.ts index cb98b8972b..b52397c979 100644 --- a/packages/backend-core/src/users/test/utils.spec.ts +++ b/packages/backend-core/src/users/test/utils.spec.ts @@ -2,39 +2,39 @@ import { User, UserGroup } from "@budibase/types" import { generator, structures } from "../../../tests" import { DBTestConfiguration } from "../../../tests/extra" import { getGlobalDB } from "../../context" -import { isCreator } from "../utils" +import { isCreatorSync, creatorsInList } from "../utils" const config = new DBTestConfiguration() describe("Users", () => { - it("User is a creator if it is configured as a global builder", async () => { + it("User is a creator if it is configured as a global builder", () => { const user: User = structures.users.user({ builder: { global: true } }) - expect(await isCreator(user)).toBe(true) + expect(isCreatorSync(user, [])).toBe(true) }) - it("User is a creator if it is configured as a global admin", async () => { + it("User is a creator if it is configured as a global admin", () => { const user: User = structures.users.user({ admin: { global: true } }) - expect(await isCreator(user)).toBe(true) + expect(isCreatorSync(user, [])).toBe(true) }) - it("User is a creator if it is configured with creator permission", async () => { + it("User is a creator if it is configured with creator permission", () => { const user: User = structures.users.user({ builder: { creator: true } }) - expect(await isCreator(user)).toBe(true) + expect(isCreatorSync(user, [])).toBe(true) }) - it("User is a creator if it is a builder in some application", async () => { + it("User is a creator if it is a builder in some application", () => { const user: User = structures.users.user({ builder: { apps: ["app1"] } }) - expect(await isCreator(user)).toBe(true) + expect(isCreatorSync(user, [])).toBe(true) }) - it("User is a creator if it has CREATOR permission in some application", async () => { + it("User is a creator if it has CREATOR permission in some application", () => { const user: User = structures.users.user({ roles: { app1: "CREATOR" } }) - expect(await isCreator(user)).toBe(true) + expect(isCreatorSync(user, [])).toBe(true) }) - it("User is a creator if it has ADMIN permission in some application", async () => { + it("User is a creator if it has ADMIN permission in some application", () => { const user: User = structures.users.user({ roles: { app1: "ADMIN" } }) - expect(await isCreator(user)).toBe(true) + expect(isCreatorSync(user, [])).toBe(true) }) it("User is a creator if it remains to a group with ADMIN permissions", async () => { @@ -59,7 +59,7 @@ describe("Users", () => { await db.put(group) for (let user of users) { await db.put(user) - const creator = await isCreator(user) + const creator = (await creatorsInList([user]))[0] expect(creator).toBe(true) } }) diff --git a/packages/backend-core/src/users/users.ts b/packages/backend-core/src/users/users.ts index 0bff428fa9..36abfcfb2d 100644 --- a/packages/backend-core/src/users/users.ts +++ b/packages/backend-core/src/users/users.ts @@ -22,7 +22,7 @@ import { } from "@budibase/types" import * as context from "../context" import { getGlobalDB } from "../context" -import { isCreator } from "./utils" +import { creatorsInList } from "./utils" import { UserDB } from "./db" import { dataFilters } from "@budibase/shared-core" @@ -305,8 +305,8 @@ export async function getCreatorCount() { let creators = 0 async function iterate(startPage?: string) { const page = await paginatedUsers({ bookmark: startPage }) - const creatorsEval = await Promise.all(page.data.map(isCreator)) - creators += creatorsEval.filter(creator => !!creator).length + const creatorsEval = await creatorsInList(page.data) + creators += creatorsEval.filter(creator => creator).length if (page.hasNextPage) { await iterate(page.nextPage) } diff --git a/packages/backend-core/src/users/utils.ts b/packages/backend-core/src/users/utils.ts index 91b667ce17..039f9228f9 100644 --- a/packages/backend-core/src/users/utils.ts +++ b/packages/backend-core/src/users/utils.ts @@ -16,30 +16,47 @@ export const hasAdminPermissions = sdk.users.hasAdminPermissions export const hasBuilderPermissions = sdk.users.hasBuilderPermissions export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions -export async function isCreator(user?: User | ContextUser) { +export async function creatorsInList( + users: (User | ContextUser)[], + groups?: UserGroup[] +) { + const groupIds = [ + ...new Set( + users.filter(user => user.userGroups).flatMap(user => user.userGroups!) + ), + ] + const db = context.getGlobalDB() + groups = await db.getMultiple(groupIds, { allowMissing: true }) + return users.map(user => isCreatorSync(user, groups)) +} + +// fetches groups if no provided, but is async and shouldn't be looped with +export async function isCreatorAsync(user: User | ContextUser) { + let groups: UserGroup[] = [] + if (user.userGroups) { + const db = context.getGlobalDB() + groups = await db.getMultiple(user.userGroups) + } + return isCreatorSync(user, groups) +} + +export function isCreatorSync(user: User | ContextUser, groups?: UserGroup[]) { const isCreatorByUserDefinition = sdk.users.isCreator(user) if (!isCreatorByUserDefinition && user) { - return await isCreatorByGroupMembership(user) + return isCreatorByGroupMembership(user, groups) } return isCreatorByUserDefinition } -async function isCreatorByGroupMembership(user?: User | ContextUser) { - const userGroups = user?.userGroups || [] - if (userGroups.length > 0) { - const db = context.getGlobalDB() - const groups: UserGroup[] = [] - for (let groupId of userGroups) { - try { - const group = await db.get(groupId) - groups.push(group) - } catch (e: any) { - if (e.error !== "not_found") { - throw e - } - } - } - return groups.some(group => +function isCreatorByGroupMembership( + user: User | ContextUser, + groups?: UserGroup[] +) { + const userGroups = groups?.filter( + group => user.userGroups?.indexOf(group._id!) !== -1 + ) + if (userGroups && userGroups.length > 0) { + return userGroups.some(group => Object.values(group.roles || {}).includes(BUILTIN_ROLE_IDS.ADMIN) ) } diff --git a/packages/backend-core/tests/core/users/users.spec.ts b/packages/backend-core/tests/core/users/users.spec.ts deleted file mode 100644 index b14f553266..0000000000 --- a/packages/backend-core/tests/core/users/users.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { range } from "lodash/fp" -import { structures } from "../.." - -jest.mock("../../../src/context") -jest.mock("../../../src/db") - -import * as context from "../../../src/context" -import * as db from "../../../src/db" - -import { getCreatorCount } from "../../../src/users/users" - -describe("Users", () => { - let getGlobalDBMock: jest.SpyInstance - let paginationMock: jest.SpyInstance - - beforeEach(() => { - jest.resetAllMocks() - - getGlobalDBMock = jest.spyOn(context, "getGlobalDB") - paginationMock = jest.spyOn(db, "pagination") - - jest.spyOn(db, "getGlobalUserParams") - }) - - it("retrieves the number of creators", async () => { - const getUsers = (offset: number, limit: number, creators = false) => { - const opts = creators ? { builder: { global: true } } : undefined - return range(offset, limit).map(() => structures.users.user(opts)) - } - const page1Data = getUsers(0, 8) - const page2Data = getUsers(8, 12, true) - getGlobalDBMock.mockImplementation(() => ({ - name: "fake-db", - allDocs: () => ({ - rows: [...page1Data, ...page2Data], - }), - })) - paginationMock.mockImplementationOnce(() => ({ - data: page1Data, - hasNextPage: true, - nextPage: "1", - })) - paginationMock.mockImplementation(() => ({ - data: page2Data, - hasNextPage: false, - nextPage: undefined, - })) - const creatorsCount = await getCreatorCount() - expect(creatorsCount).toBe(4) - expect(paginationMock).toHaveBeenCalledTimes(2) - }) -}) diff --git a/packages/pro b/packages/pro index b28dbd5492..2dd06c2fcb 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit b28dbd549284cf450be7f25ad85aadf614d08f0b +Subproject commit 2dd06c2fcb3cf10d5f16f5d8fe6cd344c8e905a5 diff --git a/packages/server/src/api/routes/tests/misc.spec.js b/packages/server/src/api/routes/tests/misc.spec.ts similarity index 66% rename from packages/server/src/api/routes/tests/misc.spec.js rename to packages/server/src/api/routes/tests/misc.spec.ts index fe7da1a119..425c7212d0 100644 --- a/packages/server/src/api/routes/tests/misc.spec.js +++ b/packages/server/src/api/routes/tests/misc.spec.ts @@ -1,11 +1,13 @@ -const setup = require("./utilities") -const tableUtils = require("../../controllers/table/utils") +import { handleDataImport } from "../../controllers/table/utils" +import TestConfiguration from "../../../tests/utilities/TestConfiguration" +import { AutoFieldSubType, FieldType, JsonFieldSubType } from "@budibase/types" describe("run misc tests", () => { - let request = setup.getRequest() - let config = setup.getConfig() + const config = new TestConfiguration() - afterAll(setup.afterAll) + afterAll(() => { + config.end() + }) beforeAll(async () => { await config.init() @@ -13,69 +15,67 @@ describe("run misc tests", () => { describe("/bbtel", () => { it("check if analytics enabled", async () => { - const res = await request - .get(`/api/bbtel`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(typeof res.body.enabled).toEqual("boolean") + const { enabled } = await config.api.misc.bbtel() + expect(enabled).toEqual(true) }) }) describe("/health", () => { it("should confirm healthy", async () => { - await request.get("/health").expect(200) + await config.api.misc.health() }) }) describe("/version", () => { it("should confirm version", async () => { - const res = await request.get("/version").expect(200) - const text = res.text - if (text.includes("alpha")) { - expect(text.split(".").length).toEqual(4) + const version = await config.api.misc.version() + if (version.includes("alpha")) { + expect(version.split(".").length).toEqual(4) } else { - expect(text.split(".").length).toEqual(3) + expect(version.split(".").length).toEqual(3) } }) }) describe("test table utilities", () => { it("should be able to import data", async () => { - return config.doInContext(null, async () => { + return config.doInContext("", async () => { const table = await config.createTable({ name: "table", type: "table", - key: "name", schema: { a: { - type: "string", + type: FieldType.STRING, + name: "a", constraints: { type: "string", }, }, b: { - type: "string", + name: "b", + type: FieldType.STRING, constraints: { type: "string", }, }, c: { - type: "string", + name: "c", + type: FieldType.STRING, constraints: { type: "string", }, }, d: { - type: "string", + name: "d", + type: FieldType.STRING, constraints: { type: "string", }, }, e: { name: "Auto ID", - type: "number", - subtype: "autoID", + type: FieldType.NUMBER, + subtype: AutoFieldSubType.AUTO_ID, icon: "ri-magic-line", autocolumn: true, constraints: { @@ -88,9 +88,9 @@ describe("run misc tests", () => { }, }, f: { - type: "array", + type: FieldType.ARRAY, constraints: { - type: "array", + type: JsonFieldSubType.ARRAY, presence: { allowEmpty: true, }, @@ -100,7 +100,7 @@ describe("run misc tests", () => { sortable: false, }, g: { - type: "options", + type: FieldType.OPTIONS, constraints: { type: "string", presence: false, @@ -118,16 +118,18 @@ describe("run misc tests", () => { { a: "13", b: "14", c: "15", d: "16", g: "Omega" }, ] // Shift specific row tests to the row spec - await tableUtils.handleDataImport(table, { - importRows, - user: { userId: "test" }, - }) + await handleDataImport(table, { importRows, userId: "test" }) // 4 rows imported, the auto ID starts at 1 // We expect the handleDataImport function to update the lastID + + // @ts-expect-error - fields have type FieldSchema, not specific + // subtypes. expect(table.schema.e.lastID).toEqual(4) // Array/Multi - should have added a new value to the inclusion. + // @ts-expect-error - fields have type FieldSchema, not specific + // subtypes. expect(table.schema.f.constraints.inclusion).toEqual([ "Four", "One", @@ -136,6 +138,8 @@ describe("run misc tests", () => { ]) // Options - should have a new value in the inclusion + // @ts-expect-error - fields have type FieldSchema, not specific + // subtypes. expect(table.schema.g.constraints.inclusion).toEqual([ "Alpha", "Beta", @@ -143,25 +147,25 @@ describe("run misc tests", () => { "Omega", ]) - const rows = await config.getRows() + const rows = await config.api.row.fetch(table._id!) expect(rows.length).toEqual(4) - const rowOne = rows.find(row => row.e === 1) + const rowOne = rows.find(row => row.e === 1)! expect(rowOne.a).toEqual("1") expect(rowOne.f).toEqual(["One"]) expect(rowOne.g).toEqual("Alpha") - const rowTwo = rows.find(row => row.e === 2) + const rowTwo = rows.find(row => row.e === 2)! expect(rowTwo.a).toEqual("5") expect(rowTwo.f).toEqual([]) expect(rowTwo.g).toEqual(undefined) - const rowThree = rows.find(row => row.e === 3) + const rowThree = rows.find(row => row.e === 3)! expect(rowThree.a).toEqual("9") expect(rowThree.f).toEqual(["Two", "Four"]) expect(rowThree.g).toEqual(undefined) - const rowFour = rows.find(row => row.e === 4) + const rowFour = rows.find(row => row.e === 4)! expect(rowFour.a).toEqual("13") expect(rowFour.f).toEqual(undefined) expect(rowFour.g).toEqual("Omega") diff --git a/packages/server/src/integrations/tests/TestConfiguration.js b/packages/server/src/integrations/tests/TestConfiguration.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/server/src/middleware/authorized.ts b/packages/server/src/middleware/authorized.ts index 3bead7f80d..0c4943cf5e 100644 --- a/packages/server/src/middleware/authorized.ts +++ b/packages/server/src/middleware/authorized.ts @@ -34,7 +34,7 @@ const checkAuthorized = async ( const isCreatorApi = permType === PermissionType.CREATOR const isBuilderApi = permType === PermissionType.BUILDER const isGlobalBuilder = users.isGlobalBuilder(ctx.user) - const isCreator = await users.isCreator(ctx.user) + const isCreator = await users.isCreatorAsync(ctx.user) const isBuilder = appId ? users.isBuilder(ctx.user, appId) : users.hasBuilderPermissions(ctx.user) diff --git a/packages/server/src/middleware/selfhost.ts b/packages/server/src/middleware/selfhost.ts deleted file mode 100644 index 3571931835..0000000000 --- a/packages/server/src/middleware/selfhost.ts +++ /dev/null @@ -1,12 +0,0 @@ -import env from "../environment" -import { Ctx } from "@budibase/types" - -// if added as a middleware will stop requests unless builder is in self host mode -// or cloud is in self host -export default async (ctx: Ctx, next: any) => { - if (env.SELF_HOSTED) { - await next() - return - } - ctx.throw(400, "Endpoint unavailable in cloud hosting.") -} diff --git a/packages/server/src/middleware/tests/ensureTenantAppOwnership.spec.js b/packages/server/src/middleware/tests/ensureTenantAppOwnership.spec.js deleted file mode 100644 index 5c500f8723..0000000000 --- a/packages/server/src/middleware/tests/ensureTenantAppOwnership.spec.js +++ /dev/null @@ -1,75 +0,0 @@ -import ensureTenantAppOwnership from "../ensureTenantAppOwnership" -import { tenancy, utils } from "@budibase/backend-core" - -jest.mock("@budibase/backend-core", () => ({ - ...jest.requireActual("@budibase/backend-core"), - tenancy: { - getTenantId: jest.fn(), - }, - utils: { - getAppIdFromCtx: jest.fn(), - }, -})) - -class TestConfiguration { - constructor(appId = "tenant_1") { - this.next = jest.fn() - this.throw = jest.fn() - this.middleware = ensureTenantAppOwnership - - this.ctx = { - next: this.next, - throw: this.throw, - } - - utils.getAppIdFromCtx.mockResolvedValue(appId) - } - - async executeMiddleware() { - return this.middleware(this.ctx, this.next) - } - - afterEach() { - jest.clearAllMocks() - } -} - -describe("Ensure Tenant Ownership Middleware", () => { - let config - - beforeEach(() => { - config = new TestConfiguration() - }) - - afterEach(() => { - config.afterEach() - }) - - it("calls next() when appId matches tenant ID", async () => { - tenancy.getTenantId.mockReturnValue("tenant_1") - - await config.executeMiddleware() - - expect(utils.getAppIdFromCtx).toHaveBeenCalledWith(config.ctx) - expect(config.next).toHaveBeenCalled() - }) - - it("throws when tenant appId does not match tenant ID", async () => { - const appId = "app_dev_tenant3_fce449c4d75b4e4a9c7a6980d82a3e22" - utils.getAppIdFromCtx.mockResolvedValue(appId) - tenancy.getTenantId.mockReturnValue("tenant_2") - - await config.executeMiddleware() - - expect(utils.getAppIdFromCtx).toHaveBeenCalledWith(config.ctx) - expect(config.throw).toHaveBeenCalledWith(403, "Unauthorized") - }) - - it("throws 400 when appId is missing", async () => { - utils.getAppIdFromCtx.mockResolvedValue(null) - - await config.executeMiddleware() - - expect(config.throw).toHaveBeenCalledWith(400, "appId must be provided") - }) -}) diff --git a/packages/server/src/middleware/tests/ensureTenantAppOwnership.spec.ts b/packages/server/src/middleware/tests/ensureTenantAppOwnership.spec.ts new file mode 100644 index 0000000000..c832ff645c --- /dev/null +++ b/packages/server/src/middleware/tests/ensureTenantAppOwnership.spec.ts @@ -0,0 +1,52 @@ +import { UserCtx } from "@budibase/types" +import ensureTenantAppOwnership from "../ensureTenantAppOwnership" +import { context, Header, HTTPError } from "@budibase/backend-core" + +function ctx(opts?: { appId: string }) { + const ctx = { + throw: (status: number, message: string) => { + throw new HTTPError(message, status) + }, + path: "", + request: { + headers: {}, + }, + } as unknown as UserCtx + if (opts?.appId) { + ctx.request.headers[Header.APP_ID] = opts.appId + } + return ctx +} + +describe("Ensure Tenant Ownership Middleware", () => { + const tenantId = "tenant1" + const appId = `app_dev_${tenantId}_fce449c4d75b4e4a9c7a6980d82a3e22` + + it("calls next() when appId matches tenant ID", async () => { + await context.doInTenant(tenantId, async () => { + let called = false + await ensureTenantAppOwnership(ctx({ appId }), () => { + called = true + }) + expect(called).toBe(true) + }) + }) + + it("throws when tenant appId does not match tenant ID", async () => { + let called = false + await expect(async () => { + await context.doInTenant("tenant_2", async () => { + await ensureTenantAppOwnership(ctx({ appId }), () => { + called = true + }) + }) + }).rejects.toThrow("Unauthorized") + expect(called).toBe(false) + }) + + it("throws 400 when appId is missing", async () => { + await expect(ensureTenantAppOwnership(ctx(), () => {})).rejects.toThrow( + "appId must be provided" + ) + }) +}) diff --git a/packages/server/src/middleware/tests/resourceId.spec.js b/packages/server/src/middleware/tests/resourceId.spec.js deleted file mode 100644 index 759aa46f00..0000000000 --- a/packages/server/src/middleware/tests/resourceId.spec.js +++ /dev/null @@ -1,105 +0,0 @@ -const { - paramResource, - paramSubResource, - bodyResource, - bodySubResource, - ResourceIdGetter, -} = require("../resourceId") - -class TestConfiguration { - constructor(middleware) { - this.middleware = middleware - this.ctx = { - request: {}, - } - this.next = jest.fn() - } - - setParams(params) { - this.ctx.params = params - } - - setBody(body) { - this.ctx.body = body - } - - executeMiddleware() { - return this.middleware(this.ctx, this.next) - } -} - -describe("resourceId middleware", () => { - it("calls next() when there is no request object to parse", () => { - const config = new TestConfiguration(paramResource("main")) - - config.executeMiddleware() - - expect(config.next).toHaveBeenCalled() - expect(config.ctx.resourceId).toBeUndefined() - }) - - it("generates a resourceId middleware for context query parameters", () => { - const config = new TestConfiguration(paramResource("main")) - config.setParams({ - main: "test", - }) - - config.executeMiddleware() - - expect(config.ctx.resourceId).toEqual("test") - }) - - it("generates a resourceId middleware for context query sub parameters", () => { - const config = new TestConfiguration(paramSubResource("main", "sub")) - config.setParams({ - main: "main", - sub: "test", - }) - - config.executeMiddleware() - - expect(config.ctx.resourceId).toEqual("main") - expect(config.ctx.subResourceId).toEqual("test") - }) - - it("generates a resourceId middleware for context request body", () => { - const config = new TestConfiguration(bodyResource("main")) - config.setBody({ - main: "test", - }) - - config.executeMiddleware() - - expect(config.ctx.resourceId).toEqual("test") - }) - - it("generates a resourceId middleware for context request body sub fields", () => { - const config = new TestConfiguration(bodySubResource("main", "sub")) - config.setBody({ - main: "main", - sub: "test", - }) - - config.executeMiddleware() - - expect(config.ctx.resourceId).toEqual("main") - expect(config.ctx.subResourceId).toEqual("test") - }) - - it("parses resourceIds correctly for custom middlewares", () => { - const middleware = new ResourceIdGetter("body") - .mainResource("custom") - .subResource("customSub") - .build() - let config = new TestConfiguration(middleware) - config.setBody({ - custom: "test", - customSub: "subtest", - }) - - config.executeMiddleware() - - expect(config.ctx.resourceId).toEqual("test") - expect(config.ctx.subResourceId).toEqual("subtest") - }) -}) diff --git a/packages/server/src/middleware/tests/resourceId.spec.ts b/packages/server/src/middleware/tests/resourceId.spec.ts new file mode 100644 index 0000000000..3ceb55af3c --- /dev/null +++ b/packages/server/src/middleware/tests/resourceId.spec.ts @@ -0,0 +1,93 @@ +import { Ctx } from "@budibase/types" +import { + paramResource, + paramSubResource, + bodyResource, + bodySubResource, + ResourceIdGetter, +} from "../resourceId" + +describe("resourceId middleware", () => { + it("calls next() when there is no request object to parse", () => { + const ctx = { request: {} } as Ctx + let called = false + paramResource("main")(ctx, () => { + called = true + }) + + expect(called).toBe(true) + expect(ctx.resourceId).toBeUndefined() + }) + + it("generates a resourceId middleware for context query parameters", () => { + const ctx = { request: {}, params: { main: "test" } } as unknown as Ctx + let called = false + paramResource("main")(ctx, () => { + called = true + }) + + expect(called).toBe(true) + expect(ctx.resourceId).toEqual("test") + }) + + it("generates a resourceId middleware for context query sub parameters", () => { + const ctx = { + request: {}, + params: { main: "main", sub: "test" }, + } as unknown as Ctx + let called = false + paramSubResource("main", "sub")(ctx, () => { + called = true + }) + + expect(called).toBe(true) + expect(ctx.resourceId).toEqual("main") + expect(ctx.subResourceId).toEqual("test") + }) + + it("generates a resourceId middleware for context request body", () => { + const ctx = { request: {}, body: { main: "main" } } as unknown as Ctx + let called = false + bodyResource("main")(ctx, () => { + called = true + }) + + expect(called).toBe(true) + expect(ctx.resourceId).toEqual("main") + }) + + it("generates a resourceId middleware for context request body sub fields", () => { + const ctx = { + request: {}, + body: { main: "main", sub: "test" }, + } as unknown as Ctx + let called = false + bodySubResource("main", "sub")(ctx, () => { + called = true + }) + + expect(called).toBe(true) + expect(ctx.resourceId).toEqual("main") + expect(ctx.subResourceId).toEqual("test") + }) + + it("parses resourceIds correctly for custom middlewares", () => { + const middleware = new ResourceIdGetter("body") + .mainResource("custom") + .subResource("customSub") + .build() + + const ctx = { + request: {}, + body: { custom: "test", customSub: "subTest" }, + } as unknown as Ctx + let called = false + middleware(ctx, () => { + called = true + }) + + expect(called).toBe(true) + expect(ctx.resourceId).toEqual("test") + expect(ctx.subResourceId).toEqual("subTest") + }) +}) diff --git a/packages/server/src/middleware/tests/selfhost.spec.js b/packages/server/src/middleware/tests/selfhost.spec.js deleted file mode 100644 index 03372e9783..0000000000 --- a/packages/server/src/middleware/tests/selfhost.spec.js +++ /dev/null @@ -1,43 +0,0 @@ -const selfHostMiddleware = require("../selfhost").default -const env = require("../../environment") -jest.mock("../../environment") - -class TestConfiguration { - constructor() { - this.next = jest.fn() - this.throw = jest.fn() - this.middleware = selfHostMiddleware - - this.ctx = { - next: this.next, - throw: this.throw, - } - } - - executeMiddleware() { - return this.middleware(this.ctx, this.next) - } - - afterEach() { - jest.clearAllMocks() - } -} - -describe("Self host middleware", () => { - let config - - beforeEach(() => { - config = new TestConfiguration() - }) - - afterEach(() => { - config.afterEach() - }) - - it("calls next() when SELF_HOSTED env var is set", async () => { - env.SELF_HOSTED = 1 - - await config.executeMiddleware() - expect(config.next).toHaveBeenCalled() - }) -}) diff --git a/packages/server/src/tests/utilities/api/index.ts b/packages/server/src/tests/utilities/api/index.ts index 4c96f36b43..b1d1c904ae 100644 --- a/packages/server/src/tests/utilities/api/index.ts +++ b/packages/server/src/tests/utilities/api/index.ts @@ -19,6 +19,7 @@ import { PluginAPI } from "./plugin" import { WebhookAPI } from "./webhook" import { EnvironmentAPI } from "./environment" import { UserPublicAPI } from "./public/user" +import { MiscAPI } from "./misc" export default class API { application: ApplicationAPI @@ -28,6 +29,7 @@ export default class API { datasource: DatasourceAPI environment: EnvironmentAPI legacyView: LegacyViewAPI + misc: MiscAPI permission: PermissionAPI plugin: PluginAPI query: QueryAPI @@ -53,6 +55,7 @@ export default class API { this.datasource = new DatasourceAPI(config) this.environment = new EnvironmentAPI(config) this.legacyView = new LegacyViewAPI(config) + this.misc = new MiscAPI(config) this.permission = new PermissionAPI(config) this.plugin = new PluginAPI(config) this.query = new QueryAPI(config) diff --git a/packages/server/src/tests/utilities/api/misc.ts b/packages/server/src/tests/utilities/api/misc.ts new file mode 100644 index 0000000000..e46d11e29b --- /dev/null +++ b/packages/server/src/tests/utilities/api/misc.ts @@ -0,0 +1,18 @@ +import { AnalyticsEnabledResponse } from "@budibase/types" +import { Expectations, TestAPI } from "./base" + +export class MiscAPI extends TestAPI { + health = async (expectations?: Expectations) => { + return await this._get("/health", { expectations }) + } + + version = async (expectations?: Expectations) => { + return (await this._requestRaw("get", "/version", { expectations })).text + } + + bbtel = async (expectations?: Expectations) => { + return await this._get("/api/bbtel", { + expectations, + }) + } +} diff --git a/packages/types/src/sdk/db.ts b/packages/types/src/sdk/db.ts index eb922bd81e..9f99266644 100644 --- a/packages/types/src/sdk/db.ts +++ b/packages/types/src/sdk/db.ts @@ -136,7 +136,7 @@ export interface Database { get(id?: string): Promise tryGet(id?: string): Promise getMultiple( - ids: string[], + ids?: string[], opts?: { allowMissing?: boolean; excludeDocs?: boolean } ): Promise remove(idOrDoc: Document): Promise