// Mock CouchDB service for testing jest.mock('../../services/couchdbService', () => ({ createDocument: jest.fn(), findDocumentById: jest.fn(), updateDocument: jest.fn(), findByType: jest.fn(), })); const Reward = require('../../models/Reward'); const User = require('../../models/User'); const couchdbService = require('../../services/couchdbService'); describe('Reward Model', () => { beforeEach(() => { jest.clearAllMocks(); // Reset all mocks to ensure clean state couchdbService.createDocument.mockReset(); couchdbService.findDocumentById.mockReset(); couchdbService.updateDocument.mockReset(); couchdbService.findByType.mockReset(); }); describe('Schema Validation', () => { it('should create a valid reward', async () => { const rewardData = { name: 'Coffee Voucher', description: 'Get a free coffee at participating cafes', cost: 50, category: 'food', }; const mockCreated = { _id: 'reward_123', _rev: '1-abc', type: 'reward', ...rewardData, isActive: true, redeemedBy: [], createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const reward = await Reward.create(rewardData); expect(reward._id).toBeDefined(); expect(reward.name).toBe(rewardData.name); expect(reward.description).toBe(rewardData.description); expect(reward.cost).toBe(rewardData.cost); expect(reward.category).toBe(rewardData.category); expect(reward.isActive).toBe(true); }); it('should require name field', async () => { const rewardData = { description: 'Reward without name', cost: 50, }; expect(() => new Reward(rewardData)).toThrow(); }); it('should require description field', async () => { const rewardData = { name: 'Reward without description', cost: 50, }; expect(() => new Reward(rewardData)).toThrow(); }); it('should require cost field', async () => { const rewardData = { name: 'Reward without cost', description: 'This reward has no cost', }; expect(() => new Reward(rewardData)).toThrow(); }); it('should validate cost is a positive number', async () => { const rewardData = { name: 'Invalid Cost Reward', description: 'This reward has negative cost', cost: -10, }; expect(() => new Reward(rewardData)).toThrow(); }); }); describe('Default Values', () => { it('should default isActive to true', async () => { const rewardData = { name: 'Default Active Reward', description: 'Testing default active status', cost: 25, }; const mockCreated = { _id: 'reward_123', _rev: '1-abc', type: 'reward', ...rewardData, isActive: true, redeemedBy: [], createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const reward = await Reward.create(rewardData); expect(reward.isActive).toBe(true); }); it('should default redeemedBy to empty array', async () => { const rewardData = { name: 'Default Redeemed Reward', description: 'Testing default redeemed array', cost: 25, }; const mockCreated = { _id: 'reward_123', _rev: '1-abc', type: 'reward', ...rewardData, isActive: true, redeemedBy: [], createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const reward = await Reward.create(rewardData); expect(reward.redeemedBy).toEqual([]); expect(reward.redeemedBy).toHaveLength(0); }); }); describe('Categories', () => { const validCategories = ['food', 'merchandise', 'digital', 'experience', 'donation']; validCategories.forEach(category => { it(`should accept "${category}" as valid category`, async () => { const rewardData = { name: `${category} Reward`, description: `Testing ${category} category`, cost: 50, category, }; const mockCreated = { _id: 'reward_123', _rev: '1-abc', type: 'reward', ...rewardData, isActive: true, redeemedBy: [], createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const reward = await Reward.create(rewardData); expect(reward.category).toBe(category); }); }); }); describe('Cost Validation', () => { it('should accept valid cost values', async () => { const validCosts = [10, 25, 50, 100, 500, 1000]; for (const cost of validCosts) { const rewardData = { name: `Reward costing ${cost} points`, description: `Testing cost of ${cost}`, cost, }; const mockCreated = { _id: 'reward_123', _rev: '1-abc', type: 'reward', ...rewardData, isActive: true, redeemedBy: [], createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const reward = await Reward.create(rewardData); expect(reward.cost).toBe(cost); } }); it('should reject zero cost', async () => { const rewardData = { name: 'Free Reward', description: 'This reward should not be free', cost: 0, }; expect(() => new Reward(rewardData)).toThrow(); }); }); describe('Active Status', () => { it('should allow setting active status', async () => { const rewardData = { name: 'Inactive Reward', description: 'This reward is inactive', cost: 100, isActive: false, }; const mockCreated = { _id: 'reward_123', _rev: '1-abc', type: 'reward', ...rewardData, redeemedBy: [], createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const reward = await Reward.create(rewardData); expect(reward.isActive).toBe(false); }); it('should allow toggling active status', async () => { const rewardData = { name: 'Toggle Reward', description: 'Testing status toggle', cost: 75, isActive: true, }; const mockReward = { _id: 'reward_123', _rev: '1-abc', type: 'reward', ...rewardData, redeemedBy: [], createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.findDocumentById.mockResolvedValue(mockReward); couchdbService.updateDocument.mockResolvedValue({ ...mockReward, isActive: false, _rev: '2-def' }); const reward = await Reward.findById('reward_123'); reward.isActive = false; await reward.save(); expect(reward.isActive).toBe(false); }); }); describe('Redeemed By', () => { it('should track users who redeemed the reward', async () => { const rewardData = { name: 'Popular Reward', description: 'Many users want this', cost: 50, redeemedBy: [ { userId: 'user_123', name: 'User 1', redeemedAt: '2023-11-01T10:00:00.000Z' }, { userId: 'user_456', name: 'User 2', redeemedAt: '2023-11-02T10:00:00.000Z' } ] }; const mockCreated = { _id: 'reward_123', _rev: '1-abc', type: 'reward', ...rewardData, isActive: true, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const reward = await Reward.create(rewardData); expect(reward.redeemedBy).toHaveLength(2); expect(reward.redeemedBy[0].userId).toBe('user_123'); expect(reward.redeemedBy[1].userId).toBe('user_456'); }); it('should allow adding redemption records', async () => { const rewardData = { name: 'Redeemed Reward', description: 'Testing redemption tracking', cost: 25, }; const mockReward = { _id: 'reward_123', _rev: '1-abc', type: 'reward', ...rewardData, isActive: true, redeemedBy: [], createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.findDocumentById.mockResolvedValue(mockReward); couchdbService.updateDocument.mockResolvedValue({ ...mockReward, redeemedBy: [ { userId: 'user_789', name: 'User 3', redeemedAt: '2023-11-03T10:00:00.000Z' } ], _rev: '2-def' }); const reward = await Reward.findById('reward_123'); reward.redeemedBy.push({ userId: 'user_789', name: 'User 3', redeemedAt: '2023-11-03T10:00:00.000Z' }); await reward.save(); expect(reward.redeemedBy).toHaveLength(1); expect(reward.redeemedBy[0].userId).toBe('user_789'); }); }); describe('Timestamps', () => { it('should automatically set createdAt and updatedAt', async () => { const rewardData = { name: 'Timestamp Reward', description: 'Testing timestamps', cost: 30, }; const mockCreated = { _id: 'reward_123', _rev: '1-abc', type: 'reward', ...rewardData, isActive: true, redeemedBy: [], createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.createDocument.mockResolvedValue(mockCreated); const reward = await Reward.create(rewardData); expect(reward.createdAt).toBeDefined(); expect(reward.updatedAt).toBeDefined(); expect(typeof reward.createdAt).toBe('string'); expect(typeof reward.updatedAt).toBe('string'); }); it('should update updatedAt on modification', async () => { const rewardData = { name: 'Update Test Reward', description: 'Testing update timestamp', cost: 40, }; const mockReward = { _id: 'reward_123', _rev: '1-abc', type: 'reward', ...rewardData, isActive: true, redeemedBy: [], createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.findDocumentById.mockResolvedValue(mockReward); couchdbService.updateDocument.mockResolvedValue({ ...mockReward, isActive: false, _rev: '2-def', updatedAt: '2023-01-01T00:00:01.000Z' }); const reward = await Reward.findById('reward_123'); const originalUpdatedAt = reward.updatedAt; reward.isActive = false; await reward.save(); expect(reward.updatedAt).not.toBe(originalUpdatedAt); }); }); describe('Static Methods', () => { it('should find reward by ID', async () => { const mockReward = { _id: 'reward_123', _rev: '1-abc', type: 'reward', name: 'Test Reward', description: 'Test description', cost: 50, isActive: true, redeemedBy: [], createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; couchdbService.findDocumentById.mockResolvedValue(mockReward); const reward = await Reward.findById('reward_123'); expect(reward).toBeDefined(); expect(reward._id).toBe('reward_123'); expect(reward.name).toBe('Test Reward'); }); it('should return null when reward not found', async () => { couchdbService.findDocumentById.mockResolvedValue(null); const reward = await Reward.findById('nonexistent'); expect(reward).toBeNull(); }); }); });