diff --git a/packages/auth/package.json b/packages/auth/package.json index 72ea0bc3ba..dfbb63c431 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -15,5 +15,8 @@ "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", "uuid": "^8.3.2" + }, + "devDependencies": { + "ioredis-mock": "^5.5.5" } } diff --git a/packages/auth/src/environment.js b/packages/auth/src/environment.js index 4f195337b6..5b0b141019 100644 --- a/packages/auth/src/environment.js +++ b/packages/auth/src/environment.js @@ -1,7 +1,16 @@ +function isTest() { + return ( + process.env.NODE_ENV === "jest" || + process.env.NODE_ENV === "cypress" || + process.env.JEST_WORKER_ID != null + ) +} + module.exports = { JWT_SECRET: process.env.JWT_SECRET, COUCH_DB_URL: process.env.COUCH_DB_URL, SALT_ROUNDS: process.env.SALT_ROUNDS, REDIS_URL: process.env.REDIS_URL, REDIS_PASSWORD: process.env.REDIS_PASSWORD, + isTest, } diff --git a/packages/auth/src/redis/index.js b/packages/auth/src/redis/index.js index 0cb92c4622..dc670c07fb 100644 --- a/packages/auth/src/redis/index.js +++ b/packages/auth/src/redis/index.js @@ -1,9 +1,12 @@ -const Redis = require("ioredis") +const env = require("../environment") +// ioredis mock is all in memory +const Redis = env.isTest() ? require("ioredis-mock") : require("ioredis") const { addDbPrefix, removeDbPrefix, getRedisOptions } = require("./utils") const CLUSTERED = false -let CLIENT +// for testing just generate the client once +let CLIENT = env.isTest() ? new Redis(getRedisOptions()) : null /** * Inits the system, will error if unable to connect to redis cluster (may take up to 10 seconds) otherwise @@ -12,6 +15,10 @@ let CLIENT */ function init() { return new Promise((resolve, reject) => { + // testing uses a single in memory client + if (env.isTest()) { + return resolve(CLIENT) + } // if a connection existed, close it and re-create it if (CLIENT) { CLIENT.disconnect() @@ -108,7 +115,12 @@ class RedisWrapper { if (response != null && response.key) { response.key = key } - return JSON.parse(response) + // if its not an object just return the response + try { + return JSON.parse(response) + } catch (err) { + return response + } } async store(key, value, expirySeconds = null) { diff --git a/packages/auth/src/redis/utils.js b/packages/auth/src/redis/utils.js index a29f786c41..bd4a762e1d 100644 --- a/packages/auth/src/redis/utils.js +++ b/packages/auth/src/redis/utils.js @@ -3,6 +3,8 @@ const env = require("../environment") const SLOT_REFRESH_MS = 2000 const CONNECT_TIMEOUT_MS = 10000 const SEPARATOR = "-" +const REDIS_URL = !env.REDIS_URL ? "localhost:6379" : env.REDIS_URL +const REDIS_PASSWORD = !env.REDIS_PASSWORD ? "budibase" : env.REDIS_PASSWORD exports.Databases = { PW_RESETS: "pwReset", @@ -10,20 +12,20 @@ exports.Databases = { } exports.getRedisOptions = (clustered = false) => { - const [host, port] = env.REDIS_URL.split(":") + const [host, port] = REDIS_URL.split(":") const opts = { connectTimeout: CONNECT_TIMEOUT_MS, } if (clustered) { opts.redisOptions = {} opts.redisOptions.tls = {} - opts.redisOptions.password = env.REDIS_PASSWORD + opts.redisOptions.password = REDIS_PASSWORD opts.slotsRefreshTimeout = SLOT_REFRESH_MS opts.dnsLookup = (address, callback) => callback(null, address) } else { opts.host = host opts.port = port - opts.password = env.REDIS_PASSWORD + opts.password = REDIS_PASSWORD } return { opts, host, port } } diff --git a/packages/auth/yarn.lock b/packages/auth/yarn.lock index 0dbdaadf8d..a3c95bf42c 100644 --- a/packages/auth/yarn.lock +++ b/packages/auth/yarn.lock @@ -46,6 +46,11 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + base64url@3.x.x: version "3.0.1" resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" @@ -63,6 +68,14 @@ bcryptjs@^2.4.3: resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms= +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" @@ -85,6 +98,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -154,6 +172,20 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fengari-interop@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/fengari-interop/-/fengari-interop-0.1.2.tgz#f7731dcdd2ff4449073fb7ac3c451a8841ce1e87" + integrity sha512-8iTvaByZVoi+lQJhHH9vC+c/Yaok9CwOqNQZN6JrVpjmWwW4dDkeblBXhnHC+BoI6eF4Cy5NKW3z6ICEjvgywQ== + +fengari@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/fengari/-/fengari-0.1.4.tgz#72416693cd9e43bd7d809d7829ddc0578b78b0bb" + integrity sha512-6ujqUuiIYmcgkGz8MGAdERU57EIluGGPSUgGPTsco657EHa+srq0S3/YUl/r9kx1+D+d4rGfYObd+m8K22gB1g== + dependencies: + readline-sync "^1.4.9" + sprintf-js "^1.1.1" + tmp "^0.0.33" + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -233,6 +265,17 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +ioredis-mock@^5.5.5: + version "5.5.5" + resolved "https://registry.yarnpkg.com/ioredis-mock/-/ioredis-mock-5.5.5.tgz#dec9fedd238c6ab9f56c026fc366533144f8a256" + integrity sha512-7SxCAwNtDLC8IFDptqIhOC7ajp3fciVtCrXOEOkpyjPboAGRQkJbnpNPy1NYORoWi+0/iOtUPUQckSKtSQj4DA== + dependencies: + fengari "^0.1.4" + fengari-interop "^0.1.2" + lodash "^4.17.21" + minimatch "^3.0.4" + standard-as-callback "^2.1.0" + ioredis@^4.27.1: version "4.27.1" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.27.1.tgz#4ef947b455a1b995baa4b0d7e2c4e4f75f746421" @@ -379,7 +422,7 @@ lodash.once@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= -lodash@^4.14.0: +lodash@^4.14.0, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -401,6 +444,13 @@ mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -426,6 +476,11 @@ oauth@0.9.x: resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE= +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + p-map@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" @@ -534,6 +589,11 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +readline-sync@^1.4.9: + version "1.4.10" + resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b" + integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw== + redis-commands@1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" @@ -592,6 +652,11 @@ semver@^5.6.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +sprintf-js@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" + integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== + sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" @@ -617,6 +682,13 @@ string-template@~1.0.0: resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96" integrity sha1-np8iM9wA8hhxjsN5oopWc+zKi5Y= +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" diff --git a/packages/server/src/api/index.js b/packages/server/src/api/index.js index 5d16134eb4..84ea0aaf20 100644 --- a/packages/server/src/api/index.js +++ b/packages/server/src/api/index.js @@ -5,13 +5,16 @@ const compress = require("koa-compress") const zlib = require("zlib") const { mainRoutes, staticRoutes } = require("./routes") const pkg = require("../../package.json") -const bullboard = require("bull-board") -const expressApp = require("express")() +const env = require("../environment") -expressApp.use("/bulladmin", bullboard.router) +if (!env.isTest()) { + const bullboard = require("bull-board") + const expressApp = require("express")() + + expressApp.use("/bulladmin", bullboard.router) +} const router = new Router() -const env = require("../environment") router .use( diff --git a/packages/server/src/api/routes/tests/row.spec.js b/packages/server/src/api/routes/tests/row.spec.js index 6a1c309c39..fb8ff96efa 100644 --- a/packages/server/src/api/routes/tests/row.spec.js +++ b/packages/server/src/api/routes/tests/row.spec.js @@ -402,14 +402,16 @@ describe("/rows", () => { name: "test", description: "test", attachment: [{ - url: "/test/thing", + key: `/assets/${config.getAppId()}/attachment/test/thing.csv`, }], tableId: table._id, }) // the environment needs configured for this await setup.switchToSelfHosted(async () => { const enriched = await outputProcessing(config.getAppId(), table, [row]) - expect(enriched[0].attachment[0].url).toBe(`/app-assets/assets/${config.getAppId()}/test/thing`) + expect(enriched[0].attachment[0].url).toBe( + `/prod-budi-app-assets/assets/${config.getAppId()}/attachment/test/thing.csv` + ) }) }) }) diff --git a/packages/server/src/automations/triggers.js b/packages/server/src/automations/triggers.js index 77eb32377a..f303856779 100644 --- a/packages/server/src/automations/triggers.js +++ b/packages/server/src/automations/triggers.js @@ -1,6 +1,7 @@ const CouchDB = require("../db") const emitter = require("../events/index") -const Queue = require("bull") +const env = require("../environment") +const Queue = env.isTest() ? require("../utilities/queue/inMemoryQueue") : require("bull") const { setQueues, BullAdapter } = require("bull-board") const { getAutomationParams } = require("../db/utils") const { coerce } = require("../utilities/rowProcessor") diff --git a/packages/worker/package.json b/packages/worker/package.json index e8db71fc46..1e478f8d5d 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -14,7 +14,8 @@ "scripts": { "run:docker": "node src/index.js", "dev:stack:init": "node ./scripts/dev/manage.js init", - "dev:builder": "npm run dev:stack:init && nodemon src/index.js" + "dev:builder": "npm run dev:stack:init && nodemon src/index.js", + "test": "jest --runInBand" }, "author": "Budibase", "license": "AGPL-3.0-or-later", @@ -50,5 +51,11 @@ "nodemon": "^2.0.7", "pouchdb-adapter-memory": "^7.2.2", "supertest": "^6.1.3" + }, + "jest": { + "testEnvironment": "node", + "setupFiles": [ + "./scripts/jestSetup.js" + ] } } diff --git a/packages/worker/scripts/jestSetup.js b/packages/worker/scripts/jestSetup.js new file mode 100644 index 0000000000..4e3b05b8f9 --- /dev/null +++ b/packages/worker/scripts/jestSetup.js @@ -0,0 +1,5 @@ +const env = require("../src/environment") + +env._set("NODE_ENV", "jest") +env._set("JWT_SECRET", "test-jwtsecret") +env._set("LOG_LEVEL", "silent") \ No newline at end of file diff --git a/packages/worker/src/api/controllers/admin/email.js b/packages/worker/src/api/controllers/admin/email.js index 0a29468133..04e85e7f44 100644 --- a/packages/worker/src/api/controllers/admin/email.js +++ b/packages/worker/src/api/controllers/admin/email.js @@ -1,8 +1,17 @@ const { sendEmail } = require("../../../utilities/email") +const CouchDB = require("../../../db") +const authPkg = require("@budibase/auth") + +const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name exports.sendEmail = async ctx => { const { groupId, email, userId, purpose } = ctx.request.body - const response = await sendEmail(email, purpose, { groupId, userId }) + let user + if (userId) { + const db = new CouchDB(GLOBAL_DB) + user = await db.get(userId) + } + const response = await sendEmail(email, purpose, { groupId, user }) ctx.body = { ...response, message: `Email sent to ${email}.`, diff --git a/packages/worker/src/api/controllers/admin/users.js b/packages/worker/src/api/controllers/admin/users.js index cd307bbc6b..075b82ec6c 100644 --- a/packages/worker/src/api/controllers/admin/users.js +++ b/packages/worker/src/api/controllers/admin/users.js @@ -126,7 +126,7 @@ exports.find = async ctx => { exports.invite = async ctx => { const { email } = ctx.request.body - const existing = await getGlobalUserByEmail(FIRST_USER_EMAIL) + const existing = await getGlobalUserByEmail(email) if (existing) { ctx.throw(400, "Email address already in use.") } diff --git a/packages/worker/src/api/routes/tests/auth.spec.js b/packages/worker/src/api/routes/tests/auth.spec.js new file mode 100644 index 0000000000..ea7ab829a3 --- /dev/null +++ b/packages/worker/src/api/routes/tests/auth.spec.js @@ -0,0 +1,49 @@ +const setup = require("./utilities") + +jest.mock("nodemailer") +const sendMailMock = setup.emailMock() + +describe("/api/admin/auth", () => { + let request = setup.getRequest() + let config = setup.getConfig() + let code + + beforeAll(async () => { + await config.init() + }) + + afterAll(setup.afterAll) + + it("should be able to generate password reset email", async () => { + // initially configure settings + await config.saveSmtpConfig() + await config.saveSettingsConfig() + await config.createUser("test@test.com") + const res = await request + .post(`/api/admin/auth/reset`) + .send({ + email: "test@test.com", + }) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body).toEqual({ message: "Please check your email for a reset link." }) + expect(sendMailMock).toHaveBeenCalled() + const emailCall = sendMailMock.mock.calls[0][0] + // after this URL there should be a code + const parts = emailCall.html.split("http://localhost:10000/reset/") + code = parts[1].split("\"")[0] + expect(code).toBeDefined() + }) + + it("should allow resetting user password with code", async () => { + const res = await request + .post(`/api/admin/auth/reset/update`) + .send({ + password: "newpassword", + resetCode: code, + }) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body).toEqual({ message: "password reset successfully." }) + }) +}) \ No newline at end of file diff --git a/packages/worker/src/api/routes/tests/email.spec.js b/packages/worker/src/api/routes/tests/email.spec.js index 797b0326ed..11bdb3fb1f 100644 --- a/packages/worker/src/api/routes/tests/email.spec.js +++ b/packages/worker/src/api/routes/tests/email.spec.js @@ -2,13 +2,8 @@ const setup = require("./utilities") const { EmailTemplatePurpose } = require("../../../constants") // mock the email system -const sendMailMock = jest.fn() jest.mock("nodemailer") -const nodemailer = require("nodemailer") -nodemailer.createTransport.mockReturnValue({ - sendMail: sendMailMock, - verify: jest.fn() -}) +const sendMailMock = setup.emailMock() describe("/api/admin/email", () => { let request = setup.getRequest() diff --git a/packages/worker/src/api/routes/tests/realEmail.spec.js b/packages/worker/src/api/routes/tests/realEmail.spec.js index c96b8ab561..f593b2cc09 100644 --- a/packages/worker/src/api/routes/tests/realEmail.spec.js +++ b/packages/worker/src/api/routes/tests/realEmail.spec.js @@ -16,11 +16,13 @@ describe("/api/admin/email", () => { async function sendRealEmail(purpose) { await config.saveEtherealSmtpConfig() await config.saveSettingsConfig() + const user = await config.getUser("test@test.com") const res = await request .post(`/api/admin/email/send`) .send({ email: "test@test.com", purpose, + userId: user._id, }) .set(config.defaultHeaders()) .expect("Content-Type", /json/) @@ -55,6 +57,6 @@ describe("/api/admin/email", () => { }) it("should be able to send a password recovery email", async () => { - const res = await sendRealEmail(EmailTemplatePurpose.PASSWORD_RECOVERY) + await sendRealEmail(EmailTemplatePurpose.PASSWORD_RECOVERY) }) }) \ No newline at end of file diff --git a/packages/worker/src/api/routes/tests/users.spec.js b/packages/worker/src/api/routes/tests/users.spec.js new file mode 100644 index 0000000000..135d29f0f5 --- /dev/null +++ b/packages/worker/src/api/routes/tests/users.spec.js @@ -0,0 +1,52 @@ +const setup = require("./utilities") + +jest.mock("nodemailer") +const sendMailMock = setup.emailMock() + +describe("/api/admin/users", () => { + let request = setup.getRequest() + let config = setup.getConfig() + let code + + beforeAll(async () => { + await config.init() + }) + + afterAll(setup.afterAll) + + it("should be able to generate an invitation", async () => { + // initially configure settings + await config.saveSmtpConfig() + await config.saveSettingsConfig() + const res = await request + .post(`/api/admin/users/invite`) + .send({ + email: "invite@test.com", + }) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body).toEqual({ message: "Invitation has been sent." }) + expect(sendMailMock).toHaveBeenCalled() + const emailCall = sendMailMock.mock.calls[0][0] + // after this URL there should be a code + const parts = emailCall.html.split("http://localhost:10000/invite/") + code = parts[1].split("\"")[0] + expect(code).toBeDefined() + }) + + it("should be able to create new user from invite", async () => { + const res = await request + .post(`/api/admin/users/invite/accept`) + .send({ + password: "newpassword", + inviteCode: code, + }) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body._id).toBeDefined() + const user = await config.getUser("invite@test.com") + expect(user).toBeDefined() + expect(user._id).toEqual(res.body._id) + }) +}) \ No newline at end of file diff --git a/packages/worker/src/api/routes/tests/utilities/TestConfiguration.js b/packages/worker/src/api/routes/tests/utilities/TestConfiguration.js index 0f97b50c82..d6ef6744f3 100644 --- a/packages/worker/src/api/routes/tests/utilities/TestConfiguration.js +++ b/packages/worker/src/api/routes/tests/utilities/TestConfiguration.js @@ -4,6 +4,7 @@ const supertest = require("supertest") const { jwt } = require("@budibase/auth").auth const { Cookies } = require("@budibase/auth").constants const { Configs, LOGO_URL } = require("../../../../constants") +const { getGlobalUserByEmail } = require("@budibase/auth").utils class TestConfiguration { constructor(openServer = true) { @@ -53,6 +54,12 @@ class TestConfiguration { ) } + async end() { + if (this.server) { + await this.server.close() + } + } + defaultHeaders() { const user = { _id: "us_uuid1", @@ -65,6 +72,25 @@ class TestConfiguration { } } + async getUser(email) { + return getGlobalUserByEmail(email) + } + + async createUser(email = "test@test.com", password = "test") { + const user = await this.getUser(email) + if (user) { + return user + } + await this._req( + { + email, + password, + }, + null, + controllers.users.save + ) + } + async deleteConfig(type) { try { const cfg = await this._req( diff --git a/packages/worker/src/api/routes/tests/utilities/index.js b/packages/worker/src/api/routes/tests/utilities/index.js index d63b837f6d..90f95de49b 100644 --- a/packages/worker/src/api/routes/tests/utilities/index.js +++ b/packages/worker/src/api/routes/tests/utilities/index.js @@ -7,9 +7,9 @@ exports.beforeAll = () => { request = config.getRequest() } -exports.afterAll = () => { +exports.afterAll = async () => { if (config) { - config.end() + await config.end() } request = null config = null @@ -28,3 +28,14 @@ exports.getConfig = () => { } return config } + +exports.emailMock = () => { + // mock the email system + const sendMailMock = jest.fn() + const nodemailer = require("nodemailer") + nodemailer.createTransport.mockReturnValue({ + sendMail: sendMailMock, + verify: jest.fn() + }) + return sendMailMock +} diff --git a/packages/worker/src/environment.js b/packages/worker/src/environment.js index 0adfe2afae..04c010ce16 100644 --- a/packages/worker/src/environment.js +++ b/packages/worker/src/environment.js @@ -11,7 +11,7 @@ function isTest() { } let LOADED = false -if (!LOADED && isDev()) { +if (!LOADED && isDev() && !isTest()) { require("dotenv").config() LOADED = true } diff --git a/packages/worker/src/index.js b/packages/worker/src/index.js index 8b67181fcc..f94846b370 100644 --- a/packages/worker/src/index.js +++ b/packages/worker/src/index.js @@ -12,10 +12,6 @@ const api = require("./api") const app = new Koa() -if (!env.SELF_HOSTED) { - throw "Currently this service only supports use in self hosting" -} - // set up top level koa middleware app.use(koaBody({ multipart: true })) diff --git a/packages/worker/src/utilities/templates.js b/packages/worker/src/utilities/templates.js index 4a7ca6fc7c..e4d6b7d396 100644 --- a/packages/worker/src/utilities/templates.js +++ b/packages/worker/src/utilities/templates.js @@ -45,7 +45,7 @@ exports.getSettingsTemplateContext = async (purpose, code = null) => { case EmailTemplatePurpose.INVITATION: context[TemplateBindings.INVITE_CODE] = code context[TemplateBindings.REGISTRATION_URL] = checkSlashesInUrl( - `${URL}/registration/${code}` + `${URL}/invite/${code}` ) break } diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 150a8685f1..ac60b6c188 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -287,7 +287,7 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/auth@0.0.1": +"@budibase/auth@^0.18.6": version "0.18.6" resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-0.18.6.tgz#d893005962afd9425f10e2ac8d1d495047d0d44e" integrity sha512-pdyqR8G240lToMe2OZNpw2YzuRwOlOT+cAfVHPMBxJJKF0VvZ0K500NoSUINEQPr4IfWpPSu6CQhq+ROf4pMXA== @@ -2505,6 +2505,20 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fengari-interop@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/fengari-interop/-/fengari-interop-0.1.2.tgz#f7731dcdd2ff4449073fb7ac3c451a8841ce1e87" + integrity sha512-8iTvaByZVoi+lQJhHH9vC+c/Yaok9CwOqNQZN6JrVpjmWwW4dDkeblBXhnHC+BoI6eF4Cy5NKW3z6ICEjvgywQ== + +fengari@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/fengari/-/fengari-0.1.4.tgz#72416693cd9e43bd7d809d7829ddc0578b78b0bb" + integrity sha512-6ujqUuiIYmcgkGz8MGAdERU57EIluGGPSUgGPTsco657EHa+srq0S3/YUl/r9kx1+D+d4rGfYObd+m8K22gB1g== + dependencies: + readline-sync "^1.4.9" + sprintf-js "^1.1.1" + tmp "^0.0.33" + fetch-cookie@0.10.1: version "0.10.1" resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-0.10.1.tgz#5ea88f3d36950543c87997c27ae2aeafb4b5c4d4" @@ -3130,6 +3144,17 @@ inline-process-browser@^1.0.0: falafel "^1.0.1" through2 "^0.6.5" +ioredis-mock@^5.5.5: + version "5.5.5" + resolved "https://registry.yarnpkg.com/ioredis-mock/-/ioredis-mock-5.5.5.tgz#dec9fedd238c6ab9f56c026fc366533144f8a256" + integrity sha512-7SxCAwNtDLC8IFDptqIhOC7ajp3fciVtCrXOEOkpyjPboAGRQkJbnpNPy1NYORoWi+0/iOtUPUQckSKtSQj4DA== + dependencies: + fengari "^0.1.4" + fengari-interop "^0.1.2" + lodash "^4.17.21" + minimatch "^3.0.4" + standard-as-callback "^2.1.0" + ioredis@^4.27.1: version "4.27.1" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.27.1.tgz#4ef947b455a1b995baa4b0d7e2c4e4f75f746421" @@ -3146,6 +3171,22 @@ ioredis@^4.27.1: redis-parser "^3.0.0" standard-as-callback "^2.1.0" +ioredis@^4.27.2: + version "4.27.2" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.27.2.tgz#6a79bca05164482da796f8fa010bccefd3bf4811" + integrity sha512-7OpYymIthonkC2Jne5uGWXswdhlua1S1rWGAERaotn0hGJWTSURvxdHA9G6wNbT/qKCloCja/FHsfKXW8lpTmg== + dependencies: + cluster-key-slot "^1.1.0" + debug "^4.3.1" + denque "^1.1.0" + lodash.defaults "^4.2.0" + lodash.flatten "^4.4.0" + p-map "^2.1.0" + redis-commands "1.7.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + is-accessor-descriptor@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" @@ -4403,7 +4444,7 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "^3.0.0" -lodash@^4.14.0, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.7.0: +lodash@^4.14.0, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4894,6 +4935,11 @@ optionator@^0.8.1: type-check "~0.3.2" word-wrap "~1.2.3" +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -5521,6 +5567,11 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" +readline-sync@^1.4.9: + version "1.4.10" + resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b" + integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw== + recast@^0.10.1: version "0.10.43" resolved "https://registry.yarnpkg.com/recast/-/recast-0.10.43.tgz#b95d50f6d60761a5f6252e15d80678168491ce7f" @@ -6045,6 +6096,11 @@ split2@^3.1.1: dependencies: readable-stream "^3.0.0" +sprintf-js@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" + integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -6327,6 +6383,13 @@ tiny-queue@^0.2.0: resolved "https://registry.yarnpkg.com/tiny-queue/-/tiny-queue-0.2.1.tgz#25a67f2c6e253b2ca941977b5ef7442ef97a6046" integrity sha1-JaZ/LG4lOyypQZd7XvdELvl6YEY= +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"