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:
510
backend/__tests__/geospatial.test.js
Normal file
510
backend/__tests__/geospatial.test.js
Normal file
@@ -0,0 +1,510 @@
|
||||
const request = require("supertest");
|
||||
const mongoose = require("mongoose");
|
||||
const { MongoMemoryServer } = require("mongodb-memory-server");
|
||||
const app = require("../server");
|
||||
const Street = require("../models/Street");
|
||||
const User = require("../models/User");
|
||||
const couchdbService = require("../services/couchdbService");
|
||||
|
||||
describe("Geospatial Queries", () => {
|
||||
let mongoServer;
|
||||
let testUser;
|
||||
let authToken;
|
||||
|
||||
beforeAll(async () => {
|
||||
mongoServer = await MongoMemoryServer.create();
|
||||
const mongoUri = mongoServer.getUri();
|
||||
await mongoose.connect(mongoUri);
|
||||
|
||||
// Initialize CouchDB for testing
|
||||
await couchdbService.initialize();
|
||||
|
||||
// Create test user
|
||||
testUser = new User({
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
await testUser.save();
|
||||
|
||||
// Generate auth token
|
||||
const jwt = require("jsonwebtoken");
|
||||
authToken = jwt.sign(
|
||||
{ user: { id: testUser._id.toString() } },
|
||||
process.env.JWT_SECRET || "test_secret"
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.disconnect();
|
||||
await mongoServer.stop();
|
||||
await couchdbService.shutdown();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean up streets before each test
|
||||
await Street.deleteMany({});
|
||||
});
|
||||
|
||||
describe("Street Creation with Coordinates", () => {
|
||||
test("should create street with valid GeoJSON coordinates", async () => {
|
||||
const streetData = {
|
||||
name: "Test Street",
|
||||
location: {
|
||||
type: "Point",
|
||||
coordinates: [-74.0060, 40.7128], // NYC coordinates
|
||||
},
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post("/api/streets")
|
||||
.set("x-auth-token", authToken)
|
||||
.send(streetData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.location).toBeDefined();
|
||||
expect(response.body.location.type).toBe("Point");
|
||||
expect(response.body.location.coordinates).toEqual([-74.0060, 40.7128]);
|
||||
});
|
||||
|
||||
test("should reject street with invalid coordinates", async () => {
|
||||
const streetData = {
|
||||
name: "Invalid Street",
|
||||
location: {
|
||||
type: "Point",
|
||||
coordinates: [181, 91], // Invalid coordinates
|
||||
},
|
||||
};
|
||||
|
||||
await request(app)
|
||||
.post("/api/streets")
|
||||
.set("x-auth-token", authToken)
|
||||
.send(streetData)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
test("should create streets with various coordinate formats", async () => {
|
||||
const streets = [
|
||||
{
|
||||
name: "Street 1",
|
||||
location: { type: "Point", coordinates: [0, 0] },
|
||||
},
|
||||
{
|
||||
name: "Street 2",
|
||||
location: { type: "Point", coordinates: [-122.4194, 37.7749] }, // SF
|
||||
},
|
||||
{
|
||||
name: "Street 3",
|
||||
location: { type: "Point", coordinates: [2.3522, 48.8566] }, // Paris
|
||||
},
|
||||
];
|
||||
|
||||
for (const street of streets) {
|
||||
await request(app)
|
||||
.post("/api/streets")
|
||||
.set("x-auth-token", authToken)
|
||||
.send(street)
|
||||
.expect(200);
|
||||
}
|
||||
|
||||
const allStreets = await Street.find();
|
||||
expect(allStreets).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Nearby Street Queries", () => {
|
||||
beforeEach(async () => {
|
||||
// Create test streets at various locations
|
||||
const streets = [
|
||||
{
|
||||
name: "Central Park Street",
|
||||
location: { type: "Point", coordinates: [-73.9654, 40.7829] },
|
||||
status: "available",
|
||||
},
|
||||
{
|
||||
name: "Times Square Street",
|
||||
location: { type: "Point", coordinates: [-73.9857, 40.7580] },
|
||||
status: "available",
|
||||
},
|
||||
{
|
||||
name: "Brooklyn Bridge Street",
|
||||
location: { type: "Point", coordinates: [-73.9969, 40.7061] },
|
||||
status: "adopted",
|
||||
},
|
||||
{
|
||||
name: "Far Away Street",
|
||||
location: { type: "Point", coordinates: [-118.2437, 34.0522] }, // LA
|
||||
status: "available",
|
||||
},
|
||||
];
|
||||
|
||||
await Street.insertMany(streets);
|
||||
});
|
||||
|
||||
test("should find nearby streets within small radius", async () => {
|
||||
// Query near Central Park (NYC)
|
||||
const response = await request(app)
|
||||
.get("/api/streets/nearby")
|
||||
.query({
|
||||
lng: -73.9654,
|
||||
lat: 40.7829,
|
||||
maxDistance: 1000, // 1km
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveLength(1);
|
||||
expect(response.body[0].name).toBe("Central Park Street");
|
||||
});
|
||||
|
||||
test("should find nearby streets within larger radius", async () => {
|
||||
// Query near Central Park with 5km radius
|
||||
const response = await request(app)
|
||||
.get("/api/streets/nearby")
|
||||
.query({
|
||||
lng: -73.9654,
|
||||
lat: 40.7829,
|
||||
maxDistance: 5000, // 5km
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.length).toBeGreaterThanOrEqual(2);
|
||||
const streetNames = response.body.map(s => s.name);
|
||||
expect(streetNames).toContain("Central Park Street");
|
||||
expect(streetNames).toContain("Times Square Street");
|
||||
});
|
||||
|
||||
test("should filter by status in nearby queries", async () => {
|
||||
const response = await request(app)
|
||||
.get("/api/streets/nearby")
|
||||
.query({
|
||||
lng: -73.9654,
|
||||
lat: 40.7829,
|
||||
maxDistance: 10000, // 10km
|
||||
status: "available",
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const streetNames = response.body.map(s => s.name);
|
||||
expect(streetNames).toContain("Central Park Street");
|
||||
expect(streetNames).toContain("Times Square Street");
|
||||
expect(streetNames).not.toContain("Brooklyn Bridge Street"); // adopted
|
||||
});
|
||||
|
||||
test("should return empty result for distant location", async () => {
|
||||
const response = await request(app)
|
||||
.get("/api/streets/nearby")
|
||||
.query({
|
||||
lng: 0, // Prime meridian
|
||||
lat: 0, // Equator
|
||||
maxDistance: 1000, // 1km
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bounding Box Queries", () => {
|
||||
beforeEach(async () => {
|
||||
// Create streets in a grid pattern
|
||||
const streets = [
|
||||
{ name: "SW Corner", location: { type: "Point", coordinates: [-74.0, 40.7] } },
|
||||
{ name: "SE Corner", location: { type: "Point", coordinates: [-73.9, 40.7] } },
|
||||
{ name: "NW Corner", location: { type: "Point", coordinates: [-74.0, 40.8] } },
|
||||
{ name: "NE Corner", location: { type: "Point", coordinates: [-73.9, 40.8] } },
|
||||
{ name: "Center", location: { type: "Point", coordinates: [-73.95, 40.75] } },
|
||||
{ name: "Outside Box", location: { type: "Point", coordinates: [-74.1, 40.6] } },
|
||||
];
|
||||
|
||||
await Street.insertMany(streets);
|
||||
});
|
||||
|
||||
test("should find streets within bounding box", async () => {
|
||||
const response = await request(app)
|
||||
.get("/api/streets/bounds")
|
||||
.query({
|
||||
sw_lng: -74.0,
|
||||
sw_lat: 40.7,
|
||||
ne_lng: -73.9,
|
||||
ne_lat: 40.8,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.length).toBe(5); // All except "Outside Box"
|
||||
const names = response.body.map(s => s.name);
|
||||
expect(names).toContain("SW Corner");
|
||||
expect(names).toContain("SE Corner");
|
||||
expect(names).toContain("NW Corner");
|
||||
expect(names).toContain("NE Corner");
|
||||
expect(names).toContain("Center");
|
||||
expect(names).not.toContain("Outside Box");
|
||||
});
|
||||
|
||||
test("should handle partial bounding box", async () => {
|
||||
const response = await request(app)
|
||||
.get("/api/streets/bounds")
|
||||
.query({
|
||||
sw_lng: -74.0,
|
||||
sw_lat: 40.7,
|
||||
ne_lng: -73.95,
|
||||
ne_lat: 40.75,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.length).toBe(3); // SW, NW, Center
|
||||
const names = response.body.map(s => s.name);
|
||||
expect(names).toContain("SW Corner");
|
||||
expect(names).toContain("NW Corner");
|
||||
expect(names).toContain("Center");
|
||||
});
|
||||
|
||||
test("should return empty for invalid bounding box", async () => {
|
||||
const response = await request(app)
|
||||
.get("/api/streets/bounds")
|
||||
.query({
|
||||
sw_lng: -73.95,
|
||||
sw_lat: 40.75,
|
||||
ne_lng: -74.0, // Reversed coordinates
|
||||
ne_lat: 40.7,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CouchDB Geospatial Operations", () => {
|
||||
beforeEach(async () => {
|
||||
// Create test streets in CouchDB
|
||||
const streets = [
|
||||
{
|
||||
_id: "street_test1",
|
||||
type: "street",
|
||||
name: "Downtown Street",
|
||||
location: { type: "Point", coordinates: [-74.0060, 40.7128] },
|
||||
status: "available",
|
||||
stats: { completedTasksCount: 0, reportsCount: 0 },
|
||||
},
|
||||
{
|
||||
_id: "street_test2",
|
||||
type: "street",
|
||||
name: "Uptown Street",
|
||||
location: { type: "Point", coordinates: [-73.9654, 40.7829] },
|
||||
status: "adopted",
|
||||
stats: { completedTasksCount: 5, reportsCount: 2 },
|
||||
},
|
||||
{
|
||||
_id: "street_test3",
|
||||
type: "street",
|
||||
name: "Suburban Street",
|
||||
location: { type: "Point", coordinates: [-73.8000, 40.7000] },
|
||||
status: "available",
|
||||
stats: { completedTasksCount: 1, reportsCount: 0 },
|
||||
},
|
||||
];
|
||||
|
||||
for (const street of streets) {
|
||||
await couchdbService.createDocument(street);
|
||||
}
|
||||
});
|
||||
|
||||
test("should find streets by location bounds in CouchDB", async () => {
|
||||
const bounds = [
|
||||
[-74.1, 40.7], // Southwest corner
|
||||
[-73.9, 40.8], // Northeast corner
|
||||
];
|
||||
|
||||
const streets = await couchdbService.findStreetsByLocation(bounds);
|
||||
expect(streets.length).toBe(2);
|
||||
|
||||
const names = streets.map(s => s.name);
|
||||
expect(names).toContain("Downtown Street");
|
||||
expect(names).toContain("Uptown Street");
|
||||
expect(names).not.toContain("Suburban Street");
|
||||
});
|
||||
|
||||
test("should handle empty bounds gracefully", async () => {
|
||||
const bounds = [
|
||||
[0, 0], // Far away location
|
||||
[0.1, 0.1],
|
||||
];
|
||||
|
||||
const streets = await couchdbService.findStreetsByLocation(bounds);
|
||||
expect(streets).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("should filter by status in location queries", async () => {
|
||||
const bounds = [
|
||||
[-74.1, 40.7],
|
||||
[-73.9, 40.8],
|
||||
];
|
||||
|
||||
// First get all streets in bounds
|
||||
const allStreets = await couchdbService.findStreetsByLocation(bounds);
|
||||
|
||||
// Then filter manually for available streets (since CouchDB doesn't support complex geo queries)
|
||||
const availableStreets = allStreets.filter(street => street.status === 'available');
|
||||
|
||||
expect(availableStreets.length).toBe(1);
|
||||
expect(availableStreets[0].name).toBe("Downtown Street");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Performance Tests", () => {
|
||||
beforeEach(async () => {
|
||||
// Create a large number of streets for performance testing
|
||||
const streets = [];
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
streets.push({
|
||||
name: `Street ${i}`,
|
||||
location: {
|
||||
type: "Point",
|
||||
coordinates: [
|
||||
-74 + (Math.random() * 0.2), // Random longitude in NYC area
|
||||
40.7 + (Math.random() * 0.2), // Random latitude in NYC area
|
||||
],
|
||||
},
|
||||
status: Math.random() > 0.5 ? "available" : "adopted",
|
||||
});
|
||||
}
|
||||
await Street.insertMany(streets);
|
||||
});
|
||||
|
||||
test("should handle nearby 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, // 5km
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// Should complete within 1 second even with 1000 streets
|
||||
expect(duration).toBeLessThan(1000);
|
||||
expect(response.body.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("should handle bounding box queries efficiently", async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const response = await request(app)
|
||||
.get("/api/streets/bounds")
|
||||
.query({
|
||||
sw_lng: -74.0,
|
||||
sw_lat: 40.7,
|
||||
ne_lng: -73.9,
|
||||
ne_lat: 40.8,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// Should complete within 1 second
|
||||
expect(duration).toBeLessThan(1000);
|
||||
expect(response.body.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("should handle concurrent geospatial queries", async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const queries = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
queries.push(
|
||||
request(app)
|
||||
.get("/api/streets/nearby")
|
||||
.query({
|
||||
lng: -73.9654 + (Math.random() * 0.01),
|
||||
lat: 40.7829 + (Math.random() * 0.01),
|
||||
maxDistance: 2000,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(queries);
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// Should handle 10 concurrent queries within 2 seconds
|
||||
expect(duration).toBeLessThan(2000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases and Error Handling", () => {
|
||||
test("should handle missing coordinates gracefully", async () => {
|
||||
const streetData = {
|
||||
name: "Street without coordinates",
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post("/api/streets")
|
||||
.set("x-auth-token", authToken)
|
||||
.send(streetData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.msg).toContain("location");
|
||||
});
|
||||
|
||||
test("should handle malformed GeoJSON", async () => {
|
||||
const streetData = {
|
||||
name: "Malformed Street",
|
||||
location: {
|
||||
type: "InvalidType",
|
||||
coordinates: "not an array",
|
||||
},
|
||||
};
|
||||
|
||||
await request(app)
|
||||
.post("/api/streets")
|
||||
.set("x-auth-token", authToken)
|
||||
.send(streetData)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
test("should handle extreme coordinate values", async () => {
|
||||
const streetData = {
|
||||
name: "Extreme Coordinates",
|
||||
location: {
|
||||
type: "Point",
|
||||
coordinates: [180, 90], // Maximum valid coordinates
|
||||
},
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post("/api/streets")
|
||||
.set("x-auth-token", authToken)
|
||||
.send(streetData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.location.coordinates).toEqual([180, 90]);
|
||||
});
|
||||
|
||||
test("should validate query parameters", async () => {
|
||||
await request(app)
|
||||
.get("/api/streets/nearby")
|
||||
.query({
|
||||
lng: "invalid",
|
||||
lat: 40.7128,
|
||||
maxDistance: 1000,
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
await request(app)
|
||||
.get("/api/streets/bounds")
|
||||
.query({
|
||||
sw_lng: -74.0,
|
||||
sw_lat: "invalid",
|
||||
ne_lng: -73.9,
|
||||
ne_lat: 40.8,
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user