From a748de369c42d6ab46f0ad58a481b830cd8e4073 Mon Sep 17 00:00:00 2001 From: Azima M I Date: Thu, 7 Jan 2021 10:27:17 +0800 Subject: [PATCH 01/13] chore: setup scaffolding for backend tests --- backend/__tests__/setup.js | 8 - backend/jest.config.js | 7 +- backend/package-lock.json | 182 ++++++++++++++++++ backend/package.json | 7 +- backend/tests/routes/core/auth.routes.test.ts | 83 ++++++++ backend/tests/routes/server.ts | 14 ++ backend/tests/routes/utils.ts | 11 ++ backend/tests/sequelize-mock.d.ts | 4 + .../services}/parse-csv.service.test.ts | 0 .../services}/phone-number.service.test.ts | 0 backend/tests/setup.ts | 26 +++ backend/tests/test-env.ts | 4 + backend/tsconfig.build.json | 5 + backend/tsconfig.json | 16 +- 14 files changed, 349 insertions(+), 18 deletions(-) delete mode 100644 backend/__tests__/setup.js create mode 100644 backend/tests/routes/core/auth.routes.test.ts create mode 100644 backend/tests/routes/server.ts create mode 100644 backend/tests/routes/utils.ts create mode 100644 backend/tests/sequelize-mock.d.ts rename backend/{__tests__ => tests/services}/parse-csv.service.test.ts (100%) rename backend/{__tests__ => tests/services}/phone-number.service.test.ts (100%) create mode 100644 backend/tests/setup.ts create mode 100644 backend/tests/test-env.ts create mode 100644 backend/tsconfig.build.json diff --git a/backend/__tests__/setup.js b/backend/__tests__/setup.js deleted file mode 100644 index d88a56f05..000000000 --- a/backend/__tests__/setup.js +++ /dev/null @@ -1,8 +0,0 @@ -/* eslint-disable no-console */ -global.console = { - log: jest.fn(), // console.log are ignored in tests - error: console.error, - warn: console.warn, - info: console.info, - debug: console.debug, -} diff --git a/backend/jest.config.js b/backend/jest.config.js index d6df66493..1404d524e 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -1,14 +1,17 @@ module.exports = { roots: [''], - testMatch: ['**/__tests__/**/*.(spec|test).+(ts|tsx|js)'], + testMatch: ['**/tests/**/*.(spec|test).+(ts|tsx|js)'], moduleNameMapper: { '@core/(.*)': '/src/core/$1', '@sms/(.*)': '/src/sms/$1', '@email/(.*)': '/src/email/$1', + '@telegram/(.*)': '/src/telegram/$1', + '@tests/(.*)': '/tests/$1', }, transform: { '^.+\\.(ts|tsx)$': 'ts-jest', }, testEnvironment: 'node', - setupFilesAfterEnv: ['/__tests__/setup.js'], + setupFiles: ['/tests/test-env.ts'], + setupFilesAfterEnv: ['/tests/setup.ts'], } diff --git a/backend/package-lock.json b/backend/package-lock.json index 674ce8171..d9069d66c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1282,6 +1282,12 @@ "@types/node": "*" } }, + "@types/cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", + "dev": true + }, "@types/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.6.tgz", @@ -1568,6 +1574,15 @@ "@types/node": "*" } }, + "@types/redis-mock": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@types/redis-mock/-/redis-mock-0.17.0.tgz", + "integrity": "sha512-UDKHu9otOSE1fPjgn0H7UoggqVyuRYfo3WJpdXdVmzgGmr1XIM/dTk/gRYf/bLjIK5mxpV8inA5uNBS2sVOilA==", + "dev": true, + "requires": { + "@types/redis": "*" + } + }, "@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -1590,6 +1605,25 @@ "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", "dev": true }, + "@types/superagent": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.10.tgz", + "integrity": "sha512-xAgkb2CMWUMCyVc/3+7iQfOEBE75NvuZeezvmixbUw3nmENf2tCnQkW5yQLTYqvXUQ+R6EXxdqKKbal2zM5V/g==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "@types/supertest": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.10.tgz", + "integrity": "sha512-Xt8TbEyZTnD5Xulw95GLMOkmjGICrOQyJ2jqgkSjAUR3mm7pAIzSR0NFBaMcwlzVvlpCjNwbATcWWwjNiZiFrQ==", + "dev": true, + "requires": { + "@types/superagent": "*" + } + }, "@types/swagger-jsdoc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-3.0.2.tgz", @@ -2866,6 +2900,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -4054,6 +4094,12 @@ "mime-types": "^2.1.12" } }, + "formidable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", + "dev": true + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -14933,6 +14979,12 @@ "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" }, + "redis-mock": { + "version": "0.56.3", + "resolved": "https://registry.npmjs.org/redis-mock/-/redis-mock-0.56.3.tgz", + "integrity": "sha512-ynaJhqk0Qf3Qajnwvy4aOjS4Mdf9IBkELWtjd+NYhpiqu4QCNq6Vf3Q7c++XRPGiKiwRj9HWr0crcwy7EiPjYQ==", + "dev": true + }, "redis-parser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", @@ -15476,6 +15528,17 @@ } } }, + "sequelize-mock": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/sequelize-mock/-/sequelize-mock-0.10.2.tgz", + "integrity": "sha1-GdOXHM2utbhkFwwkznkqinHxRL0=", + "dev": true, + "requires": { + "bluebird": "^3.4.6", + "inflection": "^1.10.0", + "lodash": "^4.16.4" + } + }, "sequelize-pool": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-2.3.0.tgz", @@ -16055,6 +16118,125 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, + "superagent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz", + "integrity": "sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==", + "dev": true, + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.2", + "debug": "^4.1.1", + "fast-safe-stringify": "^2.0.7", + "form-data": "^3.0.0", + "formidable": "^1.2.2", + "methods": "^1.1.2", + "mime": "^2.4.6", + "qs": "^6.9.4", + "readable-stream": "^3.6.0", + "semver": "^7.3.2" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "mime": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.7.tgz", + "integrity": "sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "supertest": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.0.1.tgz", + "integrity": "sha512-8yDNdm+bbAN/jeDdXsRipbq9qMpVF7wRsbwLgsANHqdjPsCoecmlTuqEcLQMGpmojFBhxayZ0ckXmLXYq7e+0g==", + "dev": true, + "requires": { + "methods": "1.1.2", + "superagent": "6.1.0" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/backend/package.json b/backend/package.json index 47a3004f4..dae93bd66 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,7 +4,7 @@ "description": "Backend / API server for Postman", "main": "build/server.js", "scripts": { - "build": "rimraf build && tsc", + "build": "rimraf build && tsc -p tsconfig.build.json", "dev": "npm run postbuild && tsc-watch --onSuccess \"node ./build/server.js\"", "lint-no-fix": "tsc --noEmit && eslint --ext .js,.ts --cache .", "lint": "npm run lint-no-fix -- --fix", @@ -82,6 +82,8 @@ "@types/nodemailer-direct-transport": "^1.0.31", "@types/papaparse": "^5.0.4", "@types/redis": "^2.8.17", + "@types/redis-mock": "^0.17.0", + "@types/supertest": "^2.0.10", "@types/swagger-jsdoc": "^3.0.2", "@types/swagger-ui-express": "^4.1.2", "@types/uuid": "^7.0.2", @@ -95,7 +97,10 @@ "jest": "^25.5.2", "lint-staged": "^10.2.6", "prettier": "^2.0.5", + "redis-mock": "^0.56.3", "rimraf": "^3.0.2", + "sequelize-mock": "^0.10.2", + "supertest": "^6.0.1", "ts-jest": "^25.4.0", "tsc-watch": "^4.2.3", "typescript": "^3.8.3" diff --git a/backend/tests/routes/core/auth.routes.test.ts b/backend/tests/routes/core/auth.routes.test.ts new file mode 100644 index 000000000..797e2e015 --- /dev/null +++ b/backend/tests/routes/core/auth.routes.test.ts @@ -0,0 +1,83 @@ +import request from 'supertest' +import app from '../server' +import { userModelMock } from '@tests/setup' + +describe('POST /auth/otp', () => { + test('Invalid email format', async (done) => { + const res = await request(app) + .post('/auth/otp') + .send({ email: 'user!@open' }) + expect(res.status).toBe(400) + done() + }) + + test('Non gov.sg and non-whitelisted email', async (done) => { + // Mock db query to User table to return null to mock user who is not whitelisted + userModelMock.$queueResult(null) + + const res = await request(app) + .post('/auth/otp') + .send({ email: 'user@agency.com.sg' }) + expect(res.status).toBe(401) + expect(res.body).toMatchObject({ message: 'User is not authorized' }) + done() + }) + + test('Non gov.sg and whitelisted email', async (done) => { + // Mock db query to User table to return null to mock user who is not whitelisted + userModelMock.$queueResult( + userModelMock.build({ email: 'user@agency.com.sg' }) + ) + + const res = await request(app) + .post('/auth/otp') + .send({ email: 'user@agency.com.sg' }) + expect(res.status).toBe(200) + done() + }) +}) + +describe('POST /auth/login', () => { + test('Invalid otp format', async (done) => { + const res = await request(app) + .post('/auth/login') + .send({ email: 'user@agency.gov.sg', otp: '123' }) + expect(res.status).toBe(400) + done() + }) + + test('Invalid otp', async (done) => { + const res = await request(app) + .post('/auth/login') + .send({ email: 'user@agency.gov.sg', otp: '000000' }) + expect(res.status).toBe(401) + done() + }) + + test('Valid otp', async (done) => { + // TODO + done() + }) +}) + +describe('GET /auth/userinfo', () => { + test('No existing session', async (done) => { + const res = await request(app).get('/auth/userinfo') + expect(res.status).toBe(200) + expect(res.body).toMatchObject({}) + done() + }) + + test('Existing session found', async (done) => { + // TODO + done() + }) +}) + +describe('GET /auth/logout', () => { + test('Successfully logged out', async (done) => { + const res = await request(app).get('/auth/logout') + expect(res.status).toBe(200) + done() + }) +}) diff --git a/backend/tests/routes/server.ts b/backend/tests/routes/server.ts new file mode 100644 index 000000000..fcde1fd7e --- /dev/null +++ b/backend/tests/routes/server.ts @@ -0,0 +1,14 @@ +import express from 'express' +import bodyParser from 'body-parser' +import sessionLoader from '@core/loaders/session.loader' +import { errors as celebrateErrorMiddleware } from 'celebrate' +import routes from '@core/routes' + +const app: express.Application = express() +sessionLoader({ app }) +app.use(bodyParser.json()) +app.use(bodyParser.urlencoded({ extended: false })) +app.use(routes) +app.use(celebrateErrorMiddleware()) + +export default app diff --git a/backend/tests/routes/utils.ts b/backend/tests/routes/utils.ts new file mode 100644 index 000000000..541c9abed --- /dev/null +++ b/backend/tests/routes/utils.ts @@ -0,0 +1,11 @@ +import SequelizeMock from 'sequelize-mock' + +// Mock models +const sequelizeMock = new SequelizeMock() +const userModelMock = sequelizeMock.define('user') + +jest.mock('@core/models/user/user', () => ({ + User: userModelMock, +})) + +export { userModelMock } diff --git a/backend/tests/sequelize-mock.d.ts b/backend/tests/sequelize-mock.d.ts new file mode 100644 index 000000000..9ff49dffa --- /dev/null +++ b/backend/tests/sequelize-mock.d.ts @@ -0,0 +1,4 @@ +declare module 'sequelize-mock' { + const mock: any + export default mock +} diff --git a/backend/__tests__/parse-csv.service.test.ts b/backend/tests/services/parse-csv.service.test.ts similarity index 100% rename from backend/__tests__/parse-csv.service.test.ts rename to backend/tests/services/parse-csv.service.test.ts diff --git a/backend/__tests__/phone-number.service.test.ts b/backend/tests/services/phone-number.service.test.ts similarity index 100% rename from backend/__tests__/phone-number.service.test.ts rename to backend/tests/services/phone-number.service.test.ts diff --git a/backend/tests/setup.ts b/backend/tests/setup.ts new file mode 100644 index 000000000..822197684 --- /dev/null +++ b/backend/tests/setup.ts @@ -0,0 +1,26 @@ +import SequelizeMock from 'sequelize-mock' + +/* eslint-disable no-console */ +global.console = { + ...global.console, + log: jest.fn(), // console.log are ignored in tests + error: console.error, + warn: console.warn, + info: console.info, + debug: console.debug, +} + +jest.mock('redis', () => jest.requireActual('redis-mock')) + +// Mock services +jest.mock('@core/services/mail-client.class') + +// Mock models +const sequelizeMock = new SequelizeMock() +const userModelMock = sequelizeMock.define('user') + +jest.mock('@core/models/user/user', () => ({ + User: userModelMock, +})) + +export { userModelMock } diff --git a/backend/tests/test-env.ts b/backend/tests/test-env.ts new file mode 100644 index 000000000..77fc2656b --- /dev/null +++ b/backend/tests/test-env.ts @@ -0,0 +1,4 @@ +process.env.REDIS_OTP_URI = 'redis://localhost:6379/3' +process.env.REDIS_SESSION_URI = 'redis://localhost:6379/4' +process.env.SENDGRID_PUBLIC_KEY = 'sendgridpublickey' +process.env.SESSION_SECRET = 'YxHif3SJCe16SzsKMHS' diff --git a/backend/tsconfig.build.json b/backend/tsconfig.build.json new file mode 100644 index 000000000..b792dc00f --- /dev/null +++ b/backend/tsconfig.build.json @@ -0,0 +1,5 @@ +// Excludes test in builds +{ + "extends": "./tsconfig.json", + "exclude": ["tests/"] +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 32df37e0d..6d8f4d6d2 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,6 +1,7 @@ { "include": [ "./src/**/*", + "./tests/**/*", ], "exclude": [ "node_modules" @@ -22,7 +23,7 @@ "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "./build", /* Redirect output structure to the directory. */ - "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "incremental": true, /* Enable incremental compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ @@ -50,12 +51,13 @@ /* Module Resolution Options */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - "baseUrl": "./src", /* Base directory to resolve non-absolute module names. */ + "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ "paths": { - "@core/*": ["core/*"], - "@sms/*": ["sms/*"], - "@email/*": ["email/*"], - "@telegram/*": ["telegram/*"], + "@core/*": ["src/core/*"], + "@sms/*": ["src/sms/*"], + "@email/*": ["src/email/*"], + "@telegram/*": ["src/telegram/*"], + "@tests/*": ["tests/*"], }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ @@ -74,4 +76,4 @@ "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ }, -} \ No newline at end of file +} From 04b523a3f8c157ccaa7fca3582e44865d7191f31 Mon Sep 17 00:00:00 2001 From: Azima M I Date: Mon, 11 Jan 2021 13:00:40 +0800 Subject: [PATCH 02/13] chore: add tests for campaign routes --- .secrets.baseline | 10 +- backend/.eslintrc.js | 3 +- backend/package-lock.json | 233 +++++++++++++++++- backend/package.json | 3 +- backend/tests/routes/core/auth.routes.test.ts | 56 +++-- .../tests/routes/core/campaign.routes.test.ts | 82 ++++++ backend/tests/routes/server.ts | 18 +- backend/tests/routes/utils.ts | 11 - backend/tests/setup.ts | 8 +- backend/tests/test-env.ts | 5 +- 10 files changed, 376 insertions(+), 53 deletions(-) create mode 100644 backend/tests/routes/core/campaign.routes.test.ts delete mode 100644 backend/tests/routes/utils.ts diff --git a/.secrets.baseline b/.secrets.baseline index 952441d55..75be9a701 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -4,7 +4,7 @@ "files": "package-lock.json|^.secrets.baseline$", "lines": null }, - "generated_at": "2020-10-19T02:55:47Z", + "generated_at": "2021-01-11T08:57:41Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -55,6 +55,14 @@ } ], "results": { + "backend/tests/test-env.ts": [ + { + "hashed_secret": "c237a19676ad55d8904be354990dd54f92f9572c", + "is_verified": false, + "line_number": 4, + "type": "Base64 High Entropy String" + } + ], "frontend/.env-example": [ { "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js index ba6a5fab8..35589cd58 100644 --- a/backend/.eslintrc.js +++ b/backend/.eslintrc.js @@ -1,13 +1,14 @@ module.exports = { root: false, parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], + plugins: ['@typescript-eslint', "jest"], extends: [ 'eslint:recommended', // Recommended ESLint rules 'plugin:@typescript-eslint/eslint-recommended', // Disables rules from `eslint:recommended` that are already covered by the TypeScript typechecker 'plugin:@typescript-eslint/recommended', // Recommended TypeScript rules 'prettier/@typescript-eslint', // Disables rules from `@typescript-eslint/recommended` that are covered by Prettier 'plugin:prettier/recommended', // Recommended Prettier rules + "plugin:jest/recommended" // Recommended Jest rules ], parserOptions: { sourceType: 'module', diff --git a/backend/package-lock.json b/backend/package-lock.json index d9069d66c..c0e5ba631 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -995,6 +995,32 @@ "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.1.tgz", "integrity": "sha512-pu5fxkbLQWzRbBgfFbZfHXz0KlYojOfVdUhcNfy9lef8ZhBt0pckGr8g7zv4vPX4Out5vBNvqd/az4UaVWzZ9A==" }, + "@nodelib/fs.scandir": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", + "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.4", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", + "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.4", + "fastq": "^1.6.0" + } + }, "@otplib/core": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz", @@ -1703,6 +1729,22 @@ "eslint-visitor-keys": "^1.1.0" } }, + "@typescript-eslint/scope-manager": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.12.0.tgz", + "integrity": "sha512-QVf9oCSVLte/8jvOsxmgBdOaoe2J0wtEmBr13Yz0rkBNkl5D8bfnf6G4Vhox9qqMIoG7QQoVwd2eG9DM/ge4Qg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.12.0", + "@typescript-eslint/visitor-keys": "4.12.0" + } + }, + "@typescript-eslint/types": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.12.0.tgz", + "integrity": "sha512-N2RhGeheVLGtyy+CxRmxdsniB7sMSCfsnbh8K/+RUIXYYq3Ub5+sukRCjVE80QerrUBvuEvs4fDhz5AW/pcL6g==", + "dev": true + }, "@typescript-eslint/typescript-estree": { "version": "2.25.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.25.0.tgz", @@ -1755,6 +1797,24 @@ } } }, + "@typescript-eslint/visitor-keys": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.12.0.tgz", + "integrity": "sha512-hVpsLARbDh4B9TKYz5cLbcdMIOAoBYgFPCSP9FFS/liSF+b33gVNq8JHY3QGhHNVz85hObvL7BEYLlgx553WCw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.12.0", + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + } + } + }, "abab": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", @@ -1980,6 +2040,12 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", @@ -3183,6 +3249,15 @@ "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", "dev": true }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, "dns-prefetch-control": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.2.0.tgz", @@ -3484,6 +3559,86 @@ "get-stdin": "^6.0.0" } }, + "eslint-plugin-jest": { + "version": "24.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.1.3.tgz", + "integrity": "sha512-dNGGjzuEzCE3d5EPZQ/QGtmlMotqnYWD/QpCZ1UuZlrMAdhG5rldh0N0haCvhGnUkSeuORS5VNROwF9Hrgn3Lg==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "^4.0.1" + }, + "dependencies": { + "@typescript-eslint/experimental-utils": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.12.0.tgz", + "integrity": "sha512-MpXZXUAvHt99c9ScXijx7i061o5HEjXltO+sbYfZAAHxv3XankQkPaNi5myy0Yh0Tyea3Hdq1pi7Vsh0GJb0fA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.12.0", + "@typescript-eslint/types": "4.12.0", + "@typescript-eslint/typescript-estree": "4.12.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.12.0.tgz", + "integrity": "sha512-gZkFcmmp/CnzqD2RKMich2/FjBTsYopjiwJCroxqHZIY11IIoN0l5lKqcgoAPKHt33H2mAkSfvzj8i44Jm7F4w==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.12.0", + "@typescript-eslint/visitor-keys": "4.12.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "eslint-plugin-prettier": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.3.tgz", @@ -3934,6 +4089,20 @@ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "dev": true }, + "fast-glob": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", + "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3950,6 +4119,15 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" }, + "fastq": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.10.0.tgz", + "integrity": "sha512-NL2Qc5L3iQEsyYzweq7qfgy5OtXCmGzGvhElGEd/SoFWEMOEczNh5s5ocaF01HDetxz+p8ecjNPA6cZxxIHmzA==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "fb-watchman": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", @@ -4329,6 +4507,28 @@ "type-fest": "^0.8.1" } }, + "globby": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.2.tgz", + "integrity": "sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, + "dependencies": { + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + } + } + }, "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", @@ -7502,6 +7702,12 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -15205,6 +15411,12 @@ "any-promise": "^1.3.0" } }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -15250,6 +15462,12 @@ "is-promise": "^2.1.0" } }, + "run-parallel": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz", + "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==", + "dev": true + }, "rxjs": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", @@ -16671,9 +16889,9 @@ "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" }, "ts-jest": { - "version": "25.4.0", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.4.0.tgz", - "integrity": "sha512-+0ZrksdaquxGUBwSdTIcdX7VXdwLIlSRsyjivVA9gcO+Cvr6ByqDhu/mi5+HCcb6cMkiQp5xZ8qRO7/eCqLeyw==", + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.5.1.tgz", + "integrity": "sha512-kHEUlZMK8fn8vkxDjwbHlxXRB9dHYpyzqKIGDNxbzs+Rz+ssNDSDNusEK8Fk/sDd4xE6iKoQLfFkFVaskmTJyw==", "dev": true, "requires": { "bs-logger": "0.x", @@ -16683,18 +16901,11 @@ "lodash.memoize": "4.x", "make-error": "1.x", "micromatch": "4.x", - "mkdirp": "1.x", - "resolve": "1.x", + "mkdirp": "0.x", "semver": "6.x", "yargs-parser": "18.x" }, "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", diff --git a/backend/package.json b/backend/package.json index dae93bd66..7e039bc1c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -93,6 +93,7 @@ "copyfiles": "^2.2.0", "eslint": "^6.8.0", "eslint-config-prettier": "^6.11.0", + "eslint-plugin-jest": "^24.1.3", "eslint-plugin-prettier": "^3.1.3", "jest": "^25.5.2", "lint-staged": "^10.2.6", @@ -101,7 +102,7 @@ "rimraf": "^3.0.2", "sequelize-mock": "^0.10.2", "supertest": "^6.0.1", - "ts-jest": "^25.4.0", + "ts-jest": "^25.5.1", "tsc-watch": "^4.2.3", "typescript": "^3.8.3" }, diff --git a/backend/tests/routes/core/auth.routes.test.ts b/backend/tests/routes/core/auth.routes.test.ts index 797e2e015..3dac014bf 100644 --- a/backend/tests/routes/core/auth.routes.test.ts +++ b/backend/tests/routes/core/auth.routes.test.ts @@ -1,17 +1,17 @@ import request from 'supertest' import app from '../server' +import { AuthService } from '@core/services' import { userModelMock } from '@tests/setup' describe('POST /auth/otp', () => { - test('Invalid email format', async (done) => { + test('Invalid email format', async () => { const res = await request(app) .post('/auth/otp') .send({ email: 'user!@open' }) expect(res.status).toBe(400) - done() }) - test('Non gov.sg and non-whitelisted email', async (done) => { + test('Non gov.sg and non-whitelisted email', async () => { // Mock db query to User table to return null to mock user who is not whitelisted userModelMock.$queueResult(null) @@ -19,11 +19,10 @@ describe('POST /auth/otp', () => { .post('/auth/otp') .send({ email: 'user@agency.com.sg' }) expect(res.status).toBe(401) - expect(res.body).toMatchObject({ message: 'User is not authorized' }) - done() + expect(res.body).toEqual({ message: 'User is not authorized' }) }) - test('Non gov.sg and whitelisted email', async (done) => { + test('Non gov.sg and whitelisted email', async () => { // Mock db query to User table to return null to mock user who is not whitelisted userModelMock.$queueResult( userModelMock.build({ email: 'user@agency.com.sg' }) @@ -33,51 +32,62 @@ describe('POST /auth/otp', () => { .post('/auth/otp') .send({ email: 'user@agency.com.sg' }) expect(res.status).toBe(200) - done() }) }) describe('POST /auth/login', () => { - test('Invalid otp format', async (done) => { + test('Invalid otp format', async () => { const res = await request(app) .post('/auth/login') .send({ email: 'user@agency.gov.sg', otp: '123' }) expect(res.status).toBe(400) - done() }) - test('Invalid otp', async (done) => { + test('Invalid otp', async () => { const res = await request(app) .post('/auth/login') .send({ email: 'user@agency.gov.sg', otp: '000000' }) expect(res.status).toBe(401) - done() }) - test('Valid otp', async (done) => { - // TODO - done() + test('Valid otp', async () => { + const email = 'user@agency.gov.sg' + // Mock verification of otp + AuthService.verifyOtp = jest.fn(async () => true) + // Mock user query + AuthService.findOrCreateUser = jest.fn(async () => + userModelMock.build({ email }) + ) + + const res = await request(app) + .post('/auth/login') + .send({ email, otp: '123456' }) + expect(res.status).toBe(200) + expect(res.body).toEqual({}) }) }) describe('GET /auth/userinfo', () => { - test('No existing session', async (done) => { - const res = await request(app).get('/auth/userinfo') + test('No existing session', async () => { + const res = await request(app).get('/auth/userinfo').set('Cookies', '') expect(res.status).toBe(200) - expect(res.body).toMatchObject({}) - done() + expect(res.body).toEqual({}) }) - test('Existing session found', async (done) => { - // TODO - done() + test('Existing session found', async () => { + // TODO - mock user session + // const email = 'user@agency.gov.sg' + // await request(app).post('/auth/login').send({ email, otp: '123456' }) + // const res = await request(app).get('/auth/userinfo') + // expect(res.status).toBe(200) + // expect(res.body).toEqual({}) + expect(true).toEqual(true) }) }) describe('GET /auth/logout', () => { - test('Successfully logged out', async (done) => { + test('Successfully logged out', async () => { const res = await request(app).get('/auth/logout') expect(res.status).toBe(200) - done() }) }) diff --git a/backend/tests/routes/core/campaign.routes.test.ts b/backend/tests/routes/core/campaign.routes.test.ts new file mode 100644 index 000000000..7b7243d04 --- /dev/null +++ b/backend/tests/routes/core/campaign.routes.test.ts @@ -0,0 +1,82 @@ +import request from 'supertest' +import app from '../server' +import { campaignModelMock } from '@tests/setup' +import { CampaignService } from '@core/services' +import { Campaign } from '@core/models' + +describe('GET /campaigns', () => { + test('List campaigns with default limit and offset', async () => { + campaignModelMock.$queueResult({ + rows: [campaignModelMock.build(), campaignModelMock.build()], + count: 2, + }) + const res = await request(app).get('/campaigns') + expect(res.status).toBe(200) + expect(res.body).toEqual({ + total_count: 2, + campaigns: expect.arrayContaining([ + expect.objectContaining({ id: expect.any(Number) }), + ]), + }) + }) + + test('List campaigns with defined limit and offset', async () => { + campaignModelMock.$queueResult({ + rows: [campaignModelMock.build({ id: 5 })], + count: 1, + }) + const res = await request(app) + .get('/campaigns') + .query({ limit: 1, offset: 2 }) + expect(res.status).toBe(200) + expect(res.body).toEqual({ + total_count: 1, + campaigns: expect.arrayContaining([expect.objectContaining({ id: 5 })]), + }) + }) +}) + +describe('POST /campaigns', () => { + test('Fail to create campaign', async () => { + // Create campaign returns void when campaign creation fails + CampaignService.createCampaignWithTransaction = jest.fn( + async () => new Promise((resolve) => resolve()) + ) + const res = await request(app).post('/campaigns').send({ + name: 'test', + type: 'SMS', + }) + expect(res.status).toBe(400) + expect(res.body).toEqual({ + message: 'Unable to create campaign with these parameters', + }) + }) + + test('Successfully create SMS campaign', async () => { + // Create campaign returns void when campaign creation fails + CampaignService.createCampaignWithTransaction = jest.fn( + async () => + new Promise((resolve) => resolve(campaignModelMock.build())) + ) + const res = await request(app).post('/campaigns').send({ + name: 'test', + type: 'SMS', + }) + expect(res.status).toBe(201) + expect(res.body).toEqual( + expect.objectContaining({ + id: expect.any(Number), + created_at: expect.any(String), + }) + ) + }) + + test('Create protected campaign for unsupported channel', async () => { + const res = await request(app).post('/campaigns').send({ + name: 'test', + type: 'SMS', + protect: true, + }) + expect(res.status).toBe(403) + }) +}) diff --git a/backend/tests/routes/server.ts b/backend/tests/routes/server.ts index fcde1fd7e..8dcf518d5 100644 --- a/backend/tests/routes/server.ts +++ b/backend/tests/routes/server.ts @@ -1,13 +1,29 @@ -import express from 'express' +import express, { Request, Response, NextFunction } from 'express' import bodyParser from 'body-parser' import sessionLoader from '@core/loaders/session.loader' import { errors as celebrateErrorMiddleware } from 'celebrate' import routes from '@core/routes' +const unAuthenticatedRoutes = ['auth', 'stats', 'protect', 'unsubscribe'] + const app: express.Application = express() sessionLoader({ app }) app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: false })) +app.use((req: Request, _res: Response, next: NextFunction): void => { + const mainRoute = req.path.split('/')[1] + // Continue without mocking user session for unauthenticated routes + if (unAuthenticatedRoutes.indexOf(mainRoute) > -1) { + return next() + } + if (req.session) { + req.session.user = { + id: 1, + email: 'user@agency.gov.sg', + } + } + next() +}) app.use(routes) app.use(celebrateErrorMiddleware()) diff --git a/backend/tests/routes/utils.ts b/backend/tests/routes/utils.ts deleted file mode 100644 index 541c9abed..000000000 --- a/backend/tests/routes/utils.ts +++ /dev/null @@ -1,11 +0,0 @@ -import SequelizeMock from 'sequelize-mock' - -// Mock models -const sequelizeMock = new SequelizeMock() -const userModelMock = sequelizeMock.define('user') - -jest.mock('@core/models/user/user', () => ({ - User: userModelMock, -})) - -export { userModelMock } diff --git a/backend/tests/setup.ts b/backend/tests/setup.ts index 822197684..b6ec085a8 100644 --- a/backend/tests/setup.ts +++ b/backend/tests/setup.ts @@ -17,10 +17,14 @@ jest.mock('@core/services/mail-client.class') // Mock models const sequelizeMock = new SequelizeMock() -const userModelMock = sequelizeMock.define('user') +export const userModelMock = sequelizeMock.define('users') jest.mock('@core/models/user/user', () => ({ User: userModelMock, })) -export { userModelMock } +export const campaignModelMock = sequelizeMock.define('campaigns') + +jest.mock('@core/models/campaign', () => ({ + Campaign: campaignModelMock, +})) diff --git a/backend/tests/test-env.ts b/backend/tests/test-env.ts index 77fc2656b..308c9911b 100644 --- a/backend/tests/test-env.ts +++ b/backend/tests/test-env.ts @@ -1,4 +1,5 @@ process.env.REDIS_OTP_URI = 'redis://localhost:6379/3' process.env.REDIS_SESSION_URI = 'redis://localhost:6379/4' -process.env.SENDGRID_PUBLIC_KEY = 'sendgridpublickey' -process.env.SESSION_SECRET = 'YxHif3SJCe16SzsKMHS' +process.env.SENDGRID_PUBLIC_KEY = + 'MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEKWFCI/58CSJe4uz9WX7VZZBIoeb3c1UEJ+pe3HL0ywyGA6c3Bq92+1YVKv0HHxf5mjm+t47P672gcaYarlp2LA==' +process.env.SESSION_SECRET = 'SESSIONSECRET' From 44519412e025fa8b23bd089786aedef98acfdcff Mon Sep 17 00:00:00 2001 From: Azima M I Date: Wed, 13 Jan 2021 12:42:56 +0800 Subject: [PATCH 03/13] refactor: rename model mocks --- backend/jest.config.js | 1 + backend/tests/routes/core/auth.routes.test.ts | 10 ++++------ backend/tests/routes/core/campaign.routes.test.ts | 12 ++++++------ backend/tests/setup.ts | 8 ++++---- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/backend/jest.config.js b/backend/jest.config.js index 1404d524e..e7391db94 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -1,6 +1,7 @@ module.exports = { roots: [''], testMatch: ['**/tests/**/*.(spec|test).+(ts|tsx|js)'], + testPathIgnorePatterns: ['/build/', '/node_modules/'], moduleNameMapper: { '@core/(.*)': '/src/core/$1', '@sms/(.*)': '/src/sms/$1', diff --git a/backend/tests/routes/core/auth.routes.test.ts b/backend/tests/routes/core/auth.routes.test.ts index 3dac014bf..21583aac9 100644 --- a/backend/tests/routes/core/auth.routes.test.ts +++ b/backend/tests/routes/core/auth.routes.test.ts @@ -1,7 +1,7 @@ import request from 'supertest' import app from '../server' import { AuthService } from '@core/services' -import { userModelMock } from '@tests/setup' +import { UserMock } from '@tests/setup' describe('POST /auth/otp', () => { test('Invalid email format', async () => { @@ -13,7 +13,7 @@ describe('POST /auth/otp', () => { test('Non gov.sg and non-whitelisted email', async () => { // Mock db query to User table to return null to mock user who is not whitelisted - userModelMock.$queueResult(null) + UserMock.$queueResult(null) const res = await request(app) .post('/auth/otp') @@ -24,9 +24,7 @@ describe('POST /auth/otp', () => { test('Non gov.sg and whitelisted email', async () => { // Mock db query to User table to return null to mock user who is not whitelisted - userModelMock.$queueResult( - userModelMock.build({ email: 'user@agency.com.sg' }) - ) + UserMock.$queueResult(UserMock.build({ email: 'user@agency.com.sg' })) const res = await request(app) .post('/auth/otp') @@ -56,7 +54,7 @@ describe('POST /auth/login', () => { AuthService.verifyOtp = jest.fn(async () => true) // Mock user query AuthService.findOrCreateUser = jest.fn(async () => - userModelMock.build({ email }) + UserMock.build({ email }) ) const res = await request(app) diff --git a/backend/tests/routes/core/campaign.routes.test.ts b/backend/tests/routes/core/campaign.routes.test.ts index 7b7243d04..ced17f3b3 100644 --- a/backend/tests/routes/core/campaign.routes.test.ts +++ b/backend/tests/routes/core/campaign.routes.test.ts @@ -1,13 +1,13 @@ import request from 'supertest' import app from '../server' -import { campaignModelMock } from '@tests/setup' +import { CampaignMock } from '@tests/setup' import { CampaignService } from '@core/services' import { Campaign } from '@core/models' describe('GET /campaigns', () => { test('List campaigns with default limit and offset', async () => { - campaignModelMock.$queueResult({ - rows: [campaignModelMock.build(), campaignModelMock.build()], + CampaignMock.$queueResult({ + rows: [CampaignMock.build(), CampaignMock.build()], count: 2, }) const res = await request(app).get('/campaigns') @@ -21,8 +21,8 @@ describe('GET /campaigns', () => { }) test('List campaigns with defined limit and offset', async () => { - campaignModelMock.$queueResult({ - rows: [campaignModelMock.build({ id: 5 })], + CampaignMock.$queueResult({ + rows: [CampaignMock.build({ id: 5 })], count: 1, }) const res = await request(app) @@ -56,7 +56,7 @@ describe('POST /campaigns', () => { // Create campaign returns void when campaign creation fails CampaignService.createCampaignWithTransaction = jest.fn( async () => - new Promise((resolve) => resolve(campaignModelMock.build())) + new Promise((resolve) => resolve(CampaignMock.build())) ) const res = await request(app).post('/campaigns').send({ name: 'test', diff --git a/backend/tests/setup.ts b/backend/tests/setup.ts index b6ec085a8..2a9c362c6 100644 --- a/backend/tests/setup.ts +++ b/backend/tests/setup.ts @@ -17,14 +17,14 @@ jest.mock('@core/services/mail-client.class') // Mock models const sequelizeMock = new SequelizeMock() -export const userModelMock = sequelizeMock.define('users') +export const UserMock = sequelizeMock.define('users') jest.mock('@core/models/user/user', () => ({ - User: userModelMock, + User: UserMock, })) -export const campaignModelMock = sequelizeMock.define('campaigns') +export const CampaignMock = sequelizeMock.define('campaigns') jest.mock('@core/models/campaign', () => ({ - Campaign: campaignModelMock, + Campaign: CampaignMock, })) From f3a748288fe112142cbcb67716dbfd41e5616101 Mon Sep 17 00:00:00 2001 From: Azima M I Date: Tue, 16 Feb 2021 10:59:06 +0800 Subject: [PATCH 04/13] chore: update tests to use sequelize db test data --- backend/tests/routes/core/auth.routes.test.ts | 44 +++------ .../tests/routes/core/campaign.routes.test.ts | 88 +++++++++++------ backend/tests/routes/sequelize-loader.ts | 99 +++++++++++++++++++ backend/tests/routes/server.ts | 2 +- backend/tests/sequelize-mock.d.ts | 4 - backend/tests/setup.ts | 32 ++---- backend/tests/test-env.ts | 1 + 7 files changed, 183 insertions(+), 87 deletions(-) create mode 100644 backend/tests/routes/sequelize-loader.ts delete mode 100644 backend/tests/sequelize-mock.d.ts diff --git a/backend/tests/routes/core/auth.routes.test.ts b/backend/tests/routes/core/auth.routes.test.ts index 21583aac9..4b5a2f04d 100644 --- a/backend/tests/routes/core/auth.routes.test.ts +++ b/backend/tests/routes/core/auth.routes.test.ts @@ -1,7 +1,11 @@ import request from 'supertest' import app from '../server' +import sequelizeLoader from '../sequelize-loader' import { AuthService } from '@core/services' -import { UserMock } from '@tests/setup' + +beforeAll(async () => { + await sequelizeLoader() +}) describe('POST /auth/otp', () => { test('Invalid email format', async () => { @@ -12,25 +16,13 @@ describe('POST /auth/otp', () => { }) test('Non gov.sg and non-whitelisted email', async () => { - // Mock db query to User table to return null to mock user who is not whitelisted - UserMock.$queueResult(null) - + // There are no users in the db const res = await request(app) .post('/auth/otp') .send({ email: 'user@agency.com.sg' }) expect(res.status).toBe(401) expect(res.body).toEqual({ message: 'User is not authorized' }) }) - - test('Non gov.sg and whitelisted email', async () => { - // Mock db query to User table to return null to mock user who is not whitelisted - UserMock.$queueResult(UserMock.build({ email: 'user@agency.com.sg' })) - - const res = await request(app) - .post('/auth/otp') - .send({ email: 'user@agency.com.sg' }) - expect(res.status).toBe(200) - }) }) describe('POST /auth/login', () => { @@ -52,10 +44,6 @@ describe('POST /auth/login', () => { const email = 'user@agency.gov.sg' // Mock verification of otp AuthService.verifyOtp = jest.fn(async () => true) - // Mock user query - AuthService.findOrCreateUser = jest.fn(async () => - UserMock.build({ email }) - ) const res = await request(app) .post('/auth/login') @@ -67,20 +55,20 @@ describe('POST /auth/login', () => { describe('GET /auth/userinfo', () => { test('No existing session', async () => { - const res = await request(app).get('/auth/userinfo').set('Cookies', '') + const res = await request(app).get('/auth/userinfo') expect(res.status).toBe(200) expect(res.body).toEqual({}) }) - test('Existing session found', async () => { - // TODO - mock user session - // const email = 'user@agency.gov.sg' - // await request(app).post('/auth/login').send({ email, otp: '123456' }) - // const res = await request(app).get('/auth/userinfo') - // expect(res.status).toBe(200) - // expect(res.body).toEqual({}) - expect(true).toEqual(true) - }) + // test('Existing session found', async () => { + // // Mock verification of otp + // AuthService.verifyOtp = jest.fn(async () => true) + // const email = 'user@agency.gov.sg' + // await request(app).post('/auth/login').send({ email, otp: '123456' }) + // const res = await request(app).get('/auth/userinfo') + // expect(res.status).toBe(200) + // expect(res.body).toEqual({}) + // }) }) describe('GET /auth/logout', () => { diff --git a/backend/tests/routes/core/campaign.routes.test.ts b/backend/tests/routes/core/campaign.routes.test.ts index ced17f3b3..f79ec2ff3 100644 --- a/backend/tests/routes/core/campaign.routes.test.ts +++ b/backend/tests/routes/core/campaign.routes.test.ts @@ -1,15 +1,36 @@ import request from 'supertest' import app from '../server' -import { CampaignMock } from '@tests/setup' -import { CampaignService } from '@core/services' -import { Campaign } from '@core/models' +import { Campaign, User, UserDemo } from '@core/models' +import sequelizeLoader from '../sequelize-loader' + +beforeAll(async () => { + await sequelizeLoader() + await User.destroy({ where: {} }) + await UserDemo.destroy({ where: {} }) + await User.create({ id: 1, email: 'user@agency.gov.sg' }) +}) + +afterEach(async () => { + await Campaign.destroy({ where: {} }) +}) describe('GET /campaigns', () => { test('List campaigns with default limit and offset', async () => { - CampaignMock.$queueResult({ - rows: [CampaignMock.build(), CampaignMock.build()], - count: 2, + await Campaign.create({ + name: 'campaign-1', + userId: 1, + type: 'SMS', + valid: false, + protect: false, + }) + await Campaign.create({ + name: 'campaign-2', + userId: 1, + type: 'SMS', + valid: false, + protect: false, }) + const res = await request(app).get('/campaigns') expect(res.status).toBe(200) expect(res.body).toEqual({ @@ -21,54 +42,61 @@ describe('GET /campaigns', () => { }) test('List campaigns with defined limit and offset', async () => { - CampaignMock.$queueResult({ - rows: [CampaignMock.build({ id: 5 })], - count: 1, - }) + for (let i = 1; i <= 3; i++) { + await Campaign.create({ + name: `campaign-${i}`, + userId: 1, + type: 'SMS', + valid: false, + protect: false, + }) + } + const res = await request(app) .get('/campaigns') .query({ limit: 1, offset: 2 }) expect(res.status).toBe(200) expect(res.body).toEqual({ - total_count: 1, - campaigns: expect.arrayContaining([expect.objectContaining({ id: 5 })]), + total_count: 3, + campaigns: expect.arrayContaining([ + expect.objectContaining({ name: 'campaign-1' }), + ]), }) }) }) describe('POST /campaigns', () => { - test('Fail to create campaign', async () => { - // Create campaign returns void when campaign creation fails - CampaignService.createCampaignWithTransaction = jest.fn( - async () => new Promise((resolve) => resolve()) - ) + test('Successfully create SMS campaign', async () => { const res = await request(app).post('/campaigns').send({ name: 'test', type: 'SMS', }) - expect(res.status).toBe(400) - expect(res.body).toEqual({ - message: 'Unable to create campaign with these parameters', - }) + expect(res.status).toBe(201) + expect(res.body).toEqual( + expect.objectContaining({ + name: 'test', + protect: false, + }) + ) }) - test('Successfully create SMS campaign', async () => { - // Create campaign returns void when campaign creation fails - CampaignService.createCampaignWithTransaction = jest.fn( - async () => - new Promise((resolve) => resolve(CampaignMock.build())) - ) + test('Successfully create demo SMS campaign', async () => { const res = await request(app).post('/campaigns').send({ - name: 'test', + name: 'demo', type: 'SMS', + demo_message_limit: 10, }) expect(res.status).toBe(201) expect(res.body).toEqual( expect.objectContaining({ - id: expect.any(Number), - created_at: expect.any(String), + name: 'demo', + protect: false, + demo_message_limit: 10, }) ) + + const demo = await UserDemo.findOne({ where: { userId: 1 } }) + expect(demo?.numDemosSms).toEqual(2) }) test('Create protected campaign for unsupported channel', async () => { diff --git a/backend/tests/routes/sequelize-loader.ts b/backend/tests/routes/sequelize-loader.ts new file mode 100644 index 000000000..9c4545c8d --- /dev/null +++ b/backend/tests/routes/sequelize-loader.ts @@ -0,0 +1,99 @@ +import { Sequelize, SequelizeOptions } from 'sequelize-typescript' + +import config from '@core/config' +import { + Credential, + JobQueue, + Campaign, + Worker, + User, + UserFeature, + UserCredential, + UserDemo, + Statistic, + ProtectedMessage, + Unsubscriber, +} from '@core/models' +import { + EmailMessage, + EmailTemplate, + EmailOp, + EmailBlacklist, + EmailFromAddress, +} from '@email/models' +import { SmsMessage, SmsTemplate, SmsOp } from '@sms/models' +import { + BotSubscriber, + TelegramMessage, + TelegramOp, + TelegramSubscriber, + TelegramTemplate, +} from '@telegram/models' + +import { DefaultCredentialName } from '@core/constants' +import { formatDefaultCredentialName } from '@core/utils' + +const DB_TEST_URI = config.get('database.databaseTestUri') + +const sequelizeLoader = async (): Promise => { + const sequelize = new Sequelize(DB_TEST_URI, { + dialect: 'postgres', + logging: false, + pool: config.get('database.poolOptions'), + } as SequelizeOptions) + + const coreModels = [ + Credential, + JobQueue, + Campaign, + Worker, + User, + UserFeature, + UserCredential, + UserDemo, + Statistic, + Unsubscriber, + ] + const emailModels = [ + EmailMessage, + EmailTemplate, + EmailOp, + EmailBlacklist, + ProtectedMessage, + EmailFromAddress, + ] + const smsModels = [SmsMessage, SmsTemplate, SmsOp] + const telegramModels = [ + BotSubscriber, + TelegramOp, + TelegramMessage, + TelegramTemplate, + TelegramSubscriber, + ] + sequelize.addModels([ + ...coreModels, + ...emailModels, + ...smsModels, + ...telegramModels, + ]) + + try { + await sequelize.sync() + console.log({ message: 'Test Database loaded.' }) + } catch (error) { + console.log(error.message) + console.error({ message: 'Unable to connect to test database', error }) + // process.exit(1) + } + // Create the default credential names in the credentials table + // Each name should be accompanied by an entry in Secrets Manager + await Promise.all( + [ + DefaultCredentialName.Email, + formatDefaultCredentialName(DefaultCredentialName.SMS), + formatDefaultCredentialName(DefaultCredentialName.Telegram), + ].map((name) => Credential.upsert({ name })) + ) +} + +export default sequelizeLoader diff --git a/backend/tests/routes/server.ts b/backend/tests/routes/server.ts index 8dcf518d5..9e27f0367 100644 --- a/backend/tests/routes/server.ts +++ b/backend/tests/routes/server.ts @@ -1,7 +1,7 @@ import express, { Request, Response, NextFunction } from 'express' +import { errors as celebrateErrorMiddleware } from 'celebrate' import bodyParser from 'body-parser' import sessionLoader from '@core/loaders/session.loader' -import { errors as celebrateErrorMiddleware } from 'celebrate' import routes from '@core/routes' const unAuthenticatedRoutes = ['auth', 'stats', 'protect', 'unsubscribe'] diff --git a/backend/tests/sequelize-mock.d.ts b/backend/tests/sequelize-mock.d.ts deleted file mode 100644 index 9ff49dffa..000000000 --- a/backend/tests/sequelize-mock.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module 'sequelize-mock' { - const mock: any - export default mock -} diff --git a/backend/tests/setup.ts b/backend/tests/setup.ts index 2a9c362c6..3923cd80d 100644 --- a/backend/tests/setup.ts +++ b/backend/tests/setup.ts @@ -1,30 +1,14 @@ -import SequelizeMock from 'sequelize-mock' - /* eslint-disable no-console */ -global.console = { - ...global.console, - log: jest.fn(), // console.log are ignored in tests - error: console.error, - warn: console.warn, - info: console.info, - debug: console.debug, -} +// global.console = { +// ...global.console, +// log: jest.fn(), // console.log are ignored in tests +// error: console.error, +// warn: console.warn, +// info: console.info, +// debug: console.debug, +// } jest.mock('redis', () => jest.requireActual('redis-mock')) // Mock services jest.mock('@core/services/mail-client.class') - -// Mock models -const sequelizeMock = new SequelizeMock() -export const UserMock = sequelizeMock.define('users') - -jest.mock('@core/models/user/user', () => ({ - User: UserMock, -})) - -export const CampaignMock = sequelizeMock.define('campaigns') - -jest.mock('@core/models/campaign', () => ({ - Campaign: CampaignMock, -})) diff --git a/backend/tests/test-env.ts b/backend/tests/test-env.ts index 308c9911b..e11a57b94 100644 --- a/backend/tests/test-env.ts +++ b/backend/tests/test-env.ts @@ -3,3 +3,4 @@ process.env.REDIS_SESSION_URI = 'redis://localhost:6379/4' process.env.SENDGRID_PUBLIC_KEY = 'MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEKWFCI/58CSJe4uz9WX7VZZBIoeb3c1UEJ+pe3HL0ywyGA6c3Bq92+1YVKv0HHxf5mjm+t47P672gcaYarlp2LA==' process.env.SESSION_SECRET = 'SESSIONSECRET' +process.env.DB_TEST_URI = 'postgres://localhost:5432/postmangovsg_dev_test' From 1f808c34b31fbaa393826512b01cec7b1d8ce870 Mon Sep 17 00:00:00 2001 From: Azima M I Date: Tue, 16 Feb 2021 15:32:09 +0800 Subject: [PATCH 05/13] chore: start postgres instance in travis for backend tests --- .travis.yml | 3 +++ backend/tests/routes/sequelize-loader.ts | 2 +- backend/tests/test-env.ts | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3ee80ca32..d83dc58a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ node_js: services: - docker + - postgresql cache: npm @@ -16,6 +17,8 @@ jobs: - npx lockfile-lint --type npm --path package-lock.json -o "https:" -o "file:" --allowed-hosts npm install: - npm ci + before-script: + - psql -c 'create database postmangovsg_test;' -U postgres script: - npm run lint-no-fix - npm run test diff --git a/backend/tests/routes/sequelize-loader.ts b/backend/tests/routes/sequelize-loader.ts index 9c4545c8d..179cb2aab 100644 --- a/backend/tests/routes/sequelize-loader.ts +++ b/backend/tests/routes/sequelize-loader.ts @@ -33,7 +33,7 @@ import { import { DefaultCredentialName } from '@core/constants' import { formatDefaultCredentialName } from '@core/utils' -const DB_TEST_URI = config.get('database.databaseTestUri') +const DB_TEST_URI = config.get('database.databaseUri') const sequelizeLoader = async (): Promise => { const sequelize = new Sequelize(DB_TEST_URI, { diff --git a/backend/tests/test-env.ts b/backend/tests/test-env.ts index e11a57b94..7d33c5b18 100644 --- a/backend/tests/test-env.ts +++ b/backend/tests/test-env.ts @@ -3,4 +3,4 @@ process.env.REDIS_SESSION_URI = 'redis://localhost:6379/4' process.env.SENDGRID_PUBLIC_KEY = 'MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEKWFCI/58CSJe4uz9WX7VZZBIoeb3c1UEJ+pe3HL0ywyGA6c3Bq92+1YVKv0HHxf5mjm+t47P672gcaYarlp2LA==' process.env.SESSION_SECRET = 'SESSIONSECRET' -process.env.DB_TEST_URI = 'postgres://localhost:5432/postmangovsg_dev_test' +process.env.DB_URI = 'postgres://localhost:5432/postmangovsg_test' From d0cf2e917b285ce1b749fe794e0728ba07b79f04 Mon Sep 17 00:00:00 2001 From: Azima M I Date: Thu, 8 Apr 2021 03:13:44 +0800 Subject: [PATCH 06/13] chore: add more tests to campaign routes test --- .travis.yml | 3 +- backend/package.json | 2 +- backend/tests/routes/core/auth.routes.test.ts | 93 +++++++++++--- .../tests/routes/core/campaign.routes.test.ts | 117 ++++++++++++++++-- backend/tests/routes/sequelize-loader.ts | 99 --------------- backend/tests/routes/server.ts | 43 +++---- backend/tests/setup.ts | 4 +- backend/tsconfig.json | 2 +- 8 files changed, 205 insertions(+), 158 deletions(-) delete mode 100644 backend/tests/routes/sequelize-loader.ts diff --git a/.travis.yml b/.travis.yml index d83dc58a2..b455c245f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,8 @@ jobs: install: - npm ci before-script: - - psql -c 'create database postmangovsg_test;' -U postgres + - psql -c 'create database postmangovsg_test_1;' -U postgres + - psql -c 'create database postmangovsg_test_2;' -U postgres script: - npm run lint-no-fix - npm run test diff --git a/backend/package.json b/backend/package.json index 7e039bc1c..a00c45dc5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,7 +11,7 @@ "postbuild": "npm run copy-assets", "start": "node build/server", "copy-assets": "copyfiles -u 1 src/assets/* src/**/*.sql build", - "test": "jest", + "test": "jest --maxWorkers=2", "test:watch": "jest --watch", "precommit": "lint-staged" }, diff --git a/backend/tests/routes/core/auth.routes.test.ts b/backend/tests/routes/core/auth.routes.test.ts index 4b5a2f04d..75e251b3e 100644 --- a/backend/tests/routes/core/auth.routes.test.ts +++ b/backend/tests/routes/core/auth.routes.test.ts @@ -1,10 +1,27 @@ import request from 'supertest' -import app from '../server' -import sequelizeLoader from '../sequelize-loader' -import { AuthService } from '@core/services' +import { Sequelize } from 'sequelize-typescript' +import bcrypt from 'bcrypt' +import initialiseServer from '../server' +import sequelizeLoader from '../../sequelize-loader' +import { MailService, RedisService } from '@core/services' +import { User } from '@core/models' + +const app = initialiseServer() +const appWithUserSession = initialiseServer(true) +let sequelize: Sequelize beforeAll(async () => { - await sequelizeLoader() + sequelize = await sequelizeLoader(process.env.JEST_WORKER_ID || '1') +}) + +afterEach(async () => { + await User.destroy({ where: {} }) +}) + +afterAll(async () => { + await sequelize.close() + RedisService.otpClient.quit() + RedisService.sessionClient.quit() }) describe('POST /auth/otp', () => { @@ -23,33 +40,72 @@ describe('POST /auth/otp', () => { expect(res.status).toBe(401) expect(res.body).toEqual({ message: 'User is not authorized' }) }) + + test('OTP is generated and sent to user', async () => { + const res = await request(app) + .post('/auth/otp') + .send({ email: 'user@agency.gov.sg' }) + expect(res.status).toBe(200) + + expect(MailService.mailClient.sendMail).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.stringMatching(/Your OTP is [0-9]{6}<\/b>/), + }) + ) + }) }) describe('POST /auth/login', () => { - test('Invalid otp format', async () => { + test('Invalid otp format provided', async () => { const res = await request(app) .post('/auth/login') .send({ email: 'user@agency.gov.sg', otp: '123' }) expect(res.status).toBe(400) }) - test('Invalid otp', async () => { + test('Invalid otp provided', async () => { const res = await request(app) .post('/auth/login') .send({ email: 'user@agency.gov.sg', otp: '000000' }) expect(res.status).toBe(401) }) - test('Valid otp', async () => { + test('OTP is invalidted after retries are exceeded', async () => { const email = 'user@agency.gov.sg' - // Mock verification of otp - AuthService.verifyOtp = jest.fn(async () => true) + RedisService.otpClient.set( + email, + JSON.stringify({ + retries: 1, + hash: await bcrypt.hash('123456', 10), + createdAt: 123, + }) + ) + const res = await request(app) + .post('/auth/login') + .send({ email, otp: '000000' }) + expect(res.status).toBe(401) + + // OTP should be deleted after exceeding retries + RedisService.otpClient.get(email, (_err, value) => { + expect(value).toBe(null) + }) + }) + + test('Valid otp provided', async () => { + const email = 'user@agency.gov.sg' + RedisService.otpClient.set( + email, + JSON.stringify({ + retries: 1, + hash: await bcrypt.hash('123456', 10), + createdAt: 123, + }) + ) const res = await request(app) .post('/auth/login') .send({ email, otp: '123456' }) expect(res.status).toBe(200) - expect(res.body).toEqual({}) }) }) @@ -60,20 +116,17 @@ describe('GET /auth/userinfo', () => { expect(res.body).toEqual({}) }) - // test('Existing session found', async () => { - // // Mock verification of otp - // AuthService.verifyOtp = jest.fn(async () => true) - // const email = 'user@agency.gov.sg' - // await request(app).post('/auth/login').send({ email, otp: '123456' }) - // const res = await request(app).get('/auth/userinfo') - // expect(res.status).toBe(200) - // expect(res.body).toEqual({}) - // }) + test('Existing session found', async () => { + await User.create({ id: 1, email: 'user@agency.gov.sg' }) + const res = await request(appWithUserSession).get('/auth/userinfo') + expect(res.status).toBe(200) + expect(res.body).toEqual({ id: 1, email: 'user@agency.gov.sg' }) + }) }) describe('GET /auth/logout', () => { test('Successfully logged out', async () => { - const res = await request(app).get('/auth/logout') + const res = await request(appWithUserSession).get('/auth/logout') expect(res.status).toBe(200) }) }) diff --git a/backend/tests/routes/core/campaign.routes.test.ts b/backend/tests/routes/core/campaign.routes.test.ts index f79ec2ff3..fab2e6b73 100644 --- a/backend/tests/routes/core/campaign.routes.test.ts +++ b/backend/tests/routes/core/campaign.routes.test.ts @@ -1,12 +1,16 @@ import request from 'supertest' -import app from '../server' +import { Sequelize } from 'sequelize-typescript' +import initialiseServer from '../server' import { Campaign, User, UserDemo } from '@core/models' -import sequelizeLoader from '../sequelize-loader' +import sequelizeLoader from '../../sequelize-loader' +import { RedisService } from '@core/services' +import { ChannelType } from '@core/constants' + +const app = initialiseServer(true) +let sequelize: Sequelize beforeAll(async () => { - await sequelizeLoader() - await User.destroy({ where: {} }) - await UserDemo.destroy({ where: {} }) + sequelize = await sequelizeLoader(process.env.JEST_WORKER_ID || '1') await User.create({ id: 1, email: 'user@agency.gov.sg' }) }) @@ -14,6 +18,13 @@ afterEach(async () => { await Campaign.destroy({ where: {} }) }) +afterAll(async () => { + await User.destroy({ where: {} }) + await sequelize.close() + RedisService.otpClient.quit() + RedisService.sessionClient.quit() +}) + describe('GET /campaigns', () => { test('List campaigns with default limit and offset', async () => { await Campaign.create({ @@ -69,28 +80,70 @@ describe('POST /campaigns', () => { test('Successfully create SMS campaign', async () => { const res = await request(app).post('/campaigns').send({ name: 'test', - type: 'SMS', + type: ChannelType.SMS, }) expect(res.status).toBe(201) expect(res.body).toEqual( expect.objectContaining({ name: 'test', + type: ChannelType.SMS, protect: false, }) ) }) - test('Successfully create demo SMS campaign', async () => { + test('Successfully create Email campaign', async () => { const res = await request(app).post('/campaigns').send({ - name: 'demo', - type: 'SMS', - demo_message_limit: 10, + name: 'test', + type: ChannelType.Email, + }) + expect(res.status).toBe(201) + expect(res.body).toEqual( + expect.objectContaining({ + name: 'test', + type: ChannelType.Email, + protect: false, + }) + ) + }) + + test('Successfully create Protected Email campaign', async () => { + const campaign = { + name: 'test', + type: ChannelType.Email, + protect: true, + } + const res = await request(app).post('/campaigns').send(campaign) + expect(res.status).toBe(201) + expect(res.body).toEqual(expect.objectContaining(campaign)) + }) + + test('Successfully create Telegram campaign', async () => { + const res = await request(app).post('/campaigns').send({ + name: 'test', + type: ChannelType.Telegram, }) expect(res.status).toBe(201) expect(res.body).toEqual( expect.objectContaining({ - name: 'demo', + name: 'test', + type: ChannelType.Telegram, protect: false, + }) + ) + }) + + test('Successfully create demo SMS campaign', async () => { + const campaign = { + name: 'demo', + type: ChannelType.SMS, + demo_message_limit: 10, + } + const res = await request(app).post('/campaigns').send(campaign) + expect(res.status).toBe(201) + expect(res.body).toEqual( + expect.objectContaining({ + ...campaign, demo_message_limit: 10, }) ) @@ -99,7 +152,47 @@ describe('POST /campaigns', () => { expect(demo?.numDemosSms).toEqual(2) }) - test('Create protected campaign for unsupported channel', async () => { + test('Successfully create demo Telegram campaign', async () => { + const campaign = { + name: 'demo', + type: ChannelType.Telegram, + demo_message_limit: 10, + } + const res = await request(app).post('/campaigns').send(campaign) + expect(res.status).toBe(201) + expect(res.body).toEqual( + expect.objectContaining({ + ...campaign, + demo_message_limit: 10, + }) + ) + + const demo = await UserDemo.findOne({ where: { userId: 1 } }) + expect(demo?.numDemosTelegram).toEqual(2) + }) + + test('Unable to create demo Telegram campaign after user has no demos left', async () => { + const campaign = { + name: 'demo', + type: ChannelType.Telegram, + demo_message_limit: 10, + } + await UserDemo.update({ numDemosTelegram: 0 }, { where: { userId: 1 } }) + const res = await request(app).post('/campaigns').send(campaign) + expect(res.status).toBe(400) + }) + + test('Unable to create demo campaign for unsupported channel', async () => { + const campaign = { + name: 'demo', + type: ChannelType.Email, + demo_message_limit: 10, + } + const res = await request(app).post('/campaigns').send(campaign) + expect(res.status).toBe(400) + }) + + test('Unable to create protected campaign for unsupported channel', async () => { const res = await request(app).post('/campaigns').send({ name: 'test', type: 'SMS', diff --git a/backend/tests/routes/sequelize-loader.ts b/backend/tests/routes/sequelize-loader.ts deleted file mode 100644 index 179cb2aab..000000000 --- a/backend/tests/routes/sequelize-loader.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Sequelize, SequelizeOptions } from 'sequelize-typescript' - -import config from '@core/config' -import { - Credential, - JobQueue, - Campaign, - Worker, - User, - UserFeature, - UserCredential, - UserDemo, - Statistic, - ProtectedMessage, - Unsubscriber, -} from '@core/models' -import { - EmailMessage, - EmailTemplate, - EmailOp, - EmailBlacklist, - EmailFromAddress, -} from '@email/models' -import { SmsMessage, SmsTemplate, SmsOp } from '@sms/models' -import { - BotSubscriber, - TelegramMessage, - TelegramOp, - TelegramSubscriber, - TelegramTemplate, -} from '@telegram/models' - -import { DefaultCredentialName } from '@core/constants' -import { formatDefaultCredentialName } from '@core/utils' - -const DB_TEST_URI = config.get('database.databaseUri') - -const sequelizeLoader = async (): Promise => { - const sequelize = new Sequelize(DB_TEST_URI, { - dialect: 'postgres', - logging: false, - pool: config.get('database.poolOptions'), - } as SequelizeOptions) - - const coreModels = [ - Credential, - JobQueue, - Campaign, - Worker, - User, - UserFeature, - UserCredential, - UserDemo, - Statistic, - Unsubscriber, - ] - const emailModels = [ - EmailMessage, - EmailTemplate, - EmailOp, - EmailBlacklist, - ProtectedMessage, - EmailFromAddress, - ] - const smsModels = [SmsMessage, SmsTemplate, SmsOp] - const telegramModels = [ - BotSubscriber, - TelegramOp, - TelegramMessage, - TelegramTemplate, - TelegramSubscriber, - ] - sequelize.addModels([ - ...coreModels, - ...emailModels, - ...smsModels, - ...telegramModels, - ]) - - try { - await sequelize.sync() - console.log({ message: 'Test Database loaded.' }) - } catch (error) { - console.log(error.message) - console.error({ message: 'Unable to connect to test database', error }) - // process.exit(1) - } - // Create the default credential names in the credentials table - // Each name should be accompanied by an entry in Secrets Manager - await Promise.all( - [ - DefaultCredentialName.Email, - formatDefaultCredentialName(DefaultCredentialName.SMS), - formatDefaultCredentialName(DefaultCredentialName.Telegram), - ].map((name) => Credential.upsert({ name })) - ) -} - -export default sequelizeLoader diff --git a/backend/tests/routes/server.ts b/backend/tests/routes/server.ts index 9e27f0367..284651c11 100644 --- a/backend/tests/routes/server.ts +++ b/backend/tests/routes/server.ts @@ -4,27 +4,28 @@ import bodyParser from 'body-parser' import sessionLoader from '@core/loaders/session.loader' import routes from '@core/routes' -const unAuthenticatedRoutes = ['auth', 'stats', 'protect', 'unsubscribe'] +// const unAuthenticatedRoutes = ['auth', 'stats', 'protect', 'unsubscribe'] -const app: express.Application = express() -sessionLoader({ app }) -app.use(bodyParser.json()) -app.use(bodyParser.urlencoded({ extended: false })) -app.use((req: Request, _res: Response, next: NextFunction): void => { - const mainRoute = req.path.split('/')[1] - // Continue without mocking user session for unauthenticated routes - if (unAuthenticatedRoutes.indexOf(mainRoute) > -1) { - return next() - } - if (req.session) { - req.session.user = { - id: 1, - email: 'user@agency.gov.sg', +const initialiseServer = (session?: boolean): express.Application => { + const app: express.Application = express() + sessionLoader({ app }) + app.use(bodyParser.json()) + app.use(bodyParser.urlencoded({ extended: false })) + + app.use((req: Request, _res: Response, next: NextFunction): void => { + if (session && req.session) { + req.session.user = { + id: 1, + email: 'user@agency.gov.sg', + } } - } - next() -}) -app.use(routes) -app.use(celebrateErrorMiddleware()) + next() + }) + + app.use(routes) + app.use(celebrateErrorMiddleware()) + + return app +} -export default app +export default initialiseServer diff --git a/backend/tests/setup.ts b/backend/tests/setup.ts index 3923cd80d..1eb2f3e16 100644 --- a/backend/tests/setup.ts +++ b/backend/tests/setup.ts @@ -1,4 +1,4 @@ -/* eslint-disable no-console */ +// /* eslint-disable no-console */ // global.console = { // ...global.console, // log: jest.fn(), // console.log are ignored in tests @@ -8,7 +8,5 @@ // debug: console.debug, // } -jest.mock('redis', () => jest.requireActual('redis-mock')) - // Mock services jest.mock('@core/services/mail-client.class') diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 6d8f4d6d2..74328db60 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -44,7 +44,7 @@ "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ /* Additional Checks */ - "noUnusedLocals": true, /* Report errors on unused locals. */ + "noUnusedLocals": false, /* Report errors on unused locals. */ "noUnusedParameters": true, /* Report errors on unused parameters. */ "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ From cde3392cb111e142f0a688850a3e9ca6e2bfce98 Mon Sep 17 00:00:00 2001 From: Azima M I Date: Thu, 8 Apr 2021 13:17:47 +0800 Subject: [PATCH 07/13] chore: update package-lock.json --- backend/package-lock.json | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 7c593f2d9..97def5a48 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -3247,10 +3247,14 @@ "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", "dev": true }, - "dns-prefetch-control": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.2.0.tgz", - "integrity": "sha512-hvSnros73+qyZXhHFjx2CMLwoj3Fe7eR9EJsFsqmcI1bB2OBWL/+0YzaEaKssCHnj/6crawNnUyw74Gm2EKe+Q==" + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } }, "doctrine": { "version": "3.0.0", From 913af71da61f4f3ed2b2e95a9040bb884f8720a3 Mon Sep 17 00:00:00 2001 From: Azima M I Date: Thu, 8 Apr 2021 13:53:28 +0800 Subject: [PATCH 08/13] chore: add test setup files --- backend/tests/sequelize-loader.ts | 100 ++++++++++++++++++++++++++++++ backend/tests/setup.ts | 18 +++--- 2 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 backend/tests/sequelize-loader.ts diff --git a/backend/tests/sequelize-loader.ts b/backend/tests/sequelize-loader.ts new file mode 100644 index 000000000..0cdca8fec --- /dev/null +++ b/backend/tests/sequelize-loader.ts @@ -0,0 +1,100 @@ +import { Sequelize, SequelizeOptions } from 'sequelize-typescript' +import config from '@core/config' +import { + Credential, + JobQueue, + Campaign, + Worker, + User, + UserFeature, + UserCredential, + UserDemo, + Statistic, + ProtectedMessage, + Unsubscriber, +} from '@core/models' +import { + EmailMessage, + EmailTemplate, + EmailOp, + EmailBlacklist, + EmailFromAddress, +} from '@email/models' +import { SmsMessage, SmsTemplate, SmsOp } from '@sms/models' +import { + BotSubscriber, + TelegramMessage, + TelegramOp, + TelegramSubscriber, + TelegramTemplate, +} from '@telegram/models' + +import { DefaultCredentialName } from '@core/constants' +import { formatDefaultCredentialName } from '@core/utils' + +const DB_TEST_URI = config.get('database.databaseUri') + +const sequelizeLoader = async (dbName: string): Promise => { + const sequelize = new Sequelize(`${DB_TEST_URI}_${dbName}`, { + dialect: 'postgres', + logging: false, + pool: config.get('database.poolOptions'), + } as SequelizeOptions) + + const coreModels = [ + Credential, + JobQueue, + Campaign, + Worker, + User, + UserFeature, + UserCredential, + UserDemo, + Statistic, + Unsubscriber, + ] + const emailModels = [ + EmailMessage, + EmailTemplate, + EmailOp, + EmailBlacklist, + ProtectedMessage, + EmailFromAddress, + ] + const smsModels = [SmsMessage, SmsTemplate, SmsOp] + const telegramModels = [ + BotSubscriber, + TelegramOp, + TelegramMessage, + TelegramTemplate, + TelegramSubscriber, + ] + sequelize.addModels([ + ...coreModels, + ...emailModels, + ...smsModels, + ...telegramModels, + ]) + + try { + await sequelize.sync() + console.log({ message: 'Test Database loaded.' }) + } catch (error) { + console.log(error.message) + console.error({ message: 'Unable to connect to test database', error }) + process.exit(1) + } + // Create the default credential names in the credentials table + // Each name should be accompanied by an entry in Secrets Manager + await Promise.all( + [ + DefaultCredentialName.Email, + formatDefaultCredentialName(DefaultCredentialName.SMS), + formatDefaultCredentialName(DefaultCredentialName.Telegram), + ].map((name) => Credential.upsert({ name })) + ) + + return sequelize +} + +export default sequelizeLoader diff --git a/backend/tests/setup.ts b/backend/tests/setup.ts index 1eb2f3e16..c8ff81811 100644 --- a/backend/tests/setup.ts +++ b/backend/tests/setup.ts @@ -1,12 +1,12 @@ -// /* eslint-disable no-console */ -// global.console = { -// ...global.console, -// log: jest.fn(), // console.log are ignored in tests -// error: console.error, -// warn: console.warn, -// info: console.info, -// debug: console.debug, -// } +/* eslint-disable no-console */ +global.console = { + ...global.console, + log: jest.fn(), // console.log are ignored in tests + error: console.error, + warn: console.warn, + info: console.info, + debug: console.debug, +} // Mock services jest.mock('@core/services/mail-client.class') From b28cacff33ff163be19de81544b8e0d0bbb19e3a Mon Sep 17 00:00:00 2001 From: Azima M I Date: Thu, 8 Apr 2021 13:59:43 +0800 Subject: [PATCH 09/13] chore: remove redundant node modules --- backend/package-lock.json | 26 -------------------------- backend/package.json | 3 --- backend/tests/routes/server.ts | 2 -- backend/tsconfig.json | 2 +- 4 files changed, 1 insertion(+), 32 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 97def5a48..541dc5b44 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1616,15 +1616,6 @@ "@types/node": "*" } }, - "@types/redis-mock": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@types/redis-mock/-/redis-mock-0.17.0.tgz", - "integrity": "sha512-UDKHu9otOSE1fPjgn0H7UoggqVyuRYfo3WJpdXdVmzgGmr1XIM/dTk/gRYf/bLjIK5mxpV8inA5uNBS2sVOilA==", - "dev": true, - "requires": { - "@types/redis": "*" - } - }, "@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -15141,12 +15132,6 @@ "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" }, - "redis-mock": { - "version": "0.56.3", - "resolved": "https://registry.npmjs.org/redis-mock/-/redis-mock-0.56.3.tgz", - "integrity": "sha512-ynaJhqk0Qf3Qajnwvy4aOjS4Mdf9IBkELWtjd+NYhpiqu4QCNq6Vf3Q7c++XRPGiKiwRj9HWr0crcwy7EiPjYQ==", - "dev": true - }, "redis-parser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", @@ -15702,17 +15687,6 @@ } } }, - "sequelize-mock": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/sequelize-mock/-/sequelize-mock-0.10.2.tgz", - "integrity": "sha1-GdOXHM2utbhkFwwkznkqinHxRL0=", - "dev": true, - "requires": { - "bluebird": "^3.4.6", - "inflection": "^1.10.0", - "lodash": "^4.16.4" - } - }, "sequelize-pool": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-2.3.0.tgz", diff --git a/backend/package.json b/backend/package.json index d2321073e..a503aa043 100644 --- a/backend/package.json +++ b/backend/package.json @@ -82,7 +82,6 @@ "@types/papaparse": "^5.0.4", "@types/qs": "^6.9.4", "@types/redis": "^2.8.17", - "@types/redis-mock": "^0.17.0", "@types/supertest": "^2.0.10", "@types/swagger-jsdoc": "^3.0.2", "@types/swagger-ui-express": "^4.1.2", @@ -98,9 +97,7 @@ "jest": "^25.5.2", "lint-staged": "^10.2.6", "prettier": "^2.0.5", - "redis-mock": "^0.56.3", "rimraf": "^3.0.2", - "sequelize-mock": "^0.10.2", "supertest": "^6.0.1", "ts-jest": "^25.5.1", "tsc-watch": "^4.2.3", diff --git a/backend/tests/routes/server.ts b/backend/tests/routes/server.ts index 284651c11..d127e16a3 100644 --- a/backend/tests/routes/server.ts +++ b/backend/tests/routes/server.ts @@ -4,8 +4,6 @@ import bodyParser from 'body-parser' import sessionLoader from '@core/loaders/session.loader' import routes from '@core/routes' -// const unAuthenticatedRoutes = ['auth', 'stats', 'protect', 'unsubscribe'] - const initialiseServer = (session?: boolean): express.Application => { const app: express.Application = express() sessionLoader({ app }) diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 74328db60..6d8f4d6d2 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -44,7 +44,7 @@ "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ /* Additional Checks */ - "noUnusedLocals": false, /* Report errors on unused locals. */ + "noUnusedLocals": true, /* Report errors on unused locals. */ "noUnusedParameters": true, /* Report errors on unused parameters. */ "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ From cc2ba3d26de2f4790a01c4066fc596f9f455c759 Mon Sep 17 00:00:00 2001 From: Azima M I Date: Fri, 9 Apr 2021 15:49:46 +0800 Subject: [PATCH 10/13] refactor: change folder structure of tests --- .secrets.baseline | 5 ++--- backend/jest.config.js | 6 +++--- .../core => src/core/routes/tests}/auth.routes.test.ts | 4 ++-- .../core => src/core/routes/tests}/campaign.routes.test.ts | 4 ++-- .../core/services/tests}/parse-csv.service.test.ts | 0 .../core/services/tests}/phone-number.service.test.ts | 0 backend/{tests => src/test-utils}/sequelize-loader.ts | 0 backend/{tests/routes => src/test-utils}/server.ts | 0 backend/{tests => src/test-utils}/setup.ts | 0 backend/{tests => src/test-utils}/test-env.ts | 0 backend/tsconfig.json | 2 +- 11 files changed, 10 insertions(+), 11 deletions(-) rename backend/{tests/routes/core => src/core/routes/tests}/auth.routes.test.ts (97%) rename backend/{tests/routes/core => src/core/routes/tests}/campaign.routes.test.ts (98%) rename backend/{tests/services => src/core/services/tests}/parse-csv.service.test.ts (100%) rename backend/{tests/services => src/core/services/tests}/phone-number.service.test.ts (100%) rename backend/{tests => src/test-utils}/sequelize-loader.ts (100%) rename backend/{tests/routes => src/test-utils}/server.ts (100%) rename backend/{tests => src/test-utils}/setup.ts (100%) rename backend/{tests => src/test-utils}/test-env.ts (100%) diff --git a/.secrets.baseline b/.secrets.baseline index 75be9a701..9c8e8b88a 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -4,7 +4,7 @@ "files": "package-lock.json|^.secrets.baseline$", "lines": null }, - "generated_at": "2021-01-11T08:57:41Z", + "generated_at": "2021-04-09T07:48:36Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -55,7 +55,7 @@ } ], "results": { - "backend/tests/test-env.ts": [ + "backend/src/test-utils/test-env.ts": [ { "hashed_secret": "c237a19676ad55d8904be354990dd54f92f9572c", "is_verified": false, @@ -66,7 +66,6 @@ "frontend/.env-example": [ { "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", - "is_secret": false, "is_verified": false, "line_number": 5, "type": "Basic Auth Credentials" diff --git a/backend/jest.config.js b/backend/jest.config.js index e7391db94..f4da8e327 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -7,12 +7,12 @@ module.exports = { '@sms/(.*)': '/src/sms/$1', '@email/(.*)': '/src/email/$1', '@telegram/(.*)': '/src/telegram/$1', - '@tests/(.*)': '/tests/$1', + '@test-utils/(.*)': '/src/test-utils/$1', }, transform: { '^.+\\.(ts|tsx)$': 'ts-jest', }, testEnvironment: 'node', - setupFiles: ['/tests/test-env.ts'], - setupFilesAfterEnv: ['/tests/setup.ts'], + setupFiles: ['/src/test-utils/test-env.ts'], + setupFilesAfterEnv: ['/src/test-utils/setup.ts'], } diff --git a/backend/tests/routes/core/auth.routes.test.ts b/backend/src/core/routes/tests/auth.routes.test.ts similarity index 97% rename from backend/tests/routes/core/auth.routes.test.ts rename to backend/src/core/routes/tests/auth.routes.test.ts index 75e251b3e..edcc4708b 100644 --- a/backend/tests/routes/core/auth.routes.test.ts +++ b/backend/src/core/routes/tests/auth.routes.test.ts @@ -1,8 +1,8 @@ import request from 'supertest' import { Sequelize } from 'sequelize-typescript' import bcrypt from 'bcrypt' -import initialiseServer from '../server' -import sequelizeLoader from '../../sequelize-loader' +import initialiseServer from '@test-utils/server' +import sequelizeLoader from '@test-utils/sequelize-loader' import { MailService, RedisService } from '@core/services' import { User } from '@core/models' diff --git a/backend/tests/routes/core/campaign.routes.test.ts b/backend/src/core/routes/tests/campaign.routes.test.ts similarity index 98% rename from backend/tests/routes/core/campaign.routes.test.ts rename to backend/src/core/routes/tests/campaign.routes.test.ts index fab2e6b73..ba2329e49 100644 --- a/backend/tests/routes/core/campaign.routes.test.ts +++ b/backend/src/core/routes/tests/campaign.routes.test.ts @@ -1,8 +1,8 @@ import request from 'supertest' import { Sequelize } from 'sequelize-typescript' -import initialiseServer from '../server' +import initialiseServer from '@test-utils/server' import { Campaign, User, UserDemo } from '@core/models' -import sequelizeLoader from '../../sequelize-loader' +import sequelizeLoader from '@test-utils/sequelize-loader' import { RedisService } from '@core/services' import { ChannelType } from '@core/constants' diff --git a/backend/tests/services/parse-csv.service.test.ts b/backend/src/core/services/tests/parse-csv.service.test.ts similarity index 100% rename from backend/tests/services/parse-csv.service.test.ts rename to backend/src/core/services/tests/parse-csv.service.test.ts diff --git a/backend/tests/services/phone-number.service.test.ts b/backend/src/core/services/tests/phone-number.service.test.ts similarity index 100% rename from backend/tests/services/phone-number.service.test.ts rename to backend/src/core/services/tests/phone-number.service.test.ts diff --git a/backend/tests/sequelize-loader.ts b/backend/src/test-utils/sequelize-loader.ts similarity index 100% rename from backend/tests/sequelize-loader.ts rename to backend/src/test-utils/sequelize-loader.ts diff --git a/backend/tests/routes/server.ts b/backend/src/test-utils/server.ts similarity index 100% rename from backend/tests/routes/server.ts rename to backend/src/test-utils/server.ts diff --git a/backend/tests/setup.ts b/backend/src/test-utils/setup.ts similarity index 100% rename from backend/tests/setup.ts rename to backend/src/test-utils/setup.ts diff --git a/backend/tests/test-env.ts b/backend/src/test-utils/test-env.ts similarity index 100% rename from backend/tests/test-env.ts rename to backend/src/test-utils/test-env.ts diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 6d8f4d6d2..4803d7878 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -57,7 +57,7 @@ "@sms/*": ["src/sms/*"], "@email/*": ["src/email/*"], "@telegram/*": ["src/telegram/*"], - "@tests/*": ["tests/*"], + "@test-utils/*": ["src/test-utils/*"], }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ From 44f4f5a2680c8d0507a569a6ddc8a240fd79e581 Mon Sep 17 00:00:00 2001 From: Azima M I Date: Mon, 19 Apr 2021 08:45:13 +0800 Subject: [PATCH 11/13] chore: add redis service on travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 2948286b3..f4ef74df7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ node_js: services: - docker - postgresql + - redis-server cache: npm From 8e2aba9b3f3f359c47811f6a60b9338b93a1b8bb Mon Sep 17 00:00:00 2001 From: Azima M I Date: Wed, 28 Apr 2021 11:08:31 +0800 Subject: [PATCH 12/13] refactor: shift creation of dbs from travis.yml to globalSetup --- .travis.yml | 3 -- backend/jest.config.js | 2 ++ .../src/core/routes/tests/auth.routes.test.ts | 2 +- backend/src/test-utils/global-setup.ts | 29 +++++++++++++++++++ backend/src/test-utils/global-teardown.ts | 3 ++ 5 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 backend/src/test-utils/global-setup.ts create mode 100644 backend/src/test-utils/global-teardown.ts diff --git a/.travis.yml b/.travis.yml index d94f60063..3ae7b922d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,9 +18,6 @@ jobs: - npx lockfile-lint --type npm --path package-lock.json -o "https:" -o "file:" --allowed-hosts npm install: - npm ci - before-script: - - psql -c 'create database postmangovsg_test_1;' -U postgres - - psql -c 'create database postmangovsg_test_2;' -U postgres script: - npm run lint-no-fix - npm run test diff --git a/backend/jest.config.js b/backend/jest.config.js index f4da8e327..59a9518ef 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -13,6 +13,8 @@ module.exports = { '^.+\\.(ts|tsx)$': 'ts-jest', }, testEnvironment: 'node', + globalSetup: '/src/test-utils/global-setup.ts', + globalTeardown: '/src/test-utils/global-teardown.ts', setupFiles: ['/src/test-utils/test-env.ts'], setupFilesAfterEnv: ['/src/test-utils/setup.ts'], } diff --git a/backend/src/core/routes/tests/auth.routes.test.ts b/backend/src/core/routes/tests/auth.routes.test.ts index edcc4708b..5b83574a9 100644 --- a/backend/src/core/routes/tests/auth.routes.test.ts +++ b/backend/src/core/routes/tests/auth.routes.test.ts @@ -70,7 +70,7 @@ describe('POST /auth/login', () => { expect(res.status).toBe(401) }) - test('OTP is invalidted after retries are exceeded', async () => { + test('OTP is invalidated after retries are exceeded', async () => { const email = 'user@agency.gov.sg' RedisService.otpClient.set( email, diff --git a/backend/src/test-utils/global-setup.ts b/backend/src/test-utils/global-setup.ts new file mode 100644 index 000000000..e86359979 --- /dev/null +++ b/backend/src/test-utils/global-setup.ts @@ -0,0 +1,29 @@ +import { Sequelize, SequelizeOptions } from 'sequelize-typescript' +import config from '../core/config' + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace NodeJS { + interface Global { + sequelize: Sequelize + } + } +} +const DB_URI = 'postgres://localhost:5432/postgres' +const TEST_DB = 'postmangovsg_test' +// The number of workers should match the maxWorkers +// defined in npm test command +const JEST_WORKERS = 2 + +module.exports = async () => { + global.sequelize = new Sequelize(DB_URI, { + dialect: 'postgres', + logging: false, + pool: config.get('database.poolOptions'), + } as SequelizeOptions) + + for (let i = 0; i < JEST_WORKERS; i++) { + await global.sequelize.query(`DROP DATABASE IF EXISTS ${TEST_DB}_${i}`) + await global.sequelize.query(`CREATE DATABASE ${TEST_DB}_${i}`) + } +} diff --git a/backend/src/test-utils/global-teardown.ts b/backend/src/test-utils/global-teardown.ts new file mode 100644 index 000000000..cf5586169 --- /dev/null +++ b/backend/src/test-utils/global-teardown.ts @@ -0,0 +1,3 @@ +module.exports = async function () { + await global.sequelize.close() +} From f9db01be27bbb43f507a8aa89a6838e53ca7f25c Mon Sep 17 00:00:00 2001 From: Azima M I Date: Fri, 30 Apr 2021 10:40:08 +0800 Subject: [PATCH 13/13] fix: create dbs from index 1 --- backend/src/test-utils/global-setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/test-utils/global-setup.ts b/backend/src/test-utils/global-setup.ts index e86359979..f80e22037 100644 --- a/backend/src/test-utils/global-setup.ts +++ b/backend/src/test-utils/global-setup.ts @@ -22,7 +22,7 @@ module.exports = async () => { pool: config.get('database.poolOptions'), } as SequelizeOptions) - for (let i = 0; i < JEST_WORKERS; i++) { + for (let i = 1; i <= JEST_WORKERS; i++) { await global.sequelize.query(`DROP DATABASE IF EXISTS ${TEST_DB}_${i}`) await global.sequelize.query(`CREATE DATABASE ${TEST_DB}_${i}`) }