This commit is contained in:
Martin Donnelly 2020-07-15 14:00:04 +01:00
commit fef8c825c7
8 changed files with 2683 additions and 0 deletions

55
.eslintrc.json Normal file
View File

@ -0,0 +1,55 @@
{
"parserOptions": {
"ecmaVersion": 2017,
"sourceType": "module",
"ecmaFeatures": {
"jsx": false
}
},
"env": {
"browser": false,
"node": true,
"es6": true
},
"rules": {
"arrow-spacing": "error",
"block-scoped-var": "error",
"block-spacing": "error",
"brace-style": ["error", "stroustrup", {}],
"camelcase": "error",
"comma-dangle": ["error", "never"],
"comma-spacing": ["error", { "before": false, "after": true }],
"comma-style": [1, "last"],
"consistent-this": [1, "_this"],
"curly": [1, "multi"],
"eol-last": 1,
"eqeqeq": 1,
"func-names": 1,
"indent": ["error", 2, { "SwitchCase": 1 }],
"lines-around-comment": ["error", { "beforeBlockComment": true, "allowArrayStart": true }],
"max-len": [1, 180, 2], // 2 spaces per tab, max 80 chars per line
"new-cap": 1,
"newline-before-return": "error",
"no-array-constructor": 1,
"no-inner-declarations": [1, "both"],
"no-mixed-spaces-and-tabs": 1,
"no-multi-spaces": 2,
"no-new-object": 1,
"no-shadow-restricted-names": 1,
"object-curly-spacing": ["error", "always"],
"padded-blocks": ["error", { "blocks": "never", "switches": "always" }],
"prefer-const": "error",
"prefer-template": "error",
"one-var": 0,
"quote-props": ["error", "always"],
"quotes": [1, "single"],
"radix": 1,
"semi": [1, "always"],
"space-before-blocks": [1, "always"],
"space-infix-ops": 1,
"vars-on-top": 1,
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 1 }],
"spaced-comment": ["error", "always", { "markers": ["/"] }]
}
}

162
.gitignore vendored Normal file
View File

@ -0,0 +1,162 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea/
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
artefacts/screenshots/*.png
artefacts/*.txt
artefacts/*.json
artefacts/*.html
artefacts/*
/tests/*.zip
/output/
/dist/
!/tests/data/
/tests/sink/
/debug/
/update.sh
/setup/web/
/backup/
/archive.tar.gz
/user/
/zip

121
README.md Normal file
View File

@ -0,0 +1,121 @@
# Request-multiple-urls
## Usage
```js
const requestMultipleUrls = require('request-multiple-urls');
( () => {
const urls = [
'https://ft-tech-test-example.s3-eu-west-1.amazonaws.com/ftse-fsi.json',
'https://ft-tech-test-example.s3-eu-west-1.amazonaws.com/gbp-hkd.json',
'https://ft-tech-test-example.s3-eu-west-1.amazonaws.com/gbp-usd.json'
];
requestMultipleUrls(urls).then(urlContent => {
urlContent.forEach((item) => {
if (item.hasOwnProperty('status'))
console.error(item);
else
console.log(item.data.items);
});
});
})();
```
## Test
Tests are implemented using Tape and requests are mocked using Nock. They can be run using the following from the command line:
```bash
# Run Tape test
npm run test
# Run Mocha test
npm run test:mocha
```
Currently all tests are passing:
```
> node tests/request-multiple-urls.tape.js
TAP version 13
# Test Request-multiple-urls
# Initial Rejects
# Rejects: Function called with no variables
ok 1 should reject
ok 2 should reject
# Rejects: Function called with null
ok 3 should reject
# Rejects: Function called with non array
ok 4 should reject
ok 5 should reject
ok 6 should reject
# Resolves: Function called with empty array
ok 7 should be deeply equivalent
# Resolves: Handles correct response
ok 8 should be deeply equivalent
# Resolves: Handles 404 response
ok 9 should be deeply equivalent
# Resolves: Mix 200 & 404 responses
ok 10 should be deeply equivalent
# Resolves: Real data
ok 11 should be deeply equivalent
1..11
# tests 11
# pass 11
# ok
> mocha
Test Request-multiple-urls
Initial Rejects
✓ Rejects: Function called with no parameters
✓ Rejects: Function called with null
Rejects: Function called with non array
✓ Rejects: Function called with a string
✓ Rejects: Function called with a number
✓ Rejects: Function called with an object
Resolve tests
✓ Resolves: Function called with empty array
✓ Resolves: Handles correct response
✓ Resolves: Handles 404 response
✓ Resolves: Mix 200 & 404 responses
✓ Resolves: Real data
10 passing (42ms)
```
## Dependencies
Axios was used as the sole dependency to avoid having to use either HTTP or HTTPS and the older event based handling of a request.
As Axios uses promises, a call can be made to Axios and return the promise object instead of using code similar to:
```js
https.get(newUrl, (res) => {
console.log('statusCode:', res.statusCode);
console.log('headers:', res.headers);
res.on('data', (d) => {
resolve(d)
});
}).on('error', (e) => {
reject(e);
});
```
## Thoughts
requestMultipleUrls could be modified to accept a single string, by pushing it into an array and expanding functionality slightly.
Better checking could be implemented to ensure that JSON is actually returned. This has just been written with the assumption that JSON will be returned from he remote server.

2007
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "fttest",
"version": "1.0.0",
"description": "Retrieve json from remote site",
"main": "request-multiple-urls.js",
"scripts": {
"test": "node tests/request-multiple-urls.tape.js",
"test:mocha": "mocha"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.19.2"
},
"devDependencies": {
"chai": "^4.2.0",
"eslint": "^7.4.0",
"mocha": "^8.0.1",
"nock": "^13.0.2",
"tape": "^5.0.1",
"tape-promise": "^4.0.0"
}
}

56
request-multiple-urls.js Normal file
View File

@ -0,0 +1,56 @@
const axios = require('axios');
/**
* Checks that the url is correct or not
* @param newUrl
* @returns {boolean}
*/
function checkUrl(newUrl) {
try {
new URL(newUrl);
}
catch (_) {
return false;
}
return true;
}
/**
* Makes a request to a specific url returning a promise
* @param newUrl
* @returns {*|AxiosPromise}
*/
function doRequestUrl(newUrl) {
if (!checkUrl(newUrl)) Promise.reject('ERROR:Invalid url');
return axios(newUrl);
}
/**
* Requests multiple urls from a list of urls
* @param listArray
* @returns {Promise<never>|Promise<number[]>|Promise<*[]>}
*/
function requestMultipleUrls(listArray) {
// ensure parameter is not missing
if (arguments.length === 0 || listArray === null ) return Promise.reject('ERROR:Missing listArray');
// ensure parameter is an array
if (!Array.isArray(listArray)) return Promise.reject('ERROR:listArray should be an array');
// quick exit if there is an array but it is empty
if (listArray.length === 0) return Promise.resolve([]);
const data = listArray.map((itemUrl) => doRequestUrl(itemUrl).then(res => {
// There is no major error so return the data object
return res.data;
}).catch(err => {
// reduce the error object to a smaller error message
return { 'url':err.config.url, 'status':err.response.status, 'statusText':err.response.statusText };
}));
return Promise.all(data);
}
module.exports = requestMultipleUrls;

