- Fixed 5/7 route test suites (auth, events, reports, rewards, streets) - Updated Jest configuration with global CouchDB mocks - Created comprehensive test helper utilities with proper ID generation - Fixed pagination response format expectations (.data property) - Added proper model method mocks (populate, save, toJSON, etc.) - Resolved ID validation issues for different entity types - Implemented proper CouchDB service method mocking - Updated test helpers to generate valid IDs matching validator patterns Remaining work: - posts.test.js: needs model mocking and response format fixes - tasks.test.js: needs Task model constructor fixes and mocking 🤖 Generated with [AI Assistant] Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
606 lines
19 KiB
JavaScript
606 lines
19 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
}); |