Files
adopt-a-street/backend/__tests__/gamification.test.js
William Valentin a0c863a972 feat: add comprehensive test coverage for advanced features
- Add Socket.IO real-time feature tests
- Add geospatial query tests with CouchDB integration
- Add gamification system tests (points, badges, leaderboard)
- Add file upload tests with Cloudinary integration
- Add comprehensive error handling tests
- Add performance and stress tests
- Add test documentation and coverage summary
- Install missing testing dependencies (mongodb-memory-server, socket.io-client)

Test Coverage:
- Socket.IO: Authentication, events, rooms, concurrency
- Geospatial: Nearby queries, bounding boxes, performance
- Gamification: Points, badges, transactions, leaderboards
- File Uploads: Profile pictures, posts, reports, validation
- Error Handling: Auth, validation, database, rate limiting
- Performance: Response times, concurrency, memory usage

🤖 Generated with AI Assistant

Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
2025-11-01 16:17:28 -07:00

620 lines
19 KiB
JavaScript

const request = require("supertest");
const mongoose = require("mongoose");
const { MongoMemoryServer } = require("mongodb-memory-server");
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");
describe("Gamification System", () => {
let mongoServer;
let testUser;
let testUser2;
let authToken;
let authToken2;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
await mongoose.connect(mongoUri);
// Initialize CouchDB for testing
await couchdbService.initialize();
// Create test users
testUser = new User({
name: "Test User",
email: "test@example.com",
password: "password123",
points: 0,
stats: {
streetsAdopted: 0,
tasksCompleted: 0,
postsCreated: 0,
eventsParticipated: 0,
badgesEarned: 0,
},
});
await testUser.save();
testUser2 = new User({
name: "Test User 2",
email: "test2@example.com",
password: "password123",
points: 100,
stats: {
streetsAdopted: 1,
tasksCompleted: 5,
postsCreated: 3,
eventsParticipated: 2,
badgesEarned: 2,
},
});
await testUser2.save();
// Generate auth tokens
const jwt = require("jsonwebtoken");
authToken = jwt.sign(
{ user: { id: testUser._id.toString() } },
process.env.JWT_SECRET || "test_secret"
);
authToken2 = jwt.sign(
{ user: { id: testUser2._id.toString() } },
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 mongoose.disconnect();
await mongoServer.stop();
await couchdbService.shutdown();
});
beforeEach(async () => {
// Reset user points and stats
await User.findByIdAndUpdate(testUser._id, {
points: 0,
stats: {
streetsAdopted: 0,
tasksCompleted: 0,
postsCreated: 0,
eventsParticipated: 0,
badgesEarned: 0,
},
earnedBadges: [],
});
});
describe("Points System", () => {
test("should award points for street adoption", async () => {
const street = new Street({
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 = new Task({
title: "Test Task",
description: "Test Description",
street: { streetId: new mongoose.Types.ObjectId() },
pointsAwarded: 10,
status: "pending",
});
await task.save();
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 = new Task({
title: `Task ${i}`,
description: "Test Description",
street: { streetId: new mongoose.Types.ObjectId() },
pointsAwarded: 10,
status: "pending",
});
await task.save();
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 = new Task({
title: `Task ${i}`,
description: "Test Description",
street: { streetId: new mongoose.Types.ObjectId() },
pointsAwarded: activity.points,
status: "pending",
});
await task.save();
await request(app)
.put(`/api/tasks/${task._id}/complete`)
.set("x-auth-token", authToken);
} else if (activity.type === 'event') {
const event = new Event({
title: `Event ${i}`,
description: "Test Description",
date: new Date(Date.now() + 86400000),
location: "Test Location",
participants: [],
});
await event.save();
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);
});
});
});