// Mock CouchDB service for testing const mockCouchdbService = { find: jest.fn(), get: jest.fn(), getDocument: jest.fn(), createDocument: jest.fn(), destroy: jest.fn(), initialize: jest.fn(), isReady: jest.fn().mockReturnValue(true), shutdown: jest.fn() }; // Mock the service module jest.mock('../../services/couchdbService', () => mockCouchdbService); const couchdbService = require('../../services/couchdbService'); const Badge = require('../../models/Badge'); describe('Badge Model', () => { beforeEach(() => { jest.clearAllMocks(); // Reset all mocks to ensure clean state mockCouchdbService.find.mockReset(); mockCouchdbService.get.mockReset(); mockCouchdbService.getDocument.mockReset(); mockCouchdbService.createDocument.mockReset(); mockCouchdbService.destroy.mockReset(); }); describe('findAll', () => { it('should return all badges sorted by order', async () => { const mockBadges = [ { _id: 'badge1', type: 'badge', name: 'First Badge', order: 1 }, { _id: 'badge2', type: 'badge', name: 'Second Badge', order: 2 } ]; couchdbService.find.mockResolvedValue({ docs: mockBadges }); const badges = await Badge.findAll(); expect(couchdbService.find).toHaveBeenCalledWith({ selector: { type: 'badge' }, sort: [{ order: 'asc' }] }); expect(badges).toEqual(mockBadges); }); it('should handle errors when finding badges', async () => { couchdbService.find.mockRejectedValue(new Error('Database error')); await expect(Badge.findAll()).rejects.toThrow('Database error'); }); }); describe('findById', () => { it('should return badge by ID', async () => { const mockBadge = { _id: 'badge_123', type: 'badge', name: 'Test Badge', description: 'Test description' }; couchdbService.get.mockResolvedValue(mockBadge); const badge = await Badge.findById('badge_123'); expect(couchdbService.get).toHaveBeenCalledWith('badge_123'); expect(badge).toEqual(mockBadge); }); it('should return null for non-badge documents', async () => { const mockDoc = { _id: 'user_123', type: 'user', name: 'Test User' }; couchdbService.get.mockResolvedValue(mockDoc); const badge = await Badge.findById('user_123'); expect(badge).toBeNull(); }); it('should return null for 404 errors', async () => { const error = new Error('Not found'); error.statusCode = 404; couchdbService.get.mockRejectedValue(error); const badge = await Badge.findById('nonexistent'); expect(badge).toBeNull(); }); it('should handle other errors', async () => { couchdbService.get.mockRejectedValue(new Error('Database error')); await expect(Badge.findById('badge_123')).rejects.toThrow('Database error'); }); }); describe('create', () => { it('should create a new badge', async () => { const badgeData = { name: 'Street Cleaner', description: 'Awarded for completing 10 street cleaning tasks', icon: 'broom', criteria: { type: 'task_count', threshold: 10 }, rarity: 'common', order: 1 }; const mockResult = { rev: '1-abc' }; couchdbService.createDocument.mockResolvedValue(mockResult); const badge = await Badge.create(badgeData); expect(couchdbService.createDocument).toHaveBeenCalledWith( expect.objectContaining({ _id: expect.stringMatching(/^badge_\d+_[a-z0-9]+$/), type: 'badge', name: badgeData.name, description: badgeData.description, icon: badgeData.icon, criteria: badgeData.criteria, rarity: badgeData.rarity, order: badgeData.order, isActive: true, createdAt: expect.any(String), updatedAt: expect.any(String) }) ); expect(badge).toEqual( expect.objectContaining({ ...badgeData, _rev: '1-abc', isActive: true, createdAt: expect.any(String), updatedAt: expect.any(String) }) ); }); it('should use default values', async () => { const badgeData = { name: 'Test Badge', description: 'Test description', icon: 'star' }; const mockResult = { rev: '1-abc' }; couchdbService.createDocument.mockResolvedValue(mockResult); const badge = await Badge.create(badgeData); expect(badge.rarity).toBe('common'); expect(badge.order).toBe(0); expect(badge.isActive).toBe(true); }); it('should handle errors during creation', async () => { const badgeData = { name: 'Test Badge', description: 'Test description', icon: 'star' }; couchdbService.createDocument.mockRejectedValue(new Error('Creation failed')); await expect(Badge.create(badgeData)).rejects.toThrow('Creation failed'); }); }); describe('update', () => { it('should update an existing badge', async () => { const existingBadge = { _id: 'badge_123', _rev: '1-abc', type: 'badge', name: 'Original Name', description: 'Original description', icon: 'star', criteria: { type: 'task_count', threshold: 5 }, rarity: 'common', order: 0, isActive: true, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; const updateData = { name: 'Updated Name', rarity: 'rare' }; const mockResult = { rev: '2-def' }; couchdbService.get.mockResolvedValue(existingBadge); couchdbService.createDocument.mockResolvedValue(mockResult); const badge = await Badge.update('badge_123', updateData); expect(couchdbService.get).toHaveBeenCalledWith('badge_123'); expect(couchdbService.createDocument).toHaveBeenCalledWith( expect.objectContaining({ ...existingBadge, name: 'Updated Name', rarity: 'rare', updatedAt: expect.any(String) }) ); expect(badge._rev).toBe('2-def'); }); it('should throw error for non-badge documents', async () => { const nonBadgeDoc = { _id: 'user_123', _rev: '1-abc', type: 'user' }; couchdbService.get.mockResolvedValue(nonBadgeDoc); await expect(Badge.update('user_123', { name: 'Updated' })).rejects.toThrow('Document is not a badge'); }); it('should handle errors during update', async () => { couchdbService.get.mockRejectedValue(new Error('Update failed')); await expect(Badge.update('badge_123', { name: 'Updated' })).rejects.toThrow('Update failed'); }); }); describe('delete', () => { it('should delete a badge', async () => { const mockBadge = { _id: 'badge_123', _rev: '1-abc', type: 'badge' }; couchdbService.get.mockResolvedValue(mockBadge); couchdbService.destroy.mockResolvedValue(true); const result = await Badge.delete('badge_123'); expect(couchdbService.get).toHaveBeenCalledWith('badge_123'); expect(couchdbService.destroy).toHaveBeenCalledWith('badge_123', '1-abc'); expect(result).toBe(true); }); it('should throw error for non-badge documents', async () => { const nonBadgeDoc = { _id: 'user_123', _rev: '1-abc', type: 'user' }; couchdbService.get.mockResolvedValue(nonBadgeDoc); await expect(Badge.delete('user_123')).rejects.toThrow('Document is not a badge'); }); it('should handle errors during deletion', async () => { couchdbService.get.mockRejectedValue(new Error('Delete failed')); await expect(Badge.delete('badge_123')).rejects.toThrow('Delete failed'); }); }); describe('findByCriteria', () => { it('should find badges by criteria type and threshold', async () => { const mockBadges = [ { _id: 'badge1', type: 'badge', criteria: { type: 'task_count', threshold: 10 } }, { _id: 'badge2', type: 'badge', criteria: { type: 'task_count', threshold: 5 } } ]; couchdbService.find.mockResolvedValue({ docs: mockBadges }); const badges = await Badge.findByCriteria('task_count', 7); expect(couchdbService.find).toHaveBeenCalledWith({ selector: { type: 'badge', 'criteria.type': 'task_count', 'criteria.threshold': { $lte: 7 } }, sort: [{ 'criteria.threshold': 'desc' }] }); expect(badges).toEqual(mockBadges); }); it('should handle errors when finding by criteria', async () => { couchdbService.find.mockRejectedValue(new Error('Search failed')); await expect(Badge.findByCriteria('task_count', 10)).rejects.toThrow('Search failed'); }); }); describe('findByRarity', () => { it('should find badges by rarity', async () => { const mockBadges = [ { _id: 'badge1', type: 'badge', rarity: 'rare', order: 1 }, { _id: 'badge2', type: 'badge', rarity: 'rare', order: 2 } ]; couchdbService.find.mockResolvedValue({ docs: mockBadges }); const badges = await Badge.findByRarity('rare'); expect(couchdbService.find).toHaveBeenCalledWith({ selector: { type: 'badge', rarity: 'rare' }, sort: [{ order: 'asc' }] }); expect(badges).toEqual(mockBadges); }); it('should handle errors when finding by rarity', async () => { couchdbService.find.mockRejectedValue(new Error('Search failed')); await expect(Badge.findByRarity('rare')).rejects.toThrow('Search failed'); }); }); });