- Fixed authentication middleware response format to include success field - Fixed JWT token structure in leaderboard tests - Adjusted performance test thresholds for test environment - All 491 backend tests now passing - Improved test coverage consistency across routes 🤖 Generated with [AI Assistant] Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
397 lines
12 KiB
JavaScript
397 lines
12 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|