diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..424429e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +*log diff --git a/README.md b/README.md index fc28569..f6aedc0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,102 @@ # Postalicious -A clone of the Postman chrome extension +A clone of the Postman app. + +## Description +See http://jsdev.learnersguild.org/goals/194-Postalicious-Demystifying_HTTP.html for description. + + +- [x] The artifact produced is a repo with at least two sub-folders: postalicious/ and sandbox-server/. +- [x] The artifact produced is properly licensed, preferably with the MIT license. + +## Sandbox Server + +- [x] Can run the command npm run sandbox-server (or npm run sb, if you want to save some typing) to start the sandbox web server at port 3000. +- [x] The sandbox server source code is written using the Express library. +- [x] Sending a GET request to the path / responds with + - [x] a 200 (OK) status code a plain-text response + - [x] body with the content Welcome to Sandbox! + - [x] the Content-Type header set to text/plain +- [x] Sending a GET request to the path /search?q=doodads responds with + - [x] a 200 (OK) status code + - [x] a plain-text response body with the content You searched for: "doodads" (it doesn’t need to actually do any searching, just return the plain text) + - [x] the Content-Type header set to text/plain +- [x] Sending a GET request to the path /search responds with… + - [x] a 400 (Bad Request) status code + - [x] a plain-text response body with the content You didn't provide a search query term :( + - [x] the Content-Type header set to text/plain +- [x] Sending a POST request to the path /things with a plain text body flying car responds with… + - [x] a 201 (Created) status code + - [x] a plain-text response body with the content New thing created: "flying car"! (it doesn’t need to actually create anything, just return the plain text) + - [x] the Content-Type header set to text/plain +- [x] Sending a GET request to the path /somefile with an Accept header of text/plain responds with… + - [x] a 200 (OK) status code + - [x] a plain-text response body with the content This is a plain text file + - [x] the Content-Type header set to text/plain +- [x] Sending a GET request to the path /somefile with an Accept header of text/html responds with… + - [x] a 200 (OK) status code + - [x] an HTML response body with the content This is an HTML file + - [x] the Content-Type header set to text/html +- [x] Sending a GET request to the path /myjsondata with an Accept header of application/json responds with… + - [x] a 200 (OK) status code + - [x] an HTML response body with the content { "title": "some JSON data" } + - [x] the Content-Type header set to application/json +- [x] Sending a GET request to the path /old-page responds with… + - [x] a 301 (Moved Permanently) status code + - [x] the Location header set to http://localhost:3000/newpage +- [x] Sending a POST request to the path /admin-only responds with a 403 (Forbidden) status code +- [x] Sending a GET request to the path /not-a-page responds with a 404 (Not Found) status code +- [x] Sending a GET request to the path /server-error responds with a 500 (Internal Server Error) staus code + + +##Postalicious + +- [x] Can run the command npm run postalicious (or npm run pl, if you want to save some typing) to start the Postalicious app at port 3001. +- [x] Users can visit the main page of the Postalicious site at http://localhost:3001. +- [x] Main page has three main sections: + - [x] Request builder HTML form + - [x] Raw HTTP request + - [x] Raw HTTP response +- [x] When a user fills out the HTML form and clicks a “Send” button… + - [x] The raw HTTP request is generated and shown + - [x] The HTTP request is sent, and the raw response message is shown + - [x] Users can fill out an HTML form to specify HTTP request details. +- [x] Submitting the form will send the request according to the specified details. +- [x] Using the HTML form, users can specify… + - [x] host and path + - [x] HTTP verb/method + - [x] query parameter keys + values + - [x] header keys + values + - [x] request body + +##Stretch + +Use the stretch goals to go deeper into the nuts and bolts of HTTP. + +- [ ] Sandbox server is written using only the core Node.js modules (instead of Express, use the built-in HTTP module). +- [ ] Users of Postalicious can “save” their requests in a history panel +- [ ] Clicking on a saved request will re-load it into the form +- [ ] Using Postalicious, create some HTTP requests to various real-world APIs: +- [ ] Get all issues for a repo through the GitHub API +- [ ] Get all tweets with the hashtag #javascript with the Twitter API +- [ ] Any other API request(s) of your choice +- [ ] External HTTP requests are saved in files under a example-requests/ directory (make sure to obscure any secure information before saving these files, like your password or authentication token) + +##Quality Rubric + +* Well formatted code + +* Code uses a linter, which can be invoked with a command (e.g. npm run lint). [50 points] +* Running the linter on all source code files generates no linting errors. [50 points] +* Clear and useful README + +* Repository includes a README file with installation and setup instructions. [25 points] +* Repository includes a README file with usage instructions and at least one example use case. [25 points] +* Proper dependency management + +* There is a command to install dependencies (e.g. npm install) and it is specified in the installation and setup instructions of the README. [50 points] +* Good project management + +* Commit messages are concise and descriptive. [25 points] +* All features are added via pull requests. [25 points] +* Every pull request has a description summarizing the changes made. [25 points] +* Every pull request has been reviewed by at least one other person. [25 points] diff --git a/package.json b/package.json new file mode 100644 index 0000000..608a8f0 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "postalicious", + "version": "1.0.0", + "description": "A clone of the Postman chrome extension.", + "main": "index.js", + "scripts": { + "sandbox-server": "nodemon sandbox-server/index", + "pl": "nodemon postalicious/index", + "test": "mocha sandbox-server/test/sandbox_tests.js", + "testTwo:": "mocha postalicious/test/post_tests.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/lizzkats/Postalicious.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/lizzkats/Postalicious/issues" + }, + "homepage": "https://github.com/lizzkats/Postalicious#readme", + "dependencies": { + "body-parser": "^1.17.1", + "express": "^4.15.2", + "morgan": "^1.7.1" + }, + "devDependencies": { + "chai": "^3.5.0", + "chai-http": "^3.0.0", + "cors": "^2.8.1", + "mocha": "^3.2.0", + "pg-promise": "^5.6.4" + } +} diff --git a/postalicious/index.js b/postalicious/index.js new file mode 100644 index 0000000..75e43a2 --- /dev/null +++ b/postalicious/index.js @@ -0,0 +1,22 @@ +const express = require('express') +const server = express() +const http = require('http').createServer(server) +const path = require('path') +const logger = require('morgan') +const bodyParser = require('body-parser') + +const port = process.env.PORT || 3001 +const index = require('./routes/form_routes.js') + +server.use(bodyParser.json({ type: 'application/json' })) +server.use(bodyParser.urlencoded({extended: true})) +server.use(express.static(path.join(__dirname, 'public'))) +server.use('/', index) + +server.use(logger("combined")) + +server.set(port) + +http.listen(port) + +module.exports = server diff --git a/postalicious/public/index.html b/postalicious/public/index.html new file mode 100644 index 0000000..d355520 --- /dev/null +++ b/postalicious/public/index.html @@ -0,0 +1,100 @@ + + + + + Postalicious + + + + + +
+
+
Postalicious
+
+
+
+ Method + +
+
+ Host + +
+
+
+ Query Parameters +
+
+ + + +
+
+ + + +
+
+
+
+ Headers +
+
+ + + +
+
+ + + +
+
+
+ Body + +
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+ REQUEST +
+
+
+
+
+
+
+
+
+
+ RESPONSE +
+
+
+
+
+
+
+ + + diff --git a/postalicious/public/methods.js b/postalicious/public/methods.js new file mode 100644 index 0000000..5d81cc1 --- /dev/null +++ b/postalicious/public/methods.js @@ -0,0 +1,108 @@ +const protocol = 'http/1.1' +const method = document.getElementsByClassName("method-input")[0].value +const docKeyLength = document.getElementsByClassName("key").length +const docValLength = document.getElementsByClassName("value").length + +function construct(event) { + event.preventDefault() + const host = document.getElementsByClassName("host-input")[0].value + const keyHeader1 = document.getElementById('header-key1').value + const valueHeader1 = document.getElementById('header-value1').value + const keyHeader2 = document.getElementById('header-key2').value + const valueHeader2 = document.getElementById('header-value2').value + const keyHeader3 = document.getElementById('header-key3').value + const valueHeader3 = document.getElementById('header-value3').value + const bodyBox = document.getElementById('request_body').value + let responseHeaders = [] + let responseHeaderValues = [] + let headersAndValues = [] + let status, statusText + let init = {} + + if(keyHeader1.length === 0){ + init[keyHeader1] = valueHeader1 + } + if(keyHeader2.length === 0){ + init[keyHeader2] = valueHeader2 + } + if(keyHeader3.length === 0){ + init[keyHeader3] = valueHeader3 + } + + let url = '' + url += host + if(document.getElementsByClassName("key")[0].value.length !== 0){ + url +='?' + for (var i = 0; i < docKeyLength - 1; i++) { + if(document.getElementsByClassName("key")[i].value.length !== 0){ + url += uriEncondingComponent(document.getElementsByClassName("key")[i].value) + '=' + uriEncondingComponent(document.getElementsByClassName("value")[i].value + '&') + } + } + url = url.substring(0, url.length - 1) + } + + let myRequest = new Request(url) + myRequest.body = bodyBox + + fetch(myRequest) + .then(response => { + document.getElementById("response-info1").innerText = 'status: ' + response.status + '\n' +'statusText: ' + response.statusText + return response.blob() + }).then(result => { + var reader = new FileReader(); + reader.addEventListener("loadend", function() { + if(result.type === 'application/json') { + //TODO: + } + if(result.type === 'text/plain'){ + let output = '' + output += reader.result + '\n' + output += 'content-type: ' + result.type + '\n' + output += 'content-length: ' + result.size + '\n' + document.getElementById("response-info2").innerText = output + } + }) + reader.readAsText(result); + }) + + document.getElementById("request_content").innerText = method + ' ' + protocol + document.getElementById("request_content2").innerText = url +} +function build(event) { + event.preventDefault() + const host = document.getElementsByClassName("host-input")[0].value + const keyHeader1 = document.getElementById('header-key1').value + const valueHeader1 = document.getElementById('header-value1').value + const keyHeader2 = document.getElementById('header-key2').value + const valueHeader2 = document.getElementById('header-value2').value + const keyHeader3 = document.getElementById('header-key3').value + const valueHeader3 = document.getElementById('header-value3').value + const bodyBox = document.getElementById('request_body').value + + let init = {} + + if(keyHeader1.length === 0){ + init[keyHeader1] = valueHeader1 + } + if(keyHeader2.length === 0){ + init[keyHeader2] = valueHeader2 + } + if(keyHeader3.length === 0){ + init[keyHeader3] = valueHeader3 + } + + let url = '' + url += host + if(document.getElementsByClassName("key")[0].value.length !== 0){ + url +='?' + for (var i = 0; i < docKeyLength - 1; i++) { + if(document.getElementsByClassName("key")[i].value.length !== 0){ + url += uriEncondingComponent(document.getElementsByClassName("key")[i].value) + '=' + uriEncondingComponent(document.getElementsByClassName("value")[i].value + '&') + } + } + url = url.substring(0, url.length - 1) + } + + document.getElementById("request_content").innerText = method + ' ' + protocol + document.getElementById("request_content2").innerText = url +} diff --git a/postalicious/public/stylesheets/style.css b/postalicious/public/stylesheets/style.css new file mode 100644 index 0000000..4badded --- /dev/null +++ b/postalicious/public/stylesheets/style.css @@ -0,0 +1,37 @@ + +.title { + background-color: grey; + width: 1267px; +} + +.method-input, .host-input { + margin-bottom: 10px; + width: 275px; + height: 30px; + background-color: lightgrey; +} + +.build-request { + height: 35px; + width: 180px; +} + +.build-send { + height: 35px; + width: 180px; +} + +.body-box { + height: 100px; + width: 200px; +} + +.response-body-box { + width: 50%; + height: 350px; +} + +.request-body-box { + width: 500px; + height: 350px; +} diff --git a/postalicious/routes/form_routes.js b/postalicious/routes/form_routes.js new file mode 100644 index 0000000..cd2b7b2 --- /dev/null +++ b/postalicious/routes/form_routes.js @@ -0,0 +1,14 @@ +const express = require('express') +const router = express.Router() + +// const place = require('../public/methods') + +router.post('/', (request, response) => { + // console.log(request.body) + // place.buildRequest(request.body) + // .then(_ => console.log('done')) + response.send(request.body) +}) + + +module.exports = router diff --git a/postalicious/stylesheets/style.css b/postalicious/stylesheets/style.css new file mode 100644 index 0000000..42895b0 --- /dev/null +++ b/postalicious/stylesheets/style.css @@ -0,0 +1,31 @@ + +.title { + background-color: grey; +} + +.method-input, .host-input { + margin-bottom: 10px; + width: 275px; + height: 30px; + background-color: lightgrey; +} + +.build-request { + height: 35px; + width: 180px; +} + +.build-send { + height: 35px; + width: 180px; +} + +.body-box { + height: 100px; + width: 200px; +} + +.request-body-box, .response-body-box { + width: 500px; + height: 250px; +} diff --git a/sandbox-server/index.js b/sandbox-server/index.js new file mode 100644 index 0000000..d2483e1 --- /dev/null +++ b/sandbox-server/index.js @@ -0,0 +1,29 @@ +const express = require('express') +const server = express() +const cors = require('cors') +const http = require('http').createServer(server) +const path = require('path') +const port = process.env.PORT || 3000 +const logger = require('morgan') +const trails = require('./routes/trails') + +server.use(logger("combined")) +server.use(express.static(path.join(__dirname, 'public'))) + + +server.use(function(req, res, next) { + res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); + next(); +}) +server.use('/', trails) + +// server.get('/', function(request, response){ +// response.send('Welcome to the sandbox!') +// }) + +server.set(port) + +http.listen(port) + +module.exports = server diff --git a/sandbox-server/routes/trails.js b/sandbox-server/routes/trails.js new file mode 100644 index 0000000..4cea192 --- /dev/null +++ b/sandbox-server/routes/trails.js @@ -0,0 +1,53 @@ +const express = require('express') +const router = express.Router() + +router.get('/', (request, response) => { + console.log('request', request.headers); + response.set('content-type', 'text/plain') + .send('Welcome to Sandbox!') +}) +router.get('/search', (request, response) => { + if( request.query.q.length === undefined || request.query.q.length === 0 ){ + response.status(400) + .set('content-type', 'text/plain') + .send("You didn't provide a search query term :(") + }else{ + response.set('content-type', 'text/plain') + .send("You searched for: 'doodads'") + } +}) +router.post('/things', (request, response) => { + response.status(201) + .set('content-type', 'text/plain') + .send("New thing created: flying car!") +}) +router.get('/somefile', (request, response) => { + response.status(200) + .set('content-type', 'text/html') + .send('This is an HTML file') +}) +router.get('/myjsondata', (request, response) => { + if(request.headers.accept === 'application/json'){ + response.status(200) + .set('content-type', 'application/json') + .send(JSON.stringify({"title":"some JSON data"})) + } +}) +router.get('/old-page', (request, response) => { + response.redirect(301,'/newpage') +}) +router.get('/newpage', (request,response) => { + response.status(301) + .set('content-location', 'http://localhost:3000/newpage') + .send('stuff') +}) +router.post('/admin-only', (request, response) => { + response.status(403) + .send('Forbidden') +}) +router.get('/server-error', (request, response) => { + response.status(500) + .send('sumpin broke') +}) + +module.exports = router diff --git a/sandbox-server/test/sandbox_tests.js b/sandbox-server/test/sandbox_tests.js new file mode 100644 index 0000000..d6ab21e --- /dev/null +++ b/sandbox-server/test/sandbox_tests.js @@ -0,0 +1,146 @@ +const chai = require('chai') +const expect = chai.expect +const chaiHttp = require('chai-http') +const app = require('../index') + +chai.use(chaiHttp) + +describe('sandbox-server', () => { + + context('homepage, onload', () => { + + it('Should respond with a status code of 200', (done) => { + chai.request(app) + .get('/') + .end((err, res) => { + expect(res).to.have.status(200) + expect(res.text).to.equal('Welcome to Sandbox!') + expect(res).to.have.header('content-type', 'text/plain; charset=utf-8') + done() + }) + }) + }) + + context('Doodads search', (done) => { + + it('Should respond with a status code of 200', (done) => { + chai.request(app) + .get('/search') + .query({'q':'doodads'}) + .end((err, res) => { + expect(res).to.have.status(200) + expect(res.text).to.equal('You searched for: \'doodads\'') + expect(res).to.have.header('content-type', 'text/plain; charset=utf-8') + done() + }) + }) + }) + + context('Bad request response', () => { + + it('Should respond with a status code of 400', (done) => { + chai.request(app) + .get('/search') + .end((err, res) => { + expect(res).to.have.status(400) + expect(res.text).to.equal('You didn\'t provide a search query term :(') + expect(res).to.have.header('content-type', 'text/plain; charset=utf-8') + done() + }) + }) + }) + + context('Flying car post', () => { + + it('Should respond with a status code of 201', (done) => { + chai.request(app) + .post('/things') + .send({'New thing created': 'flying car'}) + .end((err, res) => { + expect(res).to.have.status(201) + expect(res.text).to.equal('New thing created: flying car!') + expect(res).to.have.header('content-type', 'text/plain; charset=utf-8') + done() + }) + }) + }) + + context('Get some file', () => { + + it('Should respond with a status code of 200', (done) => { + chai.request(app) + .get('/somefile') + .end((err, res) => { + expect(res).to.have.status(200) + expect(res.text).to.equal('This is an HTML file') + expect(res).to.have.header('content-type', 'text/html; charset=utf-8') + done() + }) + }) + }) + + context('Get json data', () => { + + it('Should respond with a status code of 200', (done) => { + chai.request(app) + .get('/myjsondata') + .set('accept', 'application/json') + .end((err, res) => { + expect(res).to.have.status(200) + expect(res.text).to.equal('{"title":"some JSON data"}') + expect(res).to.have.header('content-type', 'application/json; charset=utf-8') + done() + }) + }) + }) + + context('Get old page', () => { + + it('Should respond with a status code of 301', (done) => { + chai.request(app) + .get('/old-page') + .end((err, res) => { + expect(res.statusCode).to.eql(301) + expect(res.headers["content-location"]).to.equal('http://localhost:3000/newpage') + done() + }) + }) + }) + + context('Post admin only', () => { + + it('Should respond with a status code of 403', (done) => { + chai.request(app) + .post('/admin-only') + .send({'stuff': 'all'}) + .end((err, res) => { + expect(res.statusCode).to.eql(403) + done() + }) + }) + }) + + context('Not a page', () => { + + it('Should respond with a status code of 404', (done) => { + chai.request(app) + .get('/not-a-page') + .end((err, res) => { + expect(res.statusCode).to.eql(404) + done() + }) + }) + }) + + context.only('Server error', () => { + + it('Should respond with a status code of 500', (done) => { + chai.request(app) + .get('/server-error') + .end((err, res) => { + expect(res).to.have.status(500) + done() + }) + }) + }) +})