Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
Binary file added BACKEND/.DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions BACKEND/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SECRET_KEY=""
3 changes: 3 additions & 0 deletions BACKEND/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
.env
note
269 changes: 269 additions & 0 deletions BACKEND/__test__/backend.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
const app = require("../app");
const request = require("supertest");
const { test, expect, beforeAll, afterAll } = require("@jest/globals");
const { User, Product, Category, Cart, sequelize } = require("../models");
const { hashPassword } = require("../helpers/bcrypt");
const { signToken } = require("../helpers/jwt");
const { queryInterface } = sequelize;
const fs = require("fs").promises;

let access_token_admin;
let access_token_user;

beforeAll(async () => {
try {
const usersData = await fs.readFile("./data/users.json", "utf8");
const users = JSON.parse(usersData).map((user) => {
if (!user.email) {
throw new Error("User missing email in users.json");
}
if (!user.password) {
throw new Error("User missing password in users.json");
}
return {
...user,
createdAt: new Date(),
updatedAt: new Date(),
password: hashPassword(user.password),
role: user.role || "user",
};
});

const categoriesData = await fs.readFile("./data/categories.json", "utf8");
const categories = JSON.parse(categoriesData).map((category) => ({
...category,
createdAt: new Date(),
updatedAt: new Date(),
}));

const productsData = await fs.readFile("./data/products.json", "utf8");
const products = JSON.parse(productsData).map((product) => ({
...product,
createdAt: new Date(),
updatedAt: new Date(),
}));

const cartsData = await fs.readFile("./data/carts.json", "utf8");
const carts = JSON.parse(cartsData).map((cart) => ({
...cart,
createdAt: new Date(),
updatedAt: new Date(),
}));

await queryInterface.bulkDelete("Carts", null, {
truncate: true,
restartIdentity: true,
cascade: true,
});
await queryInterface.bulkDelete("Products", null, {
truncate: true,
restartIdentity: true,
cascade: true,
});
await queryInterface.bulkDelete("Categories", null, {
truncate: true,
restartIdentity: true,
cascade: true,
});
await queryInterface.bulkDelete("Users", null, {
truncate: true,
restartIdentity: true,
cascade: true,
});

await queryInterface.bulkInsert("Users", users);
await queryInterface.bulkInsert("Categories", categories);
await queryInterface.bulkInsert("Products", products);
await queryInterface.bulkInsert("Carts", carts);

const adminUser = await User.findOne({
where: { email: "admin@example.com" },
});
if (!adminUser) {
throw new Error("Admin user not found in database");
}
access_token_admin = signToken({ id: adminUser.id });

const regularUser = await User.findOne({
where: { email: "user@example.com" },
});
if (!regularUser) {
throw new Error("Regular user not found in database");
}
access_token_user = signToken({ id: regularUser.id });
} catch (error) {
console.error("Error in beforeAll:", error);
throw error;
}
});

afterAll(async () => {
await queryInterface.bulkDelete("Carts", null, {
truncate: true,
restartIdentity: true,
cascade: true,
});
await queryInterface.bulkDelete("Products", null, {
truncate: true,
restartIdentity: true,
cascade: true,
});
await queryInterface.bulkDelete("Categories", null, {
truncate: true,
restartIdentity: true,
cascade: true,
});
await queryInterface.bulkDelete("Users", null, {
truncate: true,
restartIdentity: true,
cascade: true,
});
});

describe("POST /login", () => {
test("should login successfully and return access_token", async () => {
const requestBody = {
email: "admin@example.com",
password: "admin123",
};

const response = await request(app).post("/login").send(requestBody);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty("access_token");
});

test("should fail if email is missing", async () => {
const requestBody = {
password: "admin123",
};

const response = await request(app).post("/login").send(requestBody);
expect(response.status).toBe(400);
expect(response.body).toHaveProperty("message", "Email is required");
});

test("should fail if password is missing", async () => {
const requestBody = {
email: "admin@example.com",
};

const response = await request(app).post("/login").send(requestBody);
expect(response.status).toBe(400);
expect(response.body).toHaveProperty("message", "Password is required");
});

test("should fail if email is not registered", async () => {
const requestBody = {
email: "nonexistent@example.com",
password: "admin123",
};

const response = await request(app).post("/login").send(requestBody);
expect(response.status).toBe(401);
expect(response.body).toHaveProperty("message", "Invalid email/password");
});

test("should fail if password is incorrect", async () => {
const requestBody = {
email: "admin@example.com",
password: "wrongpassword",
};

const response = await request(app).post("/login").send(requestBody);
expect(response.status).toBe(401);
expect(response.body).toHaveProperty("message", "Invalid email/password");
});
});

