diff --git a/app/server.js b/app/server.js index 5201d84d..5c9bf9a3 100644 --- a/app/server.js +++ b/app/server.js @@ -1,31 +1,221 @@ -const express = require('express'); -const bodyParser = require('body-parser'); -const jwt = require('jsonwebtoken'); -const swaggerUi = require('swagger-ui-express'); -const YAML = require('yamljs'); -const swaggerDocument = YAML.load('./swagger.yaml'); // Replace './swagger.yaml' with the path to your Swagger file -const app = express(); +var http = require('http'); +var fs = require('fs'); +var finalHandler = require('finalhandler'); +var queryString = require('querystring'); +var Router = require('router'); +var bodyParser = require('body-parser'); +var uid = require('rand-token').uid; +const url = require('url'); -app.use(bodyParser.json()); +//set up variables to store data +let brands = []; +let products = []; +let users = []; +let accessTokens = ['test']; +//set up router +const myRouter = Router(); +myRouter.use(bodyParser.json()); -// Importing the data from JSON files -const users = require('../initial-data/users.json'); -const brands = require('../initial-data/brands.json'); -const products = require('../initial-data/products.json'); +//helper function to authenticate user +const findUserAccessToken = (request) => { + const reqToken = request.headers.authorization.substring(7); + if (accessTokens.includes(reqToken)) { + return users.find((user) => { + return reqToken == user.login.accessToken; + }); + } else { + return null; + } +}; -// Error handling -app.use((err, req, res, next) => { - console.error(err.stack); - res.status(500).send('Something broke!'); +//setting up server +const server = http + .createServer(function (request, response) { + myRouter(request, response, finalHandler(request, response)); + }) + .listen(8000, () => { + //reads files then stores the data into appropriate variables + fs.readFile( + './initial-data/brands.json', + 'utf8', + function (error, data) { + if (error) throw error; + brands = JSON.parse(data); + } + ); + fs.readFile( + './initial-data/products.json', + 'utf8', + function (error, data) { + if (error) throw error; + products = JSON.parse(data); + } + ); + fs.readFile( + './initial-data/users.json', + 'utf8', + function (error, data) { + if (error) throw error; + users = JSON.parse(data); + } + ); + }); + +//request that gets all the store brands +myRouter.get('/store/brands', (request, response) => { + response.writeHead(200, { 'Content-Type': 'application/json' }); + return response.end(JSON.stringify(brands)); +}); +//request that gets an product by id number +myRouter.get('/store/brands/:id/products', (request, response) => { + const product = products.find((product) => { + return product.id == request.params.id; + }); + if (!product) { + response.writeHead(404, 'Item not found'); + return response.end(); + } + response.writeHead(200, { 'Content-Type': 'application/json' }); + return response.end(JSON.stringify(product)); }); -// Swagger -app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); +//gets all store products if no query or gets a filtered list based on query +myRouter.get('/store/products', (request, response) => { + const query = request.query; + if (products.length === 0) { + response.writeHead(404, 'Inventory not found'); + return response.end(); + } + if (query) { + const filteredProducts = products.filter((product) => + product.name.toLowerCase().includes(query.toLowerCase()) + ); + response.writeHead(200, { 'Content-Type': 'application/json' }); + return response.end(JSON.stringify(filteredProducts)); + } + response.writeHead(200, { 'Content-Type': 'application/json' }); + return response.end(JSON.stringify(products)); +}); + +//logs in the user +myRouter.post('/login', (request, response) => { + if (request.body.username && request.body.password) { + //finds user by comparing enterered username and password with list of users + let user = users.find((user) => { + return ( + user.login.username == request.body.username && + user.login.password == request.body.password + ); + }); + //if user logs in it assigns them an access token + if (user) { + let newToken = uid(16); + user.login.accessToken = newToken; + accessTokens.push(newToken); + response.writeHead(200, { 'Content-Type': 'application/json' }); + return response.end(JSON.stringify(user.login.accessToken)); + } else { + response.writeHead(401, 'Invalid username or password'); + return response.end(); + } + } else { + response.writeHead(400, 'Unauthorized - Invalid Credentials Format'); + return response.end(); + } +}); +//retrieves the users cart +myRouter.get('/me/cart', (request, response) => { + const user = findUserAccessToken(request); + if (user) { + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify(user.cart)); + } else { + response.writeHead( + 401, + 'Unauthorized - User not authenticated. Please sign in' + ); + response.end(); + } +}); +//adds an item to the users cart based off of a product id sent in the request body +myRouter.post('/me/cart', (request, response) => { + const user = findUserAccessToken(request); -// Starting the server -const PORT = process.env.PORT || 3000; -app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); + if (user) { + const addedItem = products.find((item) => { + return item.id == request.body.productId; + }); + if (addedItem) { + user.cart.push(addedItem); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify(user.cart)); + } else { + response.writeHead(404, 'Item not found'); + response.end(); + } + } else { + response.writeHead(401, 'User not authenticated. Please sign in'); + response.end(); + } }); -module.exports = app; +//deletes an item from the users cart based off a product id sent in the request params +myRouter.delete('/me/cart/:productId', (request, response) => { + const user = findUserAccessToken(request); + + if (!user) { + response.writeHead(401, 'User not authenticated. Please sign in'); + response.end(); + } + + if (!user.cart) { + user.cart = []; + } + + const updateCart = user.cart.filter((item) => { + return item.id !== request.params.productId; + }); + user.cart = updateCart; + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify(user.cart)); +}); + +//changes the quantity of the item in a cart based off of product id sent in the params and a quantity number sent in the users body +myRouter.put('/me/cart/:productId', (request, response) => { + const user = findUserAccessToken(request); + + if (!user) { + response.writeHead(401, 'User not authenticated. Please sign in'); + response.end(); + } + + const quantity = request.body.quantity; + const foundItem = products.find((item) => { + return item.id == request.params.productId; + }); + + if (quantity < 1) { + response.writeHead(400, 'Quantity must be at least 1'); + response.end(); + } + + if (!foundItem) { + response.writeHead(404, 'Item not found'); + response.end(); + } + + const cartItem = user.cart.find((item) => { + return item.id == request.params.productId; + }); + + if (!cartItem) { + response.writeHead(404, 'Item not in cart'); + response.end(); + } else { + cartItem.quantity = quantity; + } + + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify(user.cart)); +}); +module.exports = server; diff --git a/initial-data/users.json b/initial-data/users.json index 9a6231e8..59fc7836 100644 --- a/initial-data/users.json +++ b/initial-data/users.json @@ -1,104 +1,108 @@ [ - { - "gender": "female", - "cart":[], - "name": { - "title": "mrs", - "first": "susanna", - "last": "richards" - }, - "location": { - "street": "2343 herbert road", - "city": "duleek", - "state": "donegal", - "postcode": 38567 - }, - "email": "susanna.richards@example.com", - "login": { - "username": "yellowleopard753", - "password": "jonjon", - "salt": "eNuMvema", - "md5": "a8be2a69c8c91684588f4e1a29442dd7", - "sha1": "f9a60bbf8b550c10712e470d713784c3ba78a68e", - "sha256": "4dca9535634c102fbadbe62dc5b37cd608f9f3ced9aacf42a5669e5a312690a0" - }, - "dob": "1954-10-09 10:47:17", - "registered": "2003-08-03 01:12:24", - "phone": "031-941-6700", - "cell": "081-032-7884", - "picture": { - "large": "https://randomuser.me/api/portraits/women/55.jpg", - "medium": "https://randomuser.me/api/portraits/med/women/55.jpg", - "thumbnail": "https://randomuser.me/api/portraits/thumb/women/55.jpg" - }, - "nat": "IE" - }, - { - "gender": "male", - "cart":[], - "name": { - "title": "mr", - "first": "salvador", - "last": "jordan" - }, - "location": { - "street": "9849 valley view ln", - "city": "burkburnett", - "state": "delaware", - "postcode": 78623 - }, - "email": "salvador.jordan@example.com", - "login": { - "username": "lazywolf342", - "password": "tucker", - "salt": "oSngghny", - "md5": "30079fb24f447efc355585fcd4d97494", - "sha1": "dbeb2d0155dad0de0ab9bbe21c062e260a61d741", - "sha256": "4f9416fa89bfd251e07da3ca0aed4d077a011d6ef7d6ed75e1d439c96d75d2b2" - }, - "dob": "1955-07-28 22:32:14", - "registered": "2010-01-10 06:52:31", - "phone": "(944)-261-2164", - "cell": "(888)-556-7285", - "picture": { - "large": "https://randomuser.me/api/portraits/men/4.jpg", - "medium": "https://randomuser.me/api/portraits/med/men/4.jpg", - "thumbnail": "https://randomuser.me/api/portraits/thumb/men/4.jpg" - }, - "nat": "US" - }, - { - "gender": "female", - "cart":[], - "name": { - "title": "mrs", - "first": "natalia", - "last": "ramos" - }, - "location": { - "street": "7934 avenida de salamanca", - "city": "madrid", - "state": "aragón", - "postcode": 43314 - }, - "email": "natalia.ramos@example.com", - "login": { - "username": "greenlion235", - "password": "waters", - "salt": "w10ZFgoO", - "md5": "19f6fb510c58be44b2df1816d88b739d", - "sha1": "18e545aee27156ee6be35596631353a14ee03007", - "sha256": "2b23b25939ece8ba943fe9abcb3074105867c267d122081a2bc6322f935ac809" - }, - "dob": "1947-03-05 15:23:07", - "registered": "2004-07-19 02:44:19", - "phone": "903-556-986", - "cell": "696-867-013", - "picture": { - "large": "https://randomuser.me/api/portraits/women/54.jpg", - "medium": "https://randomuser.me/api/portraits/med/women/54.jpg", - "thumbnail": "https://randomuser.me/api/portraits/thumb/women/54.jpg" - }, - "nat": "ES" - } -] \ No newline at end of file + { + "gender": "female", + "cart": [ + { "id": "1", "quantity": "4" }, + { "id": "2", "quantity": "2" } + ], + "name": { + "title": "mrs", + "first": "susanna", + "last": "richards" + }, + "location": { + "street": "2343 herbert road", + "city": "duleek", + "state": "donegal", + "postcode": 38567 + }, + "email": "susanna.richards@example.com", + "login": { + "username": "yellowleopard753", + "password": "jonjon", + "salt": "eNuMvema", + "md5": "a8be2a69c8c91684588f4e1a29442dd7", + "sha1": "f9a60bbf8b550c10712e470d713784c3ba78a68e", + "sha256": "4dca9535634c102fbadbe62dc5b37cd608f9f3ced9aacf42a5669e5a312690a0", + "accessToken": "test" + }, + "dob": "1954-10-09 10:47:17", + "registered": "2003-08-03 01:12:24", + "phone": "031-941-6700", + "cell": "081-032-7884", + "picture": { + "large": "https://randomuser.me/api/portraits/women/55.jpg", + "medium": "https://randomuser.me/api/portraits/med/women/55.jpg", + "thumbnail": "https://randomuser.me/api/portraits/thumb/women/55.jpg" + }, + "nat": "IE" + }, + { + "gender": "male", + "cart": [], + "name": { + "title": "mr", + "first": "salvador", + "last": "jordan" + }, + "location": { + "street": "9849 valley view ln", + "city": "burkburnett", + "state": "delaware", + "postcode": 78623 + }, + "email": "salvador.jordan@example.com", + "login": { + "username": "lazywolf342", + "password": "tucker", + "salt": "oSngghny", + "md5": "30079fb24f447efc355585fcd4d97494", + "sha1": "dbeb2d0155dad0de0ab9bbe21c062e260a61d741", + "sha256": "4f9416fa89bfd251e07da3ca0aed4d077a011d6ef7d6ed75e1d439c96d75d2b2" + }, + "dob": "1955-07-28 22:32:14", + "registered": "2010-01-10 06:52:31", + "phone": "(944)-261-2164", + "cell": "(888)-556-7285", + "picture": { + "large": "https://randomuser.me/api/portraits/men/4.jpg", + "medium": "https://randomuser.me/api/portraits/med/men/4.jpg", + "thumbnail": "https://randomuser.me/api/portraits/thumb/men/4.jpg" + }, + "nat": "US" + }, + { + "gender": "female", + "cart": [], + "name": { + "title": "mrs", + "first": "natalia", + "last": "ramos" + }, + "location": { + "street": "7934 avenida de salamanca", + "city": "madrid", + "state": "aragón", + "postcode": 43314 + }, + "email": "natalia.ramos@example.com", + "login": { + "username": "greenlion235", + "password": "waters", + "salt": "w10ZFgoO", + "md5": "19f6fb510c58be44b2df1816d88b739d", + "sha1": "18e545aee27156ee6be35596631353a14ee03007", + "sha256": "2b23b25939ece8ba943fe9abcb3074105867c267d122081a2bc6322f935ac809" + }, + "dob": "1947-03-05 15:23:07", + "registered": "2004-07-19 02:44:19", + "phone": "903-556-986", + "cell": "696-867-013", + "picture": { + "large": "https://randomuser.me/api/portraits/women/54.jpg", + "medium": "https://randomuser.me/api/portraits/med/women/54.jpg", + "thumbnail": "https://randomuser.me/api/portraits/thumb/women/54.jpg" + }, + "nat": "ES" + } +] diff --git a/package-lock.json b/package-lock.json index ab397dea..54893b8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "jsonwebtoken": "^9.0.2", "querystring": "^0.2.0", "rand-token": "^0.4.0", - "router": "^1.3.2", + "router": "^1.3.8", "swagger-ui-express": "^5.0.0", "yamljs": "^0.3.0" }, diff --git a/package.json b/package.json index afe392cf..3c7cc699 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "jsonwebtoken": "^9.0.2", "querystring": "^0.2.0", "rand-token": "^0.4.0", - "router": "^1.3.2", + "router": "^1.3.8", "swagger-ui-express": "^5.0.0", "yamljs": "^0.3.0" }, diff --git a/swagger.yaml b/swagger.yaml index 1c2eb22f..87616fde 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -1,11 +1,235 @@ swagger: '2.0' info: - version: '1.0.0' - title: 'E-Commerce API' - description: 'API for managing brands, products, and user cart' -host: 'localhost:3000' + title: Sunglasses.io + description: An API to support the Sunglasses-io platform + version: 1.0.0 +host: api.sunglasses.com schemes: - - 'http' -basePath: '/api' + - https +basePath: /v1 produces: - - 'application/json' + - application/json +tags: + - name: 'Store' + description: 'Accessing the sunglasses products' + - name: 'User' + description: 'Operations for the User' + +paths: + /store/brands: + get: + tags: + - 'Store' + summary: 'Returns Brands of sunglasses sold by store' + description: 'Returns a list of brands available' + responses: + 200: + description: Succesfull Request + schema: + type: array + items: + $ref: '#/definitions/Brand' + /store/brands/{Id}/products: + get: + tags: + - 'Store' + summary: 'Returns products by brand' + description: 'Returns all products associated with a certain brand ID' + parameters: + - name: Id + in: path + description: ID of sunglasses brand to be fetched + required: true + type: string + responses: + 200: + description: Successful Request + schema: + type: array + items: + $ref: '#/definitions/Product' + 404: + description: Item not found + /store/products: + get: + tags: + - 'Store' + summary: Returns all items in the store + description: Returns a list of all items in the store + parameters: + - name: query + in: query + required: false + type: string + responses: + 200: + description: Successful Request + schema: + type: array + items: + $ref: '#/definitions/Product' + 404: + description: Inventory not found + /login: + post: + tags: + - 'User' + summary: User Login + description: Logs in the user and returns access token + parameters: + - in: body + name: credentials + required: true + schema: + $ref: '#/definitions/Credentials' + responses: + 200: + description: Successful Login + schema: + $ref: '#/definitions/LoginResponse' + 400: + description: Invalid username or password + 401: + description: Unauthorized - Invalid Credentials Format + /me/cart: + get: + tags: + - 'User' + summary: Get Users shopping cart + description: Returns the current users shopping cart + parameters: + - name: accessToken + in: query + required: true + type: string + responses: + 200: + description: Successful Request + schema: + type: array + items: + $ref: '#/definitions/CartItem' + 401: + description: Unauthorized - User not authenticated. Please sign in + post: + tags: + - 'User' + summary: Adds an item to the cart + description: Adds an item to the current users cart + parameters: + - name: accessToken + in: query + required: true + type: string + - in: body + name: product + required: true + schema: + type: object + responses: + 200: + description: Successful Post + 401: + description: Unauthorized - User not authenticated + 404: + description: Item not found + /me/cart/{productId}: + delete: + tags: + - 'User' + summary: Remove item from the cart + description: Removes selected item from the current users cart + parameters: + - name: accessToken + in: query + required: true + type: string + - name: productId + in: path + required: true + type: integer + format: int64 + responses: + 200: + description: Item successfully removed from cart + 401: + description: Unauthorized - User not authenticated + put: + tags: + - 'User' + summary: Update quantity of item in cart + description: Update the quantity of a specific item in the current users cart + parameters: + - name: accessToken + in: query + required: true + type: string + - name: productId + in: path + required: true + type: string + - name: body + in: body + required: true + schema: + type: object + responses: + 200: + description: Successful Request + 401: + description: Unauthorized - User not authenticated + 404: + description: Item not found or Item not in cart + 400: + description: Quantity must be at least 1 +definitions: + Brand: + type: object + properties: + id: + type: string + name: + type: string + + Product: + type: object + properties: + id: + type: string + categoryId: + type: string + name: + type: string + description: + type: string + price: + type: number + imageUrls: + type: array + items: + type: string + + Credentials: + type: object + required: + - username + - password + properties: + username: + type: string + password: + type: string + + LoginResponse: + type: object + properties: + accessToken: + type: string + + CartItem: + type: object + properties: + productId: + type: string + quantity: + type: integer diff --git a/test/server.test.js b/test/server.test.js index 7ff14c8f..295cebec 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -1,14 +1,245 @@ const chai = require('chai'); const chaiHttp = require('chai-http'); const server = require('../app/server'); // Adjust the path as needed - +const fs = require('fs'); const should = chai.should(); chai.use(chaiHttp); // TODO: Write tests for the server -describe('Brands', () => {}); +describe('Store', () => { + describe('/store/brands', () => { + it('should return an array of all the brands', (done) => { + chai.request(server) + .get('/store/brands') + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an('array'); + done(); + }); + }); + }); + + describe('/store/brands/:id/products', () => { + it('should get a item based off of Id', (done) => { + let id = 1; + chai.request(server) + .get(`/store/brands/${id}/products`) + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an('object'); + done(); + }); + }); + it('should not get a product with an invalid id', (done) => { + let id = 10000; + chai.request(server) + .get(`/store/brands/${id}/products`) + .end((err, res) => { + res.should.have.status(404); + done(); + }); + }); + describe('/store/products', () => { + it('should get all products when no query is added', (done) => { + chai.request(server) + .get('/store/products') + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an('array'); + done(); + }); + }); + }); + }); +}); + +describe('/login', () => { + it('should log in the user', (done) => { + chai.request(server) + .post('/login') + .send({ + username: 'lazywolf342', + password: 'tucker', + }) + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.a('string'); + res.body.length.should.be.equal(16); + done(); + }); + }); + it('should return a 400 Invalid username or password', (done) => { + chai.request(server) + .post('/login') + .send({ + username: 'laolf342', + password: 'tucr', + }) + .end((err, res) => { + res.should.have.status(401); + done(); + }); + }); + it('should return a 400 error if either field is empty or not formatted correctly', (done) => { + chai.request(server) + .post('/login') + .send({ + username: 'laolf342', + password: '', + }) + .end((err, res) => { + res.should.have.status(400); + done(); + }); + }); +}); + +describe('User', () => { + describe('/GET /me/cart', () => { + it('should return the users cart', (done) => { + chai.request(server) + .get('/me/cart') + .set('authorization', 'Bearer test') + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an('array'); + done(); + }); + }); + it('should return a 401 error when user isnt logged in', (done) => { + chai.request(server) + .get('/me/cart') + .set('authorization', 'Bearer badtest') + .end((err, res) => { + res.should.have.status(401); + done(); + }); + }); + }); + describe('/POST /me/cart', () => { + it('should add new item to users cart', (done) => { + chai.request(server) + .post('/me/cart') + .set('authorization', 'Bearer test') + .send({ productId: '3' }) + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an('array'); + res.body.should.have.length(3); + done(); + }); + }); + it('should give a 404 if item does not exist', (done) => { + chai.request(server) + .post('/me/cart') + .set('authorization', 'Bearer test') + .send({ productId: '99999' }) + .end((err, res) => { + res.should.have.status(404); + done(); + }); + }); + it('should give a 401 if user not logged in', (done) => { + chai.request(server) + .post('/me/cart') + .set('authorization', 'Bearer badtest') + .send({ productId: '1' }) + .end((err, res) => { + res.should.have.status(401); + done(); + }); + }); + }); + describe('/DELETE /me/cart/{productId}', () => { + it('should remove an item from users cart', (done) => { + const productId = 1; + + chai.request(server) + .delete(`/me/cart/${productId}`) + .set('authorization', 'Bearer test') + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an('array'); + res.body.should.have.length(2); + done(); + }); + }); + it('should give a 401 if not logged in', (done) => { + const productId = 1; -describe('Login', () => {}); + chai.request(server) + .delete(`/me/cart/${productId}`) + .set('authorization', 'Bearer badtest') + .end((err, res) => { + res.should.have.status(401); + done(); + }); + }); + }); + describe('/PUT /me/cart/{productId}', () => { + it('should update quantity of item in cart', (done) => { + const quantity = '10'; + const productId = 2; -describe('Cart', () => {}); + chai.request(server) + .put(`/me/cart/${productId}`) + .set('authorization', 'Bearer test') + .send({ quantity: quantity }) + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an('array'); + res.body.should.deep.include({ id: '2', quantity: '10' }); + done(); + }); + }); + it('should return 401 if user not logged in', (done) => { + const quantity = '10'; + const productId = 2; + chai.request(server) + .put(`/me/cart/${productId}`) + .set('authorization', 'Bearer badtest') + .send({ quantity: quantity }) + .end((err, res) => { + res.should.have.status(401); + done(); + }); + }); + it('should return 400 if quantity is less then 1', (done) => { + const quantity = '0'; + const productId = 2; + chai.request(server) + .put(`/me/cart/${productId}`) + .set('authorization', 'Bearer test') + .send({ quantity: quantity }) + .end((err, res) => { + res.should.have.status(400); + done(); + }); + }); + it('should return 404 if item does not exist', (done) => { + const quantity = '10'; + const productId = 99999; + chai.request(server) + .put(`/me/cart/${productId}`) + .set('authorization', 'Bearer test') + .send({ quantity: quantity }) + .end((err, res) => { + res.should.have.status(404); + done(); + }); + }); + it('should return 404 status if item is not in cart', (done) => { + const quantity = '10'; + const productId = 5; + chai.request(server) + .put(`/me/cart/${productId}`) + .set('authorization', 'Bearer test') + .send({ quantity: quantity }) + .end((err, res) => { + res.should.have.status(404); + done(); + }); + }); + }); +});