Fixing test cases and bugs that they raised.

This commit is contained in:
mike12345567 2021-07-22 22:36:16 +01:00
parent 24012c2fba
commit 313302cae2
16 changed files with 86 additions and 37 deletions

View File

@ -82,13 +82,21 @@ exports.getGlobalDB = tenantId => {
} }
/** /**
* Given a koa context this tries to find the correct tenant Global DB. * Given a koa context this tries to extra what tenant is being accessed.
*/ */
exports.getGlobalDBFromCtx = ctx => { exports.getTenantIdFromCtx = ctx => {
const user = ctx.user || {} const user = ctx.user || {}
const params = ctx.request.params || {} const params = ctx.request.params || {}
const query = ctx.request.query || {} const query = ctx.request.query || {}
return exports.getGlobalDB(user.tenantId || params.tenantId || query.tenantId) return user.tenantId || params.tenantId || query.tenantId
}
/**
* Given a koa context this tries to find the correct tenant Global DB.
*/
exports.getGlobalDBFromCtx = ctx => {
const tenantId = exports.getTenantIdFromCtx(ctx)
return exports.getGlobalDB(tenantId)
} }
/** /**

View File

@ -3,6 +3,7 @@ const appController = require("../../../controllers/application")
const CouchDB = require("../../../../db") const CouchDB = require("../../../../db")
const { AppStatus } = require("../../../../db/utils") const { AppStatus } = require("../../../../db/utils")
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
const { TENANT_ID } = require("../../../../tests/utilities/structures")
function Request(appId, params) { function Request(appId, params) {
this.appId = appId this.appId = appId
@ -16,8 +17,8 @@ exports.getAllTableRows = async config => {
return req.body return req.body
} }
exports.clearAllApps = async () => { exports.clearAllApps = async (tenantId = TENANT_ID) => {
const req = { query: { status: AppStatus.DEV } } const req = { query: { status: AppStatus.DEV }, user: { tenantId } }
await appController.fetch(req) await appController.fetch(req)
const apps = req.body const apps = req.body
if (!apps || apps.length <= 0) { if (!apps || apps.length <= 0) {

View File

@ -10,20 +10,23 @@ const {
basicScreen, basicScreen,
basicLayout, basicLayout,
basicWebhook, basicWebhook,
TENANT_ID,
} = require("./structures") } = require("./structures")
const controllers = require("./controllers") const controllers = require("./controllers")
const supertest = require("supertest") const supertest = require("supertest")
const { cleanup } = require("../../utilities/fileSystem") const { cleanup } = require("../../utilities/fileSystem")
const { Cookies } = require("@budibase/auth").constants const { Cookies } = require("@budibase/auth").constants
const { jwt } = require("@budibase/auth").auth const { jwt } = require("@budibase/auth").auth
const auth = require("@budibase/auth")
const { getGlobalDB } = require("@budibase/auth/db") const { getGlobalDB } = require("@budibase/auth/db")
const { createASession } = require("@budibase/auth/sessions") const { createASession } = require("@budibase/auth/sessions")
const { user: userCache } = require("@budibase/auth/cache") const { user: userCache } = require("@budibase/auth/cache")
const CouchDB = require("../../db")
auth.init(CouchDB)
const GLOBAL_USER_ID = "us_uuid1" const GLOBAL_USER_ID = "us_uuid1"
const EMAIL = "babs@babs.com" const EMAIL = "babs@babs.com"
const PASSWORD = "babs_password" const PASSWORD = "babs_password"
const TENANT_ID = "default"
class TestConfiguration { class TestConfiguration {
constructor(openServer = true) { constructor(openServer = true) {
@ -52,7 +55,7 @@ class TestConfiguration {
request.cookies = { set: () => {}, get: () => {} } request.cookies = { set: () => {}, get: () => {} }
request.config = { jwtSecret: env.JWT_SECRET } request.config = { jwtSecret: env.JWT_SECRET }
request.appId = this.appId request.appId = this.appId
request.user = { appId: this.appId } request.user = { appId: this.appId, tenantId: TENANT_ID }
request.query = {} request.query = {}
request.request = { request.request = {
body: config, body: config,
@ -78,7 +81,7 @@ class TestConfiguration {
roles: roles || {}, roles: roles || {},
tenantId: TENANT_ID, tenantId: TENANT_ID,
} }
await createASession(id, "sessionid") await createASession(id, { sessionId: "sessionid", tenantId: TENANT_ID })
if (builder) { if (builder) {
user.builder = { global: true } user.builder = { global: true }
} }
@ -108,6 +111,7 @@ class TestConfiguration {
const auth = { const auth = {
userId: GLOBAL_USER_ID, userId: GLOBAL_USER_ID,
sessionId: "sessionid", sessionId: "sessionid",
tenantId: TENANT_ID,
} }
const app = { const app = {
roleId: BUILTIN_ROLE_IDS.ADMIN, roleId: BUILTIN_ROLE_IDS.ADMIN,
@ -334,11 +338,12 @@ class TestConfiguration {
if (!email || !password) { if (!email || !password) {
await this.createUser() await this.createUser()
} }
await createASession(userId, "sessionid") await createASession(userId, { sessionId: "sessionid", tenantId: TENANT_ID })
// have to fake this // have to fake this
const auth = { const auth = {
userId, userId,
sessionId: "sessionid", sessionId: "sessionid",
tenantId: TENANT_ID,
} }
const app = { const app = {
roleId: roleId, roleId: roleId,

View File

@ -4,6 +4,8 @@ const { createHomeScreen } = require("../../constants/screens")
const { EMPTY_LAYOUT } = require("../../constants/layouts") const { EMPTY_LAYOUT } = require("../../constants/layouts")
const { cloneDeep } = require("lodash/fp") const { cloneDeep } = require("lodash/fp")
exports.TENANT_ID = "default"
exports.basicTable = () => { exports.basicTable = () => {
return { return {
name: "TestTable", name: "TestTable",

View File

@ -3,3 +3,4 @@ const env = require("../src/environment")
env._set("NODE_ENV", "jest") env._set("NODE_ENV", "jest")
env._set("JWT_SECRET", "test-jwtsecret") env._set("JWT_SECRET", "test-jwtsecret")
env._set("LOG_LEVEL", "silent") env._set("LOG_LEVEL", "silent")
env._set("MULTI_TENANCY", true)

View File

@ -74,6 +74,7 @@ exports.reset = async ctx => {
}) })
} }
} catch (err) { } catch (err) {
console.log(err)
// don't throw any kind of error to the user, this might give away something // don't throw any kind of error to the user, this might give away something
} }
ctx.body = { ctx.body = {
@ -88,7 +89,7 @@ exports.resetUpdate = async ctx => {
const { resetCode, password } = ctx.request.body const { resetCode, password } = ctx.request.body
try { try {
const userId = await checkResetPasswordCode(resetCode) const userId = await checkResetPasswordCode(resetCode)
const db = new getGlobalDB(ctx.params.tenantId) const db = getGlobalDB(ctx.params.tenantId)
const user = await db.get(userId) const user = await db.get(userId)
user.password = await hash(password) user.password = await hash(password)
await db.put(user) await db.put(user)

View File

@ -4,7 +4,7 @@ const {
TemplateBindings, TemplateBindings,
GLOBAL_OWNER, GLOBAL_OWNER,
} = require("../../../constants") } = require("../../../constants")
const { getTemplates } = require("../../../constants/templates") const { getTemplatesCtx } = require("../../../constants/templates")
exports.save = async ctx => { exports.save = async ctx => {
const db = getGlobalDBFromCtx(ctx) const db = getGlobalDBFromCtx(ctx)
@ -45,23 +45,23 @@ exports.definitions = async ctx => {
} }
exports.fetch = async ctx => { exports.fetch = async ctx => {
ctx.body = await getTemplates(ctx) ctx.body = await getTemplatesCtx(ctx)
} }
exports.fetchByType = async ctx => { exports.fetchByType = async ctx => {
ctx.body = await getTemplates(ctx, { ctx.body = await getTemplatesCtx(ctx, {
type: ctx.params.type, type: ctx.params.type,
}) })
} }
exports.fetchByOwner = async ctx => { exports.fetchByOwner = async ctx => {
ctx.body = await getTemplates(ctx, { ctx.body = await getTemplatesCtx(ctx, {
ownerId: ctx.params.ownerId, ownerId: ctx.params.ownerId,
}) })
} }
exports.find = async ctx => { exports.find = async ctx => {
ctx.body = await getTemplates(ctx, { ctx.body = await getTemplatesCtx(ctx, {
id: ctx.params.id, id: ctx.params.id,
}) })
} }

View File

@ -265,12 +265,16 @@ exports.find = async ctx => {
} }
exports.invite = async ctx => { exports.invite = async ctx => {
const { email, userInfo } = ctx.request.body let { email, userInfo } = ctx.request.body
const tenantId = ctx.user.tenantId const tenantId = ctx.user.tenantId
const existing = await getGlobalUserByEmail(email, tenantId) const existing = await getGlobalUserByEmail(email, tenantId)
if (existing) { if (existing) {
ctx.throw(400, "Email address already in use.") ctx.throw(400, "Email address already in use.")
} }
if (!userInfo) {
userInfo = {}
}
userInfo.tenantId = tenantId
await sendEmail(tenantId, email, EmailTemplatePurpose.INVITATION, { await sendEmail(tenantId, email, EmailTemplatePurpose.INVITATION, {
subject: "{{ company }} platform invitation", subject: "{{ company }} platform invitation",
info: userInfo, info: userInfo,
@ -293,6 +297,9 @@ exports.inviteAccept = async ctx => {
email, email,
...info, ...info,
} }
ctx.user = {
tenantId: info.tenantId,
}
// this will flesh out the body response // this will flesh out the body response
await exports.save(ctx) await exports.save(ctx)
} catch (err) { } catch (err) {

View File

@ -36,8 +36,8 @@ describe("/api/global/auth", () => {
expect(sendMailMock).toHaveBeenCalled() expect(sendMailMock).toHaveBeenCalled()
const emailCall = sendMailMock.mock.calls[0][0] const emailCall = sendMailMock.mock.calls[0][0]
// after this URL there should be a code // after this URL there should be a code
const parts = emailCall.html.split(`http://localhost:10000/builder/auth/${TENANT_ID}/reset?code=`) const parts = emailCall.html.split(`http://localhost:10000/builder/auth/reset?code=`)
code = parts[1].split("\"")[0] code = parts[1].split("\"")[0].split("&")[0]
expect(code).toBeDefined() expect(code).toBeDefined()
}) })

View File

@ -1,5 +1,6 @@
const setup = require("./utilities") const setup = require("./utilities")
const { EmailTemplatePurpose } = require("../../../constants") const { EmailTemplatePurpose } = require("../../../constants")
const { TENANT_ID } = require("./utilities/structures")
// mock the email system // mock the email system
const sendMailMock = jest.fn() const sendMailMock = jest.fn()
@ -29,6 +30,7 @@ describe("/api/global/email", () => {
.send({ .send({
email: "test@test.com", email: "test@test.com",
purpose: EmailTemplatePurpose.INVITATION, purpose: EmailTemplatePurpose.INVITATION,
tenantId: TENANT_ID,
}) })
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)

View File

@ -1,4 +1,5 @@
const setup = require("./utilities") const setup = require("./utilities")
const { TENANT_ID } = require("./utilities/structures")
jest.mock("nodemailer") jest.mock("nodemailer")
const sendMailMock = setup.emailMock() const sendMailMock = setup.emailMock()
@ -31,7 +32,7 @@ describe("/api/global/users", () => {
const emailCall = sendMailMock.mock.calls[0][0] const emailCall = sendMailMock.mock.calls[0][0]
// after this URL there should be a code // after this URL there should be a code
const parts = emailCall.html.split("http://localhost:10000/builder/invite?code=") const parts = emailCall.html.split("http://localhost:10000/builder/invite?code=")
code = parts[1].split("\"")[0] code = parts[1].split("\"")[0].split("&")[0]
expect(code).toBeDefined() expect(code).toBeDefined()
}) })

View File

@ -7,8 +7,10 @@ const { Configs, LOGO_URL } = require("../../../../constants")
const { getGlobalUserByEmail } = require("@budibase/auth").utils const { getGlobalUserByEmail } = require("@budibase/auth").utils
const { createASession } = require("@budibase/auth/sessions") const { createASession } = require("@budibase/auth/sessions")
const { newid } = require("../../../../../../auth/src/hashing") const { newid } = require("../../../../../../auth/src/hashing")
const { TENANT_ID } = require("./structures")
const TENANT_ID = "default" const auth = require("@budibase/auth")
const CouchDB = require("../../../../db")
auth.init(CouchDB)
class TestConfiguration { class TestConfiguration {
constructor(openServer = true) { constructor(openServer = true) {
@ -30,7 +32,7 @@ class TestConfiguration {
request.cookies = { set: () => {}, get: () => {} } request.cookies = { set: () => {}, get: () => {} }
request.config = { jwtSecret: env.JWT_SECRET } request.config = { jwtSecret: env.JWT_SECRET }
request.appId = this.appId request.appId = this.appId
request.user = { appId: this.appId } request.user = { appId: this.appId, tenantId: TENANT_ID }
request.query = {} request.query = {}
request.request = { request.request = {
body: config, body: config,
@ -60,7 +62,7 @@ class TestConfiguration {
null, null,
controllers.users.save controllers.users.save
) )
await createASession("us_uuid1", "sessionid") await createASession("us_uuid1", { sessionId: "sessionid", tenantId: TENANT_ID })
} }
} }
@ -233,6 +235,7 @@ class TestConfiguration {
{ {
email: "testuser@test.com", email: "testuser@test.com",
password: "test@test.com", password: "test@test.com",
tenantId: TENANT_ID,
}, },
null, null,
controllers.users.adminUser controllers.users.adminUser

View File

@ -0,0 +1 @@
exports.TENANT_ID = "default"

View File

@ -6,7 +6,7 @@ const {
GLOBAL_OWNER, GLOBAL_OWNER,
} = require("../index") } = require("../index")
const { join } = require("path") const { join } = require("path")
const { getTemplateParams, getGlobalDBFromCtx } = require("@budibase/auth/db") const { getTemplateParams, getTenantIdFromCtx, getGlobalDB } = require("@budibase/auth/db")
exports.EmailTemplates = { exports.EmailTemplates = {
[EmailTemplatePurpose.PASSWORD_RECOVERY]: readStaticFile( [EmailTemplatePurpose.PASSWORD_RECOVERY]: readStaticFile(
@ -48,8 +48,13 @@ exports.addBaseTemplates = (templates, type = null) => {
return templates return templates
} }
exports.getTemplates = async (ctx, { ownerId, type, id } = {}) => { exports.getTemplatesCtx = async (ctx, opts = {}) => {
const db = getGlobalDBFromCtx(ctx) const tenantId = getTenantIdFromCtx(ctx)
return exports.getTemplates(tenantId, opts)
}
exports.getTemplates = async (tenantId, { ownerId, type, id} = {}) => {
const db = getGlobalDB(tenantId)
const response = await db.allDocs( const response = await db.allDocs(
getTemplateParams(ownerId || GLOBAL_OWNER, id, { getTemplateParams(ownerId || GLOBAL_OWNER, id, {
include_docs: true, include_docs: true,
@ -66,7 +71,10 @@ exports.getTemplates = async (ctx, { ownerId, type, id } = {}) => {
return exports.addBaseTemplates(templates, type) return exports.addBaseTemplates(templates, type)
} }
exports.getTemplateByPurpose = async (ctx, type, purpose) => { exports.getTemplateByPurpose = async ({ tenantId, ctx }, type, purpose) => {
const templates = await exports.getTemplates(ctx, { type }) if (!tenantId && ctx) {
tenantId = getTenantIdFromCtx(ctx)
}
const templates = await exports.getTemplates(tenantId, { type })
return templates.find(template => template.purpose === purpose) return templates.find(template => template.purpose === purpose)
} }

View File

@ -60,6 +60,7 @@ async function getLinkCode(purpose, email, user, info = null) {
/** /**
* Builds an email using handlebars and the templates found in the system (default or otherwise). * Builds an email using handlebars and the templates found in the system (default or otherwise).
* @param {string} tenantId the ID of the tenant which is sending the email.
* @param {string} purpose the purpose of the email being built, e.g. invitation, password reset. * @param {string} purpose the purpose of the email being built, e.g. invitation, password reset.
* @param {string} email the address which it is being sent to for contextual purposes. * @param {string} email the address which it is being sent to for contextual purposes.
* @param {object} context the context which is being used for building the email (hbs context). * @param {object} context the context which is being used for building the email (hbs context).
@ -67,14 +68,14 @@ async function getLinkCode(purpose, email, user, info = null) {
* @param {string|null} contents if using a custom template can supply contents for context. * @param {string|null} contents if using a custom template can supply contents for context.
* @return {Promise<string>} returns the built email HTML if all provided parameters were valid. * @return {Promise<string>} returns the built email HTML if all provided parameters were valid.
*/ */
async function buildEmail(purpose, email, context, { user, contents } = {}) { async function buildEmail(tenantId, purpose, email, context, { user, contents } = {}) {
// this isn't a full email // this isn't a full email
if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) { if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) {
throw `Unable to build an email of type ${purpose}` throw `Unable to build an email of type ${purpose}`
} }
let [base, body] = await Promise.all([ let [base, body] = await Promise.all([
getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE), getTemplateByPurpose({ tenantId }, TYPE, EmailTemplatePurpose.BASE),
getTemplateByPurpose(TYPE, purpose), getTemplateByPurpose({ tenantId }, TYPE, purpose),
]) ])
if (!base || !body) { if (!base || !body) {
throw "Unable to build email, missing base components" throw "Unable to build email, missing base components"
@ -147,7 +148,7 @@ exports.sendEmail = async (
purpose, purpose,
{ workspaceId, user, from, contents, subject, info } = {} { workspaceId, user, from, contents, subject, info } = {}
) => { ) => {
const db = new getGlobalDB(tenantId) const db = getGlobalDB(tenantId)
let config = (await getSmtpConfiguration(db, workspaceId)) || {} let config = (await getSmtpConfiguration(db, workspaceId)) || {}
if (Object.keys(config).length === 0 && !TEST_MODE) { if (Object.keys(config).length === 0 && !TEST_MODE) {
throw "Unable to find SMTP configuration." throw "Unable to find SMTP configuration."
@ -159,7 +160,7 @@ exports.sendEmail = async (
const message = { const message = {
from: from || config.from, from: from || config.from,
to: email, to: email,
html: await buildEmail(purpose, email, context, { user, contents }), html: await buildEmail(tenantId, purpose, email, context, { user, contents }),
} }
if (subject || config.subject) { if (subject || config.subject) {
message.subject = await processString(subject || config.subject, context) message.subject = await processString(subject || config.subject, context)

View File

@ -11,8 +11,16 @@ const env = require("../environment")
const LOCAL_URL = `http://localhost:${env.CLUSTER_PORT || 10000}` const LOCAL_URL = `http://localhost:${env.CLUSTER_PORT || 10000}`
const BASE_COMPANY = "Budibase" const BASE_COMPANY = "Budibase"
function addTenantToUrl(url, tenantId) {
if (env.MULTI_TENANCY) {
const char = url.indexOf("?") === -1 ? "?" : "&"
url += `${char}tenantId=${tenantId}`
}
return url
}
exports.getSettingsTemplateContext = async (tenantId, purpose, code = null) => { exports.getSettingsTemplateContext = async (tenantId, purpose, code = null) => {
const db = new getGlobalDB(tenantId) const db = getGlobalDB(tenantId)
// TODO: use more granular settings in the future if required // TODO: use more granular settings in the future if required
let settings = (await getScopedConfig(db, { type: Configs.SETTINGS })) || {} let settings = (await getScopedConfig(db, { type: Configs.SETTINGS })) || {}
if (!settings || !settings.platformUrl) { if (!settings || !settings.platformUrl) {
@ -26,7 +34,7 @@ exports.getSettingsTemplateContext = async (tenantId, purpose, code = null) => {
[InternalTemplateBindings.COMPANY]: settings.company || BASE_COMPANY, [InternalTemplateBindings.COMPANY]: settings.company || BASE_COMPANY,
[InternalTemplateBindings.DOCS_URL]: [InternalTemplateBindings.DOCS_URL]:
settings.docsUrl || "https://docs.budibase.com/", settings.docsUrl || "https://docs.budibase.com/",
[InternalTemplateBindings.LOGIN_URL]: checkSlashesInUrl(`${URL}/login`), [InternalTemplateBindings.LOGIN_URL]: checkSlashesInUrl(addTenantToUrl(`${URL}/login`, tenantId)),
[InternalTemplateBindings.CURRENT_DATE]: new Date().toISOString(), [InternalTemplateBindings.CURRENT_DATE]: new Date().toISOString(),
[InternalTemplateBindings.CURRENT_YEAR]: new Date().getFullYear(), [InternalTemplateBindings.CURRENT_YEAR]: new Date().getFullYear(),
} }
@ -35,13 +43,13 @@ exports.getSettingsTemplateContext = async (tenantId, purpose, code = null) => {
case EmailTemplatePurpose.PASSWORD_RECOVERY: case EmailTemplatePurpose.PASSWORD_RECOVERY:
context[InternalTemplateBindings.RESET_CODE] = code context[InternalTemplateBindings.RESET_CODE] = code
context[InternalTemplateBindings.RESET_URL] = checkSlashesInUrl( context[InternalTemplateBindings.RESET_URL] = checkSlashesInUrl(
`${URL}/builder/auth/reset?code=${code}` addTenantToUrl(`${URL}/builder/auth/reset?code=${code}`, tenantId)
) )
break break
case EmailTemplatePurpose.INVITATION: case EmailTemplatePurpose.INVITATION:
context[InternalTemplateBindings.INVITE_CODE] = code context[InternalTemplateBindings.INVITE_CODE] = code
context[InternalTemplateBindings.INVITE_URL] = checkSlashesInUrl( context[InternalTemplateBindings.INVITE_URL] = checkSlashesInUrl(
`${URL}/builder/invite?code=${code}` addTenantToUrl(`${URL}/builder/invite?code=${code}&tenantId=${tenantId}`, tenantId)
) )
break break
} }