diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f300667 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ + language: node_js + node_js: + - 'stable' + services: + - mongodb + addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - gcc-4.8 + - g++-4.8 + env: + - CXX=g++-4.8 + sudo: required + before_script: + - cd lab-zachary + - npm i + script: + - npm test + - npm run lint diff --git a/lab-zachary/.eslintrc b/lab-zachary/.eslintrc new file mode 100644 index 0000000..8dc6807 --- /dev/null +++ b/lab-zachary/.eslintrc @@ -0,0 +1,21 @@ +{ + "rules": { + "no-console": "off", + "indent": [ "error", 2 ], + "quotes": [ "error", "single" ], + "semi": ["error", "always"], + "linebreak-style": [ "error", "unix" ] + }, + "env": { + "es6": true, + "node": true, + "mocha": true, + "jasmine": true + }, + "ecmaFeatures": { + "modules": true, + "experimentalObjectRestSpread": true, + "impliedStrict": true + }, + "extends": "eslint:recommended" +} diff --git a/lab-zachary/.gitignore b/lab-zachary/.gitignore new file mode 100644 index 0000000..a7dd290 --- /dev/null +++ b/lab-zachary/.gitignore @@ -0,0 +1,130 @@ + +# Created by https://www.gitignore.io/api/node,osx,windows,linux + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Node ### +# 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 (http://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 + + +### OSX ### +*.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 +.DS_Store + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/node,osx,windows,linux \ No newline at end of file diff --git a/lab-zachary/README.md b/lab-zachary/README.md new file mode 100644 index 0000000..767005d --- /dev/null +++ b/lab-zachary/README.md @@ -0,0 +1,73 @@ +# Basic and Bearer Auth with an Express API + +This app implements Basic and Bearer Auth with API endpoints to allow for user signup and sign in, and to create, read, update and delete galleries associated with that user. + +# System Requirements + + - Terminal.app on macOS or equivalent + - node.js and npm package manager installed + + +### Installation + +Clone the repository to your local server +```sh +https://github.com/zcrumbo/16-basic_auth/tree/day-one +``` + +Install the dependencies - + +```sh +$ npm i +``` + +[HTTPie](https://httpie.org/) will be required to run the HTTP requests from your terminal window. You will need to install this with [Homebrew][1] on macOS. It is also easier to see the results of all operations by running mocha tests with the command +```sh +$ mocha +``` +or use the npm script to run all tests with debug +```sh +$ npm test +``` +Start the server with debug + +```sh +$ npm start +``` +If you want to use the debug and nodemon modules, run the npm script: +``` +npm start +``` + +### Connecting + +Commands can be sent to the api/signin and the api/signup endpoints. Once signed up, commands can be sent with the token generated during signup to api/gallery and api/gallery/:id endpoints for full CRUD functionality + + +```sh +$ POST /api/signup/ with JSON body {username='testname' password='password' email='email@email.com'} #signs up for the api and returns a unqique token that must be used in future api calls + +$ GET /api/signin with basic auth header username:password #signs into the API + +$ POST /api/gallery with token #creates a new gallery + +$ GET /api/gallery/:galleryID with token #retrieve your gallery + +$ PUT /api/gallery/:galleryID with token and JSON body {name:'galleryName', desc: 'description'} #updates specified gallery + +$ DELETE /api/gallery/:galleryID with token #deletes specified gallery + +``` + +Sending the following requests to the server will have the results below: + + * `404` response with 'not found' for unregistered endpoints and nonexistent gallery IDs + * `401` response with 'unauthorized' for bad credentials + * `200` response with a proper signin and gallery creation + * `400` response with 'bad request' if no request body was provided or the body was invalid + * `200` response with the body content for requests with valid bodies, endpoints and ids + * `204` response for successful deletions + + +[1]:https://brew.sh/ + diff --git a/lab-zachary/gulpfile.js b/lab-zachary/gulpfile.js new file mode 100644 index 0000000..4c40ff6 --- /dev/null +++ b/lab-zachary/gulpfile.js @@ -0,0 +1,23 @@ +'use strict'; + +const gulp = require('gulp'); +const eslint = require('gulp-eslint'); +const mocha = require ('gulp-mocha'); + +gulp.task('test', function(){ + gulp.src('./test/*-test.js', { read: false}) + .pipe(mocha({ reporter: 'spec'})); +}); + +gulp.task('lint', function() { + return gulp.src(['**/*.js', '!node_modules']) + .pipe(eslint()) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()); +}); + +gulp.task('dev', function(){ + gulp.watch(['**/*.js', '!node_modules/**'], ['lint']); +}); + +gulp.task('default', ['dev', 'lint', 'test']); diff --git a/lab-zachary/lib/basic-auth-middleware.js b/lab-zachary/lib/basic-auth-middleware.js new file mode 100644 index 0000000..a9481bd --- /dev/null +++ b/lab-zachary/lib/basic-auth-middleware.js @@ -0,0 +1,29 @@ +'use strict'; + +const createError = require('http-errors'); +const debug = require('debug')('cfgram:basic-auth-middleware'); + +module.exports = function(req, res, next) { + debug('basic auth'); + + var authHeader = req.headers.authorization; + if(!authHeader) return next(createError(401, 'auth header required')); + + var base64str = authHeader.split('Basic ')[1]; + if (!base64str) return next(createError(401, 'username and password required')); + + var utf8str = new Buffer(base64str, 'base64').toString(); //one method to decode auth header + //var utf8str = base64str.toString(); + + var authArr = utf8str.split(':'); + + req.auth = { + username: authArr[0], + password: authArr[1] + }; + + if(!req.auth.username) return next(createError(401, 'username required')); + if(!req.auth.password) return next(createError(401, 'password required')); + + next(); +}; diff --git a/lab-zachary/lib/bearer-auth-middleware.js b/lab-zachary/lib/bearer-auth-middleware.js new file mode 100644 index 0000000..3058f18 --- /dev/null +++ b/lab-zachary/lib/bearer-auth-middleware.js @@ -0,0 +1,32 @@ +'use strict'; + +const createError = require('http-errors'); +const jwt = require('jsonwebtoken'); +const debug = require('debug')('cfgram:bearer-auth-middleware'); +const User = require('../model/user.js'); + +//get req, res, next from other middleware and decipher the auth token in the auth header, then attach it to the request body + +module.exports = function( req, res, next) { + debug('bearer auth'); + + var authHeader = req.headers.authorization; + if (!authHeader) return next(createError(401, 'Authorization headers required')); + + var token = authHeader.split('Bearer ')[1]; + if (!token) return next(createError(401, 'token required')); + + //validate token and use jwt verify user + + jwt.verify(token, process.env.APP_SECRET, function(err, decoded) { + if (err) return next(createError(401, 'token validation error')); + if (!decoded) return next(createError(401, 'invalid token')); + User.findOne({findHash: decoded.token}) + .then( user => { + req.user = user; + next(); + }) + .catch( err => next(createError(401, err.message))); + }); +}; + diff --git a/lab-zachary/lib/error-middleware.js b/lab-zachary/lib/error-middleware.js new file mode 100644 index 0000000..7b23bc3 --- /dev/null +++ b/lab-zachary/lib/error-middleware.js @@ -0,0 +1,32 @@ +'use strict'; + +const createError = require('http-errors'); +const debug = require('debug')('cfgram:error-middleware'); + +module.exports = function(err, req, res, next){ + debug('error-middleware'); + + if(err.status) { + debug('user error'); + + console.error('message:', err.message); + console.error('name:', err.name); + + res.status(err.status).send(err.message); + next(); + return; + } + + if (err.message === 'ValidationError'){ + debug('Mongoose Error'); + err = createError(400, err.message); + res.status(err.status).send(err.message); + next(); + return; + } + + err = createError(500, 'Internal Server Error'); + res.status(err.status).send(err.message); + next(); + return; +}; diff --git a/lab-zachary/lib/s3-methods.js b/lab-zachary/lib/s3-methods.js new file mode 100644 index 0000000..d6d2ff0 --- /dev/null +++ b/lab-zachary/lib/s3-methods.js @@ -0,0 +1,28 @@ +'use strict'; + +const AWS = require('aws-sdk'); +const debug = require('debug')('cfgram:s3-methods'); +const s3 = new AWS.S3(); + +AWS.config.setPromisesDependency(require('bluebird')); + +module.exports = exports = {}; + +exports.uploadObjectProm = function(params) { + return new Promise((resolve, reject) => { + s3.upload(params, (err, data) => { + if (err) console.error(reject); + resolve(data); + }); + }); +}; + +exports.deleteObjectProm = function(params) { + debug('deleteS3Prom') ; + return new Promise((resolve, reject) => { + s3.deleteObject(params, function(err, data) { + if (reject) console.error(err); + resolve(data); + }); + }); +}; \ No newline at end of file diff --git a/lab-zachary/model/gallery.js b/lab-zachary/model/gallery.js new file mode 100644 index 0000000..878fdab --- /dev/null +++ b/lab-zachary/model/gallery.js @@ -0,0 +1,13 @@ +'use strict'; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const gallerySchema = Schema({ + name: {type: String, required: true}, + desc: {type: String, required: true}, + created: {type: Date, required: true, default: new Date}, + userID: { type: Schema.Types.ObjectId, required: true }, +}); + +module.exports = mongoose.model('gallery', gallerySchema); \ No newline at end of file diff --git a/lab-zachary/model/pic.js b/lab-zachary/model/pic.js new file mode 100644 index 0000000..e63609f --- /dev/null +++ b/lab-zachary/model/pic.js @@ -0,0 +1,17 @@ +'use strict'; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const picSchema = Schema({ + name: {type:String, required:true}, + desc: {type:String, required:true}, + userID: {type: Schema.Types.ObjectId, required: true}, + galleryID: {type: Schema.Types.ObjectId, required: true}, + created: {type: Date, required: true, default: new Date}, + imageURI: {type: String, required: true, unique: true}, + objectKey: {type: String, required: true, unique: true} +}); + +module.exports = mongoose.model('pic', picSchema); + diff --git a/lab-zachary/model/user.js b/lab-zachary/model/user.js new file mode 100644 index 0000000..bc4eace --- /dev/null +++ b/lab-zachary/model/user.js @@ -0,0 +1,74 @@ +'use strict'; + +const mongoose = require('mongoose'); +const crypto = require('crypto'); +const debug = require('debug')('cfgram:user'); +const bcrypt = require('bcrypt'); +const createError = require('http-errors'); +const jwt = require('jsonwebtoken'); + +const Schema = mongoose.Schema; + +const userSchema = Schema ({ + username: {type: String, required: true, unique: true}, + email: {type: String, required: true, unique: true}, + password: {type: String, required: true}, + findHash: {type: String, unique: true} +}); + +userSchema.methods.createPasswordHash = function(password){ + debug('createPasswordHash'); + + return new Promise((resolve, reject) => { + bcrypt.hash(password, 10, (err, hash) => { + if (err) return reject(err); + this.password = hash; + resolve(this); + }); + }); +}; + + +userSchema.methods.comparePasswordHash = function(password){ + debug('comparePasswordHash'); + + return new Promise((resolve, reject) => { + bcrypt.compare(password, this.password, (err, valid) => { + if (err) return reject(err); + if (!valid) return reject(createError(401, 'invalid password!')); + resolve(this); + }); + }); +};// + + +userSchema.methods.generateFindHash = function(){ + return new Promise((resolve, reject) => { + let tries = 0; + + _generateFindHash.call(this); + + function _generateFindHash() { + this.findHash = crypto.randomBytes(32).toString('hex'); + this.save() + .then( () => resolve(this.findHash)) + .catch( err => { + if (tries>3) return reject(err); + tries++; + _generateFindHash.call(this); + }); + } + }); +}; + +userSchema.methods.generateToken = function(){ + debug('generateToken'); + return new Promise((resolve, reject) => { + this.generateFindHash() + .then( findHash => resolve(jwt.sign({token: findHash}, process.env.APP_SECRET))) + .catch( err => reject(err)); + }); +}; + + +module.exports = mongoose.model('users', userSchema); diff --git a/lab-zachary/package.json b/lab-zachary/package.json new file mode 100644 index 0000000..a4eaffe --- /dev/null +++ b/lab-zachary/package.json @@ -0,0 +1,38 @@ +{ + "name": "lab-zachary", + "version": "1.0.0", + "description": "", + "main": "gulpfile.js", + "scripts": { + "test": "DEBUG='cfgram*' mocha", + "start": "DEBUG='cfgram*' nodemon server.js", + "lint": "gulp lint" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "aws-sdk": "^2.24.0", + "bcrypt": "^1.0.2", + "bluebird": "^3.5.0", + "body-parser": "^1.17.1", + "cors": "^2.8.1", + "debug": "^2.6.1", + "del": "^2.2.2", + "dotenv": "^4.0.0", + "express": "^4.15.2", + "http-errors": "^1.6.1", + "jsonwebtoken": "^7.3.0", + "mongoose": "^4.8.6", + "morgan": "^1.8.1", + "multer": "^1.3.0" + }, + "devDependencies": { + "chai": "^3.5.0", + "gulp": "^3.9.1", + "gulp-eslint": "^3.0.1", + "gulp-mocha": "^4.0.1", + "mocha": "^3.2.0", + "superagent": "^3.5.0" + } +} diff --git a/lab-zachary/route/auth-router.js b/lab-zachary/route/auth-router.js new file mode 100644 index 0000000..060ebf2 --- /dev/null +++ b/lab-zachary/route/auth-router.js @@ -0,0 +1,49 @@ +'use strict'; + +const Router = require('express').Router; +const jsonParser = require('body-parser').json(); +const User = require('../model/user.js'); +const createError = require('http-errors'); +const debug = require('debug')('cfgram:auth-router'); + +const basicAuth = require('../lib/basic-auth-middleware'); + +const authRouter = module.exports = Router(); + +authRouter.get('/', (req, res) => { + res.write('you made a request'); + res.end(); +}); + +authRouter.post('/api/signup', jsonParser, (req, res, next) => { + debug('POST: /api/signup'); + + if(!req.body.username) return next(createError(400, 'username required')); + if(!req.body.email) return next(createError(400, 'email address required')); + if(!req.body.password) return next(createError(400, 'password required')); + + let password = req.body.password; + delete req.body.password; + + let user = new User(req.body); + + user.createPasswordHash(password) + .then(user => user.save()) + .then(user => user.generateToken()) + .then(token => res.send(token)) + .catch(() => next(createError(500, 'user not saved'))); + +}); + +authRouter.get('/api/signin', basicAuth, (req, res, next) => { + debug('GET: /api/signin'); + + User.findOne({username: req.auth.username}) + .then( user => { + if (!user) return next(createError(401, 'user not found!')); + return user.comparePasswordHash(req.auth.password); + }) + .then( user => user.generateToken()) + .then( token => res.send(token)) + .catch(next); +}); \ No newline at end of file diff --git a/lab-zachary/route/gallery-router.js b/lab-zachary/route/gallery-router.js new file mode 100644 index 0000000..5d5954e --- /dev/null +++ b/lab-zachary/route/gallery-router.js @@ -0,0 +1,68 @@ +'use strict'; + +const Router = require('express').Router; +const jsonParser = require('body-parser').json(); +const createError = require('http-errors'); +const debug = require('debug')('cfgram:gallery-router'); + +const bearerAuth = require('../lib/bearer-auth-middleware.js'); +const Gallery = require('../model/gallery.js'); + +const galleryRouter = module.exports = Router(); + + +galleryRouter.post('/api/gallery', bearerAuth, jsonParser, (req, res, next) => { + debug('POST: /api/gallery'); + //check auth token(in middleware), get user on req body, create new gallery associated with user's id + //req has body and user properties we need to get + if(!req.body.name) return next(createError(400, 'name required')); + if(!req.body.desc) return next(createError(400, 'description required')); + req.body.userID = req.user._id; + new Gallery(req.body).save() + .then( gallery => { + if (!gallery) return next(createError(400, 'gallery not created')); + res.json(gallery); + }) + .catch(next); +}); + +galleryRouter.get('/api/gallery/:id', bearerAuth, (req, res, next) => { + debug('GET: /api/gallery/:id'); + + Gallery.findById(req.params.id, function(err) { + if(err) { + if(err.name === 'CastError') next(createError(404, 'gallery not found')); + } + + }) + .then(gallery => { + if (!gallery) return next(createError(404, 'gallery not found')); + res.json(gallery); + }) + .catch(next); +}); + +galleryRouter.put('/api/gallery/:id', bearerAuth, jsonParser, (req, res, next) => { + debug('POST: /api/gallery/:id'); + + if(!req.body.name) return next(createError(400, 'name required')); + if(!req.body.desc) return next(createError(400, 'description required')); + + Gallery.findByIdAndUpdate(req.params.id, req.body, {new:true}) + .then( gallery => { + if(!gallery) return next(createError(404, 'gallery not found')); + res.json(gallery); + }) + .catch(next); +}); + +galleryRouter.delete('/api/gallery/:id', bearerAuth, (req, res, next) => { + debug('DELETE: /api/gallery:id'); + + Gallery.findByIdAndRemove(req.params.id) + .then(gallery => { + if(!gallery) return next(createError(404, 'gallery not found')); + res.sendStatus(204); + }) + .catch(next); +}); \ No newline at end of file diff --git a/lab-zachary/route/pic-router.js b/lab-zachary/route/pic-router.js new file mode 100644 index 0000000..1a6140c --- /dev/null +++ b/lab-zachary/route/pic-router.js @@ -0,0 +1,79 @@ +'use strict'; + +const Router = require('express').Router; +const multer = require('multer'); +const fs = require('fs'); +const del = require('del'); +const path = require('path'); +const createError = require('http-errors'); +const debug = require('debug')('cfgram:pic-router'); + +const bearerAuth = require('../lib/bearer-auth-middleware.js'); +const Pic = require('../model/pic.js'); +const Gallery = require('../model/gallery.js'); +const s3Methods = require('../lib/s3-methods.js'); + +const dataDir = `${__dirname}/../data`; +const upload = multer({dest: dataDir }); + +const picRouter = module.exports = Router(); + +picRouter.post('/api/gallery/:galleryID/pic', bearerAuth, upload.single('image'), (req, res, next) => { //req.file is object passed by multer + debug('POST: /api/gallery/:galleryID/pic'); + if(!req.file) return next(createError(400, 'no file provided')); + if(!req.file.path) return next(createError(500, 'file not saved')); + + let ext = path.extname(req.file.originalname); + let params = { + ACL: 'public-read', + Bucket: process.env.AWS_BUCKET, + Key: `${req.file.filename}${ext}`, + Body: fs.createReadStream(req.file.path), + }; +//upload + Gallery.findById(req.params.galleryID) + .then( gallery => { + if(!gallery) return next(createError(404, 'gallery not found')); + this.tempGallery = gallery; + s3Methods.uploadObjectProm(params) + .then( s3Data => { + del([`${dataDir}/*`]); + new Pic({ + name: req.body.name, + desc: req.body.desc, + userID: this.tempGallery.userID, + galleryID: req.params.galleryID, + imageURI: s3Data.Location, + objectKey: s3Data.Key + }) + .save() + .then( pic => { + res.json(pic); + }) + .catch(next); + }); + }); +}); + +picRouter.delete('/api/gallery/:galleryID/pic/:picID', bearerAuth, (req, res, next) => { + debug('DELETE: /api/gallery/:galleryID/pic/:picID'); + + Pic.findById(req.params.picID) + .then( pic => { + if (!pic) return next(createError(404, 'pic not found')); + let params = { + Bucket: process.env.AWS_BUCKET, + Key: pic.objectKey + }; + s3Methods.deleteObjectProm(params) + .then( s3Data => { + debug('delete data:',s3Data); + Pic.findByIdAndRemove(req.params.picID) + .then( () => { + res.sendStatus(204); + }); + }); + }); +}); + + diff --git a/lab-zachary/server.js b/lab-zachary/server.js new file mode 100644 index 0000000..5170c53 --- /dev/null +++ b/lab-zachary/server.js @@ -0,0 +1,33 @@ +'use strict'; + +const express = require('express'); +const morgan = require('morgan'); +const cors = require('cors'); +const debug = require('debug')('cf-gram:server'); +const dotenv = require('dotenv'); +const mongoose = require('mongoose'); + +const authRouter = require('./route/auth-router.js'); +const galleryRouter = require('./route/gallery-router.js'); +const picRouter = require('./route/pic-router.js'); +const errors = require('./lib/error-middleware.js'); + +const PORT = process.env.PORT || 3000; +const app = express(); + +dotenv.load(); + +mongoose.connect(process.env.MONGODB_URI); + +app.use(cors()); +app.use(morgan('dev')); +app.use(authRouter); +app.use(galleryRouter); +app.use(picRouter); +app.use(errors); + + + +const server = module.exports = app.listen(PORT, () => debug('server up:', PORT)); + +server.isRunning = true; \ No newline at end of file diff --git a/lab-zachary/test/data/sample.jpg b/lab-zachary/test/data/sample.jpg new file mode 100644 index 0000000..98c5b24 Binary files /dev/null and b/lab-zachary/test/data/sample.jpg differ diff --git a/lab-zachary/test/gallery-test.js b/lab-zachary/test/gallery-test.js new file mode 100644 index 0000000..a34d071 --- /dev/null +++ b/lab-zachary/test/gallery-test.js @@ -0,0 +1,311 @@ +'use strict'; + +const request = require('superagent'); +const expect = require('chai').expect; +const Promise = require('bluebird'); + +const User = require('../model/user.js'); +const Gallery = require('../model/gallery.js'); + +const url = `http://localhost:${process.env.PORT}`; + +const serverToggle = require('./lib/server-toggle.js'); +const server = require('../server.js'); + + +const sampleUser = { + username: 'sampleUser', + email: 'email@test.com', + password: '1234' +}; +const sampleGallery = { + name: 'sampleName', + desc: 'sample description' +}; + + +describe('Gallery Routes', function() { + before( done => serverToggle.serverOn(server, done)); + after(done => serverToggle.serverOff(server, done)); + afterEach( done => { + Promise.all([ + User.remove({}), + Gallery.remove({}) + ]) + .then( () => done()) + .catch(done); + }); + describe('POST: /api/gallery', function () { + before( done => { + //create a user + new User(sampleUser) + .createPasswordHash(sampleUser.password) + .then( user => { + this.tempUser = user; + return user.generateToken(); + }) + .then( token => { + this.tempToken = token; + done(); + }) + .catch(done); + }); + + describe('With a valid token and body', () => { + it('should return a new gallery', done => { + request.post(`${url}/api/gallery`) + .send(sampleGallery) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.desc).to.equal(sampleGallery.desc); + expect(res.body.name).to.equal(sampleGallery.name); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + done(); + }); + }); + }); + describe('Without a valid token', () => { + it('should return a 401', done => { + request.post(`${url}/api/gallery`) + .send(sampleGallery) + .set({ + Authorization: 'Bearer ofbadnews' + }) + .end((err, res) => { + expect(res.status).to.equal(401); + expect(err.message).to.equal('Unauthorized'); + done(); + }); + }); + }); + describe('Without a valid body', () => { + it('should return a 400', done => { + request.post(`${url}/api/gallery`) + .send({bad: 'data'}) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .end((err, res) => { + expect(res.status).to.equal(400); + expect(err.message).to.equal('Bad Request'); + done(); + }); + }); + }); + }); + describe('GET: /api/gallery/:id', function() { + before( done => { + //create a user + new User(sampleUser) + .createPasswordHash(sampleUser.password) + .then( user => { + this.tempUser = user; + return user.generateToken(); + }) + .then( token => { + this.tempToken = token; + done(); + }) + .catch(done); + }); + before( done => { + //create a gallery + this.tempGallery = new Gallery(sampleGallery); + this.tempGallery.userID = this.tempUser._id; + this.tempGallery.save() + .then( gallery => { + this.tempGallery = gallery; + done(); + }) + .catch(done); + }); + describe('with a valid gallery ID', () => { + it('should return a gallery', done => { + request.get(`${url}/api/gallery/${this.tempGallery._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal(sampleGallery.name); + expect(res.body.desc).to.equal(sampleGallery.desc); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + done(); + }); + }); + }); + describe('with an invalid gallery ID', () => { + it('should return a 404', done => { + request.get(`${url}/api/gallery/badID`) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .end((err, res) => { + expect(err.message).to.equal('Not Found'); + expect(res.status).to.equal(404); + done(); + }); + }); + }); + describe('with an invalid token', () => { + it('should return a 400', done => { + request.get(`${url}/api/gallery/${this.tempGallery._id}`) + .set({ + Authorization: 'Bearer ofBadTokens' + }) + .end((err, res) => { + expect(err.message).to.equal('Unauthorized'); + expect(res.status).to.equal(401); + done(); + }); + }); + }); + }); + describe('PUT /api/gallery/:id', function() { + before( done => { + //create a user + new User(sampleUser) + .createPasswordHash(sampleUser.password) + .then( user => { + this.tempUser = user; + return user.generateToken(); + }) + .then( token => { + this.tempToken = token; + done(); + }) + .catch(done); + }); + before( done => { + //create a gallery + this.tempGallery = new Gallery(sampleGallery); + this.tempGallery.userID = this.tempUser._id; + this.tempGallery.save() + .then( gallery => { + this.tempGallery = gallery; + done(); + }) + .catch(done); + }); + describe('with a valid token', () => { + describe('with a valid body', () => { + it('should return an updated gallery', done => { + request.put(`${url}/api/gallery/${this.tempGallery._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .send({name: 'updatedName', desc: 'updatedDesc'}) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal('updatedName'); + expect(res.body.desc).to.equal('updatedDesc'); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + done(); + }); + }); + }); + describe('with an invalid body', () => { + it('should return a 400 error', done => { + request.put(`${url}/api/gallery/${this.tempGallery._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .send({badName:'thiswontupdate', badDesc:'neitherwillthis'}) + .end((err, res) => { + expect(err.message).to.equal('Bad Request'); + expect(res.status).to.equal(400); + done(); + }); + }); + }); + describe('with an invalid gallery ID', () => { + it('should return a 404', done => { + request.put(`${url}/api/gallery/badGalleryID`) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .send({name: 'thiswontupdate', desc: 'neitherwillthis'}) + .end((err, res) => { + expect(err.message).to.equal('Not Found'); + expect(res.status).to.equal(404); + done(); + }); + }); + }); + describe('with an invalid token', () => { + it('should return a 401 error', done => { + request.put(`${url}/api/gallery/${this.tempGallery._id}`) + .set({ + Authorization: 'Bearer ofBadPuns' + }) + .send({name: 'didnthaveachance', desc:'neverwillgetupdated'}) + .end((err, res) => { + expect(err.message).to.equal('Unauthorized'); + expect(res.status).to.equal(401); + done(); + }); + }); + }); + }); + }); + describe('DELETE: /api/gallery/:id', function () { + before( done => { + //create a user + new User(sampleUser) + .createPasswordHash(sampleUser.password) + .then( user => { + this.tempUser = user; + return user.generateToken(); + }) + .then( token => { + this.tempToken = token; + done(); + }) + .catch(done); + }); + before( done => { + //create a gallery + this.tempGallery = new Gallery(sampleGallery); + this.tempGallery.userID = this.tempUser._id; + this.tempGallery.save() + .then( gallery => { + this.tempGallery = gallery; + done(); + }) + .catch(done); + }); + describe('with a valid id', () => { + it('should return a 204', done => { + request.delete(`${url}/api/gallery/${this.tempGallery._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(204); + done(); + }); + }); + }); + describe('with an invalid id', () => { + it('should return a 404', done => { + request.delete(`${url}/api/gallery/${this.tempGallery._id}`) //gallery was already removed above + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .end((err, res) => { + expect(res.status).to.equal(404); + expect(err.message).to.equal('Not Found'); + done(); + }); + }); + }); + }); +}); + diff --git a/lab-zachary/test/lib/server-toggle.js b/lab-zachary/test/lib/server-toggle.js new file mode 100644 index 0000000..12798f1 --- /dev/null +++ b/lab-zachary/test/lib/server-toggle.js @@ -0,0 +1,35 @@ +'use strict'; + +const debug = require('debug')('cfgram:server-toggle'); +const PORT = process.env.PORT || 3000; + +module.exports = exports = {}; + +exports.serverOn = function(server, done){ + debug('serverOn'); + + if(!server.isRunning) { + server.listen(PORT, () => { + debug('server up:',PORT); + server.isRunning = true; + done(); + }); + return; + } + done(); +}; + +exports.serverOff = function(server, done) { + debug('serverOff'); + + if(server.isRunning) { + server.close( err => { + if (err) console.error(err); + server.isRunning = false; + debug('server down'); + done(); + }); + return; + } + done(); +}; \ No newline at end of file diff --git a/lab-zachary/test/pic-test.js b/lab-zachary/test/pic-test.js new file mode 100644 index 0000000..ea01326 --- /dev/null +++ b/lab-zachary/test/pic-test.js @@ -0,0 +1,206 @@ +'use strict'; + +const fs = require('fs'); +const request = require('superagent'); +const expect = require('chai').expect; +const Promise = require('bluebird'); +const del = require('del'); + +const Pic = require('../model/pic.js'); +const User = require('../model/user.js'); +const Gallery = require('../model/gallery.js'); + +const serverToggle = require('./lib/server-toggle.js'); +const server = require('../server.js'); +const s3Methods = require('../lib/s3-methods.js'); + +const url = `http://localhost:${process.env.PORT}`; +const sampleUser = { + username: 'sampleUser', + email: 'sample@user.com', + password: '1234' +}; +const sampleGallery = { + name: 'sample Gallery', + desc: 'sample description' +}; +const samplePic = { + name: 'samplePic', + desc: 'sample description', + image: `${__dirname}/data/sample.jpg` //not in data model. added only for test +}; + +describe('Pic Routes', function() { + before( done => serverToggle.serverOn(server, done)); + after(done => serverToggle.serverOff(server, done)); + afterEach( done => { + Promise.all([ + User.remove({}), + Gallery.remove({}), + Pic.remove({}), + del([`${__dirname}/../data/*`]) + ]) + .then( () => done()) + .catch(done); + }); + describe('POST /api/gallery/:galleryID/pic', function () { + before( done => { //new user + new User(sampleUser) + .createPasswordHash(sampleUser.password) + .then( user => user.save()) + .then( user => { + this.tempUser = user; + return user.generateToken(); + }) + .then( token => { + this.tempToken = token; + done(); + }) + .catch(done); + }); + before( done => { //new gallery + sampleGallery.userID = this.tempUser._id; + new Gallery(sampleGallery).save() + .then( gallery => { + this.tempGallery = gallery; + done(); + }) + .catch(done); + }); + after( done => { + s3Methods.deleteObjectProm({Bucket: process.env.AWS_BUCKET, Key:this.tempPic.objectKey}) + .then(() => done()) + .catch(done); + }); + describe('with an authorized user, valid galleryID, and valid data', () => { + it('should return a pic object from s3', done => { + request.post(`${url}/api/gallery/${this.tempGallery._id}/pic`) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .field('name', samplePic.name) + .field('desc', samplePic.desc) + .attach('image', samplePic.image) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal(samplePic.name); + expect(res.body.galleryID).to.equal(this.tempGallery._id.toString()); + this.tempPic = res.body; + done(); + }); + }); + }); + describe('with authorized user, valid galleryID, and missing data', () => { + it('should return with a 400 error', done => { + request.post(`${url}/api/gallery/${this.tempGallery._id}/pic`) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .field('name', samplePic.name) + .field('desc', samplePic.desc) + .end((err, res) => { + expect(res.status).to.equal(400); + expect(err.message).to.equal('Bad Request'); + done(); + }); + }); + }); + describe('with an authorized user and invalid gallery id', () => { + it('should return a 404', done => { + request.post(`${url}/api/gallery/badGalleryID/pic`) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .field('name', samplePic.name) + .field('desc', samplePic.desc) + .attach('image', samplePic.image) + .end((err, res) => { + expect(res.status).to.equal(404); + expect(err.message).to.equal('Not Found'); + done(); + }); + }); + }); + + }); + describe('DELETE /api/gallery/:galleryID/pic/:picID', function () { + before( done => { //new user + new User(sampleUser) + .createPasswordHash(sampleUser.password) + .then( user => user.save()) + .then( user => { + this.tempUser = user; + return user.generateToken(); + }) + .then( token => { + this.tempToken = token; + done(); + }) + .catch(done); + }); + before( done => { //new gallery + sampleGallery.userID = this.tempUser._id; + new Gallery(sampleGallery).save() + .then( gallery => { + this.tempGallery = gallery; + done(); + }) + .catch(done); + }); + before( done => { //new pic + + let params = { + ACL: 'public-read', + Bucket: process.env.AWS_BUCKET, + Key: `samplefilehash${Math.floor(Math.random()*100000)}.jpg`, + Body: fs.createReadStream(`${__dirname}/data/sample.jpg`), + }; + s3Methods.uploadObjectProm(params) + .then( s3Data => { + new Pic({ + name: samplePic.name, + desc: samplePic.desc, + userID: this.tempUser._id, + galleryID: this.tempGallery._id, + imageURI: s3Data.Location, + objectKey: s3Data.Key + }) + .save() + .then( pic => { + this.tempPic = pic; + done(); + }) + .catch(done); + }); + }); + describe('with a valid gallery and picture id', () => { + it('should return a 204', done => { + request.delete(`${url}/api/gallery/${this.tempGallery._id}/pic/${this.tempPic._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(204); + done(); + }); + }); + }); + describe('with an invalid picture id', () => { + it('should return a 404', done => { + request.delete(`${url}/api/gallery/${this.tempGallery._id}/pic/${this.tempPic._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .end((err, res) => { + expect(err.message).to.equal('Not Found'); + expect(res.status).to.equal(404); + done(); + }); + }); + }); + + }); +}); + diff --git a/lab-zachary/test/user-auth-test.js b/lab-zachary/test/user-auth-test.js new file mode 100644 index 0000000..fe06449 --- /dev/null +++ b/lab-zachary/test/user-auth-test.js @@ -0,0 +1,124 @@ +'use strict'; + +const request = require('superagent'); +const expect = require('chai').expect; +const User = require('../model/user.js'); + +const url = `localhost:${process.env.PORT}`; + +const serverToggle = require('./lib/server-toggle.js'); +const server = require('../server.js'); + +const sampleUser = { + username: 'testUser', + password: '1234', + email: 'fake@email.com', +}; + +describe('User Auth tests', function() { + before( done => serverToggle.serverOn(server, done)); + after(done => serverToggle.serverOff(server, done)); + describe('POST: /api/signup', function() { + describe('with a valid body', function() { + after( done => { + User.remove({}) + .then( () => done()) + .catch(done); + }); + + it('should return a token', done => { + request.post(`${url}/api/signup`) + .send(sampleUser) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.text).to.be.a('string'); + done(); + }); + }); + }); + describe('with an invalid body', function() { + it('should return a 400 error', done => { + request.post(`${url}/api/signup`) + .send('bad data') + .end((err, res) => { + expect(err.message).to.equal('Bad Request'); + expect(res.status).to.equal(400); + done(); + }); + }); + }); + }); + + describe('GET: /api/signup', function() { + describe('with valid credentials', function() { + before( done => { + this.tempUser = new User(sampleUser); + this.tempUser.createPasswordHash(this.tempUser.password) + .then( user => user.save()) + .then( () => done()) + .catch(done); + }); + after( done => { + User.remove({}) + .then( () => done()) + .catch(done); + }); + it('should return a token', done => { + request.get(`${url}/api/signin`) + .auth('testUser', '1234') + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + done(); + }); + }); + }); + describe('with invalid credentials', function () { + before( done => { + this.tempUser = new User(sampleUser); + this.tempUser.createPasswordHash(this.tempUser.password) + .then( user => user.save()) + .then( () => done()) + .catch(done); + }); + after( done => { + User.remove({}) + .then( () => done()) + .catch(done); + }); + describe('with an invalid password', () => { + it('should respond with a 401' , done => { + request.get(`${url}/api/signin`) + .auth('testUser', 'wrongPass') + .end((err, res) => { + expect(err.status).to.equal(401); + expect(res.text).to.equal('invalid password!'); + done(); + }); + }); + }); + describe('with an invalid username', () => { + it('should respond with a 401' , done => { + request.get(`${url}/api/signin`) + .auth('wrongUser', '1234') + .end((err, res) => { + expect(err.status).to.equal(401); + expect(res.text).to.equal('user not found!'); + done(); + }); + }); + }); + }); + }); + describe('for a nonexistent endpoint', function () { + it('should return a 404', done => { + request(`${url}/wrong/`) + .end((err, res) => { + expect(err.message).to.equal('Not Found'); + expect(res.status).to.equal(404); + done(); + }); + }); + }); +}); \ No newline at end of file