const request = require("supertest"); const app = require("../server"); const Street = require("../models/Street"); const User = require("../models/User"); const couchdbService = require("../services/couchdbService"); describe("Geospatial Queries", () => { let testUser; let authToken; beforeAll(async () => { // Initialize CouchDB for testing await couchdbService.initialize(); // Create test user testUser = await User.create({ name: "Test User", email: "test@example.com", password: "password123", }); // Generate auth token const jwt = require("jsonwebtoken"); authToken = jwt.sign( { user: { id: testUser._id } }, process.env.JWT_SECRET || "test_secret" ); }); afterAll(async () => { await couchdbService.shutdown(); }); beforeEach(async () => { // Clean up streets before each test const streets = await couchdbService.findByType('street'); for (const street of streets) { await couchdbService.deleteDocument(street._id, street._rev); } }); 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); }); }); });