const User = require('../../models/User'); const couchdbService = require('../../services/couchdbService'); // Mock CouchDB service for testing jest.mock('../../services/couchdbService'); describe('User Model', () => { beforeEach(() => { jest.clearAllMocks(); // Reset all mocks to ensure clean state couchdbService.findUserByEmail.mockReset(); couchdbService.findUserById.mockReset(); couchdbService.createDocument.mockReset(); couchdbService.updateDocument.mockReset(); }); describe('Schema Validation', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should create a valid user', async () => { const userData = { name: 'Test User', email: 'test@example.com', password: 'password123', }; const mockCreated = { _id: 'user_123', _rev: '1-abc', type: 'user', ...userData, isPremium: false, points: 0, adoptedStreets: [], completedTasks: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const user = await User.create(userData); expect(user._id).toBeDefined(); expect(user.name).toBe(userData.name); expect(user.email).toBe(userData.email); expect(user.isPremium).toBe(false); expect(user.points).toBe(0); expect(user.adoptedStreets).toEqual([]); expect(user.completedTasks).toEqual([]); }); it('should require name field', async () => { const userData = { email: 'test@example.com', password: 'password123', }; expect(() => new User(userData)).toThrow(); }); it('should require email field', async () => { const userData = { name: 'Test User', password: 'password123', }; expect(() => new User(userData)).toThrow(); }); it('should require password field', async () => { const userData = { name: 'Test User', email: 'test@example.com', }; expect(() => new User(userData)).toThrow(); }); it('should enforce unique email constraint', async () => { const email = 'duplicate@example.com'; const userData = { name: 'User 1', email, password: 'password123', }; // Test that we can find a user by email const existingUser = { _id: 'user1', _rev: '1-abc', type: 'user', ...userData, isPremium: false, points: 0, adoptedStreets: [], completedTasks: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.findUserByEmail.mockResolvedValue(existingUser); const user = await User.findOne({ email }); expect(user).toBeDefined(); expect(user.email).toBe(email); }); }); describe('Default Values', () => { it('should set default values correctly', async () => { const userData = { name: 'Default Test', email: 'default@example.com', password: 'password123', }; const mockCreated = { _id: 'user_123', _rev: '1-abc', type: 'user', ...userData, isPremium: false, points: 0, adoptedStreets: [], completedTasks: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const user = await User.create(userData); expect(user.isPremium).toBe(false); expect(user.points).toBe(0); expect(user.adoptedStreets).toEqual([]); expect(user.completedTasks).toEqual([]); expect(user.posts).toEqual([]); expect(user.events).toEqual([]); }); }); describe('Relationships', () => { it('should store adopted streets references', async () => { const streetId = 'street_123'; const userData = { name: 'Test User', email: 'test@example.com', password: 'password123', adoptedStreets: [streetId], }; const mockCreated = { _id: 'user_123', _rev: '1-abc', type: 'user', ...userData, isPremium: false, points: 0, completedTasks: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 1, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const user = await User.create(userData); expect(user.adoptedStreets).toHaveLength(1); expect(user.adoptedStreets[0]).toBe(streetId); }); it('should store completed tasks references', async () => { const taskId = 'task_123'; const userData = { name: 'Test User', email: 'test@example.com', password: 'password123', completedTasks: [taskId], }; const mockCreated = { _id: 'user_123', _rev: '1-abc', type: 'user', ...userData, isPremium: false, points: 0, adoptedStreets: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 1, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const user = await User.create(userData); expect(user.completedTasks).toHaveLength(1); expect(user.completedTasks[0]).toBe(taskId); }); it('should store multiple posts references', async () => { const postId1 = 'post_123'; const postId2 = 'post_456'; const userData = { name: 'Test User', email: 'test@example.com', password: 'password123', posts: [postId1, postId2], }; const mockCreated = { _id: 'user_123', _rev: '1-abc', type: 'user', ...userData, isPremium: false, points: 0, adoptedStreets: [], completedTasks: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 2, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const user = await User.create(userData); expect(user.posts).toHaveLength(2); expect(user.posts[0]).toBe(postId1); expect(user.posts[1]).toBe(postId2); }); }); describe('Timestamps', () => { it('should automatically set createdAt and updatedAt', async () => { const userData = { name: 'Test User', email: 'timestamp@example.com', password: 'password123', }; const mockCreated = { _id: 'user_123', _rev: '1-abc', type: 'user', ...userData, isPremium: false, points: 0, adoptedStreets: [], completedTasks: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const user = await User.create(userData); expect(user.createdAt).toBeDefined(); expect(user.updatedAt).toBeDefined(); expect(typeof user.createdAt).toBe('string'); expect(typeof user.updatedAt).toBe('string'); }); }); describe('Password Management', () => { it('should hash password on creation', async () => { const userData = { name: 'Test User', email: 'hash@example.com', password: 'password123', }; const mockCreated = { _id: 'user_123', _rev: '1-abc', type: 'user', ...userData, password: '$2a$10$hashedpassword', isPremium: false, points: 0, adoptedStreets: [], completedTasks: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const user = await User.create(userData); expect(user.password).toMatch(/^\$2[aby]\$\d+\$/); // bcrypt hash pattern }); it('should compare passwords correctly', async () => { const userData = { name: 'Test User', email: 'compare@example.com', password: 'password123', }; const mockCreated = { _id: 'user_123', _rev: '1-abc', type: 'user', ...userData, password: '$2a$10$hashedpassword', isPremium: false, points: 0, adoptedStreets: [], completedTasks: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const user = await User.create(userData); // Mock bcrypt.compare const bcrypt = require('bcryptjs'); bcrypt.compare = jest.fn().mockResolvedValue(true); const isMatch = await user.comparePassword('password123'); expect(isMatch).toBe(true); }); }); describe('Premium Status', () => { it('should allow setting premium status', async () => { const userData = { name: 'Premium User', email: 'premium@example.com', password: 'password123', isPremium: true, }; const mockCreated = { _id: 'user_123', _rev: '1-abc', type: 'user', ...userData, points: 0, adoptedStreets: [], completedTasks: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const user = await User.create(userData); expect(user.isPremium).toBe(true); }); it('should allow toggling premium status', async () => { const userData = { name: 'Test User', email: 'toggle@example.com', password: 'password123', isPremium: false, }; const mockUser = { _id: 'user_123', _rev: '1-abc', type: 'user', ...userData, points: 0, adoptedStreets: [], completedTasks: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.findUserById.mockResolvedValue(mockUser); couchdbService.updateDocument.mockResolvedValue({ ...mockUser, isPremium: true, _rev: '2-def' }); const user = await User.findById('user_123'); user.isPremium = true; await user.save(); expect(user.isPremium).toBe(true); }); }); describe('Points Management', () => { it('should allow incrementing points', async () => { const userData = { name: 'Test User', email: 'points@example.com', password: 'password123', points: 100, }; const mockUser = { _id: 'user_123', _rev: '1-abc', type: 'user', ...userData, isPremium: false, adoptedStreets: [], completedTasks: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.findUserById.mockResolvedValue(mockUser); couchdbService.updateDocument.mockResolvedValue({ ...mockUser, points: 150, _rev: '2-def' }); const user = await User.findById('user_123'); user.points += 50; await user.save(); expect(user.points).toBe(150); }); it('should allow decrementing points', async () => { const userData = { name: 'Test User', email: 'deduct@example.com', password: 'password123', points: 100, }; const mockUser = { _id: 'user_123', _rev: '1-abc', type: 'user', ...userData, isPremium: false, adoptedStreets: [], completedTasks: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.findUserById.mockResolvedValue(mockUser); couchdbService.updateDocument.mockResolvedValue({ ...mockUser, points: 75, _rev: '2-def' }); const user = await User.findById('user_123'); user.points -= 25; await user.save(); expect(user.points).toBe(75); }); }); describe('Profile Picture', () => { it('should store profile picture URL', async () => { const userData = { name: 'Test User', email: 'pic@example.com', password: 'password123', profilePicture: 'https://example.com/pic.jpg', cloudinaryPublicId: 'user_123', }; const mockCreated = { _id: 'user_123', _rev: '1-abc', type: 'user', ...userData, isPremium: false, points: 0, adoptedStreets: [], completedTasks: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const user = await User.create(userData); expect(user.profilePicture).toBe('https://example.com/pic.jpg'); expect(user.cloudinaryPublicId).toBe('user_123'); }); }); describe('Static Methods', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should find user by email', async () => { const mockUser = { _id: 'user_123', _rev: '1-abc', type: 'user', email: 'test@example.com', name: 'Test User', password: 'password123', isPremium: false, points: 0, adoptedStreets: [], completedTasks: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.findUserByEmail.mockResolvedValue(mockUser); const user = await User.findOne({ email: 'test@example.com' }); expect(user).toBeDefined(); expect(user.email).toBe('test@example.com'); }); it('should find user by ID', async () => { const mockUser = { _id: 'user_123', _rev: '1-abc', type: 'user', email: 'test@example.com', name: 'Test User', password: 'password123', isPremium: false, points: 0, adoptedStreets: [], completedTasks: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.findUserById.mockResolvedValue(mockUser); const user = await User.findById('user_123'); expect(user).toBeDefined(); expect(user._id).toBe('user_123'); }); it('should return null when user not found', async () => { couchdbService.findUserById.mockResolvedValue(null); const user = await User.findById('nonexistent'); expect(user).toBeNull(); }); }); describe('Helper Methods', () => { it('should return safe object without password', async () => { const userData = { name: 'Test User', email: 'safe@example.com', password: 'password123', }; const mockCreated = { _id: 'user_123', _rev: '1-abc', type: 'user', ...userData, password: '$2a$10$hashedpassword', isPremium: false, points: 0, adoptedStreets: [], completedTasks: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const user = await User.create(userData); const safeUser = user.toSafeObject(); expect(safeUser.password).toBeUndefined(); expect(safeUser.name).toBe(userData.name); expect(safeUser.email).toBe(userData.email); }); }); });