├── .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 |
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 | [](https://travis-ci.org/geshan/currency-api) [](https://codeclimate.com/github/geshan/currency-api/maintainability) [](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 | [](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 |
--------------------------------------------------------------------------------