├── .dockerignore ├── .prettierignore ├── .nowignore ├── .gitignore ├── public ├── index.js └── index.html ├── .prettierrc.yaml ├── app.arc ├── now.json ├── stryker.conf.js ├── docker-compose.yml ├── .qovery.yml ├── .snyk ├── Dockerfile ├── src ├── config.js └── exchangeRates.js ├── .travis.yml ├── cloudbuild.yaml ├── test ├── configTest.js ├── getMultipleTest.js ├── getByToCurrencyTest.js └── exchangeRatesTest.js ├── index.js ├── package.json └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.yaml 2 | *.yml 3 | -------------------------------------------------------------------------------- /.nowignore: -------------------------------------------------------------------------------- 1 | README.md 2 | node_modules 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | coverage 4 | -------------------------------------------------------------------------------- /public/index.js: -------------------------------------------------------------------------------- 1 | console.log('Hello world from client-side JS!') 2 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | trailingComma: 'es5' 2 | tabWidth: 2 3 | semi: true 4 | singleQuote: true 5 | printWidth: 120 6 | proseWrap: 'preserve' 7 | -------------------------------------------------------------------------------- /app.arc: -------------------------------------------------------------------------------- 1 | @app 2 | begin-app 3 | 4 | @static 5 | 6 | @http 7 | 8 | @tables 9 | data 10 | scopeID *String 11 | dataID **String 12 | ttl TTL 13 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "currency-api", 4 | "builds": [ 5 | { "src": "*.js", "use": "@now/node" } 6 | ], 7 | "routes": [ 8 | { "src": "/(.*)", "dest": "/index.js" } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /stryker.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | mutator: 'javascript', 4 | packageManager: 'npm', 5 | reporters: ['clear-text', 'progress', 'dashboard'], 6 | testRunner: 'mocha', 7 | transpilers: [], 8 | testFramework: 'mocha', 9 | coverageAnalysis: 'perTest', 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | web: 4 | build: 5 | context: ./ 6 | target: dev 7 | volumes: 8 | - .:/src 9 | command: npm start 10 | ports: 11 | - "8080:8080" 12 | environment: 13 | NODE_ENV: dev 14 | VIRTUAL_HOST: 'currency.test' 15 | VIRTUAL_PORT: 8080 16 | -------------------------------------------------------------------------------- /.qovery.yml: -------------------------------------------------------------------------------- 1 | application: 2 | name: currency-api 3 | project: currency-api 4 | cloud_region: aws/us-west-2 5 | publicly_accessible: true 6 | databases: 7 | - type: mysql 8 | version: "5.7" 9 | name: my-mysql-6132005 10 | routers: 11 | - name: main 12 | routes: 13 | - application_name: currency-api 14 | paths: 15 | - /* 16 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.14.1 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | SNYK-JS-LODASH-567746: 7 | - lodash: 8 | patched: '2020-05-03T03:37:20.294Z' 9 | - expressjs-utils > lodash: 10 | patched: '2020-05-03T03:37:20.294Z' 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine as base 2 | 3 | WORKDIR /src 4 | COPY package.json package-lock.json /src/ 5 | COPY . /src 6 | EXPOSE 8080 7 | 8 | FROM base as production 9 | 10 | ENV NODE_ENV=production 11 | RUN npm install --production 12 | 13 | CMD ["node", "index.js"] 14 | 15 | FROM base as dev 16 | 17 | ENV NODE_ENV=development 18 | RUN npm config set unsafe-perm true && npm install -g nodemon 19 | RUN npm install 20 | CMD ["npm", "start"] 21 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const env = process.env; 2 | const config = { 3 | db: { 4 | host: env.DB_HOST || 'remotemysql.com', 5 | user: env.DB_USER || '8VjostCYYk', 6 | password: env.DB_PASSWORD || 't87tn6wIMB', 7 | database: env.DB_NAME || `8VjostCYYk`, 8 | }, 9 | currencyConverterApi: { 10 | baseUrl: 'https://api.exchangeratesapi.io', 11 | }, 12 | itemsPerPage: env.ITEMS_PER_PAGE || 10, 13 | }; 14 | 15 | module.exports = config; 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | cache: 5 | directories: 6 | - node_modules 7 | before_script: 8 | - npm install 9 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 10 | - chmod +x ./cc-test-reporter 11 | - ./cc-test-reporter before-build --debug 12 | script: 13 | - ./node_modules/nyc/bin/nyc.js --reporter=lcov npm test 14 | after_script: 15 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 16 | 17 | notifications: 18 | email: false 19 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: gcr.io/cloud-builders/docker 3 | args: ['build', '-t', 'gcr.io/$PROJECT_ID/currency-api:${SHORT_SHA}', '--target', 'production', '.'] 4 | 5 | - name: 'gcr.io/cloud-builders/docker' 6 | args: ["push", "gcr.io/$PROJECT_ID/currency-api"] 7 | 8 | - name: 'gcr.io/cloud-builders/gcloud' 9 | args: ['beta', 'run', 'deploy', 'currency-api', '--image', 'gcr.io/$PROJECT_ID/currency-api:${SHORT_SHA}', '--region', 'us-central1', '--platform', 'managed', '--memory', '128Mi', '--max-instances', '3' , '--allow-unauthenticated'] 10 | -------------------------------------------------------------------------------- /test/configTest.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const config = require('../src/config'); 3 | 4 | describe('config', () => { 5 | describe('values', () => { 6 | it('should check that config db values are as expected', () => { 7 | assert.deepStrictEqual(config.db, { 8 | host: 'remotemysql.com', 9 | user: 'x0B9v9MaCy', 10 | password: 's7RjzSve5L', 11 | database: `x0B9v9MaCy`, 12 | }); 13 | }); 14 | 15 | it('should check that config api values are as expected', () => { 16 | assert.deepStrictEqual(config.currencyConverterApi, { 17 | baseUrl: 'https://api.exchangeratesapi.io', 18 | }); 19 | }); 20 | 21 | it('should check that config itemsPerPage values is as expected', () => { 22 | assert.equal(config.itemsPerPage, 10); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | require('express-async-errors'); 3 | const expressUtils = require('expressjs-utils'); 4 | const bodyParser = require('body-parser'); 5 | const helmet = require('helmet'); 6 | const exchangeRates = require('./src/exchangeRates'); 7 | 8 | const app = express(); 9 | app.use(helmet()); 10 | app.use(bodyParser.json()); 11 | 12 | app.get('/', (req, res) => { 13 | res.json({ 14 | message: `Please hit the api like: /api/convert/fromCurrency/toCurrency/onDate 15 | example: ${req.get('host')}/api/convert/USD/AUD/${new Date().toISOString().split('T')[0]}`, 16 | }); 17 | }); 18 | 19 | app.get('/api/convert/:fromCurrency/:toCurrency/:onDate?', async (req, res) => { 20 | console.log(`Api hit`, req.params); 21 | res.json(await exchangeRates.get(req.params)); 22 | }); 23 | 24 | app.get('/api/rates', async (req, res) => { 25 | res.json(await exchangeRates.getMultiple(req.query.page || 1)); 26 | }); 27 | 28 | app.get('/api/rates/:currency', async (req, res) => { 29 | res.json(await exchangeRates.getByToCurrency(req.query.page || 1, req.params.currency)); 30 | }); 31 | 32 | expressUtils.hc(app); 33 | expressUtils.static(app); 34 | expressUtils.errorHandler(app); 35 | expressUtils.start(app, 8080, 'dev'); 36 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hi from Begin! 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 |

15 | Howdy, Beginner! 16 |

17 |

18 | Get started by editing this file at: 19 |

20 | 21 | public/index.html 22 | 23 |
24 |
25 |

26 | View documentation at: 27 |

28 | https://docs.begin.com 29 |
30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "currency-api", 3 | "version": "1.0.0", 4 | "description": "A currency api with historical data", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "PORT=8080 nodemon -L -e js,yml,sql index.js", 8 | "test": "mocha -t 500 -b", 9 | "test-cov": "nyc mocha -t 500 -b", 10 | "mutation-cov": "stryker run", 11 | "snyk-protect": "snyk protect", 12 | "prepare": "npm run snyk-protect" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+ssh://git@github.com/geshan/currency-api.git" 17 | }, 18 | "keywords": [ 19 | "test" 20 | ], 21 | "author": "Geshan Manandhar", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/geshan/currency-api/issues" 25 | }, 26 | "homepage": "https://github.com/geshan/currency-api", 27 | "dependencies": { 28 | "axios": "^1.8.2", 29 | "body-parser": "^1.20.1", 30 | "express": "^4.17.3", 31 | "express-async-errors": "^3.1.1", 32 | "expressjs-utils": "^1.2.3", 33 | "helmet": "^4.6.0", 34 | "lodash": "^4.17.21", 35 | "namshi-node-mysql": "^2.1.0", 36 | "snyk": "^1.1064.0" 37 | }, 38 | "devDependencies": { 39 | "@architect/sandbox": "^5.9.2", 40 | "@stryker-mutator/core": "^6.3.0", 41 | "@stryker-mutator/javascript-mutator": "^4.0.0", 42 | "@stryker-mutator/mocha-framework": "^3.0.0", 43 | "@stryker-mutator/mocha-runner": "^6.3.0", 44 | "husky": "^3.0.2", 45 | "mocha": "^10.1.0", 46 | "nock": "^12.0.0", 47 | "nodemon": "^2.0.20", 48 | "nyc": "^15.0.0", 49 | "prettier": "2.0.0", 50 | "proxyquire": "^2.1.0" 51 | }, 52 | "husky": { 53 | "hooks": { 54 | "pre-commit": "pretty-quick --staged" 55 | } 56 | }, 57 | "snyk": true 58 | } 59 | -------------------------------------------------------------------------------- /test/getMultipleTest.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const nock = require('nock'); 3 | const proxyquire = require('proxyquire').noCallThru(); 4 | 5 | const mysqlStub = {}; 6 | 7 | const exchangeRates = proxyquire('./../src/exchangeRates', { 8 | 'namshi-node-mysql': _ => mysqlStub, 9 | }); 10 | const today = new Date().toISOString().split('T')[0]; 11 | 12 | describe('exchangeRates', () => { 13 | describe('get', () => { 14 | it('should return no rates from the db when no rates exists', async () => { 15 | try { 16 | mysqlStub.query = (query, params) => { 17 | if (query.startsWith(`SELECT from_currency, to_currency`)) { 18 | return []; 19 | } 20 | }; 21 | 22 | const result = await exchangeRates.getMultiple(0); 23 | assert.deepStrictEqual(result, []); 24 | } catch (err) { 25 | console.log(`err`, err); 26 | assert.strictEqual(err.message, 'should never reach here'); 27 | } 28 | }); 29 | it('should return exchange rates from the db when exchange rates exists', async () => { 30 | try { 31 | mysqlStub.query = (query, params) => { 32 | assert.equal(params.length, 2); 33 | assert.equal(params[0], 0); 34 | assert.equal(params[1], 10); 35 | if (query.startsWith(`SELECT from_currency, to_currency`)) { 36 | return [ 37 | { 38 | from_currency: 'USD', 39 | to_currency: 'AUD', 40 | rate: 1.437403, 41 | on_date: '2019-06-10T14:00:00.000Z', 42 | }, 43 | ]; 44 | } 45 | }; 46 | 47 | const result = await exchangeRates.getMultiple(1); 48 | assert.deepStrictEqual(result[0], { 49 | from_currency: 'USD', 50 | to_currency: 'AUD', 51 | rate: 1.437403, 52 | on_date: '2019-06-10T14:00:00.000Z', 53 | }); 54 | } catch (err) { 55 | console.log(`err`, err); 56 | assert.deepStrictEqual(err.message, 'should never reach here'); 57 | } 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/getByToCurrencyTest.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const proxyquire = require('proxyquire').noCallThru(); 4 | 5 | const mysqlStub = {}; 6 | 7 | const exchangeRates = proxyquire('./../src/exchangeRates', { 8 | 'namshi-node-mysql': _ => mysqlStub, 9 | }); 10 | 11 | describe('exchangeRates', () => { 12 | describe('getByToCurrency', () => { 13 | it('should return no rates for the given currency if none exist in the db', async () => { 14 | try { 15 | mysqlStub.query = (query, params) => { 16 | if ( 17 | query.startsWith( 18 | `SELECT from_currency, to_currency, rate, on_date FROM exchange_rates where to_currency = ?` 19 | ) 20 | ) { 21 | assert.deepStrictEqual(params, ['AUD', 0, 10]); 22 | return []; 23 | } 24 | }; 25 | 26 | const result = await exchangeRates.getByToCurrency(1, 'AUD'); 27 | assert.deepStrictEqual(result, []); 28 | } catch (err) { 29 | console.log(`err`, err); 30 | assert.strictEqual(err.message, 'should never reach here'); 31 | } 32 | }); 33 | 34 | it('should return rates for the given currency if they exist in the db with pagination', async () => { 35 | try { 36 | mysqlStub.query = (query, params) => { 37 | if ( 38 | query.startsWith( 39 | `SELECT from_currency, to_currency, rate, on_date FROM exchange_rates where to_currency = ?` 40 | ) 41 | ) { 42 | assert.deepStrictEqual(params, ['AUD', 10, 10]); 43 | return [ 44 | { 45 | from_currency: 'USD', 46 | to_currency: 'AUD', 47 | on_date: '2020-10-01', 48 | rate: 1.3886147039, 49 | }, 50 | { 51 | from_currency: 'USD', 52 | to_currency: 'AUD', 53 | on_date: '2019-10-01', 54 | rate: 1.4966590389, 55 | }, 56 | ]; 57 | } 58 | }; 59 | 60 | const result = await exchangeRates.getByToCurrency(2, 'AUD'); 61 | assert.deepStrictEqual(result, [ 62 | { 63 | from_currency: 'USD', 64 | to_currency: 'AUD', 65 | on_date: '2020-10-01', 66 | rate: 1.3886147039, 67 | }, 68 | { 69 | from_currency: 'USD', 70 | to_currency: 'AUD', 71 | on_date: '2019-10-01', 72 | rate: 1.4966590389, 73 | }, 74 | ]); 75 | } catch (err) { 76 | console.log(`err`, err); 77 | assert.strictEqual(err.message, 'should never reach here'); 78 | } 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/exchangeRates.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const config = require('./config'); 3 | const db = require('namshi-node-mysql')(config.db); 4 | const _ = require('lodash'); 5 | const { httpError } = require('expressjs-utils'); 6 | 7 | async function getExternal(fromCurrency, toCurrency, onDate) { 8 | let rate = 0; 9 | console.log(`Getting rate from the API not the db`); 10 | try { 11 | const response = await axios.get( 12 | `${config.currencyConverterApi.baseUrl}/${onDate}?base=${fromCurrency}&symbols=${toCurrency}` 13 | ); 14 | rate = _.get(response, `data.rates[${toCurrency}]`, 0); 15 | } catch (err) { 16 | let message = `Problem fetching error rate try a date range after 1999-01-04 and check currencies`; 17 | const remoteErrMessage = _.get(err, `response.data.error`); 18 | if (remoteErrMessage) { 19 | message = remoteErrMessage; 20 | } 21 | console.log(`Error calling currency converter API: `, err.message); 22 | throw new httpError(400, message); 23 | } 24 | if (rate === 0) { 25 | throw new httpError(400, `Error in fetching rate`); 26 | } 27 | 28 | db.query( 29 | `INSERT INTO exchange_rates (from_currency, to_currency, rate, on_date) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE rate = ?`, 30 | [fromCurrency, toCurrency, rate, onDate, rate] 31 | ) 32 | .then(result => { 33 | if (result.affectedRows === 0) { 34 | console.error(`Exchange rate of ${rate} for ${fromCurrency} to ${toCurrency} on ${onDate} could not be saved`); 35 | } 36 | }) 37 | .catch(err => { 38 | console.log(`Error while writing to db: `, err); 39 | }); //this is done async for the API to respond faster 40 | 41 | console.log(`Fetched exchange rate of ${rate} for ${fromCurrency} to ${toCurrency} of ${onDate} from the API`); 42 | return { fromCurrency, toCurrency, onDate, rate }; 43 | } 44 | 45 | async function get(params) { 46 | const today = new Date().toISOString().split('T')[0]; 47 | const { fromCurrency = 'AUD', toCurrency = 'USD', onDate = today } = params; 48 | let exchangeRates = await db.query( 49 | `SELECT rate, created_at FROM exchange_rates WHERE from_currency = ? AND to_currency = ? AND on_date = ?`, 50 | [fromCurrency, toCurrency, onDate] 51 | ); 52 | if (exchangeRates.length) { 53 | const rate = Number(exchangeRates[0].rate); 54 | console.log(`Found exchange rate of ${rate} for ${fromCurrency} to ${toCurrency} of ${onDate} in the db`); 55 | 56 | return { fromCurrency, toCurrency, onDate, rate }; 57 | } 58 | 59 | return getExternal(fromCurrency, toCurrency, onDate); 60 | } 61 | 62 | async function getMultiple(currentPage) { 63 | let offset = (currentPage - 1) * config.itemsPerPage; 64 | 65 | let allExchangeRates = await db.query( 66 | `SELECT from_currency, to_currency, rate, on_date FROM exchange_rates LIMIT ?,?`, 67 | [offset, config.itemsPerPage] 68 | ); 69 | 70 | if (allExchangeRates) { 71 | return allExchangeRates; 72 | } 73 | return []; 74 | } 75 | 76 | async function getByToCurrency(currentPage, currency) { 77 | const offset = (currentPage - 1) * config.itemsPerPage; 78 | 79 | let currencyExchangeRates = await db.query( 80 | `SELECT from_currency, to_currency, rate, on_date FROM exchange_rates where to_currency = ? LIMIT ?,?`, 81 | [currency, offset, config.itemsPerPage] 82 | ); 83 | 84 | if (currencyExchangeRates.length) { 85 | return currencyExchangeRates; 86 | } 87 | 88 | return []; 89 | } 90 | 91 | module.exports = { 92 | get, 93 | getMultiple, 94 | getByToCurrency, 95 | }; 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Currency API 2 | 3 | A simple project to show how to test a Node Express app using MNP - Mocha, Nock and Proxyquire. 4 | Code coverage is done with Istanbul (now called nyc). Rewire can be used in place of 5 | proxyquire to test private JS methods. This app is a very basic currency API. 6 | 7 | [![Build Status](https://travis-ci.org/geshan/currency-api.svg?branch=master)](https://travis-ci.org/geshan/currency-api) [![Maintainability](https://api.codeclimate.com/v1/badges/54eef9745fdb3b5c5476/maintainability)](https://codeclimate.com/github/geshan/currency-api/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/54eef9745fdb3b5c5476/test_coverage)](https://codeclimate.com/github/geshan/currency-api/test_coverage) 8 | 9 | ## Running app 10 | 11 | You can see this app running on [Zeit Now](https://currency-api.geshanm.now.sh/api/convert/USD/AUD/2019-08-05), each pull request will have it's own URL. 12 | 13 | ## Run on Google cloud run 14 | 15 | [![Run on Google Cloud](https://storage.googleapis.com/cloudrun/button.svg)](https://console.cloud.google.com/cloudshell/editor?shellonly=true&cloudshell_image=gcr.io/cloudrun/button&cloudshell_git_repo=https://github.com/geshan/currency-api.git) 16 | 17 | ## How it works 18 | 19 | The `GET` api works in the following way: 20 | 21 | 1. hit URL `/api/convert/AUD/USD/2018-07-22`. 22 | 1. Checks if the currency exchange rate is in the DB, if yes returns it. 23 | 1. If rate is not in the db it will query the `currencyconverterapi.com` free API to get the rate. 24 | 1. Returns the rate back and saves it in the DB too. 25 | 26 | ## DB script 27 | 28 | To create the db and the table, run the following sql script. 29 | 30 | ``` 31 | CREATE DATABASE currency CHARACTER SET utf8 COLLATE utf8_general_ci; 32 | CREATE TABLE IF NOT EXISTS `currency`.`exchange_rates` ( 33 | `id` INT NOT NULL AUTO_INCREMENT, 34 | `from_currency` CHAR(3) NOT NULL, 35 | `to_currency` CHAR(3) NOT NULL, 36 | `rate` DECIMAL(12,7) NOT NULL, 37 | `on_date` DATE NOT NULL, 38 | `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 39 | PRIMARY KEY (`id`), 40 | UNIQUE INDEX `rate_on_date_UNIQUE` (`from_currency` ASC, `to_currency` ASC, `on_date` ASC)) 41 | ENGINE = InnoDB; 42 | 43 | INSERT INTO `currency`.`exchange_rates` (`from_currency`, `to_currency`, `rate`, `on_date`) VALUES ('AUD', 'USD', '0.742719', '2018-07-22'); 44 | 45 | ``` 46 | 47 | ## Configs 48 | 49 | Configs for db like username, password etc are in the `/src/config.js` file. 50 | 51 | ## Run 52 | 53 | to run the app you can use `docker-compose up` the go to `http://localhost:8080/api/convert/AUD/USD/2018-07-23` on the browser. It is using the db on `remotemysql.com` so no need to setup the db locally. If you want to set it up locally change the config in `src/configs.js` or put in environment variables. 54 | 55 | ## Run tests 56 | 57 | To run the tests inside the container run `docker-compose run web npm t` 58 | 59 | To run tests just run `npm t` to watch test run `npm t -- -w`. 60 | 61 | To watch specific test(s) run `npm t -- -w -g "exchangeRates get` or even 62 | `npm t -- -w -g "should use default params if no params are provided and no results in db"` 63 | 64 | ### Code coverage 65 | 66 | To get the code coverage with Istanbul/nyc execute : `npm run test-cov`. You should see the code coverage on the cli. 67 | 68 | You can also check the code coverage on [code climate](https://codeclimate.com/github/geshan/currency-api/src/exchangeRates.js/source). 69 | 70 | ### Mutation testing 71 | 72 | Has some mutation testing done with [Stryker](https://stryker-mutator.io/stryker/quickstart). Current 73 | coverage is ~88% with mostly log lines failing. To run the mutation tests run the following after 74 | installing stryker. 75 | 76 | ```bash 77 | stryker run 78 | 79 | or 80 | 81 | npm run mutation-cov 82 | ``` 83 | -------------------------------------------------------------------------------- /test/exchangeRatesTest.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const nock = require('nock'); 3 | const proxyquire = require('proxyquire').noCallThru(); 4 | 5 | const mysqlStub = {}; 6 | 7 | const exchangeRates = proxyquire('./../src/exchangeRates', { 8 | 'namshi-node-mysql': _ => mysqlStub, 9 | }); 10 | const today = new Date().toISOString().split('T')[0]; 11 | const currencyConvertApiUrl = 'https://api.exchangeratesapi.io'; 12 | 13 | describe('exchangeRates', () => { 14 | describe('get', () => { 15 | it('should use given params and if no results are in db, inserts new rate to db while returning it', async () => { 16 | try { 17 | const onDate = '2018-12-03'; 18 | mysqlStub.query = (query, params) => { 19 | if (query.startsWith(`SELECT rate, created_at FROM exchange_rates`)) { 20 | return []; 21 | } 22 | 23 | if (query.startsWith(`INSERT INTO exchange_rates`)) { 24 | assert.ok(query.includes(`(from_currency, to_currency, rate, on_date) VALUES (?,?,?,?)`)); 25 | assert.ok(query.includes(`ON DUPLICATE KEY UPDATE rate = ?`)); 26 | assert.deepEqual(params, ['AUD', 'USD', 0.742725, onDate, 0.742725]); 27 | return Promise.resolve({ 28 | fieldCount: 0, 29 | affectedRows: 1, 30 | insertId: 5, 31 | info: '', 32 | serverStatus: 2, 33 | warningStatus: 0, 34 | }); 35 | } 36 | }; 37 | 38 | let apiResponse = { rates: {} }; 39 | apiResponse['rates']['USD'] = 0.742725; 40 | nock(currencyConvertApiUrl) 41 | .get(/2*/) 42 | .reply(200, apiResponse); 43 | 44 | const result = await exchangeRates.get({ 45 | fromCurrency: 'AUD', 46 | toCurrency: 'USD', 47 | onDate, 48 | }); 49 | assert.deepEqual(result, { 50 | fromCurrency: 'AUD', 51 | toCurrency: 'USD', 52 | onDate: '2018-12-03', 53 | rate: 0.742725, 54 | }); 55 | } catch (err) { 56 | console.log(`err`, err); 57 | assert.equal(err.message, 'should never reach here'); 58 | } 59 | }); 60 | 61 | it('should use default params if no params are provided, retuns rate from db if rate is in db', async () => { 62 | try { 63 | mysqlStub.query = (query, params) => { 64 | if (query.startsWith('SELECT rate, created_at FROM exchange_rates')) { 65 | assert.ok(query.includes(`WHERE from_currency = ? AND to_currency = ? AND on_date = ?`)); 66 | assert.deepEqual(params, ['AUD', 'USD', today], 'fromCurrency, then toCurrency then onDate respectively'); 67 | return [ 68 | { 69 | rate: '0.7427190', 70 | created_at: '2018-07-23T06:58:45.000Z', 71 | }, 72 | ]; 73 | } 74 | }; 75 | 76 | const result = await exchangeRates.get({}); 77 | assert.deepEqual(result, { 78 | fromCurrency: 'AUD', 79 | toCurrency: 'USD', 80 | onDate: today, 81 | rate: 0.742719, 82 | }); 83 | } catch (err) { 84 | console.log(`err`, err); 85 | assert.equal(err.message, 'should never reach here'); 86 | } 87 | }); 88 | 89 | it('should respond with proper error message if API sends a bad request', async () => { 90 | try { 91 | mysqlStub.query = (query, params) => { 92 | if (query.startsWith(`SELECT rate, created_at FROM exchange_rates`)) { 93 | return []; 94 | } 95 | }; 96 | let badResponse = { 97 | error: "Symbols 'NPR' are invalid for date 2018-10-11.", 98 | }; 99 | nock(currencyConvertApiUrl) 100 | .get(/2*/) 101 | .reply(400, badResponse); 102 | 103 | const result = await exchangeRates.get({ 104 | fromCurrency: 'NPR', 105 | toCurrency: 'AUD', 106 | onDate: '2018-10-05', 107 | }); 108 | assert.equal(err.message, 'should never reach here'); 109 | } catch (err) { 110 | assert.equal(err.statusCode, 400); 111 | assert.equal(err.message, "Symbols 'NPR' are invalid for date 2018-10-11."); 112 | } 113 | }); 114 | 115 | it('should use given params and no results in db returns error if response from API is wrong', async () => { 116 | try { 117 | mysqlStub.query = (query, params) => { 118 | if (query.startsWith(`SELECT rate, created_at FROM exchange_rates`)) { 119 | return []; 120 | } 121 | }; 122 | let wrongApiResponse = { rates: {} }; 123 | nock(currencyConvertApiUrl) 124 | .get(/2*/) 125 | .reply(200, wrongApiResponse); 126 | const result = await exchangeRates.get({ 127 | fromCurrency: 'USD', 128 | toCurrency: 'AUD', 129 | onDate: '2018-07-21', 130 | }); 131 | assert.equal(err.message, 'should never reach here'); 132 | } catch (err) { 133 | assert.equal(err.statusCode, 400); 134 | assert.equal(err.message, 'Error in fetching rate'); 135 | } 136 | }); 137 | it('should paginate the result with the limit of 10 rows per page', async () => { 138 | try { 139 | mysqlStub.query = (query, params) => { 140 | if (query.startsWith(`SELECT from_currency, to_currency`) && query.endsWith(`LIMIT 0 10`)) { 141 | return []; 142 | } 143 | }; 144 | const result = await exchangeRates.getMultiple({}); 145 | assert.deepStrictEqual(result, []); 146 | } catch (err) { 147 | console.log('err', err); 148 | assert.deepStrictEqual(err.message, 'Error in connecting to database'); 149 | } 150 | }); 151 | }); 152 | }); 153 | --------------------------------------------------------------------------------