diff --git a/packages/backend/package.json b/packages/backend/package.json index 00ae4305..018a2181 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -115,6 +115,7 @@ }, "devDependencies": { "@automatisch/types": "^0.9.3", + "@faker-js/faker": "^8.1.0", "@types/bcrypt": "^5.0.0", "@types/bull": "^3.15.8", "@types/cors": "^2.8.12", @@ -133,9 +134,11 @@ "@types/pino": "^7.0.5", "@types/pluralize": "^0.0.30", "@types/showdown": "^2.0.1", + "@types/supertest": "^2.0.14", "jest": "^29.7.0", "nodemon": "^2.0.13", "sinon": "^11.1.2", + "supertest": "^6.3.3", "ts-jest": "^29.1.1", "ts-node": "^10.2.1", "ts-node-dev": "^1.1.8" diff --git a/packages/backend/src/graphql/mutations/login.ts b/packages/backend/src/graphql/mutations/login.ts index 837b47ee..54a51e3e 100644 --- a/packages/backend/src/graphql/mutations/login.ts +++ b/packages/backend/src/graphql/mutations/login.ts @@ -1,6 +1,5 @@ import User from '../../models/user'; -import jwt from 'jsonwebtoken'; -import appConfig from '../../config/app'; +import createAuthTokenByUserId from '../../helpers/create-auth-token-by-user-id'; type Params = { input: { @@ -9,18 +8,13 @@ type Params = { }; }; -const TOKEN_EXPIRES_IN = '14d'; - const login = async (_parent: unknown, params: Params) => { const user = await User.query().findOne({ email: params.input.email.toLowerCase(), }); if (user && (await user.login(params.input.password))) { - const token = jwt.sign({ userId: user.id }, appConfig.appSecretKey, { - expiresIn: TOKEN_EXPIRES_IN, - }); - + const token = createAuthTokenByUserId(user.id); return { token, user }; } diff --git a/packages/backend/src/graphql/queries/get-user.test.ts b/packages/backend/src/graphql/queries/get-user.test.ts index 853787b3..fd266f13 100644 --- a/packages/backend/src/graphql/queries/get-user.test.ts +++ b/packages/backend/src/graphql/queries/get-user.test.ts @@ -1,3 +1,161 @@ -test('adds 1 + 2 to equal 3', () => { - expect(1 + 2).toBe(3); +import request from 'supertest'; +import app from '../../app'; +import createAuthTokenByUserId from '../../helpers/create-auth-token-by-user-id'; +import Crypto from 'crypto'; + +describe('getUser', () => { + it('should throw not authorized error for an unauthorized user', async () => { + const invalidUserId = '123123123'; + + const query = ` + query { + getUser(id: "${invalidUserId}") { + id + email + } + } + `; + + const response = await request(app) + .post('/graphql') + .set('Authorization', 'invalid-token') + .send({ query }) + .expect(200); + + expect(response.body.errors).toBeDefined(); + expect(response.body.errors[0].message).toEqual('Not Authorised!'); + }); + + describe('with authorized user', () => { + it('should return user data for a valid user id', async () => { + const [role] = await knex + .table('roles') + .insert({ + key: 'sample', + name: 'sample', + }) + .returning('*'); + + await knex.table('permissions').insert({ + action: 'read', + subject: 'User', + role_id: role.id, + }); + + const [currentUser] = await knex + .table('users') + .insert({ + full_name: 'Test User', + email: 'sample@sample.com', + password: 'secret', + role_id: role.id, + }) + .returning('*'); + + const [anotherUser] = await global.knex + .table('users') + .insert({ + full_name: 'Another User', + email: 'another@sample.com', + password: 'secret', + role_id: role.id, + }) + .returning('*'); + + const query = ` + query { + getUser(id: "${anotherUser.id}") { + id + email + fullName + email + createdAt + updatedAt + role { + id + name + } + } + } + `; + + const token = createAuthTokenByUserId(currentUser.id); + + const response = await request(app) + .post('/graphql') + .set('Authorization', `${token}`) + .send({ query }) + .expect(200); + + const expectedResponsePayload = { + data: { + getUser: { + createdAt: anotherUser.created_at.getTime().toString(), + email: anotherUser.email, + fullName: anotherUser.full_name, + id: anotherUser.id, + role: { id: role.id, name: role.name }, + updatedAt: anotherUser.updated_at.getTime().toString(), + }, + }, + }; + + expect(response.body).toEqual(expectedResponsePayload); + }); + + it('should return not found for invalid user id', async () => { + const [role] = await knex('roles') + .insert({ + key: 'sample', + name: 'sample', + }) + .returning('*'); + + await knex.table('permissions').insert({ + action: 'read', + subject: 'User', + role_id: role.id, + }); + + const [currentUser] = await knex + .table('users') + .insert({ + full_name: 'Test User', + email: 'sample@sample.com', + password: 'secret', + role_id: role.id, + }) + .returning('*'); + + const invalidUserId = Crypto.randomUUID(); + + const query = ` + query { + getUser(id: "${invalidUserId}") { + id + email + fullName + email + createdAt + updatedAt + role { + id + name + } + } + } + `; + + const token = createAuthTokenByUserId(currentUser.id); + + const response = await request(app) + .post('/graphql') + .set('Authorization', `${token}`) + .send({ query }) + .expect(200); + + expect(response.body.errors).toBeDefined(); + expect(response.body.errors[0].message).toEqual('NotFoundError'); + }); + }); }); diff --git a/packages/backend/src/types/global.d.ts b/packages/backend/src/types/global.d.ts index 21bbd7cb..ebe4219b 100644 --- a/packages/backend/src/types/global.d.ts +++ b/packages/backend/src/types/global.d.ts @@ -3,6 +3,8 @@ import { Knex } from 'knex'; declare global { declare namespace globalThis { // eslint-disable-next-line no-var - var knex: Knex; + var knexInstance: Knex; + // eslint-disable-next-line no-var + var knex: Knex.Transaction; } } diff --git a/packages/backend/test/setup/global-hooks.ts b/packages/backend/test/setup/global-hooks.ts index be1c2704..c2a0132f 100644 --- a/packages/backend/test/setup/global-hooks.ts +++ b/packages/backend/test/setup/global-hooks.ts @@ -1,17 +1,27 @@ +import { Model } from 'objection'; import { client as knex } from '../../src/config/database'; +import logger from '../../src/helpers/logger'; global.beforeAll(async () => { - global.knex = knex; + global.knexInstance = knex; + global.knex = null; + logger.silent = true; }); -global.beforeEach(async function () { - this.transaction = await global.knex.transaction(); +global.beforeEach(async () => { + // It's assigned as global.knex for the convenience even though + // it's a transaction. It's rolled back after each test. + // by assigning to knex, we can use it as knex.table('example') in tests files. + global.knex = await knex.transaction(); + Model.knex(global.knex); }); -global.afterEach(async function () { - await this.transaction.rollback(); +global.afterEach(async () => { + await global.knex.rollback(); + Model.knex(knex); }); global.afterAll(async () => { - global.knex.destroy(); + global.knexInstance.destroy(); + logger.silent = false; }); diff --git a/yarn.lock b/yarn.lock index bc2218e7..3c9a9cce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1678,6 +1678,11 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@faker-js/faker@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-8.1.0.tgz#e14896f1c57af2495e341dc4c7bf04125c8aeafd" + integrity sha512-38DT60rumHfBYynif3lmtxMqMqmsOQIxQgEuPZxCk2yUYN0eqWpTACgxi0VpidvsJB8CRxCpvP7B3anK85FjtQ== + "@formatjs/ecma402-abstract@1.11.1": version "1.11.1" resolved "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.1.tgz" @@ -4037,6 +4042,11 @@ dependencies: "@types/node" "*" +"@types/cookiejar@*": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" + integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== + "@types/cors@^2.8.12": version "2.8.12" resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz" @@ -4587,6 +4597,21 @@ resolved "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz" integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== +"@types/superagent@*": + version "4.1.19" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.19.tgz#33f4fa460fb9e79e5e47a96731725141c667acd0" + integrity sha512-McM1mlc7PBZpCaw0fw/36uFqo0YeA6m8JqoyE4OfqXsZCIg0hPP2xdE6FM7r6fdprDZHlJwDpydUj1R++93hCA== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@^2.0.14": + version "2.0.14" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.14.tgz#e8fb6f6feed58a0dd5c2036227865dfa6ff7411d" + integrity sha512-Q900DeeHNFF3ZYYepf/EyJfZDA2JrnWLaSQ0YNV7+2GTo8IlJzauEnDGhya+hauncpBYTYGpVHwGdssJeAQ7eA== + dependencies: + "@types/superagent" "*" + "@types/testing-library__jest-dom@^5.9.1": version "5.14.2" resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.2.tgz" @@ -6879,6 +6904,11 @@ compare-versions@^4.1.3: resolved "https://registry.npmjs.org/compare-versions/-/compare-versions-4.1.3.tgz" integrity sha512-WQfnbDcrYnGr55UwbxKiQKASnTtNnaAWVi8jZyy8NTpVAXWACSne8lMD1iaIo9AiU6mnuLvSVshCzewVuWxHUg== +component-emitter@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + component-type@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/component-type/-/component-type-1.2.1.tgz" @@ -7109,6 +7139,11 @@ cookie@0.5.0: resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookiejar@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== + copyfiles@^2.4.1: version "2.4.1" resolved "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz" @@ -7768,6 +7803,14 @@ dezalgo@^1.0.0: asap "^2.0.0" wrappy "1" +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" @@ -9080,6 +9123,11 @@ fast-redact@^3.0.0: resolved "https://registry.npmjs.org/fast-redact/-/fast-redact-3.0.2.tgz" integrity sha512-YN+CYfCVRVMUZOUPeinHNKgytM1wPI/C/UCLEi56EsY2dwwvI00kIJHJoI7pMVqGoMew8SMZ2SSfHKHULHXDsg== +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + fast-xml-parser@^4.0.11: version "4.2.5" resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz" @@ -9317,6 +9365,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" @@ -9326,6 +9383,16 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +formidable@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89" + integrity sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g== + dependencies: + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + qs "^6.11.0" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" @@ -9916,6 +9983,11 @@ he@1.2.0, he@^1.2.0: resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +hexoid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + history@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/history/-/history-5.2.0.tgz" @@ -12536,7 +12608,7 @@ merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@~1.1.2: +methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= @@ -12578,6 +12650,11 @@ mime@1.6.0: resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" @@ -16812,6 +16889,30 @@ stylis@4.1.3: resolved "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz" integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== +superagent@^8.0.5: + version "8.1.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.1.2.tgz#03cb7da3ec8b32472c9d20f6c2a57c7f3765f30b" + integrity sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.4" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.1.2" + methods "^1.1.2" + mime "2.6.0" + qs "^6.11.0" + semver "^7.3.8" + +supertest@^6.3.3: + version "6.3.3" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.3.tgz#42f4da199fee656106fd422c094cf6c9578141db" + integrity sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA== + dependencies: + methods "^1.1.2" + superagent "^8.0.5" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"