View File

@ -0,0 +1,140 @@
const assert = require('chai').assert;
const nock = require('nock');
const requestMultipleUrls = require('../request-multiple-urls');
const ftseShort = { 'item':'ftse' };
const ftsefsi = {
'data': {
'items': [
{
'symbolInput': 'FTSE:FSI',
'basic': {
'symbol': 'FTSE:FSI',
'name': 'FTSE 100 Index',
'exchange': 'FTSE International',
'exhangeCode': 'FSI',
'bridgeExchangeCode': 'GBFT',
'currency': 'GBP'
},
'quote': {
'lastPrice': 7259.31,
'openPrice': 7292.76,
'high': 7335.55,
'low': 7258.83,
'previousClosePrice': 7292.76,
'change1Day': -33.44999999999982,
'change1DayPercent': -0.45867408224046613,
'change1Week': -100.06999999999971,
'change1WeekPercent': -1.359761284238614,
'timeStamp': '2019-11-15T10:53:16',
'volume': 165239344
}
}
]
},
'timeGenerated': '2019-11-15T11:08:17'
};
describe('Test Request-multiple-urls', () => {
describe('Initial Rejects', () => {
it('Rejects: Function called with no parameters', () => {
requestMultipleUrls().
catch(err => {
assert.equal(err, 'ERROR:Missing listArray');
});
});
it('Rejects: Function called with null', () => {
requestMultipleUrls(null).
catch(err => {
assert.equal(err, 'ERROR:Missing listArray');
});
});
describe('Rejects: Function called with non array', () => {
it('Rejects: Function called with a string', () => {
requestMultipleUrls('one').
catch(err => {
assert.equal(err, 'ERROR:listArray should be an array');
});
});
it('Rejects: Function called with a number', () => {
requestMultipleUrls(2).
catch(err => {
assert.equal(err, 'ERROR:listArray should be an array');
});
});
it('Rejects: Function called with an object', () => {
requestMultipleUrls({ 'three':'four' }).
catch(err => {
assert.equal(err, 'ERROR:listArray should be an array');
});
});
});
});
describe('Resolve tests', () => {
it('Resolves: Function called with empty array', () => {
const expected = [];
requestMultipleUrls([]).
then(v => {
assert.deepEqual(v, expected);
});
});
it('Resolves: Handles correct response', () => {
const expected = [ { 'item': 'ftse' } ];
const scope = nock('https://local.test')
.get('/ftse-fsi.json')
.reply(200, ftseShort);
return requestMultipleUrls(['https://local.test/ftse-fsi.json']).then(v => {
assert.deepEqual(v, expected);
});
});
it('Resolves: Handles 404 response', () => {
const expected = [ { 'url': 'https://local.test/should-fail.json', 'status': 404, 'statusText': null } ];
const scope = nock('https://local.test')
.get('/should-fail.json')
.reply(404);
return requestMultipleUrls(['https://local.test/should-fail.json']).then(v => {
assert.deepEqual(v, expected);
});
});
it('Resolves: Mix 200 & 404 responses', () => {
const expected = [ { 'item': 'ftse' }, { 'url': 'https://local.test/should-fail.json', 'status': 404, 'statusText': null } ];
const scope = nock('https://local.test')
.get('/ftse-fsi.json')
.reply(200, ftseShort)
.get('/should-fail.json')
.reply(404);
return requestMultipleUrls(['https://local.test/ftse-fsi.json', 'https://local.test/should-fail.json']).then(v => {
assert.deepEqual(v, expected);
});
});
it('Resolves: Real data', () => {
const expected = [ftsefsi];
const scope = nock('https://local.test')
.get('/ftse-fsi.json')
.reply(200, ftsefsi);
return requestMultipleUrls(['https://local.test/ftse-fsi.json']).then(v => {
assert.deepEqual(v, expected);
});
});
});
});

View File

@ -0,0 +1,119 @@
const tape = require('tape');
const _test = require('tape-promise').default;
const test = _test(tape);
const nock = require('nock');
const requestMultipleUrls = require('../request-multiple-urls');
const ftseShort = { 'item':'ftse' };
const ftsefsi = {
'data': {
'items': [
{
'symbolInput': 'FTSE:FSI',
'basic': {
'symbol': 'FTSE:FSI',
'name': 'FTSE 100 Index',
'exchange': 'FTSE International',
'exhangeCode': 'FSI',
'bridgeExchangeCode': 'GBFT',
'currency': 'GBP'
},
'quote': {
'lastPrice': 7259.31,
'openPrice': 7292.76,
'high': 7335.55,
'low': 7258.83,
'previousClosePrice': 7292.76,
'change1Day': -33.44999999999982,
'change1DayPercent': -0.45867408224046613,
'change1Week': -100.06999999999971,
'change1WeekPercent': -1.359761284238614,
'timeStamp': '2019-11-15T10:53:16',
'volume': 165239344
}
}
]
},
'timeGenerated': '2019-11-15T11:08:17'
};
test('Test Request-multiple-urls', async function(t) {
t.test('Initial Rejects', async function (t) {
t.test('Rejects: Function called with no parameters', async function (t) {
await t.rejects(requestMultipleUrls);
await t.rejects(requestMultipleUrls());
});
t.test('Rejects: Function called with null', async function (t) {
await t.rejects(requestMultipleUrls(null));
});
t.test('Rejects: Function called with non array', async function (t) {
await t.rejects(requestMultipleUrls('one'));
await t.rejects(requestMultipleUrls(2));
await t.rejects(requestMultipleUrls({ 'three':'four' }));
});
});
t.test('Resolves: Function called with empty array', async function (t) {
// await t.doesNotReject(requestMultipleUrls([]));
const expected = [];
return requestMultipleUrls([]).then(v => {
t.deepEqual(v, expected);
});
});
t.test('Resolves: Handles correct response', async function (t) {
const expected = [ { 'item': 'ftse' } ];
const scope = nock('https://local.test')
.get('/ftse-fsi.json')
.reply(200, ftseShort);
return requestMultipleUrls(['https://local.test/ftse-fsi.json']).then(v => {
t.deepEqual(v, expected);
});
});
t.test('Resolves: Handles 404 response', async function (t) {
const expected = [ { 'url': 'https://local.test/should-fail.json', 'status': 404, 'statusText': null } ];
const scope = nock('https://local.test')
.get('/should-fail.json')
.reply(404);
return requestMultipleUrls(['https://local.test/should-fail.json']).then(v => {
t.deepEqual(v, expected);
});
});
t.test('Resolves: Mix 200 & 404 responses', async function (t) {
const expected = [ { 'item': 'ftse' }, { 'url': 'https://local.test/should-fail.json', 'status': 404, 'statusText': null } ];
const scope = nock('https://local.test')
.get('/ftse-fsi.json')
.reply(200, ftseShort)
.get('/should-fail.json')
.reply(404);
return requestMultipleUrls(['https://local.test/ftse-fsi.json', 'https://local.test/should-fail.json']).then(v => {
t.deepEqual(v, expected);
});
});
t.test('Resolves: Real data', async function (t) {
const expected = [ftsefsi];
const scope = nock('https://local.test')
.get('/ftse-fsi.json')
.reply(200, ftsefsi);
return requestMultipleUrls(['https://local.test/ftse-fsi.json']).then(v => {
t.deepEqual(v, expected);
});
});
});