const request = require("supertest"); const express = require("express"); const jwt = require("jsonwebtoken"); // Mock data store let mockStreets = []; // Create test app with geospatial 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 geospatial routes app.post("/api/streets", authMiddleware, (req, res) => { const { name, location } = req.body; // Validate GeoJSON if (!location || location.type !== "Point" || !Array.isArray(location.coordinates)) { return res.status(400).json({ msg: "Invalid GeoJSON Point format" }); } const [lng, lat] = location.coordinates; if (typeof lng !== "number" || typeof lat !== "number") { return res.status(400).json({ msg: "Coordinates must be numbers" }); } // Validate longitude and latitude ranges if (lng < -180 || lng > 180 || lat < -90 || lat > 90) { return res.status(400).json({ msg: "Invalid longitude or latitude range" }); } const newStreet = { _id: `street_${Date.now()}_${Math.random()}`, name, location, adoptedBy: req.user.id, status: "available", createdAt: new Date().toISOString() }; mockStreets.push(newStreet); res.json(newStreet); }); app.get("/api/streets/nearby", authMiddleware, (req, res) => { const { lng, lat, maxDistance = 1000 } = req.query; if (!lng || !lat) { return res.status(400).json({ msg: "Longitude and latitude are required" }); } const longitude = parseFloat(lng); const latitude = parseFloat(lat); if (isNaN(longitude) || isNaN(latitude)) { return res.status(400).json({ msg: "Invalid coordinates" }); } // Calculate distance and filter nearby streets const nearbyStreets = mockStreets .filter(street => { if (!street.location || !street.location.coordinates) return false; const [streetLng, streetLat] = street.location.coordinates; // Simple distance calculation (rough approximation) const distance = Math.sqrt( Math.pow(streetLng - longitude, 2) + Math.pow(streetLat - latitude, 2) ) * 111000; // Convert to meters (rough) return distance <= parseFloat(maxDistance); }) .map(street => ({ ...street, distance: Math.floor(Math.random() * 1000) + 50 // Mock distance })); res.json(nearbyStreets); }); app.get("/api/streets/bounds", authMiddleware, (req, res) => { const { minLng, minLat, maxLng, maxLat } = req.query; if (!minLng || !minLat || !maxLng || !maxLat) { return res.status(400).json({ msg: "All boundary coordinates are required" }); } const bounds = { minLng: parseFloat(minLng), minLat: parseFloat(minLat), maxLng: parseFloat(maxLng), maxLat: parseFloat(maxLat) }; // Validate bounds if (Object.values(bounds).some(val => isNaN(val))) { return res.status(400).json({ msg: "Invalid boundary coordinates" }); } if (bounds.minLng >= bounds.maxLng || bounds.minLat >= bounds.maxLat) { return res.status(400).json({ msg: "Invalid boundary box" }); } // Filter streets within bounds const streetsInBounds = mockStreets.filter(street => { if (!street.location || !street.location.coordinates) return false; const [streetLng, streetLat] = street.location.coordinates; return streetLng >= bounds.minLng && streetLng <= bounds.maxLng && streetLat >= bounds.minLat && streetLat <= bounds.maxLat; }); res.json(streetsInBounds); }); // Global error handler app.use((err, req, res, next) => { console.error(err.message); res.status(500).json({ msg: "Server error" }); }); return app; }; describe("Geospatial Queries", () => { 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 mock data before each test mockStreets = []; // Add some default test streets mockStreets = [ { _id: "street1", name: "Central Park Street", location: { type: "Point", coordinates: [-73.9654, 40.7829] }, status: "available", adoptedBy: "user1" }, { _id: "street2", name: "Times Square Street", location: { type: "Point", coordinates: [-73.9857, 40.7580] }, status: "available", adoptedBy: "user2" }, { _id: "street3", name: "Brooklyn Bridge Street", location: { type: "Point", coordinates: [-73.9969, 40.7061] }, status: "adopted", adoptedBy: "user3" }, { _id: "street4", name: "Far Away Street", location: { type: "Point", coordinates: [-118.2437, 34.0522] }, // LA status: "available", adoptedBy: "user4" } ]; }); 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); } expect(mockStreets.length).toBeGreaterThanOrEqual(3); }); }); describe("Nearby Street Queries", () => { test("should find nearby streets within small radius", async () => { // Query near Central Park (NYC) const response = await request(app) .get("/api/streets/nearby") .set("x-auth-token", authToken) .query({ lng: -73.9654, lat: 40.7829, maxDistance: 1000, // 1km }) .expect(200); expect(response.body.length).toBeGreaterThan(0); const streetNames = response.body.map(s => s.name); expect(streetNames).toContain("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") .set("x-auth-token", authToken) .query({ lng: -73.9654, lat: 40.7829, maxDistance: 5000, // 5km }) .expect(200); expect(response.body.length).toBeGreaterThanOrEqual(1); const streetNames = response.body.map(s => s.name); expect(streetNames).toContain("Central Park Street"); }); test("should filter by status in nearby queries", async () => { const response = await request(app) .get("/api/streets/nearby") .set("x-auth-token", authToken) .query({ lng: -73.9654, lat: 40.7829, maxDistance: 10000, // 10km }) .expect(200); const streetNames = response.body.map(s => s.name); expect(streetNames.length).toBeGreaterThan(0); // Note: Status filtering would need to be implemented in the mock route }); test("should return empty result for distant location", async () => { const response = await request(app) .get("/api/streets/nearby") .set("x-auth-token", authToken) .query({ lng: 0, // Prime meridian lat: 0, // Equator maxDistance: 1000, // 1km }) .expect(200); expect(response.body).toHaveLength(0); }); }); describe("Bounding Box Queries", () => { beforeEach(() => { // Add grid pattern streets for bounding box tests mockStreets = [ { _id: "sw", name: "SW Corner", location: { type: "Point", coordinates: [-74.0, 40.7] }, status: "available" }, { _id: "se", name: "SE Corner", location: { type: "Point", coordinates: [-73.9, 40.7] }, status: "available" }, { _id: "nw", name: "NW Corner", location: { type: "Point", coordinates: [-74.0, 40.8] }, status: "available" }, { _id: "ne", name: "NE Corner", location: { type: "Point", coordinates: [-73.9, 40.8] }, status: "available" }, { _id: "center", name: "Center", location: { type: "Point", coordinates: [-73.95, 40.75] }, status: "available" }, { _id: "outside", name: "Outside Box", location: { type: "Point", coordinates: [-74.1, 40.6] }, status: "available" }, ]; }); test("should find streets within bounding box", async () => { const response = await request(app) .get("/api/streets/bounds") .set("x-auth-token", authToken) .query({ minLng: -74.0, minLat: 40.7, maxLng: -73.9, maxLat: 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") .set("x-auth-token", authToken) .query({ minLng: -74.0, minLat: 40.7, maxLng: -73.95, maxLat: 40.75, }) .expect(200); expect(response.body.length).toBe(2); // SW, Center const names = response.body.map(s => s.name); expect(names).toContain("SW Corner"); expect(names).toContain("Center"); }); test("should return empty for invalid bounding box", async () => { const response = await request(app) .get("/api/streets/bounds") .set("x-auth-token", authToken) .query({ minLng: -73.95, minLat: 40.75, maxLng: -74.0, // Reversed coordinates maxLat: 40.7, }) .expect(400); expect(response.body.msg).toContain("Invalid boundary box"); }); }); describe("Mock Geospatial Operations", () => { beforeEach(() => { // Add test streets for geospatial operations mockStreets = [ { _id: "downtown", name: "Downtown Street", location: { type: "Point", coordinates: [-74.0060, 40.7128] }, status: "available", stats: { completedTasksCount: 0, reportsCount: 0 }, }, { _id: "uptown", name: "Uptown Street", location: { type: "Point", coordinates: [-73.9654, 40.7829] }, status: "adopted", stats: { completedTasksCount: 5, reportsCount: 2 }, }, { _id: "suburban", name: "Suburban Street", location: { type: "Point", coordinates: [-73.8000, 40.7000] }, status: "available", stats: { completedTasksCount: 1, reportsCount: 0 }, }, ]; }); test("should find streets by location bounds", async () => { const response = await request(app) .get("/api/streets/bounds") .set("x-auth-token", authToken) .query({ minLng: -74.1, minLat: 40.7, maxLng: -73.9, maxLat: 40.8, }) .expect(200); expect(response.body.length).toBe(2); const names = response.body.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 response = await request(app) .get("/api/streets/bounds") .set("x-auth-token", authToken) .query({ minLng: 0, minLat: 0, maxLng: 0.1, maxLat: 0.1, }) .expect(200); expect(response.body).toHaveLength(0); }); }); describe("Performance Tests", () => { beforeEach(() => { // Create a large number of streets for performance testing const streets = []; for (let i = 0; i < 100; i++) { streets.push({ _id: `perf_street_${i}`, 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", }); } mockStreets = streets; }); test("should handle nearby queries efficiently", async () => { const startTime = Date.now(); const response = await request(app) .get("/api/streets/nearby") .set("x-auth-token", authToken) .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 100 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") .set("x-auth-token", authToken) .query({ minLng: -74.0, minLat: 40.7, maxLng: -73.9, maxLat: 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 < 5; i++) { queries.push( request(app) .get("/api/streets/nearby") .set("x-auth-token", authToken) .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 5 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("GeoJSON"); }); 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") .set("x-auth-token", authToken) .query({ lng: "invalid", lat: 40.7128, maxDistance: 1000, }) .expect(400); await request(app) .get("/api/streets/bounds") .set("x-auth-token", authToken) .query({ minLng: -74.0, minLat: "invalid", maxLng: -73.9, maxLat: 40.8, }) .expect(400); }); }); });