From 22bddd0df6adddda83f68183569f865bd5e57d31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2020 11:29:45 -0700 Subject: [PATCH 1/2] Bump elliptic from 6.5.2 to 6.5.3 (#124) Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3. - [Release notes](https://github.com/indutny/elliptic/releases) - [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 51c77cf..19cc943 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3284,9 +3284,9 @@ electron-to-chromium@^1.3.413: integrity sha512-vcTeLpPm4+ccoYFXnepvkFt0KujdyrBU19KNEO40Pnkhta6mUi2K0Dn7NmpRcNz7BvysnSqeuIYScP003HWuYg== elliptic@^6.0.0, elliptic@^6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762" - integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw== + version "6.5.3" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== dependencies: bn.js "^4.4.0" brorand "^1.0.1" From cdcc8e93f6087daac515f92ac4046f937b0f0386 Mon Sep 17 00:00:00 2001 From: willgraf <7930703+willgraf@users.noreply.github.com> Date: Wed, 16 Sep 2020 16:52:22 -0700 Subject: [PATCH 2/2] Add tests and coverage report for the server. (#125) * introduce jest for testing. * don't use hasOwnProperty unless its on Object.prototype * add JobType as a required /predict field. * fix logger.warn bug. * change getModels from a variable to a function so it can be mocked. * call getClient on each method rather than once globally for better testability. * Update travis.yml for coverage testing against `node` and `lts/*` versions. * remove required env vars and use defaults instead. * add coverage badge to README. * Add tests for the redis module. * Add react testing library and basic client-side tests. * Fix NotFound component Typography variants. * set ReactGA to testMode if NODE_ENV is 'test' --- .gitignore | 1 + .travis.yml | 8 +- README.md | 1 + __mocks__/fileMock.js | 1 + __mocks__/styleMock.js | 1 + jest.config.js | 17 + package.json | 12 +- server/config/config.js | 5 +- server/config/redis.js | 7 +- server/config/redis.test.js | 160 ++ server/config/winston.js | 4 +- server/controllers/misc.controller.test.js | 35 + server/controllers/model.controller.js | 12 +- server/controllers/model.controller.test.js | 133 + server/controllers/predict.controller.js | 7 +- server/controllers/predict.controller.test.js | 178 ++ server/controllers/upload.controller.test.js | 88 + src/App/App.js | 1 + src/App/App.test.js | 19 + src/Data/Data.test.js | 12 + src/Faq/Faq.test.js | 12 + src/FileUpload/FileUpload.test.js | 12 + src/Footer/Footer.test.js | 12 + src/Landing/Landing.test.js | 12 + src/NavBar/NavBar.test.js | 12 + src/NotFound/NotFound.js | 4 +- src/NotFound/NotFound.test.js | 12 + src/Predict/Predict.test.js | 12 + yarn.lock | 2388 ++++++++++++++++- 29 files changed, 3097 insertions(+), 81 deletions(-) create mode 100644 __mocks__/fileMock.js create mode 100644 __mocks__/styleMock.js create mode 100644 jest.config.js create mode 100644 server/config/redis.test.js create mode 100644 server/controllers/misc.controller.test.js create mode 100644 server/controllers/model.controller.test.js create mode 100644 server/controllers/predict.controller.test.js create mode 100644 server/controllers/upload.controller.test.js create mode 100644 src/App/App.test.js create mode 100644 src/Data/Data.test.js create mode 100644 src/Faq/Faq.test.js create mode 100644 src/FileUpload/FileUpload.test.js create mode 100644 src/Footer/Footer.test.js create mode 100644 src/Landing/Landing.test.js create mode 100644 src/NavBar/NavBar.test.js create mode 100644 src/NotFound/NotFound.test.js create mode 100644 src/Predict/Predict.test.js diff --git a/.gitignore b/.gitignore index 0ee6479..b9da71c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ node_modules dist/ *.log +coverage/ diff --git a/.travis.yml b/.travis.yml index f7e7dc3..642c4b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,7 @@ dist: trusty language: node_js node_js: - - 8 - - 10 + - lts/* - node cache: @@ -17,10 +16,13 @@ cache: before_install: - travis_retry curl -o- -L https://yarnpkg.com/install.sh | bash -s - export PATH="$HOME/.yarn/bin:$PATH" -install: true + +install: + - yarn script: - yarn test + - yarn coveralls jobs: include: diff --git a/README.md b/README.md index 695eee4..147edd1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # ![DeepCell Kiosk Banner](https://raw.githubusercontent.com/vanvalenlab/kiosk-console/master/docs/images/DeepCell_Kiosk_Banner.png) [![Build Status](https://travis-ci.com/vanvalenlab/kiosk-frontend.svg?branch=master)](https://travis-ci.com/vanvalenlab/kiosk-frontend) +[![Coverage Status](https://coveralls.io/repos/github/vanvalenlab/kiosk-frontend/badge.svg)](https://coveralls.io/github/vanvalenlab/kiosk-frontend) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](/LICENSE) The `kiosk-frontend` serves as the main interaction point for end users of the DeepCell Kiosk. The NodeJS backend API and the React frontend allows them to easily create jobs through their web browser. diff --git a/__mocks__/fileMock.js b/__mocks__/fileMock.js new file mode 100644 index 0000000..86059f3 --- /dev/null +++ b/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = 'test-file-stub'; diff --git a/__mocks__/styleMock.js b/__mocks__/styleMock.js new file mode 100644 index 0000000..f053ebf --- /dev/null +++ b/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..3d84d81 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,17 @@ +module.exports = { + moduleNameMapper: { + '\\.(css|less|sass|scss)$': '/__mocks__/styleMock.js', + '\\.(gif|ttf|eot|svg)$': '/__mocks__/fileMock.js' + }, + transform: { + '^.+\\.jsx?$': 'babel-jest' + }, + verbose: true, + collectCoverage: true, + collectCoverageFrom: [ + 'server/**/*.{js,jsx}', + 'src/**/*.{js,jsx}', + '!**/node_modules/**', + '!**/vendor/**' + ] +}; diff --git a/package.json b/package.json index 864c9fe..fdf2d39 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ }, "main": "index.js", "scripts": { - "test": "echo \"No tests specified\" && exit 0", + "test": "NODE_ENV=test jest --detectOpenHandles --no-cache", + "coveralls": "NODE_ENV=test jest --coverage && cat ./coverage/lcov.info | coveralls", "clean": "rm -rf ./dist", "build:client": "webpack --mode production --config webpack.prod.js", "build:server": "babel -d ./dist/server ./server -s", @@ -65,18 +66,27 @@ "@babel/polyfill": "^7.10.1", "@babel/preset-env": "^7.10.2", "@babel/preset-react": "^7.10.1", + "@testing-library/jest-dom": "^5.11.4", + "@testing-library/react": "^11.0.4", "axios": "^0.19.0", "babel-eslint": "^10.1.0", + "babel-jest": "^26.3.0", "babel-loader": "^8.1.0", "compression-webpack-plugin": "^4.0.0", "concurrently": "4.1.2", + "coveralls": "^3.1.0", "css-loader": "^3.6.0", "eslint": "7.2.0", "eslint-plugin-import": "2.21.2", "eslint-plugin-react": "7.20.0", "html-webpack-plugin": "4.3.0", + "ioredis-mock": "^4.21.3", + "jest": "^26.4.2", "nodemon": "^2.0.4", + "react-test-renderer": "^16.13.1", "style-loader": "1.2.1", + "supertest": "^4.0.2", + "tmp": "^0.2.1", "webpack": "4.43.0", "webpack-cli": "3.3.11", "webpack-dev-server": "3.11.0" diff --git a/server/config/config.js b/server/config/config.js index 120d3ea..764f0a6 100644 --- a/server/config/config.js +++ b/server/config/config.js @@ -14,8 +14,7 @@ const envVarsSchema = Joi.object({ CLOUD_PROVIDER: Joi.string() .description('The cloud platform to interact with.') .valid('gke', 'aws') - .default('aws') - .required(), + .default('aws'), MODEL_PREFIX: Joi.string() .description('S3 Folder in which models are saved') .default('models'), @@ -36,7 +35,7 @@ const envVarsSchema = Joi.object({ .default('deepcell-output'), HOSTNAME: Joi.string() .description('Kubernetes pod name'), - REDIS_HOST: Joi.string().required() + REDIS_HOST: Joi.string().default('localhost') .description('Redis DB host url'), REDIS_PORT: Joi.number() .default(6379), diff --git a/server/config/redis.js b/server/config/redis.js index b382621..4f1060b 100644 --- a/server/config/redis.js +++ b/server/config/redis.js @@ -29,13 +29,12 @@ function getClient() { return createBasicClient(); } -const client = getClient(); - function isArray(a) { return (!!a) && (a.constructor === Array); } async function hget(key, field) { + const client = getClient(); const hgetAsync = promisify(client.hget).bind(client); try { const value = await hgetAsync(key, field); @@ -48,6 +47,7 @@ async function hget(key, field) { } async function hmget(key, fields) { + const client = getClient(); const hmgetAsync = promisify(client.hmget).bind(client); try { const value = await hmgetAsync(key, ...fields); @@ -60,6 +60,7 @@ async function hmget(key, fields) { } async function expire(key, expireTime) { + const client = getClient(); const expireAsync = promisify(client.expire).bind(client); try { const value = await expireAsync(key, expireTime); @@ -72,6 +73,7 @@ async function expire(key, expireTime) { } async function lpush(queueName, redisKey) { + const client = getClient(); const lpushAsync = promisify(client.lpush).bind(client); try { let response; @@ -89,6 +91,7 @@ async function lpush(queueName, redisKey) { } async function hmset(redisHash, values) { + const client = getClient(); const hmsetAsync = promisify(client.hmset).bind(client); try { const response = await hmsetAsync([redisHash, ...values]); diff --git a/server/config/redis.test.js b/server/config/redis.test.js new file mode 100644 index 0000000..1f95b21 --- /dev/null +++ b/server/config/redis.test.js @@ -0,0 +1,160 @@ +// import MockRedis from 'ioredis-mock'; + +import redis from './redis'; +import config from './config'; + +const mocks = { redis: null }; + +jest.mock('ioredis', () => { + const Redis = require('ioredis-mock'); + if (typeof Redis === 'object') { + // the first mock is an ioredis shim because ioredis-mock depends on it + // https://github.com/stipsan/ioredis-mock/blob/master/src/index.js#L101-L111 + return { + Command: { _transformer: { argument: {}, reply: {} } } + }; + } + // second mock for our code + return function(...args) { + const dummyData = { + data: { + jobId: { + 'status': 'done', + 'otherKey': 'testValue' + } + } + }; + const instance = new Redis({...args, ...dummyData}); + mocks.redis = instance; + return instance; + } +}); + +jest.mock('../config/config', () => { + return { + redis: { + sentinelEnabled: false + } + }; +}); + +describe('Redis tests', () => { + + beforeEach(() => { + if (mocks.redis != null) { + mocks.redis.hmset('jobId', ['status', 'done', 'otherKey', 'testValue']); + } + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('Test HGET', () => { + + it('should get the correct value', async done => { + let value = await redis.hget('jobId', 'status'); + expect(value).toBe('done'); + + config.redis.sentinelEnabled = true; + value = await redis.hget('jobId', 'status'); + expect(value).toBe('done'); + + done(); + }); + + }); + + describe('Test HMGET', () => { + + it('should get the correct values', async done => { + let value = await redis.hmget('jobId', ['status', 'otherKey']); + expect(value).toMatchObject(['done', 'testValue']); + + config.redis.sentinelEnabled = true; + value = await redis.hmget('jobId', ['status', 'otherKey']); + expect(value).toMatchObject(['done', 'testValue']); + done(); + }); + + }); + + describe('Test EXPIRE', () => { + + it('should expire the key', async done => { + let value = await redis.expire('jobId', 1); + expect(value).toBe(1); + + config.redis.sentinelEnabled = true; + value = await redis.expire('jobId', 1); + expect(value).toBe(1); + done(); + }); + + it('should return 0 if no valid key', async done => { + let value = await redis.expire('otherKey', 1); + expect(value).toBe(0); + + config.redis.sentinelEnabled = true; + value = await redis.expire('anotherKey', 1); + expect(value).toBe(0); + done(); + }); + }); + + describe('Test LPUSH', () => { + + it('should push a single value to a queue', async done => { + let value = await redis.lpush('testQueue0', 'newKey'); + expect(value).toBe(1); + + let response = await mocks.redis.llen('testQueue0'); + expect(response).toBe(1); + + config.redis.sentinelEnabled = true; + value = await redis.lpush('testQueue1', 'newKey'); + expect(value).toBe(1); + + response = await mocks.redis.llen('testQueue1'); + expect(response).toBe(1); + done(); + }); + + it('should push an array of values to a queue', async done => { + let value = await redis.lpush('testQueue2', ['newKey', 'otherNewKey']); + expect(value).toBe(2); + + let response = await mocks.redis.llen('testQueue2'); + expect(response).toBe(2); + + config.redis.sentinelEnabled = true; + value = await redis.lpush('testQueue3', ['newKey', 'otherNewKey']); + expect(value).toBe(2); + + response = await mocks.redis.llen('testQueue3'); + expect(response).toBe(2); + done(); + }); + }); + + describe('Test HMSET', () => { + + it('should set multiple values', async done => { + const newStatus = 'success'; + let value = await redis.hmset('jobId', ['status', newStatus]); + expect(value).toBe('OK'); + + let response = await mocks.redis.hget('jobId', 'status'); + expect(response).toBe(newStatus); + + config.redis.sentinelEnabled = true; + value = await redis.hmset('jobId', ['status', newStatus]); + expect(value).toBe('OK'); + + response = await mocks.redis.hget('jobId', 'status'); + expect(response).toBe(newStatus); + done(); + }); + }); + +}); diff --git a/server/config/winston.js b/server/config/winston.js index 805d4d5..a0b56ce 100644 --- a/server/config/winston.js +++ b/server/config/winston.js @@ -34,8 +34,6 @@ const logger = winston.createLogger({ }); // eslint-disable-next-line no-unused-vars -logger.stream.write = (message, encoding) => { - logger.info(message); -}; +logger.stream.write = (message, encoding) => logger.info(message); export default logger; diff --git a/server/controllers/misc.controller.test.js b/server/controllers/misc.controller.test.js new file mode 100644 index 0000000..aa530f8 --- /dev/null +++ b/server/controllers/misc.controller.test.js @@ -0,0 +1,35 @@ +import supertest from 'supertest'; + +import app from '../index'; +import swaggerSpec from '../config/swagger'; + +describe('Miscellaneous Controller Tests', () => { + + describe('GET /api/swagger.json', () => { + + it('should return a JSON Swagger spec', async done => { + const request = supertest(app); + const response = await request.get('/api/swagger.json'); + + expect(response.status).toBe(200); + expect(response.body).toMatchObject(swaggerSpec); + done(); + }); + + }); + + describe('GET /api/health-check', () => { + + it('should return 200 OK', async done => { + const request = supertest(app); + const response = await request.get('/api/health-check'); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toBe('OK'); + done(); + }); + + }); + +}); diff --git a/server/controllers/model.controller.js b/server/controllers/model.controller.js index 94e95ea..a4da6c1 100644 --- a/server/controllers/model.controller.js +++ b/server/controllers/model.controller.js @@ -24,7 +24,7 @@ function getModelObject(allModels) { const cleanModels = {}; for (let i = 0; i < allModels.length; ++i) { let modelVersion = allModels[i].replace(modelPrefix, '').split('/', 2); - if (cleanModels.hasOwnProperty(modelVersion[0])) { + if (Object.prototype.hasOwnProperty.call(cleanModels, modelVersion[0])) { cleanModels[modelVersion[0]].push(modelVersion[1]); } else { cleanModels[modelVersion[0]] = [modelVersion[1]]; @@ -107,6 +107,14 @@ async function getGcpModels(req, res) { } } -const getModels = config.cloud == 'aws' ? getAwsModels : getGcpModels; +async function getModels(req, res) { + let response; + if (config.cloud === 'aws') { + response = await getAwsModels(req, res); + } else { + response = await getGcpModels(req, res); + } + return response; +} export default { getModels }; diff --git a/server/controllers/model.controller.test.js b/server/controllers/model.controller.test.js new file mode 100644 index 0000000..bf8cc0c --- /dev/null +++ b/server/controllers/model.controller.test.js @@ -0,0 +1,133 @@ +import supertest from 'supertest'; + +import app from '../index'; +import config from '../config/config'; + +const mockGcpResponse = [ + [], null, { prefixes: [`${config.model.prefix}/Model`] } +]; + +const mockGcpResponse2 = [ + [], null, { prefixes: [`${config.model.prefix}/Model/0`] } +]; + +const mockAwsModel = `${config.model.prefix}/Model`; +const mockAwsResponse = [{ Prefix: mockAwsModel }]; + +const mockAwsModel2 = `${config.model.prefix}/Model2`; +const mockAwsResponse2 = [{ Prefix: mockAwsModel2 }]; + +const mockModelPrefix = config.model.prefix; + +jest.mock('aws-sdk', () => { + return { + S3: jest.fn(() => ({ + listObjectsV2: jest.fn((params) => { + return { + promise() { + const isTruncated = !Object.prototype.hasOwnProperty.call( + params, 'ContinuationToken') && !params.Prefix.includes('Model'); + + let response; + if (isTruncated) { + response = mockAwsResponse; + } else if (params.Prefix.endsWith('Model')) { + response = [ + { Prefix: `${mockAwsModel}/0`}, + { Prefix: `${mockAwsModel}/1`} + ]; + } else if (params.Prefix.endsWith('Model2')) { + response = [ + { Prefix: `${mockAwsModel2}/0`}, + { Prefix: `${mockAwsModel2}/1`} + ]; + } else { + response = mockAwsResponse2; + } + + return Promise.resolve({ + NextContinuationToken: 'token', + IsTruncated: isTruncated, + CommonPrefixes: response + }); + } + }; + }) + })), + config: { + update: () => true + } + }; +}); + +jest.mock('@google-cloud/storage', () => ({ + Storage: jest.fn(() => { + return { + bucket: jest.fn(() => { + return { + getFiles: jest.fn((params) => { + if (params.prefix === 'models') { + return mockGcpResponse; + } + return mockGcpResponse2; + }) + }; + }) + }; + }) +})); + +jest.mock('../config/multer', () => ({ + single: jest.fn(() => { + return (req, res, next) => { + req.file = { + originalname: 'sample.name', + mimetype: 'sample.type', + path: 'sample.url' + }; + return next(); + }; + }) +})); + +jest.mock('../config/config', () => ({ + gcp: {}, + aws: {}, + uploadDirectory: '/test/', + model: { prefix: 'models' } +})); + +describe('Model Controller Tests', () => { + + describe('GET /api/models', () => { + + it('should get models from AWS bucket', async done => { + config.cloud = 'aws'; + const request = supertest(app); + const response = await request.get('/api/models'); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('models'); + expect(response.body.models).toHaveProperty('Model'); + expect(response.body.models.Model).toMatchObject(['0', '1']); + expect(response.body.models).toHaveProperty('Model2'); + expect(response.body.models.Model2).toMatchObject(['0', '1']); + done(); + }); + + it('should get models from GCP bucket', async done => { + config.cloud = 'gcp'; + + const request = supertest(app); + const response = await request.get('/api/models'); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('models'); + expect(response.body.models).toHaveProperty('Model'); + expect(response.body.models.Model).toMatchObject(['0']); + done(); + }); + + }); + +}); diff --git a/server/controllers/predict.controller.js b/server/controllers/predict.controller.js index 1d32ef0..a4f6027 100644 --- a/server/controllers/predict.controller.js +++ b/server/controllers/predict.controller.js @@ -13,10 +13,11 @@ function isValidPredictdata(data) { const requiredKeys = [ // 'modelName', // 'modelVersion', - 'imageName' + 'imageName', + 'jobType' ]; for (let key of requiredKeys) { - if (!data.hasOwnProperty(key)) { + if (!Object.prototype.hasOwnProperty.call(data, key)) { return false; } } @@ -64,7 +65,7 @@ async function expireHash(req, res) { try { const value = await redis.expire(redisHash, expireTime); if (parseInt(value) == 0) { - logger.warning(`Hash "${redisHash}" not found`); + logger.warn(`Hash "${redisHash}" not found`); return res.status(httpStatus.NOT_FOUND).send({ value }); } logger.debug(`Expiring hash ${redisHash} in ${expireTime} seconds: ${value}`); diff --git a/server/controllers/predict.controller.test.js b/server/controllers/predict.controller.test.js new file mode 100644 index 0000000..5928a15 --- /dev/null +++ b/server/controllers/predict.controller.test.js @@ -0,0 +1,178 @@ +import MockRedis from 'ioredis-mock'; +import supertest from 'supertest'; + +import app from '../index'; + +jest.mock('../config/redis', () => { + const mockRedis = new MockRedis({ + data: { + jobId: { + 'status': 'done', + 'otherKey': 'testValue' + } + } + }); + return mockRedis; +}); + +jest.mock('../config/config', () => { + return { + jobTypes: ['testJob1', 'testJob2'], + aws: {}, + gcp: {}, + uploadDirectory: 'uploads' + }; +}); + +describe('Predict Controller Tests', () => { + + afterAll(() => { + jest.resetAllMocks(); + }); + + describe('predictController.predict', () => { + it('should create a job in redis', async done => { + const request = supertest(app); + const response = await request.post('/api/predict') + .set('content-type', 'application/json') + .send({ jobType: 'testJob1', imageName: 'test.zip' }); + + expect(response.statusCode).toEqual(200); + expect(response.body).toHaveProperty('hash'); + // TODO: check Redis entries for valid data. + done(); + }); + + it('should return a 400 for bad request body', async done => { + const request = supertest(app); + const response = await request.post('/api/predict') + .set('content-type', 'application/json') + .send({ hash: 'jobId' }); + + expect(response.statusCode).toEqual(400); + done(); + }); + + it('should return a 400 for bad job type', async done => { + const request = supertest(app); + const response = await request.post('/api/predict') + .set('content-type', 'application/json') + .send({ jobType: 'invalidJobType', imageName: 'test.zip' }); + + expect(response.statusCode).toEqual(400); + done(); + }); + + // it('should return a 500 when an error is raised', async done => { + + // jest.doMock('../config/config', () => { + // return { + // hmset: () => { + // throw new Error('on purpose'); + // } + // }; + // }); + // const request = supertest(app); + // const response = await request.post('/api/predict') + // .set('content-type', 'application/json') + // .send({ jobType: 'testJob1', imageName: 'test.tif'}); + + // expect(response.statusCode).toEqual(500); + // done(); + // }); + }); + + describe('predictController.getJobStatus', () => { + + it('should get the correct job status', async done => { + const request = supertest(app); + const response = await request.post('/api/status') + .set('content-type', 'application/json') + .send({ hash: 'jobId' }); + + expect(response.statusCode).toEqual(200); + expect(response.body).toHaveProperty('status'); + expect(response.body.status).toEqual('done'); + done(); + }); + + it('should return null for invalid record', async done => { + const request = supertest(app); + const response = await request.post('/api/status') + .set('content-type', 'application/json') + .send({ hash: 'invalidJobId' }); + + expect(response.statusCode).toEqual(200); + expect(response.body).toHaveProperty('status'); + expect(response.body.status).toEqual(null); + done(); + }); + }); + + describe('predictController.getJobTypes', () => { + + it('should return the jobs supportin in config', async done => { + const request = supertest(app); + const response = await request.get('/api/jobtypes'); + + expect(response.statusCode).toEqual(200); + expect(response.body).toHaveProperty('jobTypes'); + expect(response.body.jobTypes).toEqual(['testJob1', 'testJob2']); + done(); + }); + }); + + describe('predictController.getKey', () => { + + it('should return the correct redis value', async done => { + const request = supertest(app); + const response = await request.post('/api/redis') + .set('content-type', 'application/json') + .send({ hash: 'jobId', key: 'otherKey' }); + + expect(response.statusCode).toEqual(200); + expect(response.body).toHaveProperty('value'); + expect(response.body.value).toEqual('testValue'); + done(); + }); + + it('should return the multiple redis values', async done => { + const request = supertest(app); + const response = await request.post('/api/redis') + .set('content-type', 'application/json') + .send({ hash: 'jobId', key: ['status', 'otherKey'] }); + + expect(response.statusCode).toEqual(200); + expect(response.body).toHaveProperty('value'); + expect(response.body.value).toEqual(['done', 'testValue']); + done(); + }); + }); + + describe('predictController.expireHash', () => { + + it('should expire the hash', async done => { + const request = supertest(app); + const response = await request.post('/api/redis/expire') + .set('content-type', 'application/json') + .send({ hash: 'jobId', expireIn: 1 }); + + expect(response.statusCode).toEqual(200); + expect(response.body).toHaveProperty('value'); + expect(response.body.value).toEqual(1); + done(); + }); + + it('should return 0 if there is no hash found', async done => { + const request = supertest(app); + const response = await request.post('/api/redis/expire') + .set('content-type', 'application/json') + .send({ hash: 'invalidJobId' }); + + expect(response.statusCode).toEqual(404); + expect(response.body).toHaveProperty('value'); + expect(response.body.value).toEqual(0); + done(); + }); + }); +}); diff --git a/server/controllers/upload.controller.test.js b/server/controllers/upload.controller.test.js new file mode 100644 index 0000000..e342c08 --- /dev/null +++ b/server/controllers/upload.controller.test.js @@ -0,0 +1,88 @@ +// Import the dependencies for testing +import supertest from 'supertest'; +import tmp from 'tmp'; +import { PassThrough } from 'stream'; + +import app from '../index'; +import config from '../config/config'; + +jest.mock('../config/config', () => ({ + gcp: {}, + aws: {}, + uploadDirectory: '/test/' +})); + +const mockStream = new PassThrough(); + +jest.mock('../config/multer', () => { + return { + single: jest.fn(() => { + return (req, res, next) => { + req.file = { + originalname: 'sample.name', + mimetype: 'sample.type', + path: 'sample.url', + }; + return next(); + }; + }) + }; +}); + +jest.mock('@google-cloud/storage', () => { + return { + Storage: jest.fn(() => { + return { + bucket: jest.fn(() => { + return { + file: jest.fn(() => { + return { + createWriteStream: jest.fn(() => mockStream), + makePublic: jest.fn(() => Promise.resolve(true)) + }; + }), + }; + }) + }; + }) + }; +}); + +describe('Upload Controller Tests', () => { + + describe('POST /api/upload', () => { + + it('should upload file using multer S3', async done => { + config.cloud = 'aws'; + const tmpobj = tmp.fileSync({postfix: '.png'}); + const request = supertest(app); + const response = await request.post('/api/upload') + .attach('file', tmpobj.name); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('imageURL'); + done(); + }); + + it('should upload file using multer', async done => { + config.cloud = 'gcp'; + + const tmpobj = tmp.fileSync({postfix: '.png'}); + const request = supertest(app); + + const response = request.post('/api/upload') + .attach('file', tmpobj.name); + + setTimeout(() => { + mockStream.end(); + }, 10); + + const resolved = await response; + expect(resolved.status).toBe(200); + expect(resolved.body).toHaveProperty('imageURL'); + done(); + }); + + }); + +}); diff --git a/src/App/App.js b/src/App/App.js index b5ac3fc..b121e7b 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -24,6 +24,7 @@ if (process.env.NODE_ENV !== 'production') { ReactGA.initialize(process.env.GA_TRACKING_ID || 'UA-000000000-0', { debug: process.env.NODE_ENV !== 'production', + testMode: process.env.NODE_ENV === 'test', }); const withTracker = (WrappedComponent, options = {}) => { diff --git a/src/App/App.test.js b/src/App/App.test.js new file mode 100644 index 0000000..65f7f54 --- /dev/null +++ b/src/App/App.test.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { MemoryRouter } from 'react-router-dom'; +import ReactGA from 'react-ga'; + +import App from './App'; + +describe(' component tests', () => { + beforeEach(() => { + ReactGA.testModeAPI.resetCalls(); + }); + + it('should render with a
tag', () => { + const { container } = render(, { wrapper: MemoryRouter }); + const element = container.querySelector('main'); + expect(element).toBeInTheDocument(); + }); +}); diff --git a/src/Data/Data.test.js b/src/Data/Data.test.js new file mode 100644 index 0000000..97f83fc --- /dev/null +++ b/src/Data/Data.test.js @@ -0,0 +1,12 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import Data from './Data'; + +describe(' component tests', () => { + it(' renders with header', () => { + const { getByText } = render(); + const element = getByText('Example Image Data'); + expect(element).toBeInTheDocument(); + }); +}); diff --git a/src/Faq/Faq.test.js b/src/Faq/Faq.test.js new file mode 100644 index 0000000..204e5d4 --- /dev/null +++ b/src/Faq/Faq.test.js @@ -0,0 +1,12 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import Faq from './Faq'; + +describe(' component tests', () => { + it(' renders with header', () => { + const { getByText } = render(); + const element = getByText('Frequently Asked Questions'); + expect(element).toBeInTheDocument(); + }); +}); diff --git a/src/FileUpload/FileUpload.test.js b/src/FileUpload/FileUpload.test.js new file mode 100644 index 0000000..e644925 --- /dev/null +++ b/src/FileUpload/FileUpload.test.js @@ -0,0 +1,12 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import FileUpload from './FileUpload'; + +describe(' component tests', () => { + it(' renders with Dropzone component', () => { + const { getByText } = render(); + const element = getByText(/Drag and Drop/i); + expect(element).toBeInTheDocument(); + }); +}); diff --git a/src/Footer/Footer.test.js b/src/Footer/Footer.test.js new file mode 100644 index 0000000..27649d0 --- /dev/null +++ b/src/Footer/Footer.test.js @@ -0,0 +1,12 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import Footer from './Footer'; + +describe('