describe("POST /products", () => {
test("should create a product successfully", async () => {
const product = {
name: "Cubic Lemari Pakaian Minimalis / Wardrobe Baju / LUNA LPM 301 - SonomaBl - Grey",
description:
"Store your favorite clothes in the LUNA LPM 301 wardrobe, equipped with racks, a table, and a beautiful mirror that can be opened.",
price: 1029000,
stock: 587,
imgUrl:
"https://res.cloudinary.com/dpjqm8kkk/image/upload/v1723522920/hacktiv8/branded/cubic-lemari-pakaian-minimalis-wardrobe-baju-luna-lpm-301-sonomabl-grey-qswxtjtx14i.jpg",
categoryId: 2,
userId: 1,
};

const response = await request(app)
.post("/products")
.set("Authorization", `Bearer ${access_token_admin}`)
.send(product);

expect(response.status).toBe(201);
expect(response.body).toHaveProperty("id");
expect(response.body).toHaveProperty("name", product.name);
expect(response.body).toHaveProperty("description", product.description);
expect(response.body).toHaveProperty("price", product.price);
expect(response.body).toHaveProperty("stock", product.stock);
expect(response.body).toHaveProperty("imgUrl", product.imgUrl);
expect(response.body).toHaveProperty("categoryId", product.categoryId);
expect(response.body).toHaveProperty("userId", product.userId);

// Verify product is saved in the database
const savedProduct = await Product.findOne({
where: { name: product.name },
});
expect(savedProduct).toBeTruthy();
});

test("should fail with invalid token", async () => {
const product = {
name: "Kota kembang Bandung",
description: "Tourist attractions and snacks in Bandung",
price: 100000,
stock: 10,
imgUrl: "https://suarautama.id/wp-content/uploads/2024/10/images-15.jpeg",
categoryId: 1,
userId: 2,
};

const response = await request(app)
.post("/products")
.set("Authorization", "Bearer invalid_token")
.send(product);

expect(response.status).toBe(401);
expect(response.body).toHaveProperty("message", "Unauthorized access");
});

test("should fail when required fields are missing", async () => {
const product = {
description: "Tourist attractions and snacks in Bandung",
imgUrl: "https://suarautama.id/wp-content/uploads/2024/10/images-15.jpeg",
categoryId: 1,
};

const response = await request(app)
.post("/products")
.set("Authorization", `Bearer ${access_token_admin}`)
.send(product);

expect(response.status).toBe(400);
expect(response.body).toHaveProperty("message", "Name is required!");
});

test("should fail for non-admin user", async () => {
const product = {
name: "Non-Admin Product",
description: "A product created by a non-admin user",
price: 500000,
stock: 100,
imgUrl: "https://example.com/image.jpg",
categoryId: 1,
userId: 2,
};

const response = await request(app)
.post("/products")
.set("Authorization", `Bearer ${access_token_user}`)
.send(product);

expect(response.status).toBe(403);
expect(response.body).toHaveProperty("message", "Admin access required");
});
});
66 changes: 66 additions & 0 deletions BACKEND/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
if(process.env.NODE_ENV !== "production"){
require("dotenv").config();
}

const express = require("express");
const ProductController = require("./controllers/ProductController");
const CategoryController = require("./controllers/CategoryController");
const UserController = require("./controllers/UserController");
const CartController = require("./controllers/CartController");
const authentication = require("./middlewares/authentication");
const { authorizationAdmin } = require("./middlewares/authorization");
const errorHandler = require("./middlewares/errorHandler");


const app = express();
const port = process.env.PORT || 3000;

const cors = require("cors")
app.use(cors())

app.use(express.urlencoded({ extended: true }));
app.use(express.json());


// User endpoints
app.post("/register", UserController.register);
app.post("/login", UserController.login);
app.post("/login/google", UserController.googleLogin);

app.use(authentication);

// Product endpoints
app.get("/products", ProductController.getProduct);
app.get("/products/:id", ProductController.getProductById);
app.post("/products",authorizationAdmin, ProductController.postProduct);
app.put("/products/:id",authorizationAdmin, ProductController.putProductById);
app.delete("/products/:id",authorizationAdmin, ProductController.deleteProductById);

// Category endpoints
app.get("/categories", CategoryController.getCategory);
app.get("/categories/:id", CategoryController.getCategorybyId);
app.post("/categories",authorizationAdmin, CategoryController.postCategory);
app.put(
"/categories/:id",
authorizationAdmin, CategoryController.updateCategoryById
);
app.delete(
"/categories/:id",
authorizationAdmin, CategoryController.deleteCategoryById
);

// Cart endpoints
app.post("/carts", CartController.postCart);
app.get("/carts", CartController.getCart);
app.put("/carts/:id", CartController.updateCartItem);
app.delete("/carts/:id", CartController.deleteCartItem);

// Error handler middleware
app.use(errorHandler);

app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});


module.exports = app
19 changes: 19 additions & 0 deletions BACKEND/config/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"development": {
"username": "postgres",
"password": "postgres",
"database": "database_ecommerce",
"host": "127.0.0.1",
"dialect": "postgres"
},
"test": {
"username": "postgres",
"password": "postgres",
"database": "database_ecommerce",
"host": "127.0.0.1",
"dialect": "postgres"
},
"production": {
"use_env_variable": "DATABASE_URL"
}
}
Loading