diff --git a/README.md b/README.md
index e01a84e..d5b55a0 100644
--- a/README.md
+++ b/README.md
@@ -102,3 +102,58 @@ $ npm start
+
+## ๐งช Backend Unit & Integration Testing with Jasmine
+
+This project uses the Jasmine framework for backend unit and integration tests. The tests cover:
+- User model (password hashing, schema, password comparison)
+- Authentication routes (signup, login, logout)
+- Passport authentication logic (via integration tests)
+
+### Prerequisites
+- **Node.js** and **npm** installed
+- **MongoDB** running locally (default: `mongodb://127.0.0.1:27017`)
+
+### Installation
+Install all required dependencies:
+```sh
+npm install
+npm install --save-dev jasmine @types/jasmine supertest express-session passport passport-local bcryptjs
+```
+
+### Running the Tests
+1. **Start MongoDB** (if not already running):
+ ```sh
+ mongod
+ ```
+2. **Run Jasmine tests:**
+ ```sh
+ npx jasmine
+ ```
+
+### Test Files
+- `spec/user.model.spec.cjs` โ Unit tests for the User model
+- `spec/auth.routes.spec.cjs` โ Integration tests for authentication routes
+
+### Jasmine Configuration
+The Jasmine config (`spec/support/jasmine.mjs`) is set to recognize `.cjs`, `.js`, and `.mjs` test files:
+```js
+spec_files: [
+ "**/*[sS]pec.?(m)js",
+ "**/*[sS]pec.cjs"
+]
+```
+
+### Troubleshooting
+- **No specs found:** Ensure your test files have the correct extension and are in the `spec/` directory.
+- **MongoDB connection errors:** Make sure MongoDB is running and accessible.
+- **Missing modules:** Install any missing dev dependencies with `npm install --save-dev `.
+
+### What Was Covered
+- Jasmine is set up and configured for backend testing.
+- All major backend modules are covered by unit/integration tests.
+- Tests are passing and verified.
+
+---
+
+For any questions or to add more tests (including frontend), see the contribution guidelines or open an issue.
diff --git a/package.json b/package.json
index 0959253..d19ed07 100644
--- a/package.json
+++ b/package.json
@@ -26,18 +26,25 @@
},
"devDependencies": {
"@eslint/js": "^9.13.0",
+ "@types/jasmine": "^5.1.8",
"@types/node": "^22.10.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react-redux": "^7.1.34",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
+ "bcryptjs": "^3.0.2",
"eslint": "^9.13.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
+ "express-session": "^1.18.2",
"globals": "^15.11.0",
+ "jasmine": "^5.9.0",
+ "passport": "^0.7.0",
+ "passport-local": "^1.0.0",
"postcss": "^8.4.47",
+ "supertest": "^7.1.4",
"tailwindcss": "^3.4.14",
"vite": "^5.4.10"
}
diff --git a/spec/auth.routes.spec.cjs b/spec/auth.routes.spec.cjs
new file mode 100644
index 0000000..c5ac003
--- /dev/null
+++ b/spec/auth.routes.spec.cjs
@@ -0,0 +1,96 @@
+const mongoose = require('mongoose');
+const express = require('express');
+const request = require('supertest');
+const session = require('express-session');
+const passport = require('passport');
+const User = require('../backend/models/User');
+const authRoutes = require('../backend/routes/auth');
+
+// Setup Express app for testing
+function createTestApp() {
+ const app = express();
+ app.use(express.json());
+ app.use(session({ secret: 'test', resave: false, saveUninitialized: false }));
+ app.use(passport.initialize());
+ app.use(passport.session());
+ require('../backend/config/passportConfig');
+ app.use('/auth', authRoutes);
+ return app;
+}
+
+describe('Auth Routes', () => {
+ let app;
+
+ beforeAll(async () => {
+ await mongoose.connect('mongodb://127.0.0.1:27017/github_tracker_test', {
+ useNewUrlParser: true,
+ useUnifiedTopology: true,
+ });
+ app = createTestApp();
+ });
+
+ afterAll(async () => {
+ await mongoose.connection.db.dropDatabase();
+ await mongoose.disconnect();
+ });
+
+ afterEach(async () => {
+ await User.deleteMany({});
+ });
+
+ it('should sign up a new user', async () => {
+ const res = await request(app)
+ .post('/auth/signup')
+ .send({ username: 'testuser', email: 'test@example.com', password: 'password123' });
+ expect(res.status).toBe(201);
+ expect(res.body.message).toBe('User created successfully');
+ const user = await User.findOne({ email: 'test@example.com' });
+ expect(user).toBeTruthy();
+ });
+
+ it('should not sign up a user with existing email', async () => {
+ await new User({ username: 'testuser', email: 'test@example.com', password: 'password123' }).save();
+ const res = await request(app)
+ .post('/auth/signup')
+ .send({ username: 'testuser2', email: 'test@example.com', password: 'password456' });
+ expect(res.status).toBe(400);
+ expect(res.body.message).toBe('User already exists');
+ });
+
+ it('should login a user with correct credentials', async () => {
+ await request(app)
+ .post('/auth/signup')
+ .send({ username: 'testuser', email: 'test@example.com', password: 'password123' });
+ const agent = request.agent(app);
+ const res = await agent
+ .post('/auth/login')
+ .send({ email: 'test@example.com', password: 'password123' });
+ expect(res.status).toBe(200);
+ expect(res.body.message).toBe('Login successful');
+ expect(res.body.user.email).toBe('test@example.com');
+ });
+
+ it('should not login a user with wrong password', async () => {
+ await request(app)
+ .post('/auth/signup')
+ .send({ username: 'testuser', email: 'test@example.com', password: 'password123' });
+ const agent = request.agent(app);
+ const res = await agent
+ .post('/auth/login')
+ .send({ email: 'test@example.com', password: 'wrongpassword' });
+ expect(res.status).toBe(401);
+ });
+
+ it('should logout a logged-in user', async () => {
+ await request(app)
+ .post('/auth/signup')
+ .send({ username: 'testuser', email: 'test@example.com', password: 'password123' });
+ const agent = request.agent(app);
+ await agent
+ .post('/auth/login')
+ .send({ email: 'test@example.com', password: 'password123' });
+ const res = await agent.get('/auth/logout');
+ expect(res.status).toBe(200);
+ expect(res.body.message).toBe('Logged out successfully');
+ });
+});
\ No newline at end of file
diff --git a/spec/support/jasmine.mjs b/spec/support/jasmine.mjs
new file mode 100644
index 0000000..9973490
--- /dev/null
+++ b/spec/support/jasmine.mjs
@@ -0,0 +1,15 @@
+export default {
+ spec_dir: "spec",
+ spec_files: [
+ "**/*[sS]pec.?(m)js",
+ "**/*[sS]pec.cjs"
+ ],
+ helpers: [
+ "helpers/**/*.?(m)js"
+ ],
+ env: {
+ stopSpecOnExpectationFailure: false,
+ random: true,
+ forbidDuplicateNames: true
+ }
+}
diff --git a/spec/user.model.spec.cjs b/spec/user.model.spec.cjs
new file mode 100644
index 0000000..236d9bd
--- /dev/null
+++ b/spec/user.model.spec.cjs
@@ -0,0 +1,50 @@
+const mongoose = require('mongoose');
+const bcrypt = require('bcryptjs');
+const User = require('../backend/models/User');
+
+describe('User Model', () => {
+ beforeAll(async () => {
+ await mongoose.connect('mongodb://127.0.0.1:27017/github_tracker_test', {
+ useNewUrlParser: true,
+ useUnifiedTopology: true,
+ });
+ });
+
+ afterAll(async () => {
+ await mongoose.connection.db.dropDatabase();
+ await mongoose.disconnect();
+ });
+
+ afterEach(async () => {
+ await User.deleteMany({});
+ });
+
+ it('should create a user with hashed password', async () => {
+ const userData = { username: 'testuser', email: 'test@example.com', password: 'password123' };
+ const user = new User(userData);
+ await user.save();
+ expect(user.password).not.toBe(userData.password);
+ const isMatch = await bcrypt.compare('password123', user.password);
+ expect(isMatch).toBeTrue();
+ });
+
+ it('should not hash password again if not modified', async () => {
+ const userData = { username: 'testuser2', email: 'test2@example.com', password: 'password123' };
+ const user = new User(userData);
+ await user.save();
+ const originalHash = user.password;
+ user.username = 'updateduser';
+ await user.save();
+ expect(user.password).toBe(originalHash);
+ });
+
+ it('should compare passwords correctly', async () => {
+ const userData = { username: 'testuser3', email: 'test3@example.com', password: 'password123' };
+ const user = new User(userData);
+ await user.save();
+ const isMatch = await user.comparePassword('password123');
+ expect(isMatch).toBeTrue();
+ const isNotMatch = await user.comparePassword('wrongpassword');
+ expect(isNotMatch).toBeFalse();
+ });
+});
\ No newline at end of file