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>
This commit is contained in:
562
backend/__tests__/performance.test.js
Normal file
562
backend/__tests__/performance.test.js
Normal file
@@ -0,0 +1,562 @@
|
||||
const request = require("supertest");
|
||||
const mongoose = require("mongoose");
|
||||
const { MongoMemoryServer } = require("mongodb-memory-server");
|
||||
const app = require("../server");
|
||||
const User = require("../models/User");
|
||||
const Street = require("../models/Street");
|
||||
const Task = require("../models/Task");
|
||||
const Event = require("../models/Event");
|
||||
const Post = require("../models/Post");
|
||||
|
||||
describe("Performance Tests", () => {
|
||||
let mongoServer;
|
||||
let testUsers = [];
|
||||
let authTokens = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
mongoServer = await MongoMemoryServer.create();
|
||||
const mongoUri = mongoServer.getUri();
|
||||
await mongoose.connect(mongoUri);
|
||||
|
||||
// Create multiple test users for concurrent testing
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const user = new User({
|
||||
name: `Test User ${i}`,
|
||||
email: `test${i}@example.com`,
|
||||
password: "password123",
|
||||
points: Math.floor(Math.random() * 1000),
|
||||
});
|
||||
await user.save();
|
||||
testUsers.push(user);
|
||||
|
||||
const jwt = require("jsonwebtoken");
|
||||
const token = jwt.sign(
|
||||
{ user: { id: user._id.toString() } },
|
||||
process.env.JWT_SECRET || "test_secret"
|
||||
);
|
||||
authTokens.push(token);
|
||||
}
|
||||
|
||||
// Create test data
|
||||
await createTestData();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.disconnect();
|
||||
await mongoServer.stop();
|
||||
});
|
||||
|
||||
async function createTestData() {
|
||||
// Create streets
|
||||
const streets = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
streets.push({
|
||||
name: `Street ${i}`,
|
||||
location: {
|
||||
type: "Point",
|
||||
coordinates: [
|
||||
-74 + (Math.random() * 0.1),
|
||||
40.7 + (Math.random() * 0.1),
|
||||
],
|
||||
},
|
||||
status: Math.random() > 0.5 ? "available" : "adopted",
|
||||
});
|
||||
}
|
||||
await Street.insertMany(streets);
|
||||
|
||||
// Create tasks
|
||||
const tasks = [];
|
||||
for (let i = 0; i < 200; i++) {
|
||||
tasks.push({
|
||||
title: `Task ${i}`,
|
||||
description: `Description for task ${i}`,
|
||||
street: { streetId: streets[Math.floor(Math.random() * streets.length)]._id },
|
||||
pointsAwarded: Math.floor(Math.random() * 20) + 5,
|
||||
status: Math.random() > 0.3 ? "pending" : "completed",
|
||||
});
|
||||
}
|
||||
await Task.insertMany(tasks);
|
||||
|
||||
// Create events
|
||||
const events = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
events.push({
|
||||
title: `Event ${i}`,
|
||||
description: `Description for event ${i}`,
|
||||
date: new Date(Date.now() + Math.random() * 30 * 24 * 60 * 60 * 1000),
|
||||
location: `Location ${i}`,
|
||||
status: "upcoming",
|
||||
participants: [],
|
||||
});
|
||||
}
|
||||
await Event.insertMany(events);
|
||||
|
||||
// Create posts
|
||||
const posts = [];
|
||||
for (let i = 0; i < 150; i++) {
|
||||
posts.push({
|
||||
user: {
|
||||
userId: testUsers[Math.floor(Math.random() * testUsers.length)]._id,
|
||||
name: `User ${i}`,
|
||||
},
|
||||
content: `Post content ${i}`,
|
||||
likes: [],
|
||||
commentsCount: 0,
|
||||
});
|
||||
}
|
||||
await Post.insertMany(posts);
|
||||
}
|
||||
|
||||
describe("API Response Times", () => {
|
||||
test("should respond to basic requests quickly", async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
await request(app)
|
||||
.get("/api/health")
|
||||
.expect(200);
|
||||
|
||||
const endTime = Date.now();
|
||||
const responseTime = endTime - startTime;
|
||||
|
||||
// Health check should be very fast (< 50ms)
|
||||
expect(responseTime).toBeLessThan(50);
|
||||
});
|
||||
|
||||
test("should handle street listing efficiently", async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const response = await request(app)
|
||||
.get("/api/streets")
|
||||
.expect(200);
|
||||
|
||||
const endTime = Date.now();
|
||||
const responseTime = endTime - startTime;
|
||||
|
||||
// Should respond within 200ms even with 100 streets
|
||||
expect(responseTime).toBeLessThan(200);
|
||||
expect(response.body.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("should handle paginated requests efficiently", async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const response = await request(app)
|
||||
.get("/api/streets?page=1&limit=10")
|
||||
.expect(200);
|
||||
|
||||
const endTime = Date.now();
|
||||
const responseTime = endTime - startTime;
|
||||
|
||||
// Pagination should be fast (< 100ms)
|
||||
expect(responseTime).toBeLessThan(100);
|
||||
expect(response.body.docs).toHaveLength(10);
|
||||
});
|
||||
|
||||
test("should handle geospatial queries efficiently", async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const response = await request(app)
|
||||
.get("/api/streets/nearby")
|
||||
.query({
|
||||
lng: -73.9654,
|
||||
lat: 40.7829,
|
||||
maxDistance: 5000,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const endTime = Date.now();
|
||||
const responseTime = endTime - startTime;
|
||||
|
||||
// Geospatial queries should be efficient (< 300ms)
|
||||
expect(responseTime).toBeLessThan(300);
|
||||
});
|
||||
|
||||
test("should handle complex queries efficiently", async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Test a complex query with multiple filters
|
||||
const response = await request(app)
|
||||
.get("/api/tasks")
|
||||
.query({
|
||||
status: "pending",
|
||||
limit: 20,
|
||||
sort: "createdAt",
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const endTime = Date.now();
|
||||
const responseTime = endTime - startTime;
|
||||
|
||||
// Complex queries should still be reasonable (< 400ms)
|
||||
expect(responseTime).toBeLessThan(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Concurrent Request Handling", () => {
|
||||
test("should handle concurrent read requests", async () => {
|
||||
const startTime = Date.now();
|
||||
const concurrentRequests = 50;
|
||||
|
||||
const promises = [];
|
||||
for (let i = 0; i < concurrentRequests; i++) {
|
||||
promises.push(request(app).get("/api/streets"));
|
||||
}
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
const endTime = Date.now();
|
||||
const totalTime = endTime - startTime;
|
||||
|
||||
// All requests should succeed
|
||||
responses.forEach((response) => {
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
// Should handle 50 concurrent requests within 2 seconds
|
||||
expect(totalTime).toBeLessThan(2000);
|
||||
|
||||
// Average response time should be reasonable
|
||||
const avgResponseTime = totalTime / concurrentRequests;
|
||||
expect(avgResponseTime).toBeLessThan(100);
|
||||
});
|
||||
|
||||
test("should handle concurrent write requests", async () => {
|
||||
const startTime = Date.now();
|
||||
const concurrentRequests = 20;
|
||||
|
||||
const promises = [];
|
||||
for (let i = 0; i < concurrentRequests; i++) {
|
||||
promises.push(
|
||||
request(app)
|
||||
.post("/api/posts")
|
||||
.set("x-auth-token", authTokens[i % authTokens.length])
|
||||
.send({
|
||||
content: `Concurrent post ${i}`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
const endTime = Date.now();
|
||||
const totalTime = endTime - startTime;
|
||||
|
||||
// Most requests should succeed (some might fail due to rate limiting)
|
||||
const successCount = responses.filter(r => r.status === 200).length;
|
||||
expect(successCount).toBeGreaterThan(15);
|
||||
|
||||
// Should handle concurrent writes within 5 seconds
|
||||
expect(totalTime).toBeLessThan(5000);
|
||||
});
|
||||
|
||||
test("should handle mixed read/write workload", async () => {
|
||||
const startTime = Date.now();
|
||||
const operations = [];
|
||||
|
||||
// Mix of different operations
|
||||
for (let i = 0; i < 30; i++) {
|
||||
// Read operations
|
||||
operations.push(request(app).get("/api/streets"));
|
||||
operations.push(request(app).get("/api/events"));
|
||||
|
||||
// Write operations
|
||||
operations.push(
|
||||
request(app)
|
||||
.post("/api/posts")
|
||||
.set("x-auth-token", authTokens[i % authTokens.length])
|
||||
.send({ content: `Mixed post ${i}` })
|
||||
);
|
||||
}
|
||||
|
||||
const responses = await Promise.all(operations);
|
||||
const endTime = Date.now();
|
||||
const totalTime = endTime - startTime;
|
||||
|
||||
// Most operations should succeed
|
||||
const successCount = responses.filter(r => r.status === 200).length;
|
||||
expect(successCount).toBeGreaterThan(50);
|
||||
|
||||
// Should handle mixed workload within 3 seconds
|
||||
expect(totalTime).toBeLessThan(3000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Memory Usage", () => {
|
||||
test("should not leak memory during repeated operations", async () => {
|
||||
const initialMemory = process.memoryUsage().heapUsed;
|
||||
|
||||
// Perform many operations
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await request(app).get("/api/streets");
|
||||
await request(app).get("/api/events");
|
||||
await request(app).get("/api/tasks");
|
||||
|
||||
// Force garbage collection if available
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
}
|
||||
|
||||
const finalMemory = process.memoryUsage().heapUsed;
|
||||
const memoryIncrease = finalMemory - initialMemory;
|
||||
|
||||
// Memory increase should be reasonable (< 50MB)
|
||||
expect(memoryIncrease).toBeLessThan(50 * 1024 * 1024);
|
||||
});
|
||||
|
||||
test("should handle large result sets efficiently", async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Request a large result set
|
||||
const response = await request(app)
|
||||
.get("/api/streets?limit=100")
|
||||
.expect(200);
|
||||
|
||||
const endTime = Date.now();
|
||||
const responseTime = endTime - startTime;
|
||||
|
||||
// Should handle large results efficiently
|
||||
expect(responseTime).toBeLessThan(500);
|
||||
expect(response.body.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Database Performance", () => {
|
||||
test("should use database indexes effectively", async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Query that should use indexes
|
||||
await request(app)
|
||||
.get("/api/streets")
|
||||
.query({ status: "available" });
|
||||
|
||||
const endTime = Date.now();
|
||||
const queryTime = endTime - startTime;
|
||||
|
||||
// Indexed queries should be fast
|
||||
expect(queryTime).toBeLessThan(100);
|
||||
});
|
||||
|
||||
test("should handle database connection pooling", async () => {
|
||||
const startTime = Date.now();
|
||||
const concurrentDbOperations = 30;
|
||||
|
||||
const promises = [];
|
||||
for (let i = 0; i < concurrentDbOperations; i++) {
|
||||
promises.push(
|
||||
request(app)
|
||||
.get(`/api/streets/${new mongoose.Types.ObjectId()}`)
|
||||
.expect(404)
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
const endTime = Date.now();
|
||||
const totalTime = endTime - startTime;
|
||||
|
||||
// Connection pooling should handle concurrent operations efficiently
|
||||
expect(totalTime).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
test("should handle aggregation queries efficiently", async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Test leaderboard (aggregation) performance
|
||||
const response = await request(app)
|
||||
.get("/api/rewards/leaderboard")
|
||||
.expect(200);
|
||||
|
||||
const endTime = Date.now();
|
||||
const queryTime = endTime - startTime;
|
||||
|
||||
// Aggregation should be reasonably fast
|
||||
expect(queryTime).toBeLessThan(300);
|
||||
expect(response.body.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rate Limiting Performance", () => {
|
||||
test("should handle rate limiting efficiently", async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Make requests that approach rate limit
|
||||
const promises = [];
|
||||
for (let i = 0; i < 95; i++) { // Just under the limit
|
||||
promises.push(
|
||||
request(app)
|
||||
.get("/api/streets")
|
||||
.set("x-auth-token", authTokens[i % authTokens.length])
|
||||
);
|
||||
}
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
const endTime = Date.now();
|
||||
const totalTime = endTime - startTime;
|
||||
|
||||
// Should handle requests near rate limit efficiently
|
||||
expect(totalTime).toBeLessThan(2000);
|
||||
|
||||
const successCount = responses.filter(r => r.status === 200).length;
|
||||
expect(successCount).toBeGreaterThan(90);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Stress Tests", () => {
|
||||
test("should handle sustained load", async () => {
|
||||
const duration = 5000; // 5 seconds
|
||||
const startTime = Date.now();
|
||||
let requestCount = 0;
|
||||
|
||||
while (Date.now() - startTime < duration) {
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(request(app).get("/api/health"));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
requestCount += 10;
|
||||
}
|
||||
|
||||
const actualDuration = Date.now() - startTime;
|
||||
const requestsPerSecond = (requestCount / actualDuration) * 1000;
|
||||
|
||||
// Should handle at least 50 requests per second
|
||||
expect(requestsPerSecond).toBeGreaterThan(50);
|
||||
});
|
||||
|
||||
test("should maintain performance under load", async () => {
|
||||
const baselineTime = await measureResponseTime("/api/streets");
|
||||
|
||||
// Apply load
|
||||
const loadPromises = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
loadPromises.push(request(app).get("/api/events"));
|
||||
}
|
||||
await Promise.all(loadPromises);
|
||||
|
||||
// Measure performance after load
|
||||
const afterLoadTime = await measureResponseTime("/api/streets");
|
||||
|
||||
// Performance should not degrade significantly
|
||||
const performanceDegradation = (afterLoadTime - baselineTime) / baselineTime;
|
||||
expect(performanceDegradation).toBeLessThan(0.5); // Less than 50% degradation
|
||||
});
|
||||
|
||||
async function measureResponseTime(endpoint) {
|
||||
const startTime = Date.now();
|
||||
await request(app).get(endpoint);
|
||||
return Date.now() - startTime;
|
||||
}
|
||||
});
|
||||
|
||||
describe("Resource Limits", () => {
|
||||
test("should handle large payloads efficiently", async () => {
|
||||
const largeContent = "x".repeat(10000); // 10KB content
|
||||
|
||||
const startTime = Date.now();
|
||||
const response = await request(app)
|
||||
.post("/api/posts")
|
||||
.set("x-auth-token", authTokens[0])
|
||||
.send({ content: largeContent })
|
||||
.expect(200);
|
||||
|
||||
const endTime = Date.now();
|
||||
const responseTime = endTime - startTime;
|
||||
|
||||
// Should handle large payloads reasonably
|
||||
expect(responseTime).toBeLessThan(1000);
|
||||
expect(response.body.content).toBe(largeContent);
|
||||
});
|
||||
|
||||
test("should reject oversized payloads quickly", async () => {
|
||||
const oversizedContent = "x".repeat(1000000); // 1MB content
|
||||
|
||||
const startTime = Date.now();
|
||||
const response = await request(app)
|
||||
.post("/api/posts")
|
||||
.set("x-auth-token", authTokens[0])
|
||||
.send({ content: oversizedContent })
|
||||
.expect(413);
|
||||
|
||||
const endTime = Date.now();
|
||||
const responseTime = endTime - startTime;
|
||||
|
||||
// Should reject oversized payloads quickly
|
||||
expect(responseTime).toBeLessThan(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Caching Performance", () => {
|
||||
test("should cache static responses efficiently", async () => {
|
||||
// First request
|
||||
const startTime1 = Date.now();
|
||||
await request(app).get("/api/health");
|
||||
const firstRequestTime = Date.now() - startTime1;
|
||||
|
||||
// Second request (potentially cached)
|
||||
const startTime2 = Date.now();
|
||||
await request(app).get("/api/health");
|
||||
const secondRequestTime = Date.now() - startTime2;
|
||||
|
||||
// Second request should be faster (if cached)
|
||||
// Note: This test depends on implementation of caching
|
||||
expect(secondRequestTime).toBeLessThanOrEqual(firstRequestTime);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Scalability Tests", () => {
|
||||
test("should handle increasing data volumes", async () => {
|
||||
// Create additional data
|
||||
const additionalStreets = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
additionalStreets.push({
|
||||
name: `Additional Street ${i}`,
|
||||
location: {
|
||||
type: "Point",
|
||||
coordinates: [-74 + Math.random() * 0.1, 40.7 + Math.random() * 0.1],
|
||||
},
|
||||
status: "available",
|
||||
});
|
||||
}
|
||||
await Street.insertMany(additionalStreets);
|
||||
|
||||
// Measure performance with increased data
|
||||
const startTime = Date.now();
|
||||
const response = await request(app)
|
||||
.get("/api/streets")
|
||||
.query({ limit: 50 })
|
||||
.expect(200);
|
||||
|
||||
const endTime = Date.now();
|
||||
const responseTime = endTime - startTime;
|
||||
|
||||
// Should maintain performance with more data
|
||||
expect(responseTime).toBeLessThan(300);
|
||||
expect(response.body.length).toBe(50);
|
||||
});
|
||||
|
||||
test("should handle user growth efficiently", async () => {
|
||||
// Create additional users
|
||||
const additionalUsers = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
additionalUsers.push({
|
||||
name: `Additional User ${i}`,
|
||||
email: `additional${i}@example.com`,
|
||||
password: "password123",
|
||||
points: Math.floor(Math.random() * 1000),
|
||||
});
|
||||
}
|
||||
await User.insertMany(additionalUsers);
|
||||
|
||||
// Test leaderboard performance with more users
|
||||
const startTime = Date.now();
|
||||
const response = await request(app)
|
||||
.get("/api/rewards/leaderboard")
|
||||
.expect(200);
|
||||
|
||||
const endTime = Date.now();
|
||||
const responseTime = endTime - startTime;
|
||||
|
||||
// Should handle more users efficiently
|
||||
expect(responseTime).toBeLessThan(400);
|
||||
expect(response.body.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user