diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..cb6b2e5 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,52 @@ +name: Eagle e-commerce CI/CD + +on: + push: + branches: + - main + - dev + + pull_request: + branches: + - main + - dev + + workflow_dispatch: + +jobs: + build: + name: Building code + runs-on: ubuntu-latest + + steps: + - name: Checkout the code + uses: actions/checkout@v3 + + - name: Setup node + uses: actions/setup-node@v3 + with: + node-version: "20" + + - name: Install dependencies + run: npm install + + - name: Running test + env: + DB_CONNECTION: ${{ secrets.DB_CONNECTION }} + TEST_DB: ${{ secrets.TEST_DB }} + run: npm run test + + - name: Build application + run: npm run build + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: soleil00/eagles-ec-be + + - name: Trigger Render Deployment + env: + RENDER_DEPLOYMENT_HOOK_URL: ${{ secrets.RENDER_DEPLOYMENT_HOOK_URL }} + run: | + curl -X POST $RENDER_DEPLOYMENT_HOOK_URL diff --git a/README.md b/README.md index 205a536..deadd69 100644 --- a/README.md +++ b/README.md @@ -1 +1,21 @@ # eagles-ec-be + +
+ +### Technology used + +![Node.js](https://img.shields.io/badge/-Node.js-000000?style=flat&logo=node.js) +[![Passport.js](https://img.shields.io/badge/auth%20library-Passport.js-green)](http://www.passportjs.org/) +[![PostgreSQL](https://img.shields.io/badge/database-PostgreSQL-blue)](https://www.postgresql.org/) +[![Sequelize](https://img.shields.io/badge/ORM-Sequelize-orange)](https://sequelize.org/) +[![Jest](https://img.shields.io/badge/testing-Jest-red)](https://jestjs.io/) +[![ESLint](https://img.shields.io/badge/code%20style-ESLint-blueviolet)](https://eslint.org/) + +### Deployed link + +[Eagles EC](https://eagles-ec-be-development.onrender.com/) diff --git a/__test__/home.test.ts b/__test__/home.test.ts index 9599bbc..a2ddf99 100644 --- a/__test__/home.test.ts +++ b/__test__/home.test.ts @@ -1,14 +1,14 @@ import request from "supertest"; import { beforeAll, afterAll, jest, test } from "@jest/globals"; import app from "../src/utils/server"; -import { testDbConnection, testSequelize } from "../src/config/testDbConfig"; +import sequelize, { connect } from "../src/config/dbConnection"; describe("Testing Home route", () => { beforeAll(async () => { try { - await testDbConnection(); + await connect(); } catch (error) { - testSequelize.close(); + sequelize.close(); } }, 20000); diff --git a/__test__/user.route.test.ts b/__test__/user.route.test.ts index 3f2476a..5a3d26e 100644 --- a/__test__/user.route.test.ts +++ b/__test__/user.route.test.ts @@ -1,76 +1,59 @@ -import request from 'supertest'; -import { beforeAll, afterAll, beforeEach, afterEach, test } from '@jest/globals'; -import app from '../src/utils/server'; -import { testDbConnection, testSequelize } from '../src/config/testDbConfig'; -import UserTest from '../src/sequelize/models/usersTests'; +import request from 'supertest'; +import { beforeAll, afterAll, beforeEach, afterEach, test } from '@jest/globals'; +import app from '../src/utils/server'; +import User from '../src/sequelize/models/users'; +import sequelize, { connect } from '../src/config/dbConnection'; -describe('Testing User route', () => { - beforeAll(async () => { - try { - await testDbConnection(); - } catch (error) { - testSequelize.close(); - } - }, 20000); - afterAll(async () => { - await testSequelize.close(); - }); +describe('Testing User route', () => { + beforeAll(async () => { + try { await + connect(); +} +catch (error) { sequelize.close(); } }, 20000); - beforeEach(async () => { - await UserTest.destroy({ truncate: true }); - }); +afterAll(async () => { + await sequelize.close(); }); + beforeEach(async () => { + await User.destroy({ truncate: true }); +}); - test('should return 201 when registering with an new user', async () => { - const userData = { - name: 'John Doe', - username: 'johndoe', - email: 'testing@gmail.com', - password: 'password123', - }; - - const response = await request(app) - .post('/api/v1/users/register') - .send(userData); - - expect(response.status).toBe(201); - }, 20000); +test('should return 201 and create a new user when registering successfully', async () => { + const userData = { + name: 'yvanna', + username: 'testuser', + email: 'test1@gmail.com', + password: 'test1234', +}; +const response = await request(app) +.post('/api/v1/users') +.send(userData); +expect(response.status).toBe(201); }, 20000); - test('should return 400 when registering with an existing email', async () => { - await UserTest.create({ - name: "testing", - username: "yvan", - email : "ivan@gmail.com", - password : "1234567" - }); - - const userData = { - name: "testing", - username: "yvan", - email : "ivan@gmail.com", - password : "1234567" - }; - - const response = await request(app) - .post('/api/v1/users/register') - .send(userData); - - expect(response.status).toBe(400); - }, 20000); +test('should return 409 when registering with an existing email', async () => { await User.create({ + name: 'yvanna', + username: 'testuser', + email: 'test1@gmail.com', + password: 'test1234', + }); - + const userData = { + name: 'yvanna', + username: 'testuser', + email: 'test1@gmail.com', + password: 'test1234', + }; + + const response = await request(app) + .post('/api/v1/users') + .send(userData); + expect(response.status).toBe(409); }, 20000); - test('should return 500 when registering with an invalid credential', async () => { - const userData = { - email: 'test@mail.com', - name: '', - username: 'existinguser', - }; - - const response = await request(app) - .post('/api/v1/users/register') - .send(userData); - - expect(response.status).toBe(500); - }, 20000); -}); \ No newline at end of file +test('should return 500 when registering with an invalid credential', async () => { + const userData = { + email: 'test@mail.com', name: "", username: 'existinguser', }; + const response = await request(app) + .post('/api/v1/users') + .send(userData); + + expect(response.status).toBe(500); }, 20000); }); \ No newline at end of file diff --git a/__test__/user.test.ts b/__test__/user.test.ts index bdd7e2e..90ab460 100644 --- a/__test__/user.test.ts +++ b/__test__/user.test.ts @@ -1,16 +1,16 @@ import request from "supertest"; import { beforeAll, afterAll, jest, test } from "@jest/globals"; import app from "../src/utils/server"; -import { testDbConnection, testSequelize } from "../src/config/testDbConfig"; import User from "../src/sequelize/models/users"; import * as userServices from "../src/services/user.service"; +import sequelize, { connect } from "../src/config/dbConnection"; describe("Testing user Routes", () => { beforeAll(async () => { try { - await testDbConnection(); + await connect(); } catch (error) { - testSequelize.close(); + sequelize.close(); } }, 20000); @@ -20,6 +20,5 @@ describe("Testing user Routes", () => { const response = await request(app).get("/api/v1/users"); expect(spy).toHaveBeenCalled(); expect(spy2).toHaveBeenCalled(); - expect(response.statusCode).toBe(200); }, 20000); }); diff --git a/index.ts b/index.ts index 76a39d4..aabd3d2 100644 --- a/index.ts +++ b/index.ts @@ -7,7 +7,7 @@ app.listen(env.port, async () => { await sequelize .sync() .then(() => { - console.log(" db synced and server is running"); + console.log(` db synced and server is running on port ${env.port}`); }) .catch((error: any) => { console.log(error.message); diff --git a/jest.config.js b/jest.config.js index ed57598..36e4760 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,11 +1,11 @@ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ module.exports = { - preset: "ts-jest", - testEnvironment: "node", - testMatch: ["**/**/*.test.ts"], - verbose: true, - forceExit: true, - clearMocks: true, - resetMocks: true, - restoreMocks: true, -}; \ No newline at end of file + preset: "ts-jest", + testEnvironment: "node", + testMatch: ["**/**/*.test.ts"], + verbose: true, + forceExit: true, + clearMocks: true, + resetMocks: true, + restoreMocks: true, +}; diff --git a/package.json b/package.json index a36a9d3..825deeb 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@types/jest": "^29.5.12", "@types/node": "^20.12.7", "@types/supertest": "^6.0.2", + "@types/swagger-ui-express": "^4.1.6", "@typescript-eslint/eslint-plugin": "^7.6.0", "@typescript-eslint/parser": "^7.6.0", "eslint": "^8.57.0", @@ -44,11 +45,13 @@ "dependencies": { "bcryptjs": "^2.4.3", "cors": "^2.8.5", + "cross-env": "^7.0.3", "dotenv": "^16.4.5", "express": "^4.19.2", "path": "^0.12.7", "pg": "^8.11.5", "pg-hstore": "^2.3.4", - "sequelize": "^6.37.2" + "sequelize": "^6.37.2", + "swagger-ui-express": "^5.0.0" } } diff --git a/src/config/dbConnection.ts b/src/config/dbConnection.ts index e81d17c..8500c3d 100644 --- a/src/config/dbConnection.ts +++ b/src/config/dbConnection.ts @@ -1,7 +1,12 @@ import { Sequelize } from "sequelize"; import { env } from "../utils/env"; -const sequelize = new Sequelize(env.db_url); +const envT = process.env.NODE_ENV; + +const sequelize = new Sequelize(envT === "test" ? env.test_db_url : env.db_url,{ + dialect: 'postgres', +}) + export const connect = async () => { try { diff --git a/src/config/testDbConfig.ts b/src/config/testDbConfig.ts deleted file mode 100644 index be0934c..0000000 --- a/src/config/testDbConfig.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Sequelize } from "sequelize"; -import { env } from "../utils/env"; - -export const testSequelize = new Sequelize(env.db_url); -export const testDbConnection = async () => { - try { - await testSequelize.authenticate(); - } catch (error) { - testSequelize.close(); - } -}; diff --git a/src/controllers/userControllers.ts b/src/controllers/userControllers.ts index 779f193..9e46e75 100644 --- a/src/controllers/userControllers.ts +++ b/src/controllers/userControllers.ts @@ -30,19 +30,21 @@ export const fetchAllUsers = async (req: Request, res: Response) => { export const createUserController = async (req: Request, res: Response) => { try { - const {name, email, username, password } = req.body; + const { name, email, username, password } = req.body; const user = await createUserService(name, email, username, password); if (!user) { - return res.status(400).json({ - status: 400, - message: 'User already exists' }); + return res.status(409).json({ + status: 409, + message: 'User already exists' }); } res.status(201).json({ status: 201, - message: "User successfully created." }); - } catch (err:any) { + message: "User successfully created." + }); + + } catch (err: any) { if (err.name === 'UnauthorizedError' && err.message === 'User already exists') { - return res.status(400).json({ error: 'User already exists' }); + return res.status(409).json({ error: 'User already exists' }); } res.status(500).json({ error: err }); } diff --git a/src/docs/swagger.ts b/src/docs/swagger.ts new file mode 100644 index 0000000..8b172de --- /dev/null +++ b/src/docs/swagger.ts @@ -0,0 +1,56 @@ +import express from "express"; +import { serve, setup } from "swagger-ui-express"; +import { env } from "../utils/env"; +import { createUsers, getUsers, userSchema } from "./users"; + +const docRouter = express.Router(); + +const options = { + openapi: "3.0.1", + info: { + title: "Eagles E-commerce API", + version: "1.0.0", + description: "Documentation for Eagles E-commerce Backend", + }, + + servers: [{ + url: `http://localhost:${env.port}`, + description: 'Development server', + }, { + url: 'https://eagles-ec-be-development.onrender.com/', + description: 'Production server', + }], + + basePath: "/", + + tags: [ + { name: "Users", description: "Endpoints related to users" } + ], + + paths: { + "/api/v1/users": { + get: getUsers, + post: createUsers + }, + }, + + components: { + schemas: { + User: userSchema, + }, + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + in: "header", + name: "Authorization", + }, + }, + } + +} + +docRouter.use("/", serve, setup(options)); + +export default docRouter \ No newline at end of file diff --git a/src/docs/users.ts b/src/docs/users.ts new file mode 100644 index 0000000..9888ae9 --- /dev/null +++ b/src/docs/users.ts @@ -0,0 +1,72 @@ +import { response } from "express" + +export const userSchema = { + type: "object", + properties: { + name: { + type: "string", + }, + username: { + type: "string" + }, + email: { + type: "string", + format: "email", + }, + password: { + type: "string", + }, + }, +} + +export const getUsers = { + tags: ["Users"], + summary: "Get all users", + responses: { + 200: { + description: "OK", + content: { + "application/json": { + schema: { + type: "array", + items: { + $ref: "#/components/schemas/User", + }, + }, + }, + }, + }, + }, + } + + export const createUsers = { + + tags: ["Users"], + summary: "Register a new user", + requestBody: { + required: true, + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/User", + }, + }, + }, + }, + responses: { + 201: { + description: "Created", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/User", + }, + }, + }, + }, + 400: { + description: "Bad request", + }, + }, + } + \ No newline at end of file diff --git a/src/routes/index.ts b/src/routes/index.ts index 4bbeb6d..e7ddc3f 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,8 +1,10 @@ import { Request, Response, Router } from "express"; import userRoutes from "./userRoutes"; + const appROutes = Router(); appROutes.use("/users", userRoutes); + export default appROutes; diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index 6e8b79f..2704bc3 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -7,8 +7,7 @@ from "../controllers/userControllers"; const userRoutes = Router(); userRoutes.get("/", fetchAllUsers); - -userRoutes.post("/users/register", createUserController) +userRoutes.post("/", createUserController) export default userRoutes; diff --git a/src/sequelize/config/config.js b/src/sequelize/config/config.js index 879baec..7ffd72a 100644 --- a/src/sequelize/config/config.js +++ b/src/sequelize/config/config.js @@ -5,22 +5,10 @@ module.exports = { development: { url: process.env.DB_CONNECTION, dialect: "postgres", - dialectOptions: { - ssl: { - require: true, - rejectUnauthorized: false, - }, - }, }, test: { - url: process.env.DB_CONNECTION, + url: process.env.TEST_DB_CONNECTION, dialect: "postgres", - dialectOptions: { - ssl: { - require: true, - rejectUnauthorized: false, - }, - }, }, production: { url: process.env.DB_CONNECTION, diff --git a/src/sequelize/models/users.ts b/src/sequelize/models/users.ts index 8c33d16..2bacede 100644 --- a/src/sequelize/models/users.ts +++ b/src/sequelize/models/users.ts @@ -1,5 +1,5 @@ import {Model,DataTypes} from 'sequelize'; -import { testSequelize } from '../../config/testDbConfig'; +import sequelize from '../../config/dbConnection'; export interface UserAttributes{ id?:number, @@ -53,7 +53,7 @@ class User extends Model