diff --git a/app/server.js b/app/server.js index 5201d84d..497c36aa 100644 --- a/app/server.js +++ b/app/server.js @@ -5,6 +5,7 @@ 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(); +require('dotenv').config(); app.use(bodyParser.json()); @@ -22,10 +23,135 @@ app.use((err, req, res, next) => { // Swagger app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); +//Middleware + +const authenticateJWT = (req, res, next) => { + const authHeader = req.headers.authorization; + if(!authHeader) { + return res.sendStatus(401); + } + + if(authHeader) { + const token = authHeader.split(' ')[1]; + + jwt.verify(token, process.env.JWT_SECRET, (err, user) => { + if(err) { + return res.sendStatus(403); + } + + req.user = user; + next(); + }); + } +}; + // Starting the server const PORT = process.env.PORT || 3000; + app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); + +app.get('/brands', (req, res) => { + return res.json(brands); +}); + +app.get('/brands/:id/products', (req, res) => { + + if(!brands.some(brand => brand.id === req.params.id)) { + return res.status(404).send("Brand not found") + } + const brandProducts = products.filter((product) => product.categoryId == req.params.id) + res.json(brandProducts) +}) + +app.get('/products', (req, res) => { + return res.json(products) +}) + + +app.post('/login', (req, res) => { + + if(!req.body.username || !req.body.password) { + return res.status(400).send("Username and password are required") + } + + let user = users.find((user) => user.login.username === req.body.username) + + if(!user) { + return res.status(401).send("User not found") + } + + if(user.login.password !== req.body.password) { + return res.status(401).send("Invalid password") + } + + if(user.login.password === req.body.password) { + const token = jwt.sign(user, process.env.JWT_SECRET) + res.json({ accessToken: token }) + } + +}); + + +app.get('/me/cart', authenticateJWT, (req, res) => { + let user = users.find((user) => user.login.username === req.user.login.username) + res.json(user.cart); +}); + +app.post('/me/cart', authenticateJWT, (req, res) => { + if(req.body == {}) { + return res.status(400).send("Product is required") + } + if(!req.body.productId || !req.body.name || !req.body.price || !req.body.quantity) { + return res.status(400).send("Product is invalid") + } + let user = users.find((user) => user.login.username === req.user.login.username) + user.cart.push(req.body) + res.status(201).json (user.cart); +}); + + +app.delete('/me/cart/:productId', authenticateJWT, (req, res) => { + let userIndex = users.findIndex((user) => user.login.username === req.user.login.username); + + if (userIndex === -1) { + return res.status(404).send("User not found"); + } + + let user = users[userIndex]; + user.cart = user.cart.filter((product) => product.id != req.params.productId); + + // Update the user in the users array + users[userIndex] = user; + res.json(user.cart); +}); + +app.post('/me/cart/:productId', authenticateJWT, (req, res) => { + let userIndex = users.findIndex((user) => user.login.username === req.user.login.username); + + if (userIndex === -1) { + return res.status(404).send("User not found"); + } + + let user = users[userIndex]; + + + let cartProductIndex = user.cart.findIndex((product) => product.id == req.params.productId); + + + // update the quantity of the product + if (cartProductIndex === -1) { + return res.status(404).send("Product not found"); +} else { + user.cart[cartProductIndex].quantity = req.body.quantity; +} + + // Update the user in the users array + users[userIndex] = user; + + res.json(user.cart); +}); + module.exports = app; diff --git a/initial-data/users.json b/initial-data/users.json index 9a6231e8..312bf4ab 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": "female", + "cart": [], + "name": { + "title": "mrs", + "first": "susanna", + "last": "richards" }, - { - "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" + "location": { + "street": "2343 herbert road", + "city": "duleek", + "state": "donegal", + "postcode": 38567 }, - { - "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 + "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": [ + { "id": 1, "name": "product 1", "price": 100, "quantity": 2 }, + { "id": 2, "name": "product 2", "price": 200, "quantity": 1 }, + { "id": 3, "name": "product 3", "price": 300, "quantity": 3 } + ], + "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/swagger.yaml b/swagger.yaml index 1c2eb22f..54f72db1 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -1,11 +1,173 @@ -swagger: '2.0' +swagger: "2.0" info: - version: '1.0.0' - title: 'E-Commerce API' - description: 'API for managing brands, products, and user cart' -host: 'localhost:3000' -schemes: - - 'http' -basePath: '/api' -produces: - - 'application/json' + version: "1.0.0" + title: sunglasses-io-api + description: The API for Sunglasses.io +paths: + /brands: + get: + summary: returns all sunglasses brands + responses: + "200": + description: All the brands + content: + application/json: + schema: + type: array + items: + type: string + format: string + example: "Oakley" + /brands/{id}/products: + get: + summary: Get list of products for a specific brand + parameters: + - name: id + in: query + description: brand id + required: yes + type: string + responses: + "200": + description: products for brand with specific ID + content: + application/json: + schema: + type: array + items: + type: string + format: string + example: "RX7047" + /products: + get: + summary: Get list of all products + responses: + "200": + description: all products in the store + content: + application/json: + schema: + type: array + items: + $ref: "#/definitions/Product" + /login: + post: + summary: User login + parameters: + - name: username + in: query + description: input username + required: true + type: string + - name: password + in: query + description: input password + required: true + type: string + responses: + "200": + description: successfully logged in + /me/cart: + get: + summary: contents of a logged in user's cart + parameters: + - name: user + in: query + description: logged in user token + required: true + type: string + responses: + "200": + description: returns items in users cart + schema: + type: array + items: + $ref: "#/definitions/Product" + post: + summary: add products to a cart + parameters: + - name: id + in: query + description: id of item + required: true + type: string + - name: user + in: query + description: user token + required: true + type: string + responses: + "200": + description: adds an item to the users cart + /me/cart/{productId}: + delete: + summary: deletes an item from the cart + parameters: + - name: id + in: query + description: item id to delete + required: true + type: string + - name: user + in: query + description: user token + required: true + type: string + responses: + "200": + description: item is deleted from the cart + post: + summary: change the quantity of an item in the cart + parameters: + - name: id + in: query + description: id of item to update + required: yes + type: string + - name: user + in: query + description: user token + required: yes + type: string + - name: new quantity + in: query + description: new quantity amount to update + required: yes + type: string + responses: + "200": + description: item quantity is successfully updated + +definitions: + Brand: + type: object + properties: + id: + type: string + description: unique id for each brand + name: + type: string + description: brand name + Product: + type: object + properties: + id: + type: string + description: unique id for each product + name: + type: string + description: name of product + categoryId: + type: string + description: ID of product category + description: + type: string + description: description of the product + price: + type: number + description: price + imageUrls: + type: array + items: + type: string + description: url to product image diff --git a/test/server.test.js b/test/server.test.js index 7ff14c8f..0932170e 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -7,8 +7,222 @@ chai.use(chaiHttp); // TODO: Write tests for the server -describe('Brands', () => {}); +const brandThreeEx = [ + { + "id": "6", + "categoryId": "3", + "name": "glas", + "description": "Pretty awful glasses", + "price":10, + "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] + }, + { + "id": "7", + "categoryId": "3", + "name": "QDogs Glasses", + "description": "They bark", + "price":1500, + "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] + } +] -describe('Login', () => {}); +describe('Brands', () => { + describe("/GET brand", () => { + it('should GET all the brands', done => { + chai + .request(server) + .get("/brands") + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an("array"); + done(err); + }) + }) + }) + describe("/GET products for brand", () => { + it('should not GET the products for a brand id that does not exist', done => { + chai + .request(server) + .get("/brands/6/products") + .end((err, res) => { + res.should.have.status(404); + done(err) + }) + }) + + it('should GET the products for a paticular brand ID', done => { + chai + .request(server) + .get("/brands/3/products") + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an("array"); + res.body.should.eql(brandThreeEx) + done(err) + }) + }) + }) + describe("/GET products", () => { + it('should get a list of all products', done => { + chai + .request(server) + .get('/products') + .end((err, res) => { + res.should.have.status(200) + res.body.should.be.an("array"); + done(err) + }) + }) + }) +}); -describe('Cart', () => {}); +describe('/POST Login', () => { + it('should not POST login if username is blank', done => { + const passwordOnly = {username: '', password: 'helloWorld'} + chai + .request(server) + .post('/login') + .send(passwordOnly) + .end((err, res) => { + res.should.have.status(400) + done(err) + }) + }) + it('should not POST login if password is blank', done => { + const usernameOnly = {username: 'hello', password: ''} + chai + .request(server) + .post('/login') + .send(usernameOnly) + .end((err, res) => { + res.should.have.status(400) + done(err) + }) + }) + it('should not POST login if username and password are blank', done => { + const blank = {username: '', password: ''} + chai + .request(server) + .post('/login') + .send(blank) + .end((err, res) => { + res.should.have.status(400) + done(err) + }) + }) + it('should POST login if username and password are not blank', done => { + const login = {username: 'yellowleopard753', password: 'jonjon'} + chai + .request(server) + .post('/login') + .send(login) + .end((err, res) => { + res.should.have.status(200) + res.body.should.be.an('object') + done(err) + }) +})}); + +describe('Cart', () => { + describe('/GET cart', () => { + it('should not GET cart if no token is provided', done => { + chai + .request(server) + .get('/me/cart') + .end((err, res) => { + res.should.have.status(401) + done(err) + }) + }) + it('should GET cart if token is provided', done => { + chai + .request(server) + .get('/me/cart') + .set('Authorization', 'Bearer ' + process.env.AUTH_TOKEN) + .end((err, res) => { + res.should.have.status(200) + done(err) + }) + }) + }) + describe('/POST cart', () => { + it('should not POST to cart if no token is provided', done => { + chai + .request(server) + .post('/me/cart') + .end((err, res) => { + res.should.have.status(401) + done(err) + }) + }) + it('should POST to cart if token is provided', done => { + const product = {productId: 1, name: "Product 4", price: 400, quantity: 2} + chai + .request(server) + .post('/me/cart') + .set('Authorization', 'Bearer ' + process.env.AUTH_TOKEN) + .send(product) + .end((err, res) => { + res.should.have.status(201) + done(err) + }) + }) + + it('should not POST to cart if product is empty', done => { + const product = {} + chai + .request(server) + .post('/me/cart') + .set('Authorization', 'Bearer ' + process.env.AUTH_TOKEN) + .send(product) + .end((err, res) => { + res.should.have.status(400) + done(err) + }) + }) + it('should not POST to cart if product does not contain id, name, price, and quantity', done => { + const product = {productId: 1, name: "Product 4", price: 400} + chai + .request(server) + .post('/me/cart') + .set('Authorization', 'Bearer ' + process.env.AUTH_TOKEN) + .send(product) + .end((err, res) => { + res.should.have.status(400) + done(err) + }) + }) + }) + describe('/DELETE cart product', () => { + it('should not DELETE cart product if no token is provided', done => { + chai + .request(server) + .delete('/me/cart/1') + .end((err, res) => { + res.should.have.status(401) + done(err) + }) + }) + it('should DELETE cart product if token is provided', done => { + chai + .request(server) + .delete('/me/cart/1') + .set('Authorization', 'Bearer ' + process.env.AUTH_TOKEN) + .end((err, res) => { + res.should.have.status(200) + done(err) + }) + }) + it('should return cart with product deleted', done => { + chai + .request(server) + .get('/me/cart') + .set('Authorization', 'Bearer ' + process.env.AUTH_TOKEN) + .end((err, res) => { + res.should.have.status(200) + res.body.should.be.an('array').that.does.not.include({productId: 1, name: "Product 4", price: 400, quantity: 2}) + done(err) + }) + }) + }) + })