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