diff --git a/__tests__/chat.test.js b/__tests__/chat.test.js new file mode 100644 index 0000000..89fd943 --- /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..bea6686 --- /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..79e955f --- /dev/null +++ b/__tests__/user.test.js @@ -0,0 +1,213 @@ +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), + 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(); + } +})); + +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('PUT /api/user/password', () => { + it('should return 400 for invalid body', async () => { + const res = await request(app) + .put('/api/user/password') + .set('Authorization', 'Bearer mocktoken') + .send({}); + expect(res.statusCode).toBe(400); + expect(res.body.success).toBe(false); + }); + 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) + .put('/api/user/password') + .set('Authorization', 'Bearer mocktoken') + .send({ + currentPassword: 'password123', + newPassword: 'newpassword123' + }); + expect([200, 204]).toContain(res.statusCode); + expect(res.body.success).toBe(true); + }); + }); +}); 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"