From 0bad2dd9ae9cbfa7d87798075b54af9bffb7edc2 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Fri, 11 Nov 2022 11:10:07 +0000 Subject: [PATCH] Support path variable tenancy detection, add /api/system/* tests, update no tenancy matchers to be more accurate --- .../{deprovision.js => deprovision.ts} | 45 +++++------ packages/backend-core/src/index.ts | 2 +- .../middleware/{matchers.js => matchers.ts} | 20 +++-- .../backend-core/src/middleware/tenancy.ts | 4 +- packages/backend-core/src/tenancy/tenancy.ts | 29 ++++++-- packages/backend-core/src/utils.ts | 13 ++-- .../backend-core/tests/utilities/index.ts | 1 + .../tests/utilities/mocks/accounts.ts | 2 + .../tests/utilities/structures/accounts.ts | 22 ++++-- .../types/src/documents/platform/index.ts | 1 + .../types/src/documents/platform/tenants.ts | 5 ++ packages/types/src/sdk/auth.ts | 8 ++ packages/types/src/sdk/middleware/matchers.ts | 18 +++++ packages/types/src/sdk/middleware/tenancy.ts | 1 + packages/worker/scripts/jestSetup.js | 5 +- .../system/{environment.js => environment.ts} | 5 +- .../system/{status.js => status.ts} | 7 +- .../src/api/controllers/system/tenants.ts | 59 ++------------- packages/worker/src/api/index.ts | 31 +++++++- packages/worker/src/api/routes/global/auth.js | 32 +------- .../api/routes/global/tests/configs.spec.ts | 4 +- .../api/routes/global/tests/license.spec.ts | 28 +++++++ .../src/api/routes/global/tests/roles.spec.ts | 24 ++++++ .../api/routes/global/tests/templates.spec.ts | 32 ++++++++ .../routes/global/tests/workspaces.spec.ts | 26 +++++++ .../src/api/routes/system/environment.js | 8 -- .../src/api/routes/system/environment.ts | 8 ++ .../worker/src/api/routes/system/status.js | 8 -- .../worker/src/api/routes/system/status.ts | 8 ++ .../worker/src/api/routes/system/tenants.js | 12 --- .../worker/src/api/routes/system/tenants.ts | 13 ++++ .../routes/system/tests/environment.spec.ts | 30 ++++++++ .../routes/system/tests/migrations.spec.ts | 64 ++++++++++++++++ .../api/routes/system/tests/restore.spec.ts | 37 ++++++++++ .../api/routes/system/tests/status.spec.ts | 49 ++++++++++++ .../api/routes/system/tests/tenants.spec.ts | 32 ++++++++ .../src/middleware/tests/tenancy.spec.ts | 74 +++++++++++++++++++ .../worker/src/tests/TestConfiguration.ts | 53 ++++++++----- packages/worker/src/tests/api/base.ts | 16 ++++ packages/worker/src/tests/api/environment.ts | 18 +++++ packages/worker/src/tests/api/index.ts | 15 ++++ packages/worker/src/tests/api/migrations.ts | 22 ++++++ packages/worker/src/tests/api/restore.ts | 14 ++++ packages/worker/src/tests/api/self.ts | 8 ++ packages/worker/src/tests/api/status.ts | 15 ++++ packages/worker/src/tests/api/tenants.ts | 15 ++++ packages/worker/src/tests/index.ts | 4 + .../worker/src/tests/structures/accounts.ts | 24 ------ packages/worker/src/tests/structures/index.ts | 6 +- packages/worker/src/tests/structures/users.ts | 1 + 50 files changed, 765 insertions(+), 213 deletions(-) rename packages/backend-core/src/context/{deprovision.js => deprovision.ts} (58%) rename packages/backend-core/src/middleware/{matchers.js => matchers.ts} (61%) create mode 100644 packages/types/src/documents/platform/tenants.ts rename packages/worker/src/api/controllers/system/{environment.js => environment.ts} (69%) rename packages/worker/src/api/controllers/system/{status.js => status.ts} (54%) create mode 100644 packages/worker/src/api/routes/global/tests/license.spec.ts create mode 100644 packages/worker/src/api/routes/global/tests/roles.spec.ts create mode 100644 packages/worker/src/api/routes/global/tests/templates.spec.ts create mode 100644 packages/worker/src/api/routes/global/tests/workspaces.spec.ts delete mode 100644 packages/worker/src/api/routes/system/environment.js create mode 100644 packages/worker/src/api/routes/system/environment.ts delete mode 100644 packages/worker/src/api/routes/system/status.js create mode 100644 packages/worker/src/api/routes/system/status.ts delete mode 100644 packages/worker/src/api/routes/system/tenants.js create mode 100644 packages/worker/src/api/routes/system/tenants.ts create mode 100644 packages/worker/src/api/routes/system/tests/environment.spec.ts create mode 100644 packages/worker/src/api/routes/system/tests/migrations.spec.ts create mode 100644 packages/worker/src/api/routes/system/tests/restore.spec.ts create mode 100644 packages/worker/src/api/routes/system/tests/status.spec.ts create mode 100644 packages/worker/src/api/routes/system/tests/tenants.spec.ts create mode 100644 packages/worker/src/middleware/tests/tenancy.spec.ts create mode 100644 packages/worker/src/tests/api/base.ts create mode 100644 packages/worker/src/tests/api/environment.ts create mode 100644 packages/worker/src/tests/api/migrations.ts create mode 100644 packages/worker/src/tests/api/restore.ts create mode 100644 packages/worker/src/tests/api/status.ts create mode 100644 packages/worker/src/tests/api/tenants.ts delete mode 100644 packages/worker/src/tests/structures/accounts.ts diff --git a/packages/backend-core/src/context/deprovision.js b/packages/backend-core/src/context/deprovision.ts similarity index 58% rename from packages/backend-core/src/context/deprovision.js rename to packages/backend-core/src/context/deprovision.ts index ba3c2d8449..2723b1841e 100644 --- a/packages/backend-core/src/context/deprovision.js +++ b/packages/backend-core/src/context/deprovision.ts @@ -1,15 +1,16 @@ -const { getGlobalUserParams, getAllApps } = require("../db/utils") -const { doWithDB } = require("../db") -const { doWithGlobalDB } = require("../tenancy") -const { StaticDatabases } = require("../db/constants") +import { getGlobalUserParams, getAllApps } from "../db/utils" +import { doWithDB } from "../db" +import { doWithGlobalDB } from "../tenancy" +import { StaticDatabases } from "../db/constants" +import { App, Tenants, User } from "@budibase/types" const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name -const removeTenantFromInfoDB = async tenantId => { +const removeTenantFromInfoDB = async (tenantId: string) => { try { - await doWithDB(PLATFORM_INFO_DB, async infoDb => { - let tenants = await infoDb.get(TENANT_DOC) + await doWithDB(PLATFORM_INFO_DB, async (infoDb: any) => { + const tenants = (await infoDb.get(TENANT_DOC)) as Tenants tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId) await infoDb.put(tenants) @@ -20,14 +21,14 @@ const removeTenantFromInfoDB = async tenantId => { } } -exports.removeUserFromInfoDB = async dbUser => { - await doWithDB(PLATFORM_INFO_DB, async infoDb => { +export const removeUserFromInfoDB = async (dbUser: User) => { + await doWithDB(PLATFORM_INFO_DB, async (infoDb: any) => { const keys = [dbUser._id, dbUser.email] const userDocs = await infoDb.allDocs({ keys, include_docs: true, }) - const toDelete = userDocs.rows.map(row => { + const toDelete = userDocs.rows.map((row: any) => { return { ...row.doc, _deleted: true, @@ -37,18 +38,18 @@ exports.removeUserFromInfoDB = async dbUser => { }) } -const removeUsersFromInfoDB = async tenantId => { - return doWithGlobalDB(tenantId, async db => { +const removeUsersFromInfoDB = async (tenantId: string) => { + return doWithGlobalDB(tenantId, async (db: any) => { try { const allUsers = await db.allDocs( getGlobalUserParams(null, { include_docs: true, }) ) - await doWithDB(PLATFORM_INFO_DB, async infoDb => { - const allEmails = allUsers.rows.map(row => row.doc.email) + await doWithDB(PLATFORM_INFO_DB, async (infoDb: any) => { + const allEmails = allUsers.rows.map((row: any) => row.doc.email) // get the id docs - let keys = allUsers.rows.map(row => row.id) + let keys = allUsers.rows.map((row: any) => row.id) // and the email docs keys = keys.concat(allEmails) // retrieve the docs and delete them @@ -56,7 +57,7 @@ const removeUsersFromInfoDB = async tenantId => { keys, include_docs: true, }) - const toDelete = userDocs.rows.map(row => { + const toDelete = userDocs.rows.map((row: any) => { return { ...row.doc, _deleted: true, @@ -71,8 +72,8 @@ const removeUsersFromInfoDB = async tenantId => { }) } -const removeGlobalDB = async tenantId => { - return doWithGlobalDB(tenantId, async db => { +const removeGlobalDB = async (tenantId: string) => { + return doWithGlobalDB(tenantId, async (db: any) => { try { await db.destroy() } catch (err) { @@ -82,11 +83,11 @@ const removeGlobalDB = async tenantId => { }) } -const removeTenantApps = async tenantId => { +const removeTenantApps = async (tenantId: string) => { try { - const apps = await getAllApps({ all: true }) + const apps = (await getAllApps({ all: true })) as App[] const destroyPromises = apps.map(app => - doWithDB(app.appId, db => db.destroy()) + doWithDB(app.appId, (db: any) => db.destroy()) ) await Promise.allSettled(destroyPromises) } catch (err) { @@ -96,7 +97,7 @@ const removeTenantApps = async tenantId => { } // can't live in tenancy package due to circular dependency on db/utils -exports.deleteTenant = async tenantId => { +export const deleteTenant = async (tenantId: string) => { await removeTenantFromInfoDB(tenantId) await removeUsersFromInfoDB(tenantId) await removeGlobalDB(tenantId) diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 17393b8ac3..f356a56cbf 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -11,7 +11,7 @@ import env from "./environment" import tenancy from "./tenancy" import featureFlags from "./featureFlags" import * as sessions from "./security/sessions" -import deprovisioning from "./context/deprovision" +import * as deprovisioning from "./context/deprovision" import auth from "./auth" import constants from "./constants" import * as dbConstants from "./db/constants" diff --git a/packages/backend-core/src/middleware/matchers.js b/packages/backend-core/src/middleware/matchers.ts similarity index 61% rename from packages/backend-core/src/middleware/matchers.js rename to packages/backend-core/src/middleware/matchers.ts index 3d5065c069..e40a9a61b3 100644 --- a/packages/backend-core/src/middleware/matchers.js +++ b/packages/backend-core/src/middleware/matchers.ts @@ -1,27 +1,35 @@ +import { BBContext, EndpointMatcher, RegexMatcher } from "@budibase/types" + const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g -exports.buildMatcherRegex = patterns => { +export const buildMatcherRegex = ( + patterns: EndpointMatcher[] +): RegexMatcher[] => { if (!patterns) { return [] } return patterns.map(pattern => { - const isObj = typeof pattern === "object" && pattern.route - const method = isObj ? pattern.method : "GET" + let route = pattern.route + const method = pattern.method const strict = pattern.strict ? pattern.strict : false - let route = isObj ? pattern.route : pattern + // if there is a param in the route + // use a wildcard pattern const matches = route.match(PARAM_REGEX) if (matches) { for (let match of matches) { - const pattern = "/.*" + (match.endsWith("/") ? "/" : "") + const suffix = match.endsWith("/") ? "/" : "" + const pattern = "/.*" + suffix route = route.replace(match, pattern) + console.log(route) } } + return { regex: new RegExp(route), method, strict, route } }) } -exports.matches = (ctx, options) => { +export const matches = (ctx: BBContext, options: RegexMatcher[]) => { return options.find(({ regex, method, strict, route }) => { let urlMatch if (strict) { diff --git a/packages/backend-core/src/middleware/tenancy.ts b/packages/backend-core/src/middleware/tenancy.ts index 880daa9b5b..03dd9d11e6 100644 --- a/packages/backend-core/src/middleware/tenancy.ts +++ b/packages/backend-core/src/middleware/tenancy.ts @@ -1,5 +1,6 @@ import { doInTenant, getTenantIDFromCtx } from "../tenancy" import { buildMatcherRegex, matches } from "./matchers" +import { Headers } from "../constants" import { BBContext, EndpointMatcher, @@ -9,7 +10,7 @@ import { const tenancy = ( allowQueryStringPatterns: EndpointMatcher[], - noTenancyPatterns: EndpointMatcher, + noTenancyPatterns: EndpointMatcher[], opts = { noTenancyRequired: false } ) => { const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns) @@ -28,6 +29,7 @@ const tenancy = ( } const tenantId = getTenantIDFromCtx(ctx, tenantOpts) + ctx.set(Headers.TENANT_ID, tenantId as string) return doInTenant(tenantId, next) } } diff --git a/packages/backend-core/src/tenancy/tenancy.ts b/packages/backend-core/src/tenancy/tenancy.ts index 0d3bf54d07..66d170d3f8 100644 --- a/packages/backend-core/src/tenancy/tenancy.ts +++ b/packages/backend-core/src/tenancy/tenancy.ts @@ -214,12 +214,31 @@ export const getTenantIDFromCtx = ( // e.g. tenant.budibase.app or tenant.local.com const requestHost = ctx.host // parse the tenant id from the difference - const tenantId = requestHost.substring( - 0, - requestHost.indexOf(`.${platformHost}`) + if (requestHost.includes(platformHost)) { + const tenantId = requestHost.substring( + 0, + requestHost.indexOf(`.${platformHost}`) + ) + if (tenantId) { + return tenantId + } + } + } + + if (isAllowed(TenantResolutionStrategy.PATH)) { + // params - have to parse manually due to koa-router not run yet + const match = ctx.matched.find( + (m: any) => !!m.paramNames.find((p: any) => p.name === "tenantId") ) - if (tenantId) { - return tenantId + if (match) { + const params = match.params( + ctx.originalUrl, + match.captures(ctx.originalUrl), + {} + ) + if (params.tenantId) { + return params.tenantId + } } } diff --git a/packages/backend-core/src/utils.ts b/packages/backend-core/src/utils.ts index 03b4747ee3..215288b93c 100644 --- a/packages/backend-core/src/utils.ts +++ b/packages/backend-core/src/utils.ts @@ -8,7 +8,12 @@ import userCache from "./cache/user" import { getSessionsForUser, invalidateSessions } from "./security/sessions" import * as events from "./events" import tenancy from "./tenancy" -import { App, BBContext, TenantResolutionStrategy } from "@budibase/types" +import { + App, + BBContext, + PlatformLogoutOpts, + TenantResolutionStrategy, +} from "@budibase/types" import { SetOption } from "cookies" const APP_PREFIX = DocumentType.APP + SEPARATOR @@ -189,12 +194,6 @@ export const getBuildersCount = async () => { return builders.length } -interface PlatformLogoutOpts { - ctx: BBContext - userId: string - keepActiveSession: boolean -} - /** * Logs a user out from budibase. Re-used across account portal and builder. */ diff --git a/packages/backend-core/tests/utilities/index.ts b/packages/backend-core/tests/utilities/index.ts index 1e73be4c17..fb12717c89 100644 --- a/packages/backend-core/tests/utilities/index.ts +++ b/packages/backend-core/tests/utilities/index.ts @@ -1,2 +1,3 @@ export * as mocks from "./mocks" export * as structures from "./structures" +export { generator } from "./structures" diff --git a/packages/backend-core/tests/utilities/mocks/accounts.ts b/packages/backend-core/tests/utilities/mocks/accounts.ts index 79436443db..cb4c68b65e 100644 --- a/packages/backend-core/tests/utilities/mocks/accounts.ts +++ b/packages/backend-core/tests/utilities/mocks/accounts.ts @@ -1,7 +1,9 @@ export const getAccount = jest.fn() export const getAccountByTenantId = jest.fn() +export const getStatus = jest.fn() jest.mock("../../../src/cloud/accounts", () => ({ getAccount, getAccountByTenantId, + getStatus, })) diff --git a/packages/backend-core/tests/utilities/structures/accounts.ts b/packages/backend-core/tests/utilities/structures/accounts.ts index 5d23962575..f1718aecc0 100644 --- a/packages/backend-core/tests/utilities/structures/accounts.ts +++ b/packages/backend-core/tests/utilities/structures/accounts.ts @@ -1,23 +1,29 @@ import { generator, uuid } from "." -import { AuthType, CloudAccount, Hosting } from "@budibase/types" import * as db from "../../../src/db/utils" +import { Account, AuthType, CloudAccount, Hosting } from "@budibase/types" -export const cloudAccount = (): CloudAccount => { +export const account = (): Account => { return { accountId: uuid(), + tenantId: generator.word(), + email: generator.email(), + tenantName: generator.word(), + hosting: Hosting.SELF, createdAt: Date.now(), verified: true, verificationSent: true, - tier: "", - email: generator.email(), - tenantId: generator.word(), - hosting: Hosting.CLOUD, + tier: "FREE", // DEPRECATED authType: AuthType.PASSWORD, - password: generator.word(), - tenantName: generator.word(), name: generator.name(), size: "10+", profession: "Software Engineer", + } +} + +export const cloudAccount = (): CloudAccount => { + return { + ...account(), + hosting: Hosting.CLOUD, budibaseUserId: db.generateGlobalUserID(), } } diff --git a/packages/types/src/documents/platform/index.ts b/packages/types/src/documents/platform/index.ts index 1a7cef91cf..8f57ce85fa 100644 --- a/packages/types/src/documents/platform/index.ts +++ b/packages/types/src/documents/platform/index.ts @@ -1,3 +1,4 @@ export * from "./info" export * from "./users" export * from "./accounts" +export * from "./tenants" diff --git a/packages/types/src/documents/platform/tenants.ts b/packages/types/src/documents/platform/tenants.ts new file mode 100644 index 0000000000..dd994df1d2 --- /dev/null +++ b/packages/types/src/documents/platform/tenants.ts @@ -0,0 +1,5 @@ +import { Document } from "../document" + +export interface Tenants extends Document { + tenantIds: string[] +} diff --git a/packages/types/src/sdk/auth.ts b/packages/types/src/sdk/auth.ts index 6a040abf77..d61b679c62 100644 --- a/packages/types/src/sdk/auth.ts +++ b/packages/types/src/sdk/auth.ts @@ -1,3 +1,5 @@ +import { BBContext } from "./koa" + export interface AuthToken { userId: string tenantId: string @@ -25,3 +27,9 @@ export interface SessionKey { export interface ScannedSession { value: Session } + +export interface PlatformLogoutOpts { + ctx: BBContext + userId: string + keepActiveSession: boolean +} diff --git a/packages/types/src/sdk/middleware/matchers.ts b/packages/types/src/sdk/middleware/matchers.ts index 026c3924b0..fc4ceb323e 100644 --- a/packages/types/src/sdk/middleware/matchers.ts +++ b/packages/types/src/sdk/middleware/matchers.ts @@ -1,4 +1,22 @@ export interface EndpointMatcher { + /** + * The HTTP Path. e.g. /api/things/:thingId + */ route: string + /** + * The HTTP Verb. e.g. GET, POST, etc. + * ALL is also accepted to cover all verbs. + */ method: string + /** + * The route must match exactly - not just begins with + */ + strict?: boolean +} + +export interface RegexMatcher { + regex: RegExp + method: string + strict: boolean + route: string } diff --git a/packages/types/src/sdk/middleware/tenancy.ts b/packages/types/src/sdk/middleware/tenancy.ts index dd94b680c3..8bb362d049 100644 --- a/packages/types/src/sdk/middleware/tenancy.ts +++ b/packages/types/src/sdk/middleware/tenancy.ts @@ -9,4 +9,5 @@ export enum TenantResolutionStrategy { HEADER = "header", QUERY = "query", SUBDOMAIN = "subdomain", + PATH = "path", } diff --git a/packages/worker/scripts/jestSetup.js b/packages/worker/scripts/jestSetup.js index 8ee2d33d70..7539a65980 100644 --- a/packages/worker/scripts/jestSetup.js +++ b/packages/worker/scripts/jestSetup.js @@ -1,10 +1,13 @@ const env = require("../src/environment") -env._set("SELF_HOSTED", "1") +env._set("SELF_HOSTED", "0") env._set("NODE_ENV", "jest") env._set("JWT_SECRET", "test-jwtsecret") env._set("LOG_LEVEL", "silent") env._set("MULTI_TENANCY", true) +env._set("PLATFORM_URL", "http://localhost:10000") +env._set("INTERNAL_API_KEY", "test") +env._set("DISABLE_ACCOUNT_PORTAL", false) const { mocks } = require("@budibase/backend-core/tests") diff --git a/packages/worker/src/api/controllers/system/environment.js b/packages/worker/src/api/controllers/system/environment.ts similarity index 69% rename from packages/worker/src/api/controllers/system/environment.js rename to packages/worker/src/api/controllers/system/environment.ts index 4edf1ff8d3..8ae0fcda3f 100644 --- a/packages/worker/src/api/controllers/system/environment.js +++ b/packages/worker/src/api/controllers/system/environment.ts @@ -1,6 +1,7 @@ -const env = require("../../../environment") +import { BBContext } from "@budibase/types" +import env from "../../../environment" -exports.fetch = async ctx => { +export const fetch = async (ctx: BBContext) => { ctx.body = { multiTenancy: !!env.MULTI_TENANCY, cloud: !env.SELF_HOSTED, diff --git a/packages/worker/src/api/controllers/system/status.js b/packages/worker/src/api/controllers/system/status.ts similarity index 54% rename from packages/worker/src/api/controllers/system/status.js rename to packages/worker/src/api/controllers/system/status.ts index 9d2bd6ecda..b763a67d4f 100644 --- a/packages/worker/src/api/controllers/system/status.js +++ b/packages/worker/src/api/controllers/system/status.ts @@ -1,7 +1,8 @@ -const accounts = require("@budibase/backend-core/accounts") -const env = require("../../../environment") +import { accounts } from "@budibase/backend-core" +import env from "../../../environment" +import { BBContext } from "@budibase/types" -exports.fetch = async ctx => { +export const fetch = async (ctx: BBContext) => { if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { const status = await accounts.getStatus() ctx.body = status diff --git a/packages/worker/src/api/controllers/system/tenants.ts b/packages/worker/src/api/controllers/system/tenants.ts index d6e6261c22..8fd1446bcb 100644 --- a/packages/worker/src/api/controllers/system/tenants.ts +++ b/packages/worker/src/api/controllers/system/tenants.ts @@ -1,60 +1,17 @@ -const { StaticDatabases, doWithDB } = require("@budibase/backend-core/db") -const { getTenantId } = require("@budibase/backend-core/tenancy") -const { deleteTenant } = require("@budibase/backend-core/deprovision") +import { BBContext } from "@budibase/types" +import { deprovisioning } from "@budibase/backend-core" import { quotas } from "@budibase/pro" -export const exists = async (ctx: any) => { - const tenantId = ctx.request.params - ctx.body = { - exists: await doWithDB( - StaticDatabases.PLATFORM_INFO.name, - async (db: any) => { - let exists = false - try { - const tenantsDoc = await db.get( - StaticDatabases.PLATFORM_INFO.docs.tenants - ) - if (tenantsDoc) { - exists = tenantsDoc.tenantIds.indexOf(tenantId) !== -1 - } - } catch (err) { - // if error it doesn't exist - } - return exists - } - ), - } -} +const _delete = async (ctx: BBContext) => { + const user = ctx.user! + const tenantId = ctx.params.tenantId -export const fetch = async (ctx: any) => { - ctx.body = await doWithDB( - StaticDatabases.PLATFORM_INFO.name, - async (db: any) => { - let tenants = [] - try { - const tenantsDoc = await db.get( - StaticDatabases.PLATFORM_INFO.docs.tenants - ) - if (tenantsDoc) { - tenants = tenantsDoc.tenantIds - } - } catch (err) { - // if error it doesn't exist - } - return tenants - } - ) -} - -const _delete = async (ctx: any) => { - const tenantId = getTenantId() - - if (ctx.params.tenantId !== tenantId) { - ctx.throw(403, "Unauthorized") + if (tenantId !== user.tenantId) { + ctx.throw(403, "Tenant ID does not match current user") } try { - await deleteTenant(tenantId) + await deprovisioning.deleteTenant(tenantId) await quotas.bustCache() ctx.status = 204 } catch (err) { diff --git a/packages/worker/src/api/index.ts b/packages/worker/src/api/index.ts index 22ff159dff..28fbbdb4b5 100644 --- a/packages/worker/src/api/index.ts +++ b/packages/worker/src/api/index.ts @@ -7,11 +7,12 @@ import { errors, auth, middleware } from "@budibase/backend-core" import { APIError } from "@budibase/types" const PUBLIC_ENDPOINTS = [ - // old deprecated endpoints kept for backwards compat + // deprecated single tenant sso callback { route: "/api/admin/auth/google/callback", method: "GET", }, + // deprecated single tenant sso callback { route: "/api/admin/auth/oidc/callback", method: "GET", @@ -51,10 +52,12 @@ const PUBLIC_ENDPOINTS = [ route: "api/system/status", method: "GET", }, + // TODO: This should be an internal api { route: "/api/global/users/tenant/:id", method: "GET", }, + // TODO: This should be an internal api { route: "/api/system/restored", method: "POST", @@ -62,17 +65,37 @@ const PUBLIC_ENDPOINTS = [ ] const NO_TENANCY_ENDPOINTS = [ - ...PUBLIC_ENDPOINTS, + // system endpoints are not specific to any tenant { route: "/api/system", method: "ALL", }, + // tenant is determined in request body + // used for creating the tenant { - route: "/api/global/users/self", + route: "/api/global/users/init", + method: "POST", + }, + // deprecated single tenant sso callback + { + route: "/api/admin/auth/google/callback", method: "GET", }, + // deprecated single tenant sso callback { - route: "/api/global/self", + route: "/api/admin/auth/oidc/callback", + method: "GET", + }, + // tenant is determined from code in redis + { + route: "/api/global/users/invite/accept", + method: "POST", + }, + // global user search - no tenancy + // :id is user id + // TODO: this should really be `/api/system/users/:id` + { + route: "/api/global/users/tenant/:id", method: "GET", }, ] diff --git a/packages/worker/src/api/routes/global/auth.js b/packages/worker/src/api/routes/global/auth.js index 1c292cdc7f..674279a6f4 100644 --- a/packages/worker/src/api/routes/global/auth.js +++ b/packages/worker/src/api/routes/global/auth.js @@ -2,7 +2,6 @@ const Router = require("@koa/router") const authController = require("../../controllers/global/auth") const { joiValidator } = require("@budibase/backend-core/auth") const Joi = require("joi") -const { updateTenantId } = require("@budibase/backend-core/tenancy") const router = new Router() @@ -29,43 +28,28 @@ function buildResetUpdateValidation() { }).required().unknown(false)) } -function updateTenant(ctx, next) { - if (ctx.params) { - updateTenantId(ctx.params.tenantId) - } - return next() -} - router .post( "/api/global/auth/:tenantId/login", buildAuthValidation(), - updateTenant, authController.authenticate ) .post( "/api/global/auth/:tenantId/reset", buildResetValidation(), - updateTenant, authController.reset ) .post( "/api/global/auth/:tenantId/reset/update", buildResetUpdateValidation(), - updateTenant, authController.resetUpdate ) .post("/api/global/auth/logout", authController.logout) .post("/api/global/auth/init", authController.setInitInfo) .get("/api/global/auth/init", authController.getInitInfo) - .get( - "/api/global/auth/:tenantId/google", - updateTenant, - authController.googlePreAuth - ) + .get("/api/global/auth/:tenantId/google", authController.googlePreAuth) .get( "/api/global/auth/:tenantId/datasource/:provider", - updateTenant, authController.datasourcePreAuth ) // single tenancy endpoint @@ -75,29 +59,19 @@ router authController.datasourceAuth ) // multi-tenancy endpoint - .get( - "/api/global/auth/:tenantId/google/callback", - updateTenant, - authController.googleAuth - ) + .get("/api/global/auth/:tenantId/google/callback", authController.googleAuth) .get( "/api/global/auth/:tenantId/datasource/:provider/callback", - updateTenant, authController.datasourceAuth ) .get( "/api/global/auth/:tenantId/oidc/configs/:configId", - updateTenant, authController.oidcPreAuth ) // single tenancy endpoint .get("/api/global/auth/oidc/callback", authController.oidcAuth) // multi-tenancy endpoint - .get( - "/api/global/auth/:tenantId/oidc/callback", - updateTenant, - authController.oidcAuth - ) + .get("/api/global/auth/:tenantId/oidc/callback", authController.oidcAuth) // deprecated - used by the default system before tenancy .get("/api/admin/auth/google/callback", authController.googleAuth) .get("/api/admin/auth/oidc/callback", authController.oidcAuth) diff --git a/packages/worker/src/api/routes/global/tests/configs.spec.ts b/packages/worker/src/api/routes/global/tests/configs.spec.ts index 31510c03dd..99376988da 100644 --- a/packages/worker/src/api/routes/global/tests/configs.spec.ts +++ b/packages/worker/src/api/routes/global/tests/configs.spec.ts @@ -235,7 +235,7 @@ describe("configs", () => { expect(events.org.nameUpdated).toBeCalledTimes(1) expect(events.org.logoUpdated).toBeCalledTimes(1) expect(events.org.platformURLUpdated).toBeCalledTimes(1) - config.modeAccount() + config.modeCloud() }) }) @@ -257,7 +257,7 @@ describe("configs", () => { expect(events.org.nameUpdated).toBeCalledTimes(1) expect(events.org.logoUpdated).toBeCalledTimes(1) expect(events.org.platformURLUpdated).toBeCalledTimes(1) - config.modeAccount() + config.modeCloud() }) }) }) diff --git a/packages/worker/src/api/routes/global/tests/license.spec.ts b/packages/worker/src/api/routes/global/tests/license.spec.ts new file mode 100644 index 0000000000..a1566730ea --- /dev/null +++ b/packages/worker/src/api/routes/global/tests/license.spec.ts @@ -0,0 +1,28 @@ +import { TestConfiguration, API } from "../../../../tests" + +describe("/api/global/license", () => { + const config = new TestConfiguration() + const api = new API(config) + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("POST /api/global/license/activate", () => {}) + + describe("POST /api/global/license/refresh", () => {}) + + describe("GET /api/global/license/info", () => {}) + + describe("DELETE /api/global/license/info", () => {}) + + describe("GET /api/global/license/usage", () => {}) +}) diff --git a/packages/worker/src/api/routes/global/tests/roles.spec.ts b/packages/worker/src/api/routes/global/tests/roles.spec.ts new file mode 100644 index 0000000000..c9d6d085a5 --- /dev/null +++ b/packages/worker/src/api/routes/global/tests/roles.spec.ts @@ -0,0 +1,24 @@ +import { TestConfiguration, API } from "../../../../tests" + +describe("/api/global/roles", () => { + const config = new TestConfiguration() + const api = new API(config) + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("GET /api/global/roles", () => {}) + + describe("GET /api/global/roles/:appId", () => {}) + + describe("DELETE /api/global/roles/:appId", () => {}) +}) diff --git a/packages/worker/src/api/routes/global/tests/templates.spec.ts b/packages/worker/src/api/routes/global/tests/templates.spec.ts new file mode 100644 index 0000000000..8986639637 --- /dev/null +++ b/packages/worker/src/api/routes/global/tests/templates.spec.ts @@ -0,0 +1,32 @@ +import { TestConfiguration, API } from "../../../../tests" + +describe("/api/global/template", () => { + const config = new TestConfiguration() + const api = new API(config) + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("GET /api/global/template/definitions", () => {}) + + describe("POST /api/global/template", () => {}) + + describe("GET /api/global/template", () => {}) + + describe("GET /api/global/template/:type", () => {}) + + describe("GET /api/global/template/:ownerId", () => {}) + + describe("GET /api/global/template/:id", () => {}) + + describe("DELETE /api/global/template/:id/:rev", () => {}) +}) diff --git a/packages/worker/src/api/routes/global/tests/workspaces.spec.ts b/packages/worker/src/api/routes/global/tests/workspaces.spec.ts new file mode 100644 index 0000000000..f7ab1d9a7a --- /dev/null +++ b/packages/worker/src/api/routes/global/tests/workspaces.spec.ts @@ -0,0 +1,26 @@ +import { TestConfiguration, API } from "../../../../tests" + +describe("/api/global/workspaces", () => { + const config = new TestConfiguration() + const api = new API(config) + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("GET /api/global/workspaces", () => {}) + + describe("DELETE /api/global/workspaces/:id", () => {}) + + describe("GET /api/global/workspaces", () => {}) + + describe("GET /api/global/workspaces/:id", () => {}) +}) diff --git a/packages/worker/src/api/routes/system/environment.js b/packages/worker/src/api/routes/system/environment.js deleted file mode 100644 index 3d34046317..0000000000 --- a/packages/worker/src/api/routes/system/environment.js +++ /dev/null @@ -1,8 +0,0 @@ -const Router = require("@koa/router") -const controller = require("../../controllers/system/environment") - -const router = new Router() - -router.get("/api/system/environment", controller.fetch) - -module.exports = router diff --git a/packages/worker/src/api/routes/system/environment.ts b/packages/worker/src/api/routes/system/environment.ts new file mode 100644 index 0000000000..360ec7ed84 --- /dev/null +++ b/packages/worker/src/api/routes/system/environment.ts @@ -0,0 +1,8 @@ +import Router from "@koa/router" +import * as controller from "../../controllers/system/environment" + +const router = new Router() + +router.get("/api/system/environment", controller.fetch) + +export default router diff --git a/packages/worker/src/api/routes/system/status.js b/packages/worker/src/api/routes/system/status.js deleted file mode 100644 index 17d2f8a5a6..0000000000 --- a/packages/worker/src/api/routes/system/status.js +++ /dev/null @@ -1,8 +0,0 @@ -const Router = require("@koa/router") -const controller = require("../../controllers/system/status") - -const router = new Router() - -router.get("/api/system/status", controller.fetch) - -module.exports = router diff --git a/packages/worker/src/api/routes/system/status.ts b/packages/worker/src/api/routes/system/status.ts new file mode 100644 index 0000000000..a5b393b421 --- /dev/null +++ b/packages/worker/src/api/routes/system/status.ts @@ -0,0 +1,8 @@ +import Router from "@koa/router" +import * as controller from "../../controllers/system/status" + +const router = new Router() + +router.get("/api/system/status", controller.fetch) + +export default router diff --git a/packages/worker/src/api/routes/system/tenants.js b/packages/worker/src/api/routes/system/tenants.js deleted file mode 100644 index 6247e76058..0000000000 --- a/packages/worker/src/api/routes/system/tenants.js +++ /dev/null @@ -1,12 +0,0 @@ -const Router = require("@koa/router") -const controller = require("../../controllers/system/tenants") -const { adminOnly } = require("@budibase/backend-core/auth") - -const router = new Router() - -router - .get("/api/system/tenants/:tenantId/exists", controller.exists) - .get("/api/system/tenants", adminOnly, controller.fetch) - .delete("/api/system/tenants/:tenantId", adminOnly, controller.delete) - -module.exports = router diff --git a/packages/worker/src/api/routes/system/tenants.ts b/packages/worker/src/api/routes/system/tenants.ts new file mode 100644 index 0000000000..7feb73a234 --- /dev/null +++ b/packages/worker/src/api/routes/system/tenants.ts @@ -0,0 +1,13 @@ +import Router from "@koa/router" +import * as controller from "../../controllers/system/tenants" +import { middleware } from "@budibase/backend-core" + +const router = new Router() + +router.delete( + "/api/system/tenants/:tenantId", + middleware.adminOnly, + controller.delete +) + +export default router diff --git a/packages/worker/src/api/routes/system/tests/environment.spec.ts b/packages/worker/src/api/routes/system/tests/environment.spec.ts new file mode 100644 index 0000000000..5cc3b0c6f1 --- /dev/null +++ b/packages/worker/src/api/routes/system/tests/environment.spec.ts @@ -0,0 +1,30 @@ +import { TestConfiguration, API } from "../../../../tests" + +describe("/api/system/environment", () => { + const config = new TestConfiguration() + const api = new API(config) + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("GET /api/system/environment", () => { + it("returns the expected environment", async () => { + const env = await api.environment.getEnvironment() + expect(env.body).toEqual({ + cloud: true, + disableAccountPortal: false, + isDev: false, + multiTenancy: true, + }) + }) + }) +}) diff --git a/packages/worker/src/api/routes/system/tests/migrations.spec.ts b/packages/worker/src/api/routes/system/tests/migrations.spec.ts new file mode 100644 index 0000000000..0e3883f9ef --- /dev/null +++ b/packages/worker/src/api/routes/system/tests/migrations.spec.ts @@ -0,0 +1,64 @@ +const migrateFn = jest.fn() + +import { TestConfiguration, API } from "../../../../tests" + +jest.mock("../../../../migrations", () => { + return { + ...jest.requireActual("../../../../migrations"), + migrate: migrateFn, + } +}) + +describe("/api/system/migrations", () => { + const config = new TestConfiguration() + const api = new API(config) + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("POST /api/system/migrations/run", () => { + it("fails with no internal api key", async () => { + const res = await api.migrations.runMigrations({ + headers: {}, + status: 403, + }) + expect(res.text).toBe("Unauthorized - no public worker access") + expect(migrateFn).toBeCalledTimes(0) + }) + + it("runs migrations", async () => { + const res = await api.migrations.runMigrations() + expect(res.text).toBe("OK") + expect(migrateFn).toBeCalledTimes(1) + }) + }) + + describe("DELETE /api/system/migrations/definitions", () => { + it("fails with no internal api key", async () => { + const res = await api.migrations.getMigrationDefinitions({ + headers: {}, + status: 403, + }) + expect(res.text).toBe("Unauthorized - no public worker access") + }) + + it("returns definitions", async () => { + const res = await api.migrations.getMigrationDefinitions() + expect(res.body).toEqual([ + { + name: "global_info_sync_users", + type: "global", + }, + ]) + }) + }) +}) diff --git a/packages/worker/src/api/routes/system/tests/restore.spec.ts b/packages/worker/src/api/routes/system/tests/restore.spec.ts new file mode 100644 index 0000000000..1da9389608 --- /dev/null +++ b/packages/worker/src/api/routes/system/tests/restore.spec.ts @@ -0,0 +1,37 @@ +import { TestConfiguration, API } from "../../../../tests" + +describe("/api/system/restore", () => { + const config = new TestConfiguration() + const api = new API(config) + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("POST /api/global/restore", () => { + it("doesn't allow restore in cloud", async () => { + const res = await api.restore.restored({ status: 405 }) + expect(res.body).toEqual({ + message: "This operation is not allowed in cloud.", + status: 405, + }) + }) + + it("restores in self host", async () => { + config.modeSelf() + const res = await api.restore.restored() + expect(res.body).toEqual({ + message: "System prepared after restore.", + }) + config.modeCloud() + }) + }) +}) diff --git a/packages/worker/src/api/routes/system/tests/status.spec.ts b/packages/worker/src/api/routes/system/tests/status.spec.ts new file mode 100644 index 0000000000..98308556d1 --- /dev/null +++ b/packages/worker/src/api/routes/system/tests/status.spec.ts @@ -0,0 +1,49 @@ +import { TestConfiguration, API } from "../../../../tests" +import { accounts } from "@budibase/backend-core" +import { mocks } from "@budibase/backend-core/tests" + +describe("/api/system/status", () => { + const config = new TestConfiguration() + const api = new API(config) + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("GET /api/system/status", () => { + it("returns status in self host", async () => { + config.modeSelf() + const res = await api.status.getStatus() + expect(res.body).toEqual({ + health: { + passing: true, + }, + }) + expect(accounts.getStatus).toBeCalledTimes(0) + config.modeCloud() + }) + + it("returns status in cloud", async () => { + const value = { + health: { + passing: false, + }, + } + + mocks.accounts.getStatus.mockReturnValueOnce(value) + + const res = await api.status.getStatus() + + expect(accounts.getStatus).toBeCalledTimes(1) + expect(res.body).toEqual(value) + }) + }) +}) diff --git a/packages/worker/src/api/routes/system/tests/tenants.spec.ts b/packages/worker/src/api/routes/system/tests/tenants.spec.ts new file mode 100644 index 0000000000..0491d43e8c --- /dev/null +++ b/packages/worker/src/api/routes/system/tests/tenants.spec.ts @@ -0,0 +1,32 @@ +import { TestConfiguration, API } from "../../../../tests" + +describe("/api/global/workspaces", () => { + const config = new TestConfiguration() + const api = new API(config) + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("DELETE /api/system/tenants/:tenantId", () => { + it("allows deleting the current tenant", async () => { + const user = await config.createTenant() + await config.createSession(user) + const res = await api.tenants.delete(user.tenantId, { + headers: config.authHeaders(user), + }) + }) + + it("rejects deleting another tenant", () => {}) + + it("requires admin", () => {}) + }) +}) diff --git a/packages/worker/src/middleware/tests/tenancy.spec.ts b/packages/worker/src/middleware/tests/tenancy.spec.ts new file mode 100644 index 0000000000..04de41b9cf --- /dev/null +++ b/packages/worker/src/middleware/tests/tenancy.spec.ts @@ -0,0 +1,74 @@ +import { TestConfiguration, API, structures } from "../../tests" +import { constants } from "@budibase/backend-core" + +describe("tenancy middleware", () => { + const config = new TestConfiguration() + const api = new API(config) + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it("should get tenant id from user", async () => { + const user = await config.createTenant() + await config.createSession(user) + const res = await api.self.getSelf(user) + expect(res.headers[constants.Headers.TENANT_ID]).toBe(user.tenantId) + }) + + it("should get tenant id from header", async () => { + const tenantId = structures.uuid() + const headers = { + [constants.Headers.TENANT_ID]: tenantId, + } + const res = await config.request + .get(`/api/global/configs/checklist`) + .set(headers) + expect(res.headers[constants.Headers.TENANT_ID]).toBe(tenantId) + }) + + it("should get tenant id from query param", async () => { + const tenantId = structures.uuid() + const res = await config.request.get( + `/api/global/configs/checklist?tenantId=${tenantId}` + ) + expect(res.headers[constants.Headers.TENANT_ID]).toBe(tenantId) + }) + + it("should get tenant id from subdomain", async () => { + const tenantId = structures.uuid() + const headers = { + host: `${tenantId}.localhost:10000`, + } + const res = await config.request + .get(`/api/global/configs/checklist`) + .set(headers) + expect(res.headers[constants.Headers.TENANT_ID]).toBe(tenantId) + }) + + it("should get tenant id from path variable", async () => { + const user = await config.createTenant() + const res = await config.request + .post(`/api/global/auth/${user.tenantId}/login`) + .send({ + username: user.email, + password: user.password, + }) + expect(res.headers[constants.Headers.TENANT_ID]).toBe(user.tenantId) + }) + + it("should throw when no tenant id is found", async () => { + const res = await config.request.get(`/api/global/configs/checklist`) + expect(res.status).toBe(403) + expect(res.text).toBe("Tenant id not set") + expect(res.headers[constants.Headers.TENANT_ID]).toBe(undefined) + }) +}) diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index 746e1ccf1b..31305066da 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -2,6 +2,7 @@ import "./mocks" import dbConfig from "../db" dbConfig.init() import env from "../environment" +import { env as coreEnv } from "@budibase/backend-core" import controllers from "./controllers" const supertest = require("supertest") import { Configs } from "../constants" @@ -12,13 +13,13 @@ import { Headers, sessions, auth, + constants, } from "@budibase/backend-core" -import { TENANT_ID, TENANT_1, CSRF_TOKEN } from "./structures" -import structures from "./structures" +import structures, { TENANT_ID, TENANT_1, CSRF_TOKEN } from "./structures" import { CreateUserResponse, User, AuthToken } from "@budibase/types" enum Mode { - ACCOUNT = "account", + CLOUD = "cloud", SELF = "self", } @@ -31,11 +32,11 @@ class TestConfiguration { constructor( opts: { openServer: boolean; mode: Mode } = { openServer: true, - mode: Mode.ACCOUNT, + mode: Mode.CLOUD, } ) { - if (opts.mode === Mode.ACCOUNT) { - this.modeAccount() + if (opts.mode === Mode.CLOUD) { + this.modeCloud() } else if (opts.mode === Mode.SELF) { this.modeSelf() } @@ -54,20 +55,24 @@ class TestConfiguration { // MODES - modeAccount = () => { - env.SELF_HOSTED = false - // @ts-ignore - env.MULTI_TENANCY = true - // @ts-ignore - env.DISABLE_ACCOUNT_PORTAL = false + setMultiTenancy = (value: boolean) => { + env._set("MULTI_TENANCY", value) + coreEnv._set("MULTI_TENANCY", value) + } + + setSelfHosted = (value: boolean) => { + env._set("SELF_HOSTED", value) + coreEnv._set("SELF_HOSTED", value) + } + + modeCloud = () => { + this.setSelfHosted(false) + this.setMultiTenancy(true) } modeSelf = () => { - env.SELF_HOSTED = true - // @ts-ignore - env.MULTI_TENANCY = false - // @ts-ignore - env.DISABLE_ACCOUNT_PORTAL = true + this.setSelfHosted(true) + this.setMultiTenancy(false) } // UTILS @@ -114,6 +119,16 @@ class TestConfiguration { // TENANCY + createTenant = async (tenantId?: string): Promise => { + // create user / new tenant + if (!tenantId) { + tenantId = structures.uuid() + } + return tenancy.doInTenant(tenantId, async () => { + return this.createUser() + }) + } + getTenantId() { try { return tenancy.getTenantId() @@ -179,6 +194,10 @@ class TestConfiguration { } } + internalAPIHeaders() { + return { [constants.Headers.API_KEY]: env.INTERNAL_API_KEY } + } + async getUser(email: string): Promise { return tenancy.doInTenant(this.getTenantId(), () => { return users.getGlobalUserByEmail(email) diff --git a/packages/worker/src/tests/api/base.ts b/packages/worker/src/tests/api/base.ts new file mode 100644 index 0000000000..c1263ed5cb --- /dev/null +++ b/packages/worker/src/tests/api/base.ts @@ -0,0 +1,16 @@ +import TestConfiguration from "../TestConfiguration" + +export interface TestAPIOpts { + headers?: any + status?: number +} + +export abstract class TestAPI { + config: TestConfiguration + request: any + + protected constructor(config: TestConfiguration) { + this.config = config + this.request = config.request + } +} diff --git a/packages/worker/src/tests/api/environment.ts b/packages/worker/src/tests/api/environment.ts new file mode 100644 index 0000000000..9847c2e369 --- /dev/null +++ b/packages/worker/src/tests/api/environment.ts @@ -0,0 +1,18 @@ +import TestConfiguration from "../TestConfiguration" + +export class EnvironmentAPI { + config: TestConfiguration + request: any + + constructor(config: TestConfiguration) { + this.config = config + this.request = config.request + } + + getEnvironment = () => { + return this.request + .get(`/api/system/environment`) + .expect("Content-Type", /json/) + .expect(200) + } +} diff --git a/packages/worker/src/tests/api/index.ts b/packages/worker/src/tests/api/index.ts index a12e489a78..bc0271b9c6 100644 --- a/packages/worker/src/tests/api/index.ts +++ b/packages/worker/src/tests/api/index.ts @@ -5,6 +5,11 @@ import { ConfigAPI } from "./configs" import { EmailAPI } from "./email" import { SelfAPI } from "./self" import { UserAPI } from "./users" +import { EnvironmentAPI } from "./environment" +import { MigrationAPI } from "./migrations" +import { StatusAPI } from "./status" +import { RestoreAPI } from "./restore" +import { TenantAPI } from "./tenants" export default class API { accounts: AccountAPI @@ -13,6 +18,11 @@ export default class API { emails: EmailAPI self: SelfAPI users: UserAPI + environment: EnvironmentAPI + migrations: MigrationAPI + status: StatusAPI + restore: RestoreAPI + tenants: TenantAPI constructor(config: TestConfiguration) { this.accounts = new AccountAPI(config) @@ -21,5 +31,10 @@ export default class API { this.emails = new EmailAPI(config) this.self = new SelfAPI(config) this.users = new UserAPI(config) + this.environment = new EnvironmentAPI(config) + this.migrations = new MigrationAPI(config) + this.status = new StatusAPI(config) + this.restore = new RestoreAPI(config) + this.tenants = new TenantAPI(config) } } diff --git a/packages/worker/src/tests/api/migrations.ts b/packages/worker/src/tests/api/migrations.ts new file mode 100644 index 0000000000..6038cbd5d8 --- /dev/null +++ b/packages/worker/src/tests/api/migrations.ts @@ -0,0 +1,22 @@ +import TestConfiguration from "../TestConfiguration" +import { TestAPI, TestAPIOpts } from "./base" + +export class MigrationAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + runMigrations = (opts?: TestAPIOpts) => { + return this.request + .post(`/api/system/migrations/run`) + .set(opts?.headers ? opts.headers : this.config.internalAPIHeaders()) + .expect(opts?.status ? opts.status : 200) + } + + getMigrationDefinitions = (opts?: TestAPIOpts) => { + return this.request + .get(`/api/system/migrations/definitions`) + .set(opts?.headers ? opts.headers : this.config.internalAPIHeaders()) + .expect(opts?.status ? opts.status : 200) + } +} diff --git a/packages/worker/src/tests/api/restore.ts b/packages/worker/src/tests/api/restore.ts new file mode 100644 index 0000000000..6069c20185 --- /dev/null +++ b/packages/worker/src/tests/api/restore.ts @@ -0,0 +1,14 @@ +import TestConfiguration from "../TestConfiguration" +import { TestAPI, TestAPIOpts } from "./base" + +export class RestoreAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + restored = (opts?: TestAPIOpts) => { + return this.request + .post(`/api/system/restored`) + .expect(opts?.status ? opts.status : 200) + } +} diff --git a/packages/worker/src/tests/api/self.ts b/packages/worker/src/tests/api/self.ts index b1cd4a48c6..da634e0db3 100644 --- a/packages/worker/src/tests/api/self.ts +++ b/packages/worker/src/tests/api/self.ts @@ -18,4 +18,12 @@ export class SelfAPI { .expect("Content-Type", /json/) .expect(200) } + + getSelf = (user: User) => { + return this.request + .get(`/api/global/self`) + .set(this.config.authHeaders(user)) + .expect("Content-Type", /json/) + .expect(200) + } } diff --git a/packages/worker/src/tests/api/status.ts b/packages/worker/src/tests/api/status.ts new file mode 100644 index 0000000000..c42060631f --- /dev/null +++ b/packages/worker/src/tests/api/status.ts @@ -0,0 +1,15 @@ +import TestConfiguration from "../TestConfiguration" + +export class StatusAPI { + config: TestConfiguration + request: any + + constructor(config: TestConfiguration) { + this.config = config + this.request = config.request + } + + getStatus = () => { + return this.request.get(`/api/system/status`).expect(200) + } +} diff --git a/packages/worker/src/tests/api/tenants.ts b/packages/worker/src/tests/api/tenants.ts new file mode 100644 index 0000000000..d4d47a6e4f --- /dev/null +++ b/packages/worker/src/tests/api/tenants.ts @@ -0,0 +1,15 @@ +import TestConfiguration from "../TestConfiguration" +import { TestAPI, TestAPIOpts } from "./base" + +export class TenantAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + delete = (tenantId: string, opts?: TestAPIOpts) => { + return this.request + .delete(`/api/system/tenants/${tenantId}`) + .set(opts?.headers) + .expect(opts?.status ? opts.status : 200) + } +} diff --git a/packages/worker/src/tests/index.ts b/packages/worker/src/tests/index.ts index 6ab1e83955..e58040b468 100644 --- a/packages/worker/src/tests/index.ts +++ b/packages/worker/src/tests/index.ts @@ -1,10 +1,14 @@ +import { generator } from "@budibase/backend-core/tests" import TestConfiguration from "./TestConfiguration" import structures from "./structures" import mocks from "./mocks" import API from "./api" +import { v4 as uuid } from "uuid" const pkg = { structures, + generator, + uuid, TENANT_1: structures.TENANT_1, mocks, TestConfiguration, diff --git a/packages/worker/src/tests/structures/accounts.ts b/packages/worker/src/tests/structures/accounts.ts deleted file mode 100644 index df6b993684..0000000000 --- a/packages/worker/src/tests/structures/accounts.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Account, AuthType, Hosting, CloudAccount } from "@budibase/types" -import { v4 as uuid } from "uuid" -import { utils } from "@budibase/backend-core" - -export const account = (): Account => { - return { - email: `${uuid()}@test.com`, - tenantId: utils.newid(), - hosting: Hosting.SELF, - authType: AuthType.SSO, - accountId: uuid(), - createdAt: Date.now(), - verified: true, - verificationSent: true, - tier: "FREE", - } -} - -export const cloudAccount = (): CloudAccount => { - return { - ...account(), - budibaseUserId: uuid(), - } -} diff --git a/packages/worker/src/tests/structures/index.ts b/packages/worker/src/tests/structures/index.ts index a3029b0105..3a4c3324df 100644 --- a/packages/worker/src/tests/structures/index.ts +++ b/packages/worker/src/tests/structures/index.ts @@ -1,16 +1,18 @@ +import { structures } from "@budibase/backend-core/tests" import configs from "./configs" import * as users from "./users" import * as groups from "./groups" -import * as accounts from "./accounts" +import { v4 as uuid } from "uuid" const TENANT_ID = "default" const TENANT_1 = "tenant1" const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306" const pkg = { + ...structures, + uuid, configs, users, - accounts, TENANT_ID, TENANT_1, CSRF_TOKEN, diff --git a/packages/worker/src/tests/structures/users.ts b/packages/worker/src/tests/structures/users.ts index 4bf24ec780..bef9f38586 100644 --- a/packages/worker/src/tests/structures/users.ts +++ b/packages/worker/src/tests/structures/users.ts @@ -5,6 +5,7 @@ import { v4 as uuid } from "uuid" export const newEmail = () => { return `${uuid()}@test.com` } + export const user = (userProps?: any): User => { return { email: newEmail(),