const request = require("supertest"); const app = require("../server"); const User = require("../models/User"); const Task = require("../models/Task"); const Street = require("../models/Street"); const Event = require("../models/Event"); const Post = require("../models/Post"); const couchdbService = require("../services/couchdbService"); const { generateTestId } = require('./utils/idGenerator'); describe("Gamification System", () => { let testUser; let testUser2; let authToken; let authToken2; beforeAll(async () => { // Initialize CouchDB for testing await couchdbService.initialize(); // Create test users testUser = await User.create({ name: "Test User", email: "test@example.com", password: "password123", points: 0, stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0, }, }); testUser2 = await User.create({ name: "Test User 2", email: "test2@example.com", password: "password123", points: 100, stats: { streetsAdopted: 1, tasksCompleted: 5, postsCreated: 3, eventsParticipated: 2, badgesEarned: 2, }, }); // Generate auth tokens const jwt = require("jsonwebtoken"); authToken = jwt.sign( { user: { id: testUser._id } }, process.env.JWT_SECRET || "test_secret" ); authToken2 = jwt.sign( { user: { id: testUser2._id } }, process.env.JWT_SECRET || "test_secret" ); // Create test badges in CouchDB const badges = [ { _id: "badge_starter", type: "badge", name: "Street Starter", description: "Adopt your first street", icon: "🏠", rarity: "common", criteria: { type: "street_adoptions", threshold: 1 }, isActive: true, order: 1, }, { _id: "badge_task_master", type: "badge", name: "Task Master", description: "Complete 10 tasks", icon: "✅", rarity: "rare", criteria: { type: "task_completions", threshold: 10 }, isActive: true, order: 2, }, { _id: "badge_social_butterfly", type: "badge", name: "Social Butterfly", description: "Create 20 posts", icon: "🦋", rarity: "epic", criteria: { type: "post_creations", threshold: 20 }, isActive: true, order: 3, }, { _id: "badge_event_enthusiast", type: "badge", name: "Event Enthusiast", description: "Participate in 5 events", icon: "🎉", rarity: "rare", criteria: { type: "event_participations", threshold: 5 }, isActive: true, order: 4, }, { _id: "badge_point_collector", type: "badge", name: "Point Collector", description: "Earn 500 points", icon: "💰", rarity: "legendary", criteria: { type: "points_earned", threshold: 500 }, isActive: true, order: 5, }, ]; for (const badge of badges) { await couchdbService.createDocument(badge); } }); afterAll(async () => { await couchdbService.shutdown(); }); beforeEach(async () => { // Reset user points and stats const user = await User.findById(testUser._id); user.points = 0; user.stats = { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0, }; user.earnedBadges = []; await user.save(); }); describe("Points System", () => { test("should award points for street adoption", async () => { const street = await Street.create({ name: "Test Street", location: { type: "Point", coordinates: [-74.0060, 40.7128] }, status: "available", }); await street.save(); const response = await request(app) .put(`/api/streets/adopt/${street._id}`) .set("x-auth-token", authToken) .expect(200); expect(response.body.pointsAwarded).toBe(50); expect(response.body.newBalance).toBe(50); // Verify user points updated const updatedUser = await User.findById(testUser._id); expect(updatedUser.points).toBe(50); expect(updatedUser.stats.streetsAdopted).toBe(1); }); test("should award points for task completion", async () => { const task = await Task.create({ title: "Test Task", description: "Test Description", street: { streetId: generateTestId() }, pointsAwarded: 10, status: "pending", }); const response = await request(app) .put(`/api/tasks/${task._id}/complete`) .set("x-auth-token", authToken) .expect(200); expect(response.body.pointsAwarded).toBe(10); expect(response.body.newBalance).toBe(10); const updatedUser = await User.findById(testUser._id); expect(updatedUser.points).toBe(10); expect(updatedUser.stats.tasksCompleted).toBe(1); }); test("should award points for event participation", async () => { const event = new Event({ title: "Test Event", description: "Test Description", date: new Date(Date.now() + 86400000), location: "Test Location", participants: [], }); await event.save(); const response = await request(app) .put(`/api/events/rsvp/${event._id}`) .set("x-auth-token", authToken) .expect(200); expect(response.body.pointsAwarded).toBe(15); expect(response.body.newBalance).toBe(15); const updatedUser = await User.findById(testUser._id); expect(updatedUser.points).toBe(15); expect(updatedUser.stats.eventsParticipated).toBe(1); }); test("should award points for post creation", async () => { const postData = { content: "This is a test post", }; const response = await request(app) .post("/api/posts") .set("x-auth-token", authToken) .send(postData) .expect(200); // Points are awarded through CouchDB service const updatedUser = await User.findById(testUser._id); expect(updatedUser.points).toBe(5); expect(updatedUser.stats.postsCreated).toBe(1); }); test("should track point transactions", async () => { // Create some activity to generate transactions const street = new Street({ name: "Test Street", location: { type: "Point", coordinates: [-74.0060, 40.7128] }, status: "available", }); await street.save(); await request(app) .put(`/api/streets/adopt/${street._id}`) .set("x-auth-token", authToken); // Check CouchDB for transactions const transactions = await couchdbService.findByType('point_transaction', { 'user.userId': testUser._id.toString() }); expect(transactions.length).toBe(1); expect(transactions[0].amount).toBe(50); expect(transactions[0].description).toBe('Street adoption'); expect(transactions[0].balanceAfter).toBe(50); }); test("should prevent negative points", async () => { // Try to deduct more points than user has await expect( couchdbService.updateUserPoints(testUser._id.toString(), -100, "Penalty") ).rejects.toThrow(); const user = await User.findById(testUser._id); expect(user.points).toBe(0); }); }); describe("Badge System", () => { test("should award street adoption badge", async () => { // Adopt a street const street = new Street({ name: "Test Street", location: { type: "Point", coordinates: [-74.0060, 40.7128] }, status: "available", }); await street.save(); await request(app) .put(`/api/streets/adopt/${street._id}`) .set("x-auth-token", authToken); // Check if badge was awarded const updatedUser = await User.findById(testUser._id); expect(updatedUser.earnedBadges.length).toBe(1); expect(updatedUser.earnedBadges[0].name).toBe("Street Starter"); expect(updatedUser.stats.badgesEarned).toBe(1); }); test("should award task completion badge", async () => { // Complete 10 tasks for (let i = 0; i < 10; i++) { const task = await Task.create({ title: `Task ${i}`, description: "Test Description", street: { streetId: generateTestId() }, pointsAwarded: 10, status: "pending", }); await request(app) .put(`/api/tasks/${task._id}/complete`) .set("x-auth-token", authToken); } // Check if badge was awarded const updatedUser = await User.findById(testUser._id); const taskMasterBadge = updatedUser.earnedBadges.find( (badge) => badge.name === "Task Master" ); expect(taskMasterBadge).toBeDefined(); expect(taskMasterBadge.rarity).toBe("rare"); }); test("should track badge progress", async () => { // Create 5 posts (out of 20 needed for Social Butterfly badge) for (let i = 0; i < 5; i++) { await request(app) .post("/api/posts") .set("x-auth-token", authToken) .send({ content: `Test post ${i}` }); } // Check badge progress in CouchDB const userBadges = await couchdbService.findByType('user_badge', { userId: testUser._id.toString() }); const socialButterflyProgress = userBadges.find( (badge) => badge.badgeId === "badge_social_butterfly" ); expect(socialButterflyProgress).toBeDefined(); expect(socialButterflyProgress.progress).toBe(25); // 5/20 = 25% }); test("should not award duplicate badges", async () => { // Adopt a street twice (second attempt should fail but still check badges) const street = new Street({ name: "Test Street", location: { type: "Point", coordinates: [-74.0060, 40.7128] }, status: "available", }); await street.save(); await request(app) .put(`/api/streets/adopt/${street._id}`) .set("x-auth-token", authToken); // Try to adopt again (should fail) await request(app) .put(`/api/streets/adopt/${street._id}`) .set("x-auth-token", authToken) .expect(400); // Should still only have one badge const updatedUser = await User.findById(testUser._id); const streetStarterBadges = updatedUser.earnedBadges.filter( (badge) => badge.name === "Street Starter" ); expect(streetStarterBadges.length).toBe(1); }); test("should award point-based badge", async () => { // Accumulate 500 points through various activities const activities = [ { type: 'street', points: 50, count: 4 }, // 4 street adoptions = 200 points { type: 'task', points: 10, count: 20 }, // 20 tasks = 200 points { type: 'event', points: 15, count: 6 }, // 6 events = 90 points { type: 'post', points: 5, count: 2 }, // 2 posts = 10 points ]; for (const activity of activities) { for (let i = 0; i < activity.count; i++) { if (activity.type === 'street') { const street = new Street({ name: `Street ${i}`, location: { type: "Point", coordinates: [-74.0060, 40.7128] }, status: "available", }); await street.save(); await request(app) .put(`/api/streets/adopt/${street._id}`) .set("x-auth-token", authToken); } else if (activity.type === 'task') { const task = await Task.create({ title: `Task ${i}`, description: "Test Description", street: { streetId: generateTestId() }, pointsAwarded: activity.points, status: "pending", }); await request(app) .put(`/api/tasks/${task._id}/complete`) .set("x-auth-token", authToken); } else if (activity.type === 'event') { const event = await Event.create({ title: `Event ${i}`, description: "Test Description", date: new Date(Date.now() + 86400000), location: "Test Location", participants: [], }); await request(app) .put(`/api/events/rsvp/${event._id}`) .set("x-auth-token", authToken); } else if (activity.type === 'post') { await request(app) .post("/api/posts") .set("x-auth-token", authToken) .send({ content: `Test post ${i}` }); } } } // Check if Point Collector badge was awarded const updatedUser = await User.findById(testUser._id); const pointCollectorBadge = updatedUser.earnedBadges.find( (badge) => badge.name === "Point Collector" ); expect(pointCollectorBadge).toBeDefined(); expect(pointCollectorBadge.rarity).toBe("legendary"); }); }); describe("Leaderboard System", () => { beforeEach(async () => { // Set up users with different point levels await User.findByIdAndUpdate(testUser._id, { points: 250 }); await User.findByIdAndUpdate(testUser2._id, { points: 450 }); // Create a third user const testUser3 = new User({ name: "Leader User", email: "leader@example.com", password: "password123", points: 750, stats: { streetsAdopted: 5, tasksCompleted: 25, postsCreated: 10, eventsParticipated: 8, badgesEarned: 4, }, }); await testUser3.save(); }); test("should return leaderboard in correct order", async () => { const response = await request(app) .get("/api/rewards/leaderboard") .expect(200); expect(response.body.length).toBe(3); expect(response.body[0].points).toBe(750); expect(response.body[0].name).toBe("Leader User"); expect(response.body[1].points).toBe(450); expect(response.body[2].points).toBe(250); }); test("should limit leaderboard results", async () => { const response = await request(app) .get("/api/rewards/leaderboard?limit=2") .expect(200); expect(response.body.length).toBe(2); expect(response.body[0].points).toBe(750); expect(response.body[1].points).toBe(450); }); test("should include user stats in leaderboard", async () => { const response = await request(app) .get("/api/rewards/leaderboard") .expect(200); const leader = response.body[0]; expect(leader.stats).toBeDefined(); expect(leader.stats.streetsAdopted).toBe(5); expect(leader.stats.tasksCompleted).toBe(25); expect(leader.stats.badgesEarned).toBe(4); }); test("should handle empty leaderboard", async () => { // Delete all users await User.deleteMany({}); const response = await request(app) .get("/api/rewards/leaderboard") .expect(200); expect(response.body).toHaveLength(0); }); }); describe("Point Transactions", () => { test("should create transaction record for point changes", async () => { await couchdbService.updateUserPoints( testUser._id.toString(), 25, "Test transaction", { entityType: "Test", entityId: "test123", entityName: "Test Entity" } ); const transactions = await couchdbService.findByType('point_transaction', { 'user.userId': testUser._id.toString() }); expect(transactions.length).toBe(1); expect(transactions[0].amount).toBe(25); expect(transactions[0].description).toBe("Test transaction"); expect(transactions[0].relatedEntity.entityType).toBe("Test"); expect(transactions[0].relatedEntity.entityId).toBe("test123"); expect(transactions[0].balanceAfter).toBe(25); }); test("should track transaction history", async () => { // Create multiple transactions await couchdbService.updateUserPoints(testUser._id.toString(), 50, "Street adoption"); await couchdbService.updateUserPoints(testUser._id.toString(), 10, "Task completion"); await couchdbService.updateUserPoints(testUser._id.toString(), 15, "Event participation"); const transactions = await couchdbService.findByType('point_transaction', { 'user.userId': testUser._id.toString() }, { sort: [{ createdAt: 'desc' }] }); expect(transactions.length).toBe(3); expect(transactions[0].amount).toBe(15); // Most recent expect(transactions[1].amount).toBe(10); expect(transactions[2].amount).toBe(50); // Oldest }); test("should categorize transactions correctly", async () => { await couchdbService.updateUserPoints(testUser._id.toString(), 50, "Street adoption"); await couchdbService.updateUserPoints(testUser._id.toString(), 10, "Completed task: Test task"); await couchdbService.updateUserPoints(testUser._id.toString(), 5, "Created post: Test post"); await couchdbService.updateUserPoints(testUser._id.toString(), 15, "Joined event: Test event"); const transactions = await couchdbService.findByType('point_transaction', { 'user.userId': testUser._id.toString() }); const types = transactions.map(t => t.type); expect(types).toContain('street_adoption'); expect(types).toContain('task_completion'); expect(types).toContain('post_creation'); expect(types).toContain('event_participation'); }); }); describe("Performance Tests", () => { test("should handle concurrent point updates", async () => { const startTime = Date.now(); const promises = []; for (let i = 0; i < 50; i++) { promises.push( couchdbService.updateUserPoints( testUser._id.toString(), 5, `Concurrent transaction ${i}` ) ); } await Promise.all(promises); const endTime = Date.now(); const duration = endTime - startTime; // Should complete within 5 seconds expect(duration).toBeLessThan(5000); // Check final balance const user = await User.findById(testUser._id); expect(user.points).toBe(250); // 50 * 5 }); test("should handle large leaderboard efficiently", async () => { // Create many users const users = []; for (let i = 0; i < 100; i++) { users.push({ name: `User ${i}`, email: `user${i}@example.com`, password: "password123", points: Math.floor(Math.random() * 1000), }); } await User.insertMany(users); const startTime = Date.now(); const response = await request(app) .get("/api/rewards/leaderboard") .expect(200); const endTime = Date.now(); const duration = endTime - startTime; // Should complete within 2 seconds even with 100+ users expect(duration).toBeLessThan(2000); expect(response.body.length).toBeGreaterThan(100); }); }); });