const request = require("supertest"); const { app, server } = require("../../server"); const couchdbService = require("../../services/couchdbService"); const User = require("../../models/User"); const PointTransaction = require("../../models/PointTransaction"); const Badge = require("../../models/Badge"); const UserBadge = require("../../models/UserBadge"); const jwt = require("jsonwebtoken"); const { clearCache } = require("../../middleware/cache"); describe("Leaderboard Routes", () => { let testUsers = []; let authToken; let testUserId; beforeAll(async () => { await couchdbService.initialize(); }); beforeEach(async () => { // Clear cache before each test clearCache(); // Create test users with varying points const userPromises = []; for (let i = 0; i < 10; i++) { const userData = { name: `Test User ${i}`, email: `testuser${i}@example.com`, password: "password123", points: (10 - i) * 100, // 1000, 900, 800, ..., 100 adoptedStreets: [], completedTasks: [], stats: { streetsAdopted: i, tasksCompleted: i * 2, postsCreated: i, eventsParticipated: i, badgesEarned: 0 } }; userPromises.push(User.create(userData)); } testUsers = await Promise.all(userPromises); // Set test user ID and create auth token testUserId = testUsers[0]._id; authToken = jwt.sign({ user: { id: testUserId } }, process.env.JWT_SECRET, { expiresIn: "1h" }); // Create some point transactions for weekly/monthly leaderboards const now = new Date(); const transactionPromises = []; // Create transactions for this week for (let i = 0; i < 5; i++) { const weeklyTransaction = { user: testUsers[i]._id, amount: (5 - i) * 50, transactionType: "earned", description: `Weekly test transaction ${i}`, balanceAfter: testUsers[i].points + (5 - i) * 50, createdAt: new Date(now.getTime() - i * 24 * 60 * 60 * 1000).toISOString() }; transactionPromises.push(PointTransaction.create(weeklyTransaction)); } // Create transactions for this month (but not this week) for (let i = 5; i < 8; i++) { const monthlyTransaction = { user: testUsers[i]._id, amount: (8 - i) * 30, transactionType: "earned", description: `Monthly test transaction ${i}`, balanceAfter: testUsers[i].points + (8 - i) * 30, createdAt: new Date(now.getTime() - (i + 5) * 24 * 60 * 60 * 1000).toISOString() }; transactionPromises.push(PointTransaction.create(monthlyTransaction)); } await Promise.all(transactionPromises); }); afterEach(async () => { // Clean up test data const deletePromises = []; // Delete test users for (const user of testUsers) { if (user._id && user._rev) { deletePromises.push( couchdbService.deleteDocument(user._id, user._rev).catch(() => {}) ); } } // Delete test transactions const transactions = await couchdbService.find({ selector: { type: "point_transaction" } }); for (const transaction of transactions) { deletePromises.push( couchdbService.deleteDocument(transaction._id, transaction._rev).catch(() => {}) ); } await Promise.all(deletePromises); testUsers = []; clearCache(); }); afterAll(async () => { await new Promise((resolve) => { server.close(resolve); }); }); describe("GET /api/leaderboard/global", () => { it("should get global leaderboard with top users", async () => { const res = await request(app) .get("/api/leaderboard/global") .expect(200); expect(res.body.success).toBe(true); expect(Array.isArray(res.body.data)).toBe(true); expect(res.body.data.length).toBeGreaterThan(0); expect(res.body.data.length).toBeLessThanOrEqual(100); // Check first user has highest points const firstUser = res.body.data[0]; expect(firstUser).toHaveProperty("rank", 1); expect(firstUser).toHaveProperty("userId"); expect(firstUser).toHaveProperty("username"); expect(firstUser).toHaveProperty("points"); expect(firstUser).toHaveProperty("streetsAdopted"); expect(firstUser).toHaveProperty("tasksCompleted"); expect(firstUser).toHaveProperty("badges"); // Verify sorting by points descending for (let i = 0; i < res.body.data.length - 1; i++) { expect(res.body.data[i].points).toBeGreaterThanOrEqual( res.body.data[i + 1].points ); } }); it("should respect limit parameter", async () => { const res = await request(app) .get("/api/leaderboard/global?limit=5") .expect(200); expect(res.body.success).toBe(true); expect(res.body.data.length).toBeLessThanOrEqual(5); expect(res.body.limit).toBe(5); }); it("should respect offset parameter", async () => { const res1 = await request(app) .get("/api/leaderboard/global?limit=5") .expect(200); const res2 = await request(app) .get("/api/leaderboard/global?limit=5&offset=5") .expect(200); expect(res2.body.success).toBe(true); expect(res2.body.offset).toBe(5); // First user in second page should have lower points than last in first page if (res2.body.data.length > 0 && res1.body.data.length === 5) { expect(res2.body.data[0].points).toBeLessThanOrEqual( res1.body.data[4].points ); } }); it("should cache leaderboard results", async () => { const res1 = await request(app) .get("/api/leaderboard/global") .expect(200); const res2 = await request(app) .get("/api/leaderboard/global") .expect(200); expect(res1.body).toEqual(res2.body); }); it("should limit maximum request to 500", async () => { const res = await request(app) .get("/api/leaderboard/global?limit=1000") .expect(200); expect(res.body.limit).toBe(500); }); }); describe("GET /api/leaderboard/weekly", () => { it("should get weekly leaderboard", async () => { const res = await request(app) .get("/api/leaderboard/weekly") .expect(200); expect(res.body.success).toBe(true); expect(res.body.timeframe).toBe("week"); expect(Array.isArray(res.body.data)).toBe(true); }); it("should only include points from this week", async () => { const res = await request(app) .get("/api/leaderboard/weekly") .expect(200); expect(res.body.success).toBe(true); // Weekly leaderboard should have users with weekly transactions if (res.body.data.length > 0) { const firstUser = res.body.data[0]; expect(firstUser).toHaveProperty("points"); expect(firstUser).toHaveProperty("rank", 1); } }); it("should respect limit and offset", async () => { const res = await request(app) .get("/api/leaderboard/weekly?limit=3&offset=1") .expect(200); expect(res.body.success).toBe(true); expect(res.body.limit).toBe(3); expect(res.body.offset).toBe(1); }); }); describe("GET /api/leaderboard/monthly", () => { it("should get monthly leaderboard", async () => { const res = await request(app) .get("/api/leaderboard/monthly") .expect(200); expect(res.body.success).toBe(true); expect(res.body.timeframe).toBe("month"); expect(Array.isArray(res.body.data)).toBe(true); }); it("should only include points from this month", async () => { const res = await request(app) .get("/api/leaderboard/monthly") .expect(200); expect(res.body.success).toBe(true); // Monthly leaderboard should have users with monthly transactions if (res.body.data.length > 0) { const firstUser = res.body.data[0]; expect(firstUser).toHaveProperty("points"); expect(firstUser).toHaveProperty("rank", 1); } }); }); describe("GET /api/leaderboard/friends", () => { it("should require authentication", async () => { const res = await request(app) .get("/api/leaderboard/friends") .expect(401); expect(res.body.success).toBe(false); }); it("should get friends leaderboard with authentication", async () => { const res = await request(app) .get("/api/leaderboard/friends") .set("x-auth-token", authToken) .expect(200); expect(res.body.success).toBe(true); expect(Array.isArray(res.body.data)).toBe(true); }); it("should include user if no friends", async () => { const res = await request(app) .get("/api/leaderboard/friends") .set("x-auth-token", authToken) .expect(200); expect(res.body.success).toBe(true); // Should at least include the user themselves const userEntry = res.body.data.find(entry => entry.userId === testUserId); if (res.body.data.length > 0) { expect(userEntry || res.body.data.length === 1).toBeTruthy(); } }); }); describe("GET /api/leaderboard/user/:userId", () => { it("should get user leaderboard position", async () => { const res = await request(app) .get(`/api/leaderboard/user/${testUserId}`) .expect(200); expect(res.body.success).toBe(true); expect(res.body.data).toHaveProperty("rank"); expect(res.body.data).toHaveProperty("userId", testUserId); expect(res.body.data).toHaveProperty("username"); expect(res.body.data).toHaveProperty("points"); expect(res.body.data).toHaveProperty("totalUsers"); expect(res.body.data).toHaveProperty("percentile"); }); it("should support timeframe parameter", async () => { const res = await request(app) .get(`/api/leaderboard/user/${testUserId}?timeframe=week`) .expect(200); expect(res.body.success).toBe(true); expect(res.body.data).toHaveProperty("rank"); }); it("should return 404 for non-existent user", async () => { const res = await request(app) .get("/api/leaderboard/user/nonexistent_user_id") .expect(404); expect(res.body.success).toBe(false); }); }); describe("GET /api/leaderboard/stats", () => { it("should get leaderboard statistics", async () => { const res = await request(app) .get("/api/leaderboard/stats") .expect(200); expect(res.body.success).toBe(true); expect(res.body.data).toHaveProperty("totalUsers"); expect(res.body.data).toHaveProperty("totalPoints"); expect(res.body.data).toHaveProperty("avgPoints"); expect(res.body.data).toHaveProperty("maxPoints"); expect(res.body.data).toHaveProperty("minPoints"); expect(res.body.data).toHaveProperty("weeklyStats"); expect(res.body.data.weeklyStats).toHaveProperty("totalPoints"); expect(res.body.data.weeklyStats).toHaveProperty("activeUsers"); expect(res.body.data.weeklyStats).toHaveProperty("transactions"); }); it("should cache statistics", async () => { const res1 = await request(app) .get("/api/leaderboard/stats") .expect(200); const res2 = await request(app) .get("/api/leaderboard/stats") .expect(200); expect(res1.body).toEqual(res2.body); }); }); describe("Edge Cases", () => { it("should handle empty leaderboard gracefully", async () => { // Delete all users first const deletePromises = testUsers.map(user => couchdbService.deleteDocument(user._id, user._rev).catch(() => {}) ); await Promise.all(deletePromises); clearCache(); const res = await request(app) .get("/api/leaderboard/global") .expect(200); expect(res.body.success).toBe(true); expect(res.body.data).toEqual([]); }); it("should handle invalid limit gracefully", async () => { const res = await request(app) .get("/api/leaderboard/global?limit=invalid") .expect(200); expect(res.body.success).toBe(true); expect(typeof res.body.limit).toBe("number"); }); it("should handle negative offset gracefully", async () => { const res = await request(app) .get("/api/leaderboard/global?offset=-5") .expect(200); expect(res.body.success).toBe(true); }); }); });