From 3d260d2c9e3c34b5b040a166cf93753aa88d263e Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Thu, 17 Jul 2025 16:41:59 -0400 Subject: [PATCH 1/4] update dev dependencies for tests --- package-lock.json | 5 +++-- package.json | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index f90eb9e..eec3cd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,9 +26,9 @@ "uuid": "^9.0.0" }, "devDependencies": { - "jest": "^29.6.2", + "jest": "^29.7.0", "nodemon": "^3.0.1", - "supertest": "^6.3.3" + "supertest": "^6.3.4" }, "engines": { "node": ">=16.0.0" @@ -5782,6 +5782,7 @@ "version": "6.3.4", "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", + "deprecated": "Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index d816526..9f6c1ff 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,9 @@ "uuid": "^9.0.0" }, "devDependencies": { - "jest": "^29.6.2", + "jest": "^29.7.0", "nodemon": "^3.0.1", - "supertest": "^6.3.3" + "supertest": "^6.3.4" }, "engines": { "node": ">=16.0.0" From 736f2b76fb20bce66278842363ec438f483da6d4 Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Thu, 17 Jul 2025 16:42:08 -0400 Subject: [PATCH 2/4] add unit and integration tests --- __tests__/chat.test.js | 45 ++++++++++ __tests__/plan.test.js | 83 ++++++++++++++++++ __tests__/user.test.js | 188 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 316 insertions(+) create mode 100644 __tests__/chat.test.js create mode 100644 __tests__/plan.test.js create mode 100644 __tests__/user.test.js diff --git a/__tests__/chat.test.js b/__tests__/chat.test.js new file mode 100644 index 0000000..9b8556f --- /dev/null +++ b/__tests__/chat.test.js @@ -0,0 +1,45 @@ +const request = require('supertest'); +const express = require('express'); + +// Mock database and unsplash services +jest.mock('../services/database', () => ({ + getTrips: jest.fn().mockResolvedValue({ + trips: [ + { id: '1', title: 'Trip 1', location: 'Paris', lastUpdated: '2024-01-01', searchData: {} } + ], + pagination: { total: 1, hasMore: false } + }), +})); +jest.mock('../services/unsplash', () => ({ + addImagesToTrips: jest.fn().mockImplementation(trips => trips) +})); +jest.mock('../middleware/auth', () => ({ + optionalAuth: (req, res, next) => next(), + authenticateToken: (req, res, next) => next() +})); + +const chatRouter = require('../routes/chat'); +const app = express(); +app.use(express.json()); +app.use('/api/chat', chatRouter); + +describe('Chat API', () => { + describe('GET /api/chat', () => { + it('should return trip history', async () => { + const res = await request(app).get('/api/chat'); + expect(res.statusCode).toBe(200); + expect(res.body.success).toBe(true); + expect(Array.isArray(res.body.trips)).toBe(true); + expect(res.body.trips.length).toBeGreaterThanOrEqual(1); + expect(res.body.pagination).toBeDefined(); + expect(res.body.metadata).toBeDefined(); + }); + + it('should return 400 for invalid query params', async () => { + const res = await request(app).get('/api/chat?limit=abc'); + expect(res.statusCode).toBe(400); + expect(res.body.success).toBe(false); + expect(res.body.error).toBe('Validation Error'); + }); + }); +}); diff --git a/__tests__/plan.test.js b/__tests__/plan.test.js new file mode 100644 index 0000000..d7bb9cf --- /dev/null +++ b/__tests__/plan.test.js @@ -0,0 +1,83 @@ +const request = require('supertest'); +const express = require('express'); + +// Mock openaiService and createTrip before requiring the router +jest.mock('../services/openai', () => ({ + generateTripPlan: jest.fn().mockResolvedValue({ + content: 'AI trip plan', + processingTime: 123, + model: 'gpt-4', + source: 'mock', + usage: { prompt_tokens: 10, completion_tokens: 20 } + }), + getServiceStatus: jest.fn().mockReturnValue({ status: 'ok', model: 'mock' }), + testConnection: jest.fn().mockResolvedValue({ success: true, message: 'ok', model: 'mock' }) +})); +jest.mock('../services/database', () => ({ + createTrip: jest.fn().mockResolvedValue(true) +})); + +const planRouter = require('../routes/plan'); +const app = express(); +app.use(express.json()); +app.use('/api/plan', planRouter); + +describe('Plan API', () => { + describe('POST /api/plan', () => { + it('should return 400 for invalid request body', async () => { + const res = await request(app) + .post('/api/plan') + .send({}); + expect(res.statusCode).toBe(400); + expect(res.body.success).toBe(false); + expect(res.body.error).toBe('Validation Error'); + }); + + it('should return 200 and a response for valid request', async () => { + const validBody = { + searchData: { + searchQuery: 'Things to do in Paris', + filters: { + timeOfDay: ['morning'], + environment: 'outdoor', + planTransit: false, + groupSize: 'solo', + planFood: false + }, + timestamp: new Date().toISOString() + }, + userMessage: 'I want a fun day outdoors' + }; + const res = await request(app) + .post('/api/plan') + .send(validBody); + expect(res.statusCode).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.response).toBe('AI trip plan'); + expect(res.body.chatId).toBeDefined(); + expect(res.body.title).toBe('Things to do in Paris'); + expect(res.body.location).toBeDefined(); + expect(res.body.metadata).toBeDefined(); + }); + }); + + describe('GET /api/plan/status', () => { + it('should return service status', async () => { + const res = await request(app).get('/api/plan/status'); + expect(res.statusCode).toBe(200); + expect(res.body.service).toBe('Plan API'); + expect(res.body.status).toBe('operational'); + expect(res.body.openai).toBeDefined(); + }); + }); + + describe('GET /api/plan/test-ai', () => { + it('should return OpenAI test result', async () => { + const res = await request(app).get('/api/plan/test-ai'); + expect([200, 503]).toContain(res.statusCode); + expect(res.body.service).toBe('OpenAI Test'); + expect(res.body.success).toBeDefined(); + expect(res.body.timestamp).toBeDefined(); + }); + }); +}); diff --git a/__tests__/user.test.js b/__tests__/user.test.js new file mode 100644 index 0000000..b20955d --- /dev/null +++ b/__tests__/user.test.js @@ -0,0 +1,188 @@ +const request = require('supertest'); +const express = require('express'); + +// Mock database and auth services +const mockCreateUser = jest.fn().mockResolvedValue({ + id: 'user-1', + email: 'test@example.com', + name: 'Test User', + profileImageUrl: null, + adventuresCount: 0, + placesVisitedCount: 0, + memberSince: '2024-01-01', + createdAt: '2024-01-01', + updatedAt: '2024-01-01' +}); +const mockGetUserByEmail = jest.fn(); +const mockUpdateUserPassword = jest.fn().mockResolvedValue(true); +const mockGetUserById = jest.fn().mockResolvedValue({ + id: 'user-1', + email: 'test@example.com', + name: 'Test User', + profileImageUrl: null, + adventuresCount: 0, + placesVisitedCount: 0, + memberSince: '2024-01-01', + createdAt: '2024-01-01', + updatedAt: '2024-01-01' +}); + +jest.mock('../services/database', () => ({ + createUser: (...args) => mockCreateUser(...args), + getUserByEmail: (...args) => mockGetUserByEmail(...args), + updateUserPassword: (...args) => mockUpdateUserPassword(...args), + getUserById: (...args) => mockGetUserById(...args) +})); +jest.mock('../middleware/auth', () => ({ + generateToken: jest.fn().mockReturnValue('mocktoken'), + authenticateToken: (req, res, next) => { + req.userId = 'user-1'; + next(); + } +})); + +const userRouter = require('../routes/user'); +const app = express(); +app.use(express.json()); +app.use('/api/user', userRouter); + +describe('User API', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('POST /api/user/signup', () => { + it('should return 400 for invalid signup body', async () => { + const res = await request(app) + .post('/api/user/signup') + .send({}); + expect(res.statusCode).toBe(400); + expect(res.body.success).toBe(false); + expect(res.body.error).toBe('Validation Error'); + }); + it('should return 201 for valid signup', async () => { + mockGetUserByEmail.mockResolvedValueOnce(null); // Simulate user does not exist + const res = await request(app) + .post('/api/user/signup') + .send({ + email: 'test@example.com', + password: 'password123', + name: 'Test User' + }); + expect([200, 201]).toContain(res.statusCode); + expect(res.body.success).toBe(true); + expect(res.body.user).toBeDefined(); + expect(res.body.user.email).toBe('test@example.com'); + }); + it('should return 409 for duplicate signup', async () => { + mockGetUserByEmail.mockResolvedValueOnce({ id: 'user-1', email: 'test@example.com' }); + const res = await request(app) + .post('/api/user/signup') + .send({ + email: 'test@example.com', + password: 'password123', + name: 'Test User' + }); + expect(res.statusCode).toBe(409); + expect(res.body.success).toBe(false); + expect(res.body.error).toBe('User Already Exists'); + }); + }); + + describe('POST /api/user/login', () => { + it('should return 400 for invalid login body', async () => { + const res = await request(app) + .post('/api/user/login') + .send({}); + expect(res.statusCode).toBe(400); + expect(res.body.success).toBe(false); + expect(res.body.error).toBe('Validation Error'); + }); + it('should return 200 for valid login', async () => { + mockGetUserByEmail.mockResolvedValueOnce({ + id: 'user-1', + email: 'test@example.com', + name: 'Test User', + password: '$2b$12$saltsaltsaltsaltsaltsaltsaltsaltsaltsaltsaltsalt', + profileImageUrl: null, + adventuresCount: 0, + placesVisitedCount: 0, + memberSince: '2024-01-01', + createdAt: '2024-01-01', + updatedAt: '2024-01-01' + }); + jest.spyOn(require('bcrypt'), 'compare').mockResolvedValue(true); + const res = await request(app) + .post('/api/user/login') + .send({ + email: 'test@example.com', + password: 'password123' + }); + expect(res.statusCode).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.token).toBe('mocktoken'); + expect(res.body.user).toBeDefined(); + expect(res.body.user.email).toBe('test@example.com'); + }); + it('should return 401 for invalid password', async () => { + mockGetUserByEmail.mockResolvedValueOnce({ + id: 'user-1', + email: 'test@example.com', + name: 'Test User', + password: '$2b$12$saltsaltsaltsaltsaltsaltsaltsaltsaltsaltsaltsalt', + profileImageUrl: null, + adventuresCount: 0, + placesVisitedCount: 0, + memberSince: '2024-01-01', + createdAt: '2024-01-01', + updatedAt: '2024-01-01' + }); + jest.spyOn(require('bcrypt'), 'compare').mockResolvedValue(false); + const res = await request(app) + .post('/api/user/login') + .send({ + email: 'test@example.com', + password: 'wrongpassword' + }); + expect(res.statusCode).toBe(401); + expect(res.body.success).toBe(false); + expect(res.body.error).toBe('Invalid Credentials'); + }); + }); + + describe('GET /api/user/profile', () => { + it('should return user profile for authenticated user', async () => { + const res = await request(app) + .get('/api/user/profile') + .set('Authorization', 'Bearer mocktoken'); + expect(res.statusCode).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.user).toBeDefined(); + expect(res.body.user.email).toBe('test@example.com'); + }); + }); + + describe('POST /api/user/change-password', () => { + it('should return 400 for invalid body', async () => { + const res = await request(app) + .post('/api/user/change-password') + .set('Authorization', 'Bearer mocktoken') + .send({}); + expect(res.statusCode).toBe(400); + expect(res.body.success).toBe(false); + expect(res.body.error).toBe('Validation Error'); + }); + it('should return 200 for valid password change', async () => { + jest.spyOn(require('bcrypt'), 'compare').mockResolvedValue(true); + const res = await request(app) + .post('/api/user/change-password') + .set('Authorization', 'Bearer mocktoken') + .send({ + currentPassword: 'password123', + newPassword: 'newpassword123' + }); + expect([200, 204]).toContain(res.statusCode); + expect(res.body.success).toBe(true); + }); + }); +}); From 4bd4a11397a9b7e24e55a9d04e95ac3d1b4ef87e Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Fri, 18 Jul 2025 01:31:57 -0400 Subject: [PATCH 3/4] nic --- __tests__/chat.test.js | 2 +- __tests__/plan.test.js | 2 +- __tests__/user.test.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/__tests__/chat.test.js b/__tests__/chat.test.js index 9b8556f..89fd943 100644 --- a/__tests__/chat.test.js +++ b/__tests__/chat.test.js @@ -42,4 +42,4 @@ describe('Chat API', () => { expect(res.body.error).toBe('Validation Error'); }); }); -}); +}); diff --git a/__tests__/plan.test.js b/__tests__/plan.test.js index d7bb9cf..bea6686 100644 --- a/__tests__/plan.test.js +++ b/__tests__/plan.test.js @@ -80,4 +80,4 @@ describe('Plan API', () => { expect(res.body.timestamp).toBeDefined(); }); }); -}); +}); diff --git a/__tests__/user.test.js b/__tests__/user.test.js index b20955d..ed2ede4 100644 --- a/__tests__/user.test.js +++ b/__tests__/user.test.js @@ -185,4 +185,4 @@ describe('User API', () => { expect(res.body.success).toBe(true); }); }); -}); +}); From 17751ca946a3d0a6281e4569bd49c7dfa0a04b9b Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Fri, 18 Jul 2025 01:36:56 -0400 Subject: [PATCH 4/4] fix user test cases --- __tests__/user.test.js | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/__tests__/user.test.js b/__tests__/user.test.js index ed2ede4..79e955f 100644 --- a/__tests__/user.test.js +++ b/__tests__/user.test.js @@ -31,11 +31,24 @@ jest.mock('../services/database', () => ({ createUser: (...args) => mockCreateUser(...args), getUserByEmail: (...args) => mockGetUserByEmail(...args), updateUserPassword: (...args) => mockUpdateUserPassword(...args), - getUserById: (...args) => mockGetUserById(...args) + getUserById: (...args) => mockGetUserById(...args), + getTrips: jest.fn().mockResolvedValue({ pagination: { total: 0 } }) })); jest.mock('../middleware/auth', () => ({ generateToken: jest.fn().mockReturnValue('mocktoken'), authenticateToken: (req, res, next) => { + // Mock the actual user object that the middleware would set + req.user = { + id: 'user-1', + email: 'test@example.com', + name: 'Test User', + profileImageUrl: null, + adventuresCount: 0, + placesVisitedCount: 0, + memberSince: '2024-01-01', + createdAt: '2024-01-01', + updatedAt: '2024-01-01' + }; req.userId = 'user-1'; next(); } @@ -162,20 +175,32 @@ describe('User API', () => { }); }); - describe('POST /api/user/change-password', () => { + describe('PUT /api/user/password', () => { it('should return 400 for invalid body', async () => { const res = await request(app) - .post('/api/user/change-password') + .put('/api/user/password') .set('Authorization', 'Bearer mocktoken') .send({}); expect(res.statusCode).toBe(400); expect(res.body.success).toBe(false); - expect(res.body.error).toBe('Validation Error'); }); it('should return 200 for valid password change', async () => { + // Mock getUserByEmail to return user with password_hash for password change + mockGetUserByEmail.mockResolvedValueOnce({ + id: 'user-1', + email: 'test@example.com', + name: 'Test User', + password_hash: '$2b$12$saltsaltsaltsaltsaltsaltsaltsaltsaltsaltsaltsalt', + profileImageUrl: null, + adventuresCount: 0, + placesVisitedCount: 0, + memberSince: '2024-01-01', + createdAt: '2024-01-01', + updatedAt: '2024-01-01' + }); jest.spyOn(require('bcrypt'), 'compare').mockResolvedValue(true); const res = await request(app) - .post('/api/user/change-password') + .put('/api/user/password') .set('Authorization', 'Bearer mocktoken') .send({ currentPassword: 'password123',