From 8c51c5cd64f5de3e61b90151715163fc300649a3 Mon Sep 17 00:00:00 2001 From: acl13 <128934431+acl13@users.noreply.github.com> Date: Tue, 4 Mar 2025 19:07:41 -0500 Subject: [PATCH 01/10] create swagger documentation --- swagger.yaml | 172 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 164 insertions(+), 8 deletions(-) diff --git a/swagger.yaml b/swagger.yaml index 1c2eb22f..6b7b4397 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -1,11 +1,167 @@ -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' + version: "1.0.0" + title: "E-Commerce API" + description: "API for managing brands, products, and user cart" +host: "localhost:3000" schemes: - - 'http' -basePath: '/api' + - "http" +basePath: "/api" produces: - - 'application/json' + - "application/json" +paths: + /brands: + get: + summary: "Returns a list of brands" + responses: + "200": + description: "A list of brands" + schema: + type: array + items: + $ref: "#/definitions/Brand" + default: + description: "Unexpected error" + schema: + $ref: "#/definitions/Error" + /brands/{id}/products: + get: + summary: "Returns a list of products for a specific brand" + parameters: + - name: id + in: path + required: true + type: integer + responses: + "200": + description: "List of brand products" + schema: + type: array + items: + $ref: "#/definitions/Product" + default: + description: "Unexpected error" + schema: + $ref: "#/definitions/Error" + /products: + get: + summary: "Returns a list of products" + responses: + "200": + description: "List of products" + schema: + type: array + items: + $ref: "#/definitions/Product" + default: + description: "Unexpected error" + schema: + $ref: "#/definitions/Error" + /login: + post: + summary: "Allows user to login" + parameters: + - in: body + name: login + description: "The login information (username and password) for the user" + schema: + $ref: "#/definitions/Login" + responses: + "200": + description: "Successful login" + "401": + description: "Unauthorized" + default: + description: "Unexpected error" + schema: + $ref: "#/definitions/Error" + /me/cart: + get: + summary: "Gets the cart for an authenticated user" + responses: + "200": + description: "User cart" + default: + description: "Unexpected error" + schema: + $ref: "#/definitions/Error" + post: + summary: "Adds item to user cart" + responses: + "200": + description: "Product successfully added to cart" + default: + description: "Unexpected error" + schema: + $ref: "#/definitions/Error" + /me/cart/{productId}: + post: + summary: "Edits the quantity of a particular item in the cart" + parameters: + - name: productId + in: path + required: true + type: integer + responses: + "200": + description: "Product quantity successfully updated" + default: + description: "Unexpected error" + schema: + $ref: "#/definitions/Error" + delete: + summary: "Deletes a particular item from the cart" + parameters: + - name: productId + in: path + required: true + type: integer + responses: + "200": + description: "Product successfully deleted" + default: + description: "Unexpected error" + schema: + $ref: "#/definitions/Error" +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: integer + imageUrls: + type: array + items: + type: string + Login: + type: object + properties: + username: + type: string + password: + type: string + Error: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + fields: + type: string From 0591751e7a6d38e0bc32cba58fb082505788223e Mon Sep 17 00:00:00 2001 From: acl13 <128934431+acl13@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:22:03 -0500 Subject: [PATCH 02/10] write and test endpoints GET /brands and GET /products --- app/server.js | 36 ++++++++++------- test/server.test.js | 95 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 112 insertions(+), 19 deletions(-) diff --git a/app/server.js b/app/server.js index 5201d84d..6577eaee 100644 --- a/app/server.js +++ b/app/server.js @@ -1,31 +1,41 @@ -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 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(); app.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'); +const users = require("../initial-data/users.json"); +const brands = require("../initial-data/brands.json"); +const products = require("../initial-data/products.json"); // Error handling app.use((err, req, res, next) => { - console.error(err.stack); - res.status(500).send('Something broke!'); + console.error(err.stack); + res.status(500).send("Something broke!"); }); // Swagger -app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); +app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); + +app.get("/brands", (request, response) => { + response.send(brands); + return response.end(); +}); + +app.get("/products", (request, response) => { + response.send(products); + return response.end(); +}); // Starting the server const PORT = process.env.PORT || 3000; app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); + console.log(`Server running on port ${PORT}`); }); module.exports = app; diff --git a/test/server.test.js b/test/server.test.js index 7ff14c8f..6a307314 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -1,14 +1,97 @@ -const chai = require('chai'); -const chaiHttp = require('chai-http'); -const server = require('../app/server'); // Adjust the path as needed +const chai = require("chai"); +const chaiHttp = require("chai-http"); +const server = require("../app/server"); // Adjust the path as needed const should = chai.should(); chai.use(chaiHttp); // TODO: Write tests for the server -describe('Brands', () => {}); +describe("Brands", () => { + describe("/GET brands", () => { + 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"); + res.body.length.should.be.eql(5); + done(); + }); + }); + }); -describe('Login', () => {}); + // describe('/GET brands/:id/products', () => { + // it('should GET all the products for a specific brand', (done) => { + // // Write test logic here + // (done); + // }) + // }) +}); -describe('Cart', () => {}); +describe("Products", () => { + describe("/GET products", () => { + it("should GET all the products", (done) => { + chai + .request(server) + .get("/products") + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an("array"); + res.body.length.should.be.eql(11); + done(); + }); + }); + }); +}); + +describe("Login", () => { + // describe("/POST login", () => { + // it("logs in an authenticated user", (done) => { + // //Write test logic here + // done(); + // }); + // }); +}); + +describe("Cart", () => { + // describe('/GET me/cart', () => { + // it('gets the cart for an authenticated user', () => { + // //arrange: login user/get user cart + // // write test logic here + // done() + // }) + // }) + // + // + // + // describe("/POST me/cart", () => { + // it("adds an item to user's cart", (done) => { + // //arrange: login user, need product id in request body? + // //write test logic here + // done(); + // }); + // }); + // + // + // + // describe("/POST me/cart/:productId", () => { + // it("Updates the quantity of a particular item in the user's cart", (done) => { + // // arrange: login user + // // will need to check cart quantity? + // // write test logic here + // done(); + // }); + // }); + // + // + // + // describe("/DELETE me/cart/:productId", () => { + // it("deletes an item from the cart", (done) => { + // // arrange: login user + // // write test logic here + // done(); + // }); + // }); +}); From 78ea8db5957d04dbc2a3380aad7bae61d9221e90 Mon Sep 17 00:00:00 2001 From: acl13 <128934431+acl13@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:14:37 -0500 Subject: [PATCH 03/10] write and test endpoint GET /brands/:id/products --- app/server.js | 11 +++++++++++ test/server.test.js | 20 ++++++++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/app/server.js b/app/server.js index 6577eaee..69039845 100644 --- a/app/server.js +++ b/app/server.js @@ -32,6 +32,17 @@ app.get("/products", (request, response) => { return response.end(); }); +app.get("/brands/:id/products", (request, response) => { + // Get all products that match the brand id + // The sample data provided has a categoryId property that aligns with the brand id numbers - I think it would be better named "brandId" but chose not to change the existing data + const filteredProducts = products.filter((product) => { + return product.categoryId == request.params.id; + }); + + response.send(filteredProducts); + return response.end(); +}); + // Starting the server const PORT = process.env.PORT || 3000; app.listen(PORT, () => { diff --git a/test/server.test.js b/test/server.test.js index 6a307314..a15ad9ee 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -22,12 +22,19 @@ describe("Brands", () => { }); }); - // describe('/GET brands/:id/products', () => { - // it('should GET all the products for a specific brand', (done) => { - // // Write test logic here - // (done); - // }) - // }) + describe("/GET brands/:id/products", () => { + it("should GET all the products for a specific brand", (done) => { + chai + .request(server) + .get("/brands/1/products") + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an("array"); + res.body.length.should.be.eql(3); + done(); + }); + }); + }); }); describe("Products", () => { @@ -49,6 +56,7 @@ describe("Products", () => { describe("Login", () => { // describe("/POST login", () => { // it("logs in an authenticated user", (done) => { + //get login info from request params // //Write test logic here // done(); // }); From 5d3f6dff0926b05f2825de5b3771e4c3f6036851 Mon Sep 17 00:00:00 2001 From: acl13 <128934431+acl13@users.noreply.github.com> Date: Thu, 6 Mar 2025 11:29:47 -0500 Subject: [PATCH 04/10] write & test enpoint POST /login --- app/server.js | 32 +++++++++++++++++++ test/server.test.js | 76 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 101 insertions(+), 7 deletions(-) diff --git a/app/server.js b/app/server.js index 69039845..c894180a 100644 --- a/app/server.js +++ b/app/server.js @@ -43,6 +43,38 @@ app.get("/brands/:id/products", (request, response) => { return response.end(); }); +app.post("/login", (request, response) => { + const { username, password } = request.body; + + const user = users.find((user) => { + return user.login.username == username && user.login.password == password; + }); + + if (!username || !password) { + response.status(401); + response.send({ message: "Both a username and password are required" }); + return response.end(); + } + + if (user) { + response.status(200); + response.send(user); + return response.end(); + } else { + response.status(401); + response.send({ message: "Incorrect username or password" }); + return response.end(); + } +}); + +// const { username, password } = req.body; + +// if (username === 'user' && password === 'password') { +// res.status(200).send({ message: 'Login successful' }); +// } else { +// res.status(401).send({ message: 'Invalid credentials' }); +// } + // Starting the server const PORT = process.env.PORT || 3000; app.listen(PORT, () => { diff --git a/test/server.test.js b/test/server.test.js index a15ad9ee..7a7e1d69 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -54,13 +54,75 @@ describe("Products", () => { }); describe("Login", () => { - // describe("/POST login", () => { - // it("logs in an authenticated user", (done) => { - //get login info from request params - // //Write test logic here - // done(); - // }); - // }); + describe("/POST login", () => { + it("logs in an authenticated user", (done) => { + let 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"); + res.body.should.have.property("gender"); + res.body.should.have.property("cart"); + res.body.should.have.property("name"); + res.body.should.have.property("location"); + res.body.should.have.property("email"); + res.body.should.have.property("login"); + res.body.should.have.property("dob"); + res.body.should.have.property("registered"); + res.body.should.have.property("phone"); + res.body.should.have.property("cell"); + res.body.should.have.property("picture"); + done(); + }); + }); + + //add test for error handling + it("throws error for incomplete login", (done) => { + let login = { + username: "Bob", + }; + + chai + .request(server) + .post("/login") + .send(login) + .end((err, res) => { + res.should.have.status(401); + res.body.should.be.an("object"); + res.body.message.should.be.a("string"); + res.body.message.should.be.eql( + "Both a username and password are required" + ); + done(); + }); + }); + + it("throws error for unathorized login", (done) => { + let login = { + username: "Bob", + password: "bobpass", + }; + + chai + .request(server) + .post("/login") + .send(login) + .end((err, res) => { + res.should.have.status(401); + res.body.should.be.an("object"); + res.body.message.should.be.a("string"); + res.body.message.should.be.eql("Incorrect username or password"); + done(); + }); + }); + }); }); describe("Cart", () => { From 3dc314f450762bbb816812921e628abfe7f8e999 Mon Sep 17 00:00:00 2001 From: acl13 <128934431+acl13@users.noreply.github.com> Date: Thu, 6 Mar 2025 15:10:30 -0500 Subject: [PATCH 05/10] create authentication token when user logs in --- app/server.js | 34 ++++++++++++++++++++++++---------- test/server.test.js | 14 ++++++++++++++ 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/app/server.js b/app/server.js index c894180a..df099f46 100644 --- a/app/server.js +++ b/app/server.js @@ -13,6 +13,17 @@ const users = require("../initial-data/users.json"); const brands = require("../initial-data/brands.json"); const products = require("../initial-data/products.json"); +// const authenticate = (req, res, next) => { +// try { +// const token = req.headers.authorization.split(" ")[1]; // Bearer +// const decoded = jwt.verify(token, "secretKey"); +// req.userId = decoded.userId; +// next(); +// } catch (error) { +// res.status(401).json({ message: "Authentication failed" }); +// } +// }; + // Error handling app.use((err, req, res, next) => { console.error(err.stack); @@ -39,10 +50,19 @@ app.get("/brands/:id/products", (request, response) => { return product.categoryId == request.params.id; }); - response.send(filteredProducts); - return response.end(); + if (filteredProducts.length === 0) { + response.status(404); + response.send({ message: "No products found" }); + return response.end(); + } else { + response.send(filteredProducts); + return response.end(); + } }); +let token = jwt.sign("user", "secretKey"); +console.log(token); + app.post("/login", (request, response) => { const { username, password } = request.body; @@ -57,7 +77,9 @@ app.post("/login", (request, response) => { } if (user) { + let token = jwt.sign({ user }, "secretKey", { expiresIn: "1hr" }); response.status(200); + response.set("Authorization", token); response.send(user); return response.end(); } else { @@ -67,14 +89,6 @@ app.post("/login", (request, response) => { } }); -// const { username, password } = req.body; - -// if (username === 'user' && password === 'password') { -// res.status(200).send({ message: 'Login successful' }); -// } else { -// res.status(401).send({ message: 'Invalid credentials' }); -// } - // Starting the server const PORT = process.env.PORT || 3000; app.listen(PORT, () => { diff --git a/test/server.test.js b/test/server.test.js index 7a7e1d69..e15d8bd8 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -34,6 +34,18 @@ describe("Brands", () => { done(); }); }); + + it("should return an error if the brand does not exist", (done) => { + chai + .request(server) + .get("/brands/6/products") + .end((err, res) => { + res.should.have.status(404); + res.body.message.should.be.a("string"); + res.body.message.should.be.eql("No products found"); + done(); + }); + }); }); }); @@ -67,6 +79,7 @@ describe("Login", () => { .send(login) .end((err, res) => { res.should.have.status(200); + res.header.authorization.should.be.a("string"); res.body.should.be.an("object"); res.body.should.have.property("gender"); res.body.should.have.property("cart"); @@ -79,6 +92,7 @@ describe("Login", () => { res.body.should.have.property("phone"); res.body.should.have.property("cell"); res.body.should.have.property("picture"); + res.body.should.have.property("nat"); done(); }); }); From 99225db2ce9b40dafbbc305d7472235650a6ccf6 Mon Sep 17 00:00:00 2001 From: acl13 <128934431+acl13@users.noreply.github.com> Date: Thu, 6 Mar 2025 18:12:09 -0500 Subject: [PATCH 06/10] create jwt for authenticated user --- app/server.js | 51 ++++++++++++++++++++++++++------------------- test/server.test.js | 47 ++++++++++++++++++++++++++++++++++------- 2 files changed, 69 insertions(+), 29 deletions(-) diff --git a/app/server.js b/app/server.js index df099f46..cbbc5813 100644 --- a/app/server.js +++ b/app/server.js @@ -13,17 +13,6 @@ const users = require("../initial-data/users.json"); const brands = require("../initial-data/brands.json"); const products = require("../initial-data/products.json"); -// const authenticate = (req, res, next) => { -// try { -// const token = req.headers.authorization.split(" ")[1]; // Bearer -// const decoded = jwt.verify(token, "secretKey"); -// req.userId = decoded.userId; -// next(); -// } catch (error) { -// res.status(401).json({ message: "Authentication failed" }); -// } -// }; - // Error handling app.use((err, req, res, next) => { console.error(err.stack); @@ -35,12 +24,10 @@ app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); app.get("/brands", (request, response) => { response.send(brands); - return response.end(); }); app.get("/products", (request, response) => { response.send(products); - return response.end(); }); app.get("/brands/:id/products", (request, response) => { @@ -53,16 +40,11 @@ app.get("/brands/:id/products", (request, response) => { if (filteredProducts.length === 0) { response.status(404); response.send({ message: "No products found" }); - return response.end(); } else { response.send(filteredProducts); - return response.end(); } }); -let token = jwt.sign("user", "secretKey"); -console.log(token); - app.post("/login", (request, response) => { const { username, password } = request.body; @@ -73,19 +55,44 @@ app.post("/login", (request, response) => { if (!username || !password) { response.status(401); response.send({ message: "Both a username and password are required" }); - return response.end(); } if (user) { - let token = jwt.sign({ user }, "secretKey", { expiresIn: "1hr" }); + try { + let token = jwt.sign({ user }, "secretKey", { expiresIn: "1hr" }); + } catch (err) { + console.log(err); + } response.status(200); response.set("Authorization", token); response.send(user); - return response.end(); } else { response.status(401); response.send({ message: "Incorrect username or password" }); - return response.end(); + } +}); + +const authenticateUser = (req, res, next) => { + try { + const token = req.header.authorization.split(" ")[1]; // Bearer + const decoded = jwt.verify(token, "secretKey"); + req.body.username = decoded.username; + next(); + } catch (error) { + res.status(401); + res.send({ message: "Authentication failed" }); + } +}; + +app.get("/me/cart", authenticateUser, (request, response) => { + const user = users.find((user) => user.username == request.body.username); + + if (!user) { + response.status(404); + response.send({ message: "User not found" }); + } else { + response.status(200); + response.send(user.cart); } }); diff --git a/test/server.test.js b/test/server.test.js index e15d8bd8..74433202 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -140,13 +140,46 @@ describe("Login", () => { }); describe("Cart", () => { - // describe('/GET me/cart', () => { - // it('gets the cart for an authenticated user', () => { - // //arrange: login user/get user cart - // // write test logic here - // done() - // }) - // }) + describe("/GET me/cart", () => { + it("gets the cart for an authenticated user", (done) => { + //arrange: login user/get user cart + // write test logic here + let login = { + username: "yellowleopard753", + password: "jonjon", + }; + + chai + .request(server) + .post("/login") + .send(login) + .end((err, res) => { + const token = res.header.authorization.split(".")[1]; + console.log(token); + chai + .request(server) + .get("/me/cart") + .set("Authorization", token) + .end((err, res) => { + res.should.have.status(200); + done(); + }); + }); + }); + + it("throws error if user is not authenticated", (done) => { + //arrange: login user/get user cart + // write test logic here + chai + .request(server) + .get("/me/cart") + .end((err, res) => { + res.should.have.status(401); + res.body.message.should.be.eql("Authentication failed"); + done(); + }); + }); + }); // // // From ea114a47b0ada7ce5fb3081e20c23d47cff52555 Mon Sep 17 00:00:00 2001 From: acl13 <128934431+acl13@users.noreply.github.com> Date: Thu, 6 Mar 2025 21:20:18 -0500 Subject: [PATCH 07/10] write & test GET /me/cart endpoint --- app/server.js | 34 ++++++++++++---------------------- test/server.test.js | 11 +++-------- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/app/server.js b/app/server.js index cbbc5813..8db68183 100644 --- a/app/server.js +++ b/app/server.js @@ -3,7 +3,7 @@ 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 swaggerDocument = YAML.load("./swagger.yaml"); const app = express(); app.use(bodyParser.json()); @@ -58,13 +58,9 @@ app.post("/login", (request, response) => { } if (user) { - try { - let token = jwt.sign({ user }, "secretKey", { expiresIn: "1hr" }); - } catch (err) { - console.log(err); - } + let hash = user.login.sha256; response.status(200); - response.set("Authorization", token); + response.set("Authorization", hash); response.send(user); } else { response.status(401); @@ -72,24 +68,18 @@ app.post("/login", (request, response) => { } }); -const authenticateUser = (req, res, next) => { - try { - const token = req.header.authorization.split(" ")[1]; // Bearer - const decoded = jwt.verify(token, "secretKey"); - req.body.username = decoded.username; - next(); - } catch (error) { - res.status(401); - res.send({ message: "Authentication failed" }); - } +const getAuthenticatedUser = (request) => { + let authenticatedUser = users.find( + (user) => user.login.sha256 == request.headers.authorization + ); + return authenticatedUser; }; -app.get("/me/cart", authenticateUser, (request, response) => { - const user = users.find((user) => user.username == request.body.username); - +app.get("/me/cart", (request, response) => { + let user = getAuthenticatedUser(request); if (!user) { - response.status(404); - response.send({ message: "User not found" }); + response.status(401); + response.send({ message: "Authentication failed" }); } else { response.status(200); response.send(user.cart); diff --git a/test/server.test.js b/test/server.test.js index 74433202..a5382489 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -97,7 +97,6 @@ describe("Login", () => { }); }); - //add test for error handling it("throws error for incomplete login", (done) => { let login = { username: "Bob", @@ -142,8 +141,6 @@ describe("Login", () => { describe("Cart", () => { describe("/GET me/cart", () => { it("gets the cart for an authenticated user", (done) => { - //arrange: login user/get user cart - // write test logic here let login = { username: "yellowleopard753", password: "jonjon", @@ -154,22 +151,20 @@ describe("Cart", () => { .post("/login") .send(login) .end((err, res) => { - const token = res.header.authorization.split(".")[1]; - console.log(token); + const hash = res.body.login.sha256; chai .request(server) .get("/me/cart") - .set("Authorization", token) + .set("Authorization", hash) .end((err, res) => { res.should.have.status(200); + res.body.should.be.an("array"); done(); }); }); }); it("throws error if user is not authenticated", (done) => { - //arrange: login user/get user cart - // write test logic here chai .request(server) .get("/me/cart") From 36744ab17b56f1ba4f382c6ec47b7107f5b9aaba Mon Sep 17 00:00:00 2001 From: acl13 <128934431+acl13@users.noreply.github.com> Date: Fri, 7 Mar 2025 11:05:05 -0500 Subject: [PATCH 08/10] write and test POST /me/cart endpoint --- app/server.js | 34 ++++++++++++-- test/server.test.js | 107 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 123 insertions(+), 18 deletions(-) diff --git a/app/server.js b/app/server.js index 8db68183..65ce1b2e 100644 --- a/app/server.js +++ b/app/server.js @@ -58,9 +58,9 @@ app.post("/login", (request, response) => { } if (user) { - let hash = user.login.sha256; + const token = jwt.sign({ user }, "secretKey", { expiresIn: "1hr" }); + response.set("authorization", `Bearer ${token}`); response.status(200); - response.set("Authorization", hash); response.send(user); } else { response.status(401); @@ -69,14 +69,21 @@ app.post("/login", (request, response) => { }); const getAuthenticatedUser = (request) => { + const authHeader = request.headers["authorization"]; + if (!authHeader) { + return; + } + const token = authHeader.split(" ")[1]; + const decoded = jwt.verify(token, "secretKey"); + let authenticatedUser = users.find( - (user) => user.login.sha256 == request.headers.authorization + (user) => user.login.username == decoded.user.login.username ); return authenticatedUser; }; app.get("/me/cart", (request, response) => { - let user = getAuthenticatedUser(request); + const user = getAuthenticatedUser(request); if (!user) { response.status(401); response.send({ message: "Authentication failed" }); @@ -86,6 +93,25 @@ app.get("/me/cart", (request, response) => { } }); +app.post("/me/cart", (request, response) => { + const user = getAuthenticatedUser(request); + if (!user) { + response.status(401); + response.send({ message: "Authentication failed" }); + } + + const product = products.find((product) => product.id == request.body.id); + + if (!product) { + response.status(404); + response.send({ message: "Product not found" }); + } + + user.cart.push(product); + response.status(200); + response.send(user.cart); +}); + // Starting the server const PORT = process.env.PORT || 3000; app.listen(PORT, () => { diff --git a/test/server.test.js b/test/server.test.js index a5382489..0ef8c9cf 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -151,11 +151,11 @@ describe("Cart", () => { .post("/login") .send(login) .end((err, res) => { - const hash = res.body.login.sha256; + const token = res.headers.authorization; chai .request(server) .get("/me/cart") - .set("Authorization", hash) + .set("authorization", token) .end((err, res) => { res.should.have.status(200); res.body.should.be.an("array"); @@ -175,18 +175,97 @@ describe("Cart", () => { }); }); }); - // - // - // - // describe("/POST me/cart", () => { - // it("adds an item to user's cart", (done) => { - // //arrange: login user, need product id in request body? - // //write test logic here - // done(); - // }); - // }); - // - // + + describe("/POST me/cart", () => { + it("adds an item to user's cart", (done) => { + const product = { + id: "1", + categoryId: "1", + name: "Superglasses", + description: "The best glasses in the world", + price: 150, + 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", + ], + }; + + let login = { + username: "yellowleopard753", + password: "jonjon", + }; + + chai + .request(server) + .post("/login") + .send(login) + .end((err, res) => { + const token = res.headers.authorization; + chai + .request(server) + .post("/me/cart") + .set("authorization", token) + .send(product) + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an("array"); + res.body.should.have.length(1); + done(); + }); + }); + }); + + it("throws error if product is not in catalog", (done) => { + const product = { + id: "12", + categoryId: "1", + name: "Fake glasses", + description: "These glasses do not exist", + price: 100, + 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", + ], + }; + + let login = { + username: "yellowleopard753", + password: "jonjon", + }; + + chai + .request(server) + .post("/login") + .send(login) + .end((err, res) => { + const token = res.headers.authorization; + chai + .request(server) + .post("/me/cart") + .set("authorization", token) + .send(product) + .end((err, res) => { + res.should.have.status(404); + res.body.message.should.be.eql("Product not found"); + done(); + }); + }); + }); + + it("throws error if user is not authenticated", (done) => { + chai + .request(server) + .post("/me/cart") + .end((err, res) => { + res.should.have.status(401); + res.body.message.should.be.eql("Authentication failed"); + done(); + }); + }); + }); + // // describe("/POST me/cart/:productId", () => { // it("Updates the quantity of a particular item in the user's cart", (done) => { From 00043ea949186e4921ddf7bd3a3811a0312556f4 Mon Sep 17 00:00:00 2001 From: acl13 <128934431+acl13@users.noreply.github.com> Date: Fri, 7 Mar 2025 17:09:48 -0500 Subject: [PATCH 09/10] write and test POST /me/cart/:productId endpoint --- app/server.js | 58 ++++++++++ initial-data/users.json | 228 ++++++++++++++++++++++------------------ test/server.test.js | 171 ++++++++++++++++++++++++++---- 3 files changed, 337 insertions(+), 120 deletions(-) diff --git a/app/server.js b/app/server.js index 65ce1b2e..cc79feae 100644 --- a/app/server.js +++ b/app/server.js @@ -112,6 +112,64 @@ app.post("/me/cart", (request, response) => { response.send(user.cart); }); +app.delete("/me/cart/:productId", (request, response) => { + const user = getAuthenticatedUser(request); + if (!user) { + response.status(401); + response.send({ message: "Authentication failed" }); + } + + const product = products.find( + (product) => product.id == request.params.productId + ); + + if (!product) { + response.status(404); + response.send({ message: "Product not found" }); + } + + const productIndex = user.cart.indexOf(product); + user.cart.splice(productIndex, 1); + response.status(200); + response.send(user.cart); +}); + +app.post("/me/cart/:productId", (request, response) => { + const user = getAuthenticatedUser(request); + if (!user) { + response.status(401); + response.send({ message: "Authentication failed" }); + } + + const product = products.find( + (product) => product.id == request.params.productId + ); + + if (!product) { + response.status(404); + response.send({ message: "Product not found" }); + } + + // Something like an "amountInCart" property on each product would be much easier to update, but this is what I could get working with the provided data + const currentAmount = user.cart.filter( + (product) => product.id == request.params.productId + ).length; + const desiredAmount = request.body.amount; + + if (desiredAmount > currentAmount) { + for (let i = currentAmount; i < desiredAmount; i++) { + user.cart.push(product); + } + } else if (desiredAmount < currentAmount) { + for (let i = desiredAmount; i < currentAmount; i++) { + const productIndex = user.cart.indexOf(product); + user.cart.splice(productIndex, 1); + } + } + response.status(200); + response.send(user.cart); +}); + // Starting the server const PORT = process.env.PORT || 3000; app.listen(PORT, () => { diff --git a/initial-data/users.json b/initial-data/users.json index 9a6231e8..ec54581f 100644 --- a/initial-data/users.json +++ b/initial-data/users.json @@ -1,104 +1,130 @@ [ - { - "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": "2", + "categoryId": "1", + "name": "Black Sunglasses", + "description": "The best glasses in the world", + "price": 100, + "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" + ] + } + ], + "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": [ + { + "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" + ] + } + ], + "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/test/server.test.js b/test/server.test.js index 0ef8c9cf..321df3d5 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -266,23 +266,156 @@ describe("Cart", () => { }); }); - // - // describe("/POST me/cart/:productId", () => { - // it("Updates the quantity of a particular item in the user's cart", (done) => { - // // arrange: login user - // // will need to check cart quantity? - // // write test logic here - // done(); - // }); - // }); - // - // - // - // describe("/DELETE me/cart/:productId", () => { - // it("deletes an item from the cart", (done) => { - // // arrange: login user - // // write test logic here - // done(); - // }); - // }); + describe("/POST me/cart/:productId", () => { + it("Updates the quantity of a particular item in the user's cart (increase)", (done) => { + let login = { + username: "lazywolf342", + password: "tucker", + }; + + chai + .request(server) + .post("/login") + .send(login) + .end((err, res) => { + const token = res.headers.authorization; + chai + .request(server) + .post("/me/cart/2") + .set("authorization", token) + .send({ amount: 3 }) + .end((err, res) => { + console.log(res.body); + res.should.have.status(200); + res.body.should.be.an("array"); + res.body.should.have.length(3); + done(); + }); + }); + }); + + it("Updates the quantity of a particular item in the user's cart (decrease)", (done) => { + let login = { + username: "lazywolf342", + password: "tucker", + }; + + chai + .request(server) + .post("/login") + .send(login) + .end((err, res) => { + const token = res.headers.authorization; + chai + .request(server) + .post("/me/cart/2") + .set("authorization", token) + .send({ amount: 2 }) + .end((err, res) => { + console.log(res.body); + res.should.have.status(200); + res.body.should.be.an("array"); + res.body.should.have.length(2); + done(); + }); + }); + }); + + it("throws error if product is not in cart", (done) => { + let login = { + username: "yellowleopard753", + password: "jonjon", + }; + + chai + .request(server) + .post("/login") + .send(login) + .end((err, res) => { + const token = res.headers.authorization; + chai + .request(server) + .post("/me/cart/13") + .set("authorization", token) + .end((err, res) => { + res.should.have.status(404); + res.body.message.should.be.eql("Product not found"); + done(); + }); + }); + }); + + it("throws error if user is not authenticated", (done) => { + chai + .request(server) + .post("/me/cart/1") + .end((err, res) => { + res.should.have.status(401); + res.body.message.should.be.eql("Authentication failed"); + done(); + }); + }); + }); + + describe("/DELETE me/cart/:productId", () => { + it("deletes an item from the cart", (done) => { + let login = { + username: "greenlion235", + password: "waters", + }; + + chai + .request(server) + .post("/login") + .send(login) + .end((err, res) => { + const token = res.headers.authorization; + chai + .request(server) + .delete("/me/cart/7") + .set("authorization", token) + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an("array"); + res.body.should.have.length(0); + done(); + }); + }); + }); + + it("throws error if product is not in cart", (done) => { + let login = { + username: "yellowleopard753", + password: "jonjon", + }; + + chai + .request(server) + .post("/login") + .send(login) + .end((err, res) => { + const token = res.headers.authorization; + chai + .request(server) + .delete("/me/cart/13") + .set("authorization", token) + .end((err, res) => { + res.should.have.status(404); + res.body.message.should.be.eql("Product not found"); + done(); + }); + }); + }); + + it("throws error if user is not authenticated", (done) => { + chai + .request(server) + .delete("/me/cart/1") + .end((err, res) => { + res.should.have.status(401); + res.body.message.should.be.eql("Authentication failed"); + done(); + }); + }); + }); }); From e5d56933a035fb75c4941aa3d9c5334735d286a1 Mon Sep 17 00:00:00 2001 From: acl13 <128934431+acl13@users.noreply.github.com> Date: Fri, 7 Mar 2025 17:22:23 -0500 Subject: [PATCH 10/10] add project description to README --- README.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 96516fae..5523313b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,51 @@ -## Sunglasses.io Server +# Sunglasses.io Server -This project has been created by a student at Project Shift, a software engineering fellowship located in Downtown Durham. The work in this repository is wholly of the student based on a sample starter project that can be accessed by looking at the repository that this project forks. +This project is an API for an e-commerce store selling sunglasses. -If you have any questions about this project or the program in general, visit projectshift.io or email hello@projectshift.io. +## Routes + +The following routes are supported by this API + +``` +GET /brands +GET /brands/:id/products +GET /products +POST /login +GET /me/cart +DELETE /me/cart/:productId +POST /me/cart/:productId +``` + +Documentation for each route can be found in the swagger.yaml file + +## Getting Started + +### Prerequisites + +- Node.js (version 14 or above) +- npm + +### Installation + +1. Clone the repository: + + ```bash + git clone https://github.com/acl13/sunglasses-io.git + cd sunglasses-io + ``` + +2. Install dependencies: + + ```bash + npm install + ``` + +### Testing + +```bash + npm test +``` + +This project has been created by a student at Parsity, an online software engineering course. The work in this repository is wholly of the student based on a sample starter project that can be accessed by looking at the repository that this project forks. + +If you have any questions about this project or the program in general, visit [parsity.io](https://parsity.io/) or email hello@parsity.io.