// Note: CouchDB mocking is handled in jest.preSetup.js const request = require("supertest"); const express = require("express"); const jwt = require("jsonwebtoken"); const User = require("../models/User"); const Task = require("../models/Task"); const Street = require("../models/Street"); // Create test app with gamification routes const createTestApp = () => { const app = express(); app.use(express.json()); // Mock auth middleware const authMiddleware = (req, res, next) => { const token = req.header("x-auth-token"); if (!token) { return res.status(401).json({ msg: "No token, authorization denied" }); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET || "test_secret"); req.user = decoded.user; next(); } catch (err) { res.status(401).json({ msg: "Token is not valid" }); } }; // Mock gamification routes app.post("/api/tasks/complete", authMiddleware, (req, res) => { const { taskId } = req.body; if (!taskId) { return res.status(400).json({ msg: "Task ID is required" }); } // Mock task completion with points const points = Math.floor(Math.random() * 50) + 10; // 10-60 points res.json({ success: true, points, message: `Task completed! You earned ${points} points.`, newTotal: 100 + points }); }); app.get("/api/users/points", authMiddleware, (req, res) => { // Mock user points data res.json({ points: 100, level: 5, stats: { streetsAdopted: 1, tasksCompleted: 1, postsCreated: 1, eventsParticipated: 1, badgesEarned: 1 }, recentActivity: [ { type: "task_completed", points: 25, description: "Completed street cleaning", timestamp: new Date().toISOString() } ] }); }); app.get("/api/users/leaderboard", authMiddleware, (req, res) => { // Mock leaderboard const leaderboard = [ { _id: "user1", name: "User 1", points: 500, rank: 1 }, { _id: "user2", name: "User 2", points: 400, rank: 2 }, { _id: "user3", name: "User 3", points: 300, rank: 3 } ]; res.json(leaderboard); }); app.post("/api/users/award-badge", authMiddleware, (req, res) => { const { badgeType } = req.body; if (!badgeType) { return res.status(400).json({ msg: "Badge type is required" }); } res.json({ success: true, badge: { type: badgeType, name: "Test Badge", description: "Awarded for testing", icon: "test-icon", awardedAt: new Date().toISOString() } }); }); app.post("/api/streets/adopt", authMiddleware, (req, res) => { const { streetId } = req.body; if (!streetId) { return res.status(400).json({ msg: "Street ID is required" }); } res.json({ success: true, points: 50, message: "Street adopted! You earned 50 points.", street: { _id: streetId, adoptedBy: req.user.id, adoptionDate: new Date().toISOString() } }); }); app.post("/api/events/join", authMiddleware, (req, res) => { const { eventId } = req.body; if (!eventId) { return res.status(400).json({ msg: "Event ID is required" }); } res.json({ success: true, points: 15, message: "Joined event! You earned 15 points.", event: { _id: eventId, participants: [req.user.id], joinDate: new Date().toISOString() } }); }); app.get("/api/users/achievements", authMiddleware, (req, res) => { // Mock achievements res.json({ achievements: [ { _id: "achievement1", name: "First Street", description: "Adopt your first street", icon: "street-icon", unlocked: true, unlockedAt: new Date().toISOString() }, { _id: "achievement2", name: "Task Master", description: "Complete 10 tasks", icon: "task-icon", unlocked: false, progress: 7, total: 10 } ], totalAchievements: 10, unlockedAchievements: 3 }); }); // Global error handler app.use((err, req, res, next) => { console.error(err.message); res.status(500).json({ msg: "Server error" }); }); return app; }; describe("Gamification System", () => { let app; let testUser; let authToken; beforeAll(() => { app = createTestApp(); // Create mock test user testUser = { _id: "test_user_123", name: "Test User", email: "test@example.com" }; // Generate auth token authToken = jwt.sign( { user: { id: testUser._id } }, process.env.JWT_SECRET || "test_secret" ); }); beforeEach(() => { // Reset mocks before each test }); describe("Task Completion Rewards", () => { test("should award points for completing a task", async () => { const response = await request(app) .post("/api/tasks/complete") .set("x-auth-token", authToken) .send({ taskId: "task_123" }) .expect(200); expect(response.body.success).toBe(true); expect(response.body.points).toBeGreaterThan(0); expect(response.body.message).toContain("earned"); expect(response.body.newTotal).toBeGreaterThan(100); }); test("should require task ID for completion", async () => { const response = await request(app) .post("/api/tasks/complete") .set("x-auth-token", authToken) .send({}) .expect(400); expect(response.body.msg).toContain("Task ID is required"); }); test("should require authentication for task completion", async () => { const response = await request(app) .post("/api/tasks/complete") .send({ taskId: "task_123" }) .expect(401); expect(response.body.msg).toContain("authorization denied"); }); }); describe("User Points and Stats", () => { test("should retrieve user points and statistics", async () => { const response = await request(app) .get("/api/users/points") .set("x-auth-token", authToken) .expect(200); expect(response.body.points).toBe(100); expect(response.body.level).toBe(5); expect(response.body.stats).toBeDefined(); expect(response.body.stats.streetsAdopted).toBe(1); expect(response.body.stats.tasksCompleted).toBe(1); expect(response.body.recentActivity).toBeDefined(); expect(Array.isArray(response.body.recentActivity)).toBe(true); }); test("should require authentication for points retrieval", async () => { const response = await request(app) .get("/api/users/points") .expect(401); expect(response.body.msg).toContain("authorization denied"); }); }); describe("Leaderboard System", () => { test("should retrieve leaderboard with ranked users", async () => { const response = await request(app) .get("/api/users/leaderboard") .set("x-auth-token", authToken) .expect(200); expect(Array.isArray(response.body)).toBe(true); expect(response.body.length).toBeGreaterThan(0); // Check that users are properly ranked response.body.forEach((user, index) => { expect(user.rank).toBe(index + 1); expect(user.points).toBeGreaterThan(0); expect(user.name).toBeDefined(); }); // Check that leaderboard is sorted by points (descending) for (let i = 1; i < response.body.length; i++) { expect(response.body[i-1].points).toBeGreaterThanOrEqual(response.body[i].points); } }); test("should require authentication for leaderboard access", async () => { const response = await request(app) .get("/api/users/leaderboard") .expect(401); expect(response.body.msg).toContain("authorization denied"); }); }); describe("Badge System", () => { test("should award badge to user", async () => { const response = await request(app) .post("/api/users/award-badge") .set("x-auth-token", authToken) .send({ badgeType: "first_street" }) .expect(200); expect(response.body.success).toBe(true); expect(response.body.badge).toBeDefined(); expect(response.body.badge.type).toBe("first_street"); expect(response.body.badge.name).toBe("Test Badge"); expect(response.body.badge.awardedAt).toBeDefined(); }); test("should require badge type for awarding", async () => { const response = await request(app) .post("/api/users/award-badge") .set("x-auth-token", authToken) .send({}) .expect(400); expect(response.body.msg).toContain("Badge type is required"); }); test("should require authentication for badge awarding", async () => { const response = await request(app) .post("/api/users/award-badge") .send({ badgeType: "first_street" }) .expect(401); expect(response.body.msg).toContain("authorization denied"); }); }); describe("Street Adoption Rewards", () => { test("should award points for street adoption", async () => { const response = await request(app) .post("/api/streets/adopt") .set("x-auth-token", authToken) .send({ streetId: "street_123" }) .expect(200); expect(response.body.success).toBe(true); expect(response.body.points).toBe(50); expect(response.body.message).toContain("earned 50 points"); expect(response.body.street).toBeDefined(); expect(response.body.street.adoptedBy).toBe(testUser._id); }); test("should require street ID for adoption", async () => { const response = await request(app) .post("/api/streets/adopt") .set("x-auth-token", authToken) .send({}) .expect(400); expect(response.body.msg).toContain("Street ID is required"); }); }); describe("Event Participation Rewards", () => { test("should award points for joining events", async () => { const response = await request(app) .post("/api/events/join") .set("x-auth-token", authToken) .send({ eventId: "event_123" }) .expect(200); expect(response.body.success).toBe(true); expect(response.body.points).toBe(15); expect(response.body.message).toContain("earned 15 points"); expect(response.body.event).toBeDefined(); expect(response.body.event.participants).toContain(testUser._id); }); test("should require event ID for joining", async () => { const response = await request(app) .post("/api/events/join") .set("x-auth-token", authToken) .send({}) .expect(400); expect(response.body.msg).toContain("Event ID is required"); }); }); describe("Achievements System", () => { test("should retrieve user achievements", async () => { const response = await request(app) .get("/api/users/achievements") .set("x-auth-token", authToken) .expect(200); expect(response.body.achievements).toBeDefined(); expect(Array.isArray(response.body.achievements)).toBe(true); expect(response.body.totalAchievements).toBe(10); expect(response.body.unlockedAchievements).toBe(3); // Check achievement structure response.body.achievements.forEach(achievement => { expect(achievement._id).toBeDefined(); expect(achievement.name).toBeDefined(); expect(achievement.description).toBeDefined(); expect(achievement.icon).toBeDefined(); expect(typeof achievement.unlocked).toBe('boolean'); if (achievement.unlocked) { expect(achievement.unlockedAt).toBeDefined(); } else { expect(achievement.progress).toBeDefined(); expect(achievement.total).toBeDefined(); } }); }); test("should require authentication for achievements retrieval", async () => { const response = await request(app) .get("/api/users/achievements") .expect(401); expect(response.body.msg).toContain("authorization denied"); }); }); describe("Gamification Consistency", () => { test("should maintain consistent point calculations", async () => { // Complete multiple actions and verify point consistency const taskResponse = await request(app) .post("/api/tasks/complete") .set("x-auth-token", authToken) .send({ taskId: "task_123" }); const streetResponse = await request(app) .post("/api/streets/adopt") .set("x-auth-token", authToken) .send({ streetId: "street_123" }); const eventResponse = await request(app) .post("/api/events/join") .set("x-auth-token", authToken) .send({ eventId: "event_123" }); // Verify all responses have proper point awards expect(taskResponse.body.points).toBeGreaterThan(0); expect(streetResponse.body.points).toBe(50); expect(eventResponse.body.points).toBe(15); // Verify all have success messages expect(taskResponse.body.message).toContain("earned"); expect(streetResponse.body.message).toContain("earned"); expect(eventResponse.body.message).toContain("earned"); }); test("should handle concurrent gamification actions", async () => { // Test concurrent requests const actions = [ request(app) .post("/api/tasks/complete") .set("x-auth-token", authToken) .send({ taskId: "task_1" }), request(app) .post("/api/tasks/complete") .set("x-auth-token", authToken) .send({ taskId: "task_2" }), request(app) .post("/api/events/join") .set("x-auth-token", authToken) .send({ eventId: "event_1" }) ]; const responses = await Promise.all(actions); // All should succeed responses.forEach(response => { expect(response.status).toBe(200); expect(response.body.success).toBe(true); expect(response.body.points).toBeGreaterThan(0); }); }); }); });