diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0e82c00 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + +jobs: + backend-test: + runs-on: ubuntu-latest + services: + mongo: + image: mongo:7 + ports: + - 27017:27017 + env: + MONGO_URI: mongodb://localhost:27017/taskflow_test + JWT_SECRET: test_secret + NODE_ENV: test + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: npm + cache-dependency-path: backend/package-lock.json + - name: Install backend deps + run: npm ci + working-directory: backend + - name: Run backend tests + run: npm test + working-directory: backend + + docker-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build backend image + run: docker build -t taskflow-backend ./backend + - name: Build frontend image + run: docker build -t taskflow-frontend ./frontend \ No newline at end of file diff --git a/backend/src/app.js b/backend/src/app.js index 3108d3d..f473583 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -14,7 +14,7 @@ app.use(cors()); app.use(express.json()); app.use('/api/auth', authRoutes); -app.use('/api/test', testRoutes); +app.use('/api', testRoutes); app.use('/api/projects', projectRoutes); app.use('/api/tasks', taskRoutes); app.use('/api/habits', habitRoutes); @@ -23,12 +23,6 @@ app.get('/', (req, res) => { res.send('Hello from TaskFlow'); }); -app.get('/api/health', (req, res) => { - res.json({ - status: 'ok', - message: 'Backend is running' - }); -}); module.exports = app; diff --git a/backend/src/middleware/authMiddleware.js b/backend/src/middleware/authMiddleware.js index 68879ae..e5d0835 100644 --- a/backend/src/middleware/authMiddleware.js +++ b/backend/src/middleware/authMiddleware.js @@ -1,28 +1,25 @@ -const jwt = require('jsonwebtoken'); -const User = require('../models/User'); +const jwt = require("jsonwebtoken"); +const User = require("../models/User"); -const protect = async (req, res, next) =>{ - let token; +const JWT_SECRET = process.env.JWT_SECRET || "secret123"; - if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')){ - try{ - token = req.headers.authorization.split(' ')[1]; +const protect = async (req, res, next) => { + let token; - const decoded = jwt.verify(token, 'secret123'); + if (req.headers.authorization && req.headers.authorization.startsWith("Bearer")) { + try { + token = req.headers.authorization.split(" ")[1]; - req.user = await User.findById(decoded.id).select('-password'); - - next(); - } catch (error){ - return res.status(401).json({ message: 'Not authorized, invalid token' }); + const decoded = jwt.verify(token, JWT_SECRET); - } - } - - if (!token){ - return res.status(401).json({ message: 'Not authorized, no token' }); + req.user = await User.findById(decoded.id).select("-password"); + return next(); + } catch (error) { + return res.status(401).json({ message: "Not authorized, invalid token" }); } + } + return res.status(401).json({ message: "Not authorized, no token" }); }; module.exports = { protect }; \ No newline at end of file diff --git a/backend/src/routes/testRoutes.js b/backend/src/routes/testRoutes.js index 69f7859..f290a29 100644 --- a/backend/src/routes/testRoutes.js +++ b/backend/src/routes/testRoutes.js @@ -1,8 +1,30 @@ const express = require('express'); const { protect } = require('../middleware/authMiddleware'); +const mongoose = require('mongoose'); const router = express.Router(); +// Alive check +router.get('/health', (req, res) => { + res.status(200).json({ status : "ok"}); +}); + +// Ready check +router.get('/ready', async(req, res) => { + try{ + if(!mongoose.connection.db){ + return res.status(503).json({ status: "not ready", reason: "no db"}); + } + + await mongoose.connection.db.admin().ping(); + + return res.status(200).json({ status: "ready"}); + }catch (err){ + return res.status(503).json({ status: "not ready", reason: err.message}); + } +}); + + // ✅ Route protected router.get('/private', protect, (req, res) => { res.json({ diff --git a/backend/tests/api.test.js b/backend/tests/api.test.js index 956ff80..d726766 100644 --- a/backend/tests/api.test.js +++ b/backend/tests/api.test.js @@ -1,53 +1,54 @@ -const request = require('supertest'); -const app = require('../src/app'); -const mongoose = require('mongoose'); +const request = require("supertest"); +const app = require("../src/app"); +const mongoose = require("mongoose"); -let token = ''; -let projectId = ''; -let taskId = ''; +let token = ""; +let projectId = ""; +let taskId = ""; -jest.setTimeout(20000); // ✅ نزيد وقت التست لـ 20 ثانية +// make data unique per run (CI safe) +const email = `tester_${Date.now()}@test.com`; +const password = "123456"; -describe('✅ FULL API TEST', () => { +jest.setTimeout(20000); +describe(" FULL API TEST", () => { beforeAll(async () => { - await mongoose.connect('mongodb://127.0.0.1:27017/taskflow-test', { - useNewUrlParser: true, - useUnifiedTopology: true - }); - }); + const mongoUri = + process.env.MONGO_URI || "mongodb://127.0.0.1:27017/taskflow-test"; + await mongoose.connect(mongoUri); + }); afterAll(async () => { - await mongoose.connection.close(); // ✅ إغلاق الاتصال + await mongoose.connection.close(); }); - // ✅ Register - it('should register a new user', async () => { - const res = await request(app) - .post('/api/auth/register') - .send({ - name: 'Tester1', - email: 'tester123@test.com', - password: '123456' - }); + // Register + it("should register a new user", async () => { + const res = await request(app).post("/api/auth/register").send({ + name: "Tester1", + email, + password, + }); - console.log('REGISTER:', res.body); + console.log("REGISTER:", res.body); - expect(res.statusCode).toBe(201); + expect([200, 201]).toContain(res.statusCode); expect(res.body.token).toBeDefined(); + + // keep token so the rest of the suite can run + token = res.body.token; }); - // ✅ Login - it('should login the user', async () => { - const res = await request(app) - .post('/api/auth/login') - .send({ - email: 'tester@test.com', - password: '123456' - }); + // Login + it("should login the user", async () => { + const res = await request(app).post("/api/auth/login").send({ + email, + password, + }); - console.log('LOGIN:', res.body); + console.log("LOGIN:", res.body); expect(res.statusCode).toBe(200); expect(res.body.token).toBeDefined(); @@ -55,17 +56,17 @@ describe('✅ FULL API TEST', () => { token = res.body.token; }); - // ✅ Create Project - it('should create a new project', async () => { + // Create Project + it("should create a new project", async () => { const res = await request(app) - .post('/api/projects') - .set('Authorization', `Bearer ${token}`) + .post("/api/projects") + .set("Authorization", `Bearer ${token}`) .send({ - title: 'Test Project', - description: 'Project from automated test' + title: "Test Project", + description: "Project from automated test", }); - console.log('CREATE PROJECT:', res.body); + console.log("CREATE PROJECT:", res.body); expect(res.statusCode).toBe(201); expect(res.body._id).toBeDefined(); @@ -73,30 +74,31 @@ describe('✅ FULL API TEST', () => { projectId = res.body._id; }); - // ✅ Get Projects - it('should fetch user projects', async () => { + // Get Projects + it("should fetch user projects", async () => { const res = await request(app) - .get('/api/projects') - .set('Authorization', `Bearer ${token}`); + .get("/api/projects") + .set("Authorization", `Bearer ${token}`); - console.log('GET PROJECTS:', res.body); + console.log("GET PROJECTS:", res.body); expect(res.statusCode).toBe(200); + expect(Array.isArray(res.body)).toBe(true); expect(res.body.length).toBeGreaterThan(0); }); - // ✅ Create Task - it('should create a new task', async () => { + // Create Task + it("should create a new task", async () => { const res = await request(app) - .post('/api/tasks') - .set('Authorization', `Bearer ${token}`) + .post("/api/tasks") + .set("Authorization", `Bearer ${token}`) .send({ projectId, - title: 'First Test Task', - description: 'Task from automated test' + title: "First Test Task", + description: "Task from automated test", }); - console.log('CREATE TASK:', res.body); + console.log("CREATE TASK:", res.body); expect(res.statusCode).toBe(201); expect(res.body._id).toBeDefined(); @@ -104,42 +106,40 @@ describe('✅ FULL API TEST', () => { taskId = res.body._id; }); - // ✅ Get Tasks - it('should fetch project tasks', async () => { + // Get Tasks + it("should fetch project tasks", async () => { const res = await request(app) .get(`/api/tasks/${projectId}`) - .set('Authorization', `Bearer ${token}`); + .set("Authorization", `Bearer ${token}`); - console.log('GET TASKS:', res.body); + console.log("GET TASKS:", res.body); expect(res.statusCode).toBe(200); + expect(Array.isArray(res.body)).toBe(true); expect(res.body.length).toBeGreaterThan(0); }); - // ✅ Update Task - it('should update task status', async () => { + // Update Task + it("should update task status", async () => { const res = await request(app) .patch(`/api/tasks/${taskId}`) - .set('Authorization', `Bearer ${token}`) - .send({ - status: 'done' - }); + .set("Authorization", `Bearer ${token}`) + .send({ status: "done" }); - console.log('UPDATE TASK:', res.body); + console.log("UPDATE TASK:", res.body); expect(res.statusCode).toBe(200); - expect(res.body.status).toBe('done'); + expect(res.body.status).toBe("done"); }); - // ✅ Delete Task - it('should delete a task', async () => { + // Delete Task + it("should delete a task", async () => { const res = await request(app) .delete(`/api/tasks/${taskId}`) - .set('Authorization', `Bearer ${token}`); + .set("Authorization", `Bearer ${token}`); - console.log('DELETE TASK:', res.body); + console.log("DELETE TASK:", res.body); - expect(res.statusCode).toBe(204); + expect([200, 204]).toContain(res.statusCode); }); - -}); +}); \ No newline at end of file