├── test ├── green.png ├── fake-cors-server.js ├── head.test.js ├── download.test.js ├── fake-auth-server.js ├── fake-file-server.js ├── upload.test.js ├── post-stream-test.js ├── fake-server.js ├── delete.test.js ├── post.test.js ├── put.test.js ├── fake-redirection-server.js └── get.test.js ├── .gitignore ├── lib ├── get.js ├── head.js ├── delete.js ├── put.js ├── post.js ├── response-handler.js ├── status.js ├── download.js ├── option-handler.js ├── post-stream.js ├── roi.js ├── upload.js └── base.js ├── .travis.yml ├── .eslintrc.json ├── LICENSE ├── package.json ├── CONTRIBUTING.md └── README.md /test/green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bucharest-gold/roi/HEAD/test/green.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | *.log 4 | test/fixtures/db.json 5 | .vscode 6 | *.txt 7 | *.svg 8 | foo.html 9 | docs 10 | .nyc_output/ -------------------------------------------------------------------------------- /lib/get.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('./base'); 4 | 5 | class RoiGet extends Base { 6 | execute (options) { 7 | return this.action(options); 8 | } 9 | } 10 | 11 | module.exports = RoiGet; 12 | -------------------------------------------------------------------------------- /lib/head.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('./base'); 4 | 5 | class RoiHead extends Base { 6 | execute (options) { 7 | options.method = 'HEAD'; 8 | return this.action(options); 9 | } 10 | } 11 | 12 | module.exports = RoiHead; 13 | -------------------------------------------------------------------------------- /lib/delete.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('./base'); 4 | 5 | class RoiDelete extends Base { 6 | execute (options) { 7 | options.method = 'DELETE'; 8 | return this.action(options); 9 | } 10 | } 11 | 12 | module.exports = RoiDelete; 13 | -------------------------------------------------------------------------------- /lib/put.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('./base'); 4 | 5 | class RoiPut extends Base { 6 | execute (options, data) { 7 | options.method = 'PUT'; 8 | return this.actionData(options, data); 9 | } 10 | } 11 | 12 | module.exports = RoiPut; 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: required 3 | dist: trusty 4 | node_js: 5 | - "8" 6 | - "9" 7 | script: 8 | - npm run lint 9 | - npm test 10 | branches: 11 | only: 12 | - master 13 | notifications: 14 | irc: "irc.freenode.org#bucharest-gold" 15 | -------------------------------------------------------------------------------- /lib/post.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('./base'); 4 | 5 | class RoiPost extends Base { 6 | execute (options, data) { 7 | options.method = 'POST'; 8 | return this.actionData(options, data); 9 | } 10 | } 11 | 12 | module.exports = RoiPost; 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["semistandard","plugin:jest/recommended"], 3 | "rules": { 4 | "prefer-const": "error", 5 | "block-scoped-var": "error", 6 | "prefer-template": "warn", 7 | "no-use-before-define": [ 8 | "error", 9 | "nofunc" 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /lib/response-handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class ResponseHandler { 4 | 5 | static handle (body, response) { 6 | return { 7 | 'statusCode': response.statusCode, 8 | 'headers': response.headers, 9 | 'body': body 10 | }; 11 | } 12 | } 13 | 14 | module.exports = ResponseHandler; 15 | -------------------------------------------------------------------------------- /test/fake-cors-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | class FakeCorsServer { 6 | 7 | static create (port) { 8 | const root = path.join(__dirname, '.', 'root'); 9 | const server = require('http-server').createServer({ 10 | root: root, 11 | cors: true, 12 | corsHeaders: 'X-Foo' 13 | }); 14 | server.listen(port); 15 | return server; 16 | } 17 | } 18 | 19 | module.exports = FakeCorsServer; 20 | -------------------------------------------------------------------------------- /test/head.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const roi = require('../lib/roi'); 4 | const FakeServer = require('./fake-server'); 5 | 6 | test('HEAD - Checks if url exists.', () => { 7 | expect.assertions(1); 8 | const server = FakeServer.create(3051); 9 | return roi.head('http://localhost:3051/posts') 10 | .then(response => { 11 | expect(response.statusCode).toBe(200); 12 | server.close(); 13 | }).catch(e => { 14 | console.log(e); 15 | server.close(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/download.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const roi = require('../lib/roi'); 5 | const os = require('os'); 6 | const { join } = require('path'); 7 | 8 | test('DOWNLOAD.', () => { 9 | expect.assertions(2); 10 | const file = join(os.tmpdir(), 'README.md'); 11 | return roi.download('https://raw.githubusercontent.com/bucharest-gold/roi/master/README.md', file) 12 | .then(response => { 13 | expect(fs.existsSync(file)).toBe(true); 14 | expect(response.statusCode).toBe(200); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Red Hat, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. -------------------------------------------------------------------------------- /test/fake-auth-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'); 4 | const auth = require('http-auth'); 5 | 6 | class FakeAuthServer { 7 | static create () { 8 | const basic = auth.basic({ realm: 'Authenticated area.' }, 9 | (username, password, callback) => { 10 | callback(username === 'admin' && password === 'admin'); 11 | } 12 | ); 13 | const server = http.createServer(basic, (request, response) => { 14 | response.end(`${request.user} - logged.`); 15 | }); 16 | return server.listen(3005, 'localhost'); 17 | } 18 | } 19 | 20 | module.exports = FakeAuthServer; 21 | -------------------------------------------------------------------------------- /test/fake-file-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'); 4 | const path = require('path'); 5 | 6 | class FakeFileServer { 7 | static create () { 8 | const handler = (request, response) => { 9 | request.on('data', (d) => { 10 | console.log(`You posted: ${d}`); 11 | }); 12 | response.statusCode = 200; 13 | response.setHeader('Content-Type', 'application/octet-stream'); 14 | const buf = require('fs').readFileSync(path.join(__dirname, '/green.png')); 15 | response.end(buf); 16 | }; 17 | const s = http.createServer(handler); 18 | s.listen(3003); 19 | return s; 20 | } 21 | } 22 | 23 | module.exports = FakeFileServer; 24 | -------------------------------------------------------------------------------- /lib/status.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // see: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 4 | class Status { 5 | 6 | static notFound (code) { 7 | return code === 404; 8 | } 9 | 10 | static redirection (code) { 11 | return code >= 300 && code < 400; 12 | } 13 | 14 | static success (code) { 15 | return code >= 200 && code < 300; 16 | } 17 | 18 | static clientError (code) { 19 | return code >= 400 && code < 500; 20 | } 21 | 22 | static serverError (code) { 23 | return code >= 500; 24 | } 25 | 26 | static ok (code) { 27 | return (Status.success(code) && 28 | !Status.clientError(code) && 29 | !Status.serverError(code) && 30 | !Status.redirection(code)) || 31 | Status.notFound(code); 32 | } 33 | } 34 | 35 | module.exports = Status; 36 | -------------------------------------------------------------------------------- /test/upload.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const roi = require('../lib/roi'); 5 | const os = require('os'); 6 | const { join } = require('path'); 7 | 8 | test('UPLOAD.', () => { 9 | expect.assertions(1); 10 | const up = (request, response) => { 11 | request 12 | .pipe(fs.createWriteStream(join(os.tmpdir(), 'readme-uploaded.md'))) 13 | .on('finish', () => { 14 | response.end(request.headers.filename); 15 | }); 16 | }; 17 | 18 | const server = require('http').createServer(up); 19 | server.listen(3002, () => {}); 20 | 21 | const file = 'README.md'; 22 | return roi.upload('http://localhost:3002/', file) 23 | .then(response => { 24 | expect(fs.existsSync(join(os.tmpdir(), 'readme-uploaded.md'))).toBe(true); 25 | server.close(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/post-stream-test.js: -------------------------------------------------------------------------------- 1 | // 'use strict'; 2 | 3 | // const fs = require('fs'); 4 | // const test = require('tape'); 5 | // const roi = require('../lib/roi'); 6 | 7 | // const FakeFileServer = require('./fake-file-server'); 8 | 9 | // test('POST-STREAM - Post and handle binary.', t => { 10 | // const server = FakeFileServer.create(); 11 | // const options = { 12 | // endpoint: 'http://localhost:3003/', 13 | // headers: { 14 | // 'Accept': 'application/octet-stream', 15 | // 'Content-type': 'application/octet-stream' 16 | // } 17 | // }; 18 | // roi.postStream(options, {foo: 1}, './green.png') 19 | // .then(response => { 20 | // t.equal(fs.existsSync('/tmp/green.png'), true, 'The file exists.'); 21 | // server.close(); 22 | // t.end(); 23 | // }) 24 | // .catch(e => { 25 | // console.error(e); 26 | // t.fail(); 27 | // }); 28 | // }); 29 | -------------------------------------------------------------------------------- /test/fake-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jsonServer = require('json-server'); 4 | 5 | class FakeServer { 6 | 7 | static createDb () { 8 | const db = { 9 | 'posts': [ 10 | { 11 | 'title': 'foo-json2', 12 | 'author': 'bgold', 13 | 'id': 1 14 | }, 15 | { 16 | 'title': 'foo-json', 17 | 'author': 'bgold', 18 | 'id': 2 19 | }, 20 | { 21 | 'title': 'foo-json', 22 | 'author': 'bgold', 23 | 'id': 3 24 | } 25 | ], 26 | 'comments': [], 27 | 'profile': { 28 | 'name': 'bgold' 29 | } 30 | }; 31 | return db; 32 | } 33 | 34 | static create (port) { 35 | const server = jsonServer.create(); 36 | const router = jsonServer.router(this.createDb()); 37 | server.use(jsonServer.defaults()); 38 | server.use(router); 39 | const s = server.listen(port); 40 | return s; 41 | } 42 | } 43 | 44 | module.exports = FakeServer; 45 | -------------------------------------------------------------------------------- /test/delete.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const roi = require('../lib/roi'); 4 | 5 | const FakeServer = require('./fake-server'); 6 | const FakeRedirectionServer = require('./fake-redirection-server'); 7 | 8 | test('DELETE - Succeed to 404.', () => { 9 | expect.assertions(1); 10 | const server = FakeServer.create(6000); 11 | return roi.del('http://localhost:6000/foo.html') 12 | .then(response => { 13 | expect(response.statusCode).toBe(404); 14 | server.close(); 15 | }) 16 | .catch(e => { 17 | console.error(e); 18 | server.close(); 19 | }); 20 | }); 21 | 22 | test('DELETE - Redirect and delete.', () => { 23 | expect.assertions(1); 24 | const redirectServer = FakeRedirectionServer.create(); 25 | const server = FakeServer.create(3000); 26 | return roi.del('http://localhost:3001/01.html') 27 | .then(response => { 28 | expect(response.statusCode).toBe(200); 29 | redirectServer.close(); 30 | server.close(); 31 | }).catch(e => { 32 | console.error(e); 33 | server.close(); 34 | redirectServer.close(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /lib/download.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const Base = require('./base'); 5 | const OptionHandler = require('./option-handler'); 6 | const Status = require('./status'); 7 | 8 | class RoiDownload extends Base { 9 | execute (options, file) { 10 | options = OptionHandler.setup(options); 11 | options = OptionHandler.addHeaders(options); 12 | return new Promise((resolve, reject) => { 13 | const stream = fs.createWriteStream(file); 14 | const req = options.proto.request(options, (response) => { 15 | const statusCode = response.statusCode; 16 | if (Status.ok(statusCode)) { 17 | response.pipe(stream); 18 | stream.on('finish', () => { 19 | resolve(response); 20 | stream.end(); 21 | }); 22 | } else { 23 | options.endpoint = response.headers.location; 24 | this.redirects++; 25 | if (this.redirects >= this.maxRedirects) { 26 | const errorMessage = `Maximum redirects reached. (Amount of redirects allowed: ${this.redirects})`; 27 | this.redirects = 0; 28 | return reject(new Error(errorMessage)); 29 | } 30 | resolve(this.execute(options, file)); 31 | } 32 | }); 33 | req.on('error', (e) => reject(e)); 34 | req.end(); 35 | }); 36 | } 37 | 38 | } 39 | 40 | module.exports = RoiDownload; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roi", 3 | "version": "0.18.0", 4 | "description": "A dependency-free http module.", 5 | "main": "lib/roi.js", 6 | "scripts": { 7 | "test": "cross-env NODE_PENDING_DEPRECATION=1 jest --coverage", 8 | "jest-w": "jest --watch", 9 | "lint": "eslint test/*.js lib/*.js", 10 | "coverage": "nyc report --reporter=text-lcov > ./coverage/lcov.info" 11 | }, 12 | "files": [ 13 | "package.json", 14 | "README.md", 15 | "LICENSE", 16 | "lib" 17 | ], 18 | "keywords": [ 19 | "http", 20 | "promises", 21 | "node", 22 | "requests" 23 | ], 24 | "author": "Helio Frota", 25 | "license": "Apache-2.0", 26 | "devDependencies": { 27 | "corser": "^2.0.1", 28 | "cross-env": "^5.2.0", 29 | "eslint": "^3.8.1", 30 | "eslint-config-semistandard": "^7.0.0", 31 | "eslint-config-standard": "^6.2.0", 32 | "eslint-plugin-jest": "^21.26.2", 33 | "eslint-plugin-promise": "^3.8.0", 34 | "eslint-plugin-react": "^6.4.1", 35 | "eslint-plugin-standard": "^2.0.1", 36 | "http-auth": "^3.1.1", 37 | "http-server": "^0.10.0", 38 | "jest": "^22.4.4", 39 | "json-server": "^0.14.0", 40 | "lodash": "^4.17.11" 41 | }, 42 | "homepage": "https://github.com/bucharest-gold/roi#readme", 43 | "repository": { 44 | "type": "git", 45 | "url": "bucharest-gold/roi" 46 | }, 47 | "bugs": "https://github.com/bucharest-gold/roi/issues" 48 | } 49 | -------------------------------------------------------------------------------- /test/post.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const roi = require('../lib/roi'); 4 | 5 | const FakeServer = require('./fake-server'); 6 | const FakeRedirectionServer = require('./fake-redirection-server'); 7 | 8 | test('POST.', () => { 9 | expect.assertions(1); 10 | const server = FakeServer.create(3058); 11 | const foo = {}; 12 | return roi.post('http://localhost:3058/posts', foo) 13 | .then(response => { 14 | expect(response.statusCode).toBe(201); 15 | server.close(); 16 | }).catch(e => { 17 | console.error(e); 18 | server.close(); 19 | }); 20 | }); 21 | 22 | test('POST - Redirect.', () => { 23 | expect.assertions(1); 24 | const redirectServer = FakeRedirectionServer.create(); 25 | const server = FakeServer.create(3000); 26 | 27 | const foo = { 28 | title: 'foo-json', 29 | author: 'bgold' 30 | }; 31 | 32 | return roi.post('http://localhost:3001/01.html', foo) 33 | .then(response => { 34 | expect(response.statusCode).toBe(201); 35 | redirectServer.close(); 36 | server.close(); 37 | }).catch(e => { 38 | console.error(e); 39 | server.close(); 40 | }); 41 | }); 42 | 43 | test('POST - Succeed to 404.', () => { 44 | expect.assertions(1); 45 | const server = FakeServer.create(3059); 46 | return roi.post('http://localhost:3059/foo.html') 47 | .then(response => { 48 | expect(response.statusCode).toBe(404); 49 | server.close(); 50 | }) 51 | .catch(e => { 52 | console.error(e); 53 | server.close(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /lib/option-handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const url = require('url'); 4 | const http = require('http'); 5 | const https = require('https'); 6 | 7 | class OptionHandler { 8 | 9 | static setup (options) { 10 | if (!options || !options.endpoint) { 11 | throw new Error('Endpoint url not specified. Use: const options = { endpoint: \'http://endpoint_here\' }'); 12 | } 13 | url.parse(options.endpoint).protocol === 'http:' ? http : https; 14 | const parsed = url.parse(options.endpoint); 15 | options.hostname = parsed.hostname; 16 | options.port = parsed.port; 17 | options.path = parsed.path; 18 | options.proto = parsed.protocol === 'http:' ? http : https; 19 | return options; 20 | } 21 | 22 | static auth (options) { 23 | if (options.username) { 24 | return `Basic ${Buffer.from(`${options.username}:${options.password}`).toString('base64')}`; 25 | } 26 | return ''; 27 | } 28 | 29 | static addHeaders (options) { 30 | if (options.noAuth) { 31 | options.headers = { 32 | 'Accept': 'application/json,text/plain', 33 | 'Content-type': 'application/json' 34 | }; 35 | return options; 36 | } 37 | if (options.headers) { 38 | options.headers['Authorization'] = options.headers['Authorization'] || this.auth(options); 39 | } else { 40 | options.headers = { 41 | 'Accept': 'application/json,text/plain', 42 | 'Content-type': 'application/json', 43 | 'Authorization': this.auth(options) 44 | }; 45 | } 46 | return options; 47 | } 48 | 49 | } 50 | 51 | module.exports = OptionHandler; 52 | -------------------------------------------------------------------------------- /lib/post-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const Base = require('./base'); 5 | const OptionHandler = require('./option-handler'); 6 | const Status = require('./status'); 7 | 8 | class RoiPostStream extends Base { 9 | execute (options, data, file) { 10 | data = data || {}; 11 | options = OptionHandler.setup(options); 12 | const jsonData = JSON.stringify(data); 13 | options = OptionHandler.addHeaders(options); 14 | options.headers['Content-Length'] = jsonData.length; 15 | options.method = 'POST'; 16 | return new Promise((resolve, reject) => { 17 | const stream = fs.createWriteStream(file); 18 | const req = options.proto.request(options, (response) => { 19 | const statusCode = response.statusCode; 20 | if (Status.ok(statusCode)) { 21 | response.pipe(stream); 22 | stream.on('finish', () => { 23 | resolve(response); 24 | stream.close(); 25 | }); 26 | } else if (Status.redirection(statusCode)) { 27 | options.endpoint = response.headers.location; 28 | this.redirects++; 29 | if (this.redirects >= this.maxRedirects) { 30 | const errorMessage = `Maximum redirects reached. (Amount of redirects allowed: ${this.redirects})`; 31 | this.redirects = 0; 32 | return reject(new Error(errorMessage)); 33 | } 34 | resolve(this.execute(options, data, file)); 35 | } 36 | }); 37 | req.on('error', (e) => reject(e)); 38 | req.end(); 39 | }); 40 | } 41 | 42 | } 43 | 44 | module.exports = RoiPostStream; 45 | -------------------------------------------------------------------------------- /test/put.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const roi = require('../lib/roi'); 4 | 5 | const FakeServer = require('./fake-server'); 6 | const FakeRedirectionServer = require('./fake-redirection-server'); 7 | 8 | test('PUT.', () => { 9 | expect.assertions(1); 10 | const server = FakeServer.create(3061); 11 | 12 | const foo = { 13 | title: 'foo-json2', 14 | author: 'bgold' 15 | }; 16 | 17 | return roi.put('http://localhost:3061/posts/1', foo) 18 | .then(response => { 19 | expect(response.statusCode).toBe(200); 20 | server.close(); 21 | }).catch(e => { 22 | console.error(e); 23 | server.close(); 24 | }); 25 | }); 26 | 27 | test('PUT - Redirect.', () => { 28 | expect.assertions(1); 29 | const redirectServer = FakeRedirectionServer.create(); 30 | const server = FakeServer.create(3000); 31 | 32 | const foo = { 33 | title: 'foo-json', 34 | author: 'bgold' 35 | }; 36 | 37 | return roi.put('http://localhost:3001/01.html', foo) 38 | .then(response => { 39 | expect(response.statusCode).toBe(200); 40 | redirectServer.close(); 41 | server.close(); 42 | }).catch(e => { 43 | console.error(e); 44 | redirectServer.close(); 45 | server.close(); 46 | }); 47 | }); 48 | 49 | test('PUT - Succeed to 404.', () => { 50 | expect.assertions(1); 51 | const server = FakeServer.create(3062); 52 | return roi.put('http://localhost:3062/foo.html') 53 | .then(response => { 54 | expect(response.statusCode).toBe(404); 55 | server.close(); 56 | }) 57 | .catch(e => { 58 | console.error(e); 59 | server.close(); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /lib/roi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RoiGet = require('./get'); 4 | const RoiPost = require('./post'); 5 | const RoiPut = require('./put'); 6 | const RoiDelete = require('./delete'); 7 | const RoiHead = require('./head'); 8 | const RoiDownload = require('./download'); 9 | const RoiUpload = require('./upload'); 10 | const RoiPostStream = require('./post-stream'); 11 | 12 | const checkOptions = (options) => { 13 | if (typeof options === 'string') { 14 | return {endpoint: options}; 15 | } 16 | return options; 17 | }; 18 | 19 | function get (options) { 20 | options = checkOptions(options); 21 | return new RoiGet().execute(options); 22 | } 23 | 24 | function post (options, data) { 25 | options = checkOptions(options); 26 | return new RoiPost().execute(options, data); 27 | } 28 | 29 | function put (options, data) { 30 | options = checkOptions(options); 31 | return new RoiPut().execute(options, data); 32 | } 33 | 34 | function del (options) { 35 | options = checkOptions(options); 36 | return new RoiDelete().execute(options); 37 | } 38 | 39 | function head (options) { 40 | options = checkOptions(options); 41 | return new RoiHead().execute(options); 42 | } 43 | 44 | function download (options, file) { 45 | options = checkOptions(options); 46 | return new RoiDownload().execute(options, file); 47 | } 48 | 49 | function upload (options, file) { 50 | options = checkOptions(options); 51 | return new RoiUpload().execute(options, file); 52 | } 53 | 54 | function postStream (options, data, file) { 55 | options = checkOptions(options); 56 | return new RoiPostStream().execute(options, data, file); 57 | } 58 | 59 | module.exports = { 60 | get: get, 61 | post: post, 62 | put: put, 63 | del: del, 64 | head: head, 65 | download: download, 66 | upload: upload, 67 | postStream: postStream 68 | }; 69 | -------------------------------------------------------------------------------- /lib/upload.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const Base = require('./base'); 5 | const OptionHandler = require('./option-handler'); 6 | const ResponseHandler = require('./response-handler'); 7 | const Status = require('./status'); 8 | 9 | class RoiUpload extends Base { 10 | execute (options, file) { 11 | options = OptionHandler.setup(options); 12 | options = OptionHandler.addHeaders(options); 13 | options.method = 'POST'; 14 | options.headers.filename = file; 15 | return new Promise((resolve, reject) => { 16 | const req = options.proto.request(options, (response) => { 17 | let body = []; 18 | response.setEncoding('utf8'); 19 | response.on('data', d => body.push(d)); 20 | const statusCode = response.statusCode; 21 | if (Status.ok(statusCode)) { 22 | response.on('end', () => { 23 | body = body.join(''); 24 | resolve(ResponseHandler.handle(body, response)); 25 | }); 26 | } else if (Status.clientError(statusCode) || Status.serverError(statusCode)) { 27 | reject(body); 28 | } else if (Status.redirection(statusCode)) { 29 | options.endpoint = response.headers.location; 30 | this.redirects++; 31 | if (this.redirects >= this.maxRedirects) { 32 | const errorMessage = `Maximum redirects reached. (Amount of redirects allowed: ${this.redirects})`; 33 | this.redirects = 0; 34 | return reject(new Error(errorMessage)); 35 | } 36 | resolve(this.execute(options, file)); 37 | } 38 | }); 39 | req.on('error', (e) => reject(e)); 40 | const stream = fs.ReadStream(file); 41 | stream.pipe(req); 42 | stream.on('close', (res) => { 43 | req.end(); 44 | }); 45 | }); 46 | } 47 | 48 | } 49 | 50 | module.exports = RoiUpload; 51 | -------------------------------------------------------------------------------- /test/fake-redirection-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'); 4 | const url = require('url'); 5 | 6 | class FakeRedirectionServer { 7 | 8 | static create () { 9 | const server = http.createServer((request, response) => { 10 | if (url.parse(request.url).pathname === '/01.html') { 11 | if (request.method === 'POST') { 12 | response.writeHead(301, {'content-type': 'text/html', 13 | 'Location': 'http://localhost:3000/posts'}); 14 | response.end('Redirected to another server.'); 15 | } else if (request.method === 'PUT') { 16 | response.writeHead(301, {'content-type': 'text/html', 17 | 'Location': 'http://localhost:3000/posts/1'}); 18 | response.end('Redirected to another server.'); 19 | } else if (request.method === 'DELETE') { 20 | response.writeHead(301, {'content-type': 'text/html', 21 | 'Location': 'http://localhost:3000/posts/3'}); 22 | response.end('Redirected to another server.'); 23 | } else { 24 | response.writeHead(301, {'content-type': 'text/html', 25 | 'Location': 'http://localhost:3001/02.html'}); 26 | response.end('Redirected from 01.'); 27 | } 28 | } else if (url.parse(request.url).pathname === '/02.html') { 29 | response.writeHead(301, {'content-type': 'text/html', 30 | 'Location': 'http://localhost:3001/01.html'}); 31 | response.end('Redirected from 02.'); 32 | } else if (url.parse(request.url).pathname === '/05.html') { 33 | response.writeHead(301, {'content-type': 'text/html', 34 | 'Location': 'http://localhost:3000/posts'}); 35 | response.end('Redirected from 05.'); 36 | } else if (url.parse(request.url).pathname === '/06.html') { 37 | response.writeHead(301, {'content-type': 'text/html', 38 | 'Location': 'http://localhost:3000/postaaaa'}); 39 | response.end('Redirected from 06.'); 40 | } 41 | }); 42 | return server.listen(3001, 'localhost'); 43 | } 44 | } 45 | 46 | module.exports = FakeRedirectionServer; 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to roi 2 | 3 | ## Issue contributions 4 | 5 | ### Did you find a bug ? 6 | 7 | Open a [new issue](https://github.com/bucharest-gold/roi/issues/new) 8 | and be sure to include a title and clear description, as much relevant information 9 | as possible, and a code sample or a test case demonstrating the expected behavior 10 | that is not occurring. 11 | 12 | Discussions can be done via github issues or IRC channel #bucharest-gold. 13 | 14 | ## Code contributions 15 | 16 | ### Fork 17 | 18 | Fork the project [on GitHub](https://github.com/bucharest-gold/roi) 19 | and check out your copy locally. 20 | 21 | ``` 22 | git clone git@github.com:username/roi.git 23 | cd roi 24 | git remote add upstream https://github.com/bucharest-gold/roi.git 25 | ``` 26 | 27 | ### Branch 28 | 29 | Create a feature branch and start hacking: 30 | 31 | ``` 32 | git checkout -b my-contrib-branch 33 | ``` 34 | 35 | ### Commit messages 36 | 37 | Writing good commit logs is important. A commit log should describe what 38 | changed and why. Follow these guidelines when writing one: 39 | 40 | 1. The first line should be 50 characters or less and contain a short 41 | description of the change. 42 | 2. Keep the second line blank. 43 | 3. Wrap all other lines at 72 columns. 44 | 45 | Example of commit message: 46 | 47 | ``` 48 | fix a bug with download url. 49 | 50 | The download url was not using https. 51 | Body of commit message is a few lines of text, explaining things 52 | in more detail, possibly giving some background about the issue 53 | being fixed, etc. etc. 54 | 55 | The body of the commit message can be several paragraphs, and 56 | please do proper word-wrap and keep columns shorter than about 57 | 72 characters or so. That way `git log` will show things 58 | nicely even when it is indented. 59 | ``` 60 | 61 | ### Rebase to keep updated. 62 | 63 | Use `git rebase` to sync your work from time to time. 64 | 65 | ``` 66 | git fetch upstream 67 | git rebase upstream/master 68 | ``` 69 | 70 | ### Development cycle 71 | 72 | Bug fixes and features should come with tests. 73 | The tests are on `test` directory. 74 | 75 | ``` 76 | npm run lint 77 | npm test 78 | ``` 79 | 80 | ### Push 81 | 82 | ``` 83 | git push origin my-contrib-branch 84 | ``` 85 | 86 | Go to https://github.com/yourusername/roi and select your feature branch. 87 | Click the 'Pull Request' button and fill out the form. 88 | -------------------------------------------------------------------------------- /lib/base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const OptionHandler = require('./option-handler'); 4 | const ResponseHandler = require('./response-handler'); 5 | const Status = require('./status'); 6 | 7 | class Base { 8 | constructor () { 9 | this.redirects = 0; 10 | this.maxRedirects = 3; 11 | } 12 | 13 | action (options) { 14 | options = OptionHandler.setup(options); 15 | options = OptionHandler.addHeaders(options); 16 | return new Promise((resolve, reject) => { 17 | const req = options.proto.request(options, (response) => { 18 | let body = []; 19 | response.setEncoding('utf8'); 20 | response.on('data', d => body.push(d)); 21 | const statusCode = response.statusCode; 22 | if (Status.ok(statusCode)) { 23 | response.on('end', () => { 24 | body = body.join(''); 25 | resolve(ResponseHandler.handle(body, response)); 26 | }); 27 | } else if (Status.clientError(statusCode) || Status.serverError(statusCode)) { 28 | reject(body); 29 | } else if (Status.redirection(statusCode)) { 30 | options.endpoint = response.headers.location; 31 | this.redirects++; 32 | if (this.redirects >= this.maxRedirects) { 33 | const errorMessage = `Maximum redirects reached. (Amount of redirects allowed: ${this.redirects})`; 34 | this.redirects = 0; 35 | return reject(new Error(errorMessage)); 36 | } 37 | resolve(this.action(options)); 38 | } 39 | }); 40 | req.on('error', (e) => reject(e)); 41 | req.end(); 42 | }); 43 | } 44 | 45 | actionData (options, data) { 46 | data = data || {}; 47 | options = OptionHandler.setup(options); 48 | const jsonData = JSON.stringify(data); 49 | options = OptionHandler.addHeaders(options); 50 | options.headers['Content-Length'] = jsonData.length; 51 | return new Promise((resolve, reject) => { 52 | const req = options.proto.request(options, (response) => { 53 | let body = []; 54 | response.setEncoding('utf8'); 55 | response.on('data', d => body.push(d)); 56 | const statusCode = response.statusCode; 57 | if (Status.ok(statusCode)) { 58 | response.on('end', () => { 59 | body = body.join(''); 60 | resolve(ResponseHandler.handle(body, response)); 61 | }); 62 | } else if (Status.clientError(statusCode) || Status.serverError(statusCode)) { 63 | reject(body); 64 | } else if (Status.redirection(statusCode)) { 65 | options.endpoint = response.headers.location; 66 | this.redirects++; 67 | if (this.redirects >= this.maxRedirects) { 68 | const errorMessage = `Maximum redirects reached. (Amount of redirects allowed: ${this.redirects})`; 69 | this.redirects = 0; 70 | return reject(new Error(errorMessage)); 71 | } 72 | resolve(this.actionData(options, data)); 73 | } 74 | }); 75 | req.on('error', (e) => reject(e)); 76 | req.write(jsonData); 77 | req.end(); 78 | }); 79 | } 80 | } 81 | 82 | module.exports = Base; 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # roi 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/bucharest-gold/roi/badge.svg)](https://coveralls.io/github/bucharest-gold/roi) 4 | [![Build Status](https://travis-ci.org/bucharest-gold/roi.svg?branch=master)](https://travis-ci.org/bucharest-gold/roi) 5 | [![Known Vulnerabilities](https://snyk.io/test/npm/roi/badge.svg)](https://snyk.io/test/npm/roi) 6 | [![dependencies Status](https://david-dm.org/bucharest-gold/roi/status.svg)](https://david-dm.org/bucharest-gold/roi) 7 | 8 | [![NPM](https://nodei.co/npm/roi.png)](https://npmjs.org/package/roi) 9 | 10 | A dependency-free http module. 11 | 12 | ## Installation 13 | 14 | ``` 15 | npm install roi -S 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```js 21 | const roi = require('roi'); 22 | 23 | roi.get('http://localhost:3000/posts') 24 | .then(response => { 25 | console.log(response); 26 | console.log(response.statusCode); 27 | console.log(response.headers); 28 | console.log(response.body); 29 | }) 30 | .catch(e => console.log(e)); 31 | ``` 32 | 33 | ## More examples 34 | 35 | ##### POST 36 | 37 | ```js 38 | const foo = { 39 | title: 'foo-json', 40 | author: 'bgold' 41 | }; 42 | 43 | roi.post('http://localhost:3000/posts', foo) 44 | .then(response => console.log(response) 45 | .catch(e => console.log(e)); 46 | ``` 47 | 48 | ##### PUT 49 | 50 | ```js 51 | const foo = { 52 | title: 'foo-json2', 53 | author: 'bgold' 54 | }; 55 | 56 | roi.put('http://localhost:3000/posts/2', foo) 57 | .then(response => console.log(response)) 58 | .catch(e => console.log(e)); 59 | ``` 60 | 61 | ##### DELETE 62 | 63 | ```js 64 | roi.del('http://localhost:3000/posts/3') 65 | .then(response => console.log(response)) 66 | .catch(e => console.log(e)); 67 | ``` 68 | 69 | ##### HEAD 70 | 71 | ```js 72 | roi.head('http://localhost:3000/posts/3') 73 | .then(response => console.log(response.statusCode === 200)) 74 | .catch(e => console.log(e)); 75 | ``` 76 | 77 | ##### DOWNLOAD 78 | 79 | ```js 80 | roi.download('https://github.com/bucharest-gold/roi/raw/master/test/green.png', '/tmp/green.png') 81 | .then(x => console.log(x)) 82 | .catch(e => console.log(e)); 83 | ``` 84 | 85 | ##### UPLOAD 86 | 87 | ```js 88 | // Fake server side app will save the file called myFileUploaded.png : 89 | const up = (request, response) => { 90 | request 91 | .pipe(fs.createWriteStream('/tmp/myFileUploaded.png')) 92 | .on('finish', () => { 93 | response.end(request.headers.filename); 94 | }); 95 | }; 96 | const server = require('http').createServer(up); 97 | server.listen(3002, () => {}); 98 | ``` 99 | 100 | ```js 101 | // Upload and check if the uploaded file exists: 102 | roi.upload('http://localhost:3002/', '/tmp/myFile.png') 103 | .then(response => { 104 | console.log(fs.existsSync('/tmp/myFileUploaded.png')); 105 | }); 106 | ``` 107 | 108 | ##### Basic authentication 109 | 110 | ```js 111 | // Add the username and password: 112 | const options = { 113 | endpoint: 'http://localhost:3000/', 114 | username: 'admin', 115 | password: 'admin' 116 | }; 117 | roi.get(options) 118 | .then(response => console.log(response)) 119 | .catch(e => console.log(e)); 120 | ``` 121 | 122 | ##### Remove 'Authorization' header 123 | 124 | To avoid error like: 125 | 126 | `Request header field Authorization is not allowed by Access-Control-Allow-Headers.` 127 | 128 | ```js 129 | const options = { 130 | endpoint: 'http://localhost:3000/', 131 | noAuth: true 132 | }; 133 | roi.get(options) 134 | .then(response => console.log(response)) 135 | .catch(e => console.log(e)); 136 | ``` 137 | 138 | 139 | ## Contributing 140 | 141 | Please read the [contributing guide](./CONTRIBUTING.md) -------------------------------------------------------------------------------- /test/get.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const roi = require('../lib/roi'); 4 | 5 | const FakeServer = require('./fake-server'); 6 | const FakeRedirectionServer = require('./fake-redirection-server'); 7 | const FakeAuthServer = require('./fake-auth-server'); 8 | const FakeCorsServer = require('./fake-cors-server'); 9 | 10 | test('GET.', () => { 11 | expect.assertions(3); 12 | const server = FakeServer.create(5000); 13 | return roi.get('http://localhost:5000/posts') 14 | .then(response => { 15 | expect(response.statusCode).toBe(200); 16 | expect(response.headers['x-powered-by']).toBe('Express'); 17 | const result = JSON.parse(response.body); 18 | expect(result[0].id).toBe(1); 19 | server.close(); 20 | }).catch(e => { 21 | console.error(e); 22 | server.close(); 23 | }); 24 | }); 25 | 26 | test('GET - Reach maximum redirects.', () => { 27 | expect.assertions(1); 28 | const server = FakeRedirectionServer.create(); 29 | return roi.get('http://localhost:3001/01.html') 30 | .then(response => { 31 | console.log(response); 32 | }) 33 | .catch(e => { 34 | expect(e.toString()).toBe('Error: Maximum redirects reached. (Amount of redirects allowed: 3)'); 35 | server.close(); 36 | }); 37 | }); 38 | 39 | test('GET - Host not exist.', () => { 40 | expect.assertions(1); 41 | return roi.get('http://foobarfoo:9000/') 42 | .then(response => { 43 | console.log('this will not execute.'); 44 | }).catch(e => { 45 | expect(e.toString()).toBe('Error: getaddrinfo ENOTFOUND foobarfoo foobarfoo:9000'); 46 | }); 47 | }); 48 | 49 | // test('GET - Redirect and succeed.', () => { 50 | // expect.assertions(1); 51 | // const redirectServer = FakeRedirectionServer.create(); 52 | // const server = FakeServer.create(5001); 53 | // const options = {endpoint: 'http://localhost:5001/05.html'}; 54 | // return roi.get(options) 55 | // .then(response => { 56 | // expect(response.statusCode).toBe(200); 57 | // server.close(); 58 | // redirectServer.close(); 59 | // }) 60 | // .catch(e => { 61 | // console.error(e); 62 | // redirectServer.close(); 63 | // }); 64 | // }); 65 | 66 | // test('GET - Redirect and succeed to 404.', () => { 67 | // expect.assertions(1); 68 | // const redirectServer = FakeRedirectionServer.create(); 69 | // const server = FakeServer.create(5002); 70 | // const options = {endpoint: 'http://localhost:5002/06.html'}; 71 | // return roi.get(options) 72 | // .then(response => { 73 | // expect(response.statusCode).toBe(404); 74 | // server.close(); 75 | // redirectServer.close(); 76 | // }) 77 | // .catch(e => { 78 | // console.error(e); 79 | // }); 80 | // }); 81 | 82 | test('GET - Succeed to 404.', () => { 83 | expect.assertions(1); 84 | const server = FakeServer.create(5003); 85 | return roi.get('http://localhost:5003/foo.html') 86 | .then(response => { 87 | expect(response.statusCode).toBe(404); 88 | server.close(); 89 | }) 90 | .catch(e => { 91 | console.error(e); 92 | }); 93 | }); 94 | 95 | test('GET - Using custom headers.', () => { 96 | expect.assertions(3); 97 | const server = FakeServer.create(5004); 98 | const options = { 99 | endpoint: 'http://localhost:5004/posts', 100 | headers: { 101 | 'Accept': 'text/plain', 102 | 'Content-type': 'text/plain' 103 | } 104 | }; 105 | 106 | return roi.get(options) 107 | .then(response => { 108 | expect(response.statusCode).toBe(200, 'Status code: 200'); 109 | expect(response.headers['x-powered-by']).toBe('Express'); 110 | const result = JSON.parse(response.body); 111 | expect(result[0].id).toBe(1); 112 | server.close(); 113 | }).catch(e => { 114 | console.error(e); 115 | }); 116 | }); 117 | 118 | test('GET - CORS enabled.', () => { 119 | expect.assertions(1); 120 | const server = FakeCorsServer.create(3009); 121 | const options = { 122 | endpoint: 'http://127.0.0.1:3009/', 123 | method: 'OPTIONS', 124 | headers: { 125 | 'Access-Control-Request-Method': 'GET', 126 | Origin: 'http://example.com', 127 | 'Access-Control-Request-Headers': 'X-Foo' 128 | } 129 | }; 130 | return roi.get(options) 131 | .then(response => { 132 | expect(response.headers['access-control-allow-methods']).toBe('GET,HEAD,POST'); 133 | server.close(); 134 | }).catch(e => { 135 | console.error(e); 136 | server.close(); 137 | }); 138 | }); 139 | 140 | test('GET - CORS enabled 2.', () => { 141 | expect.assertions(1); 142 | const server = FakeCorsServer.create(3010); 143 | const options = { 144 | endpoint: 'http://127.0.0.1:3010/', 145 | method: 'OPTIONS', 146 | headers: { 147 | 'Access-Control-Request-Method': 'GET', 148 | Origin: 'http://example.com', 149 | 'Access-Control-Request-Headers': 'X-Bar' 150 | } 151 | }; 152 | return roi.get(options) 153 | .then(response => { 154 | expect(response.headers['access-control-allow-methods']).toBe(undefined); 155 | server.close(); 156 | }).catch(e => { 157 | console.error(e); 158 | server.close(); 159 | }); 160 | }); 161 | 162 | test('GET - login', () => { 163 | expect.assertions(1); 164 | const server = FakeAuthServer.create(); 165 | const options = { 166 | endpoint: 'http://localhost:3005', 167 | username: 'admin', 168 | password: 'admin' 169 | }; 170 | 171 | return roi.get(options) 172 | .then(result => { 173 | expect(result.body).toBe('admin - logged.'); 174 | server.close(); 175 | }).catch(e => { 176 | console.error(e); 177 | server.close(); 178 | }); 179 | }); 180 | 181 | test('GET - Unauthorized', () => { 182 | expect.assertions(1); 183 | const server = FakeAuthServer.create(); 184 | const options = { 185 | endpoint: 'http://localhost:3005', 186 | username: 'foo', 187 | password: 'bar' 188 | }; 189 | 190 | return roi.get(options) 191 | .then(response => { 192 | console.log('Should not have succeeded.'); 193 | }) 194 | .catch(e => { 195 | expect(e.toString()).toBe('401 Unauthorized'); 196 | server.close(); 197 | }); 198 | }); 199 | --------------------------------------------------------------------------------