const request = require("supertest"); const express = require("express"); const jwt = require("jsonwebtoken"); const { generateTestId } = require('./utils/idGenerator'); // Create a minimal app for testing error handling 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" }); } }; // Test route that requires authentication app.get("/api/users/profile", authMiddleware, (req, res) => { res.json({ id: req.user.id, name: "Test User" }); }); // Test route for validation errors app.post("/api/users", (req, res) => { const { name, email } = req.body; if (!name || !email) { return res.status(400).json({ msg: "Name and email are required" }); } res.json({ id: "test_id", name, email }); }); // Mock routes for testing 404 errors app.get("/api/streets/:id", (req, res) => { if (req.params.id === "nonexistent") { return res.status(404).json({ msg: "Street not found" }); } res.json({ id: req.params.id, name: "Test Street" }); }); app.get("/api/tasks/:id", (req, res) => { if (req.params.id === "nonexistent") { return res.status(404).json({ msg: "Task not found" }); } res.json({ id: req.params.id, title: "Test Task" }); }); app.get("/api/events/:id", (req, res) => { if (req.params.id === "nonexistent") { return res.status(404).json({ msg: "Event not found" }); } res.json({ id: req.params.id, title: "Test Event" }); }); app.get("/api/posts/:id", (req, res) => { if (req.params.id === "nonexistent") { return res.status(404).json({ msg: "Post not found" }); } res.json({ id: req.params.id, content: "Test Post" }); }); // Mock validation routes app.post("/api/users/register", (req, res) => { const { name, email, password } = req.body; const errors = []; if (!name) errors.push("Name is required"); if (!email) errors.push("Email is required"); if (!password) errors.push("Password is required"); if (password && password.length < 6) errors.push("Password must be at least 6 characters"); if (email && !email.includes("@")) errors.push("Invalid email format"); if (errors.length > 0) { return res.status(400).json({ msg: errors.join(", ") }); } res.json({ id: "test_id", name, email }); }); app.post("/api/streets", (req, res) => { const { name, location } = req.body; if (!name || !location) { return res.status(400).json({ msg: "Name and location are required" }); } // Validate GeoJSON if (location.type !== "Point" || !Array.isArray(location.coordinates)) { return res.status(400).json({ msg: "Invalid GeoJSON format" }); } const [lng, lat] = location.coordinates; if (lng < -180 || lng > 180 || lat < -90 || lat > 90) { return res.status(400).json({ msg: "Coordinates out of bounds" }); } res.json({ id: "test_street", name, location }); }); // Mock database error route app.get("/api/test/db-error", (req, res) => { throw new Error("Database connection failed"); }); // Mock timeout route app.get("/api/test/timeout", async (req, res) => { await new Promise(resolve => setTimeout(resolve, 5000)); // 5 second delay res.json({ msg: "This should timeout" }); }); // Mock large payload route app.post("/api/test/large-payload", (req, res) => { const contentLength = req.get('content-length'); if (contentLength && parseInt(contentLength) > 1024 * 1024) { // 1MB return res.status(413).json({ msg: "Request entity too large" }); } res.json({ msg: "Payload accepted" }); }); // Mock invalid JSON route app.post("/api/test/invalid-json", (req, res) => { try { JSON.parse(req.body); res.json({ msg: "Valid JSON" }); } catch (err) { res.status(400).json({ msg: "Invalid JSON format" }); } }); // Test route for 404 errors app.get("/api/nonexistent", (req, res) => { res.status(404).json({ msg: "Route not found" }); }); // Global error handler app.use((err, req, res, next) => { console.error(err.message); res.status(500).json({ msg: "Server error" }); }); // 404 handler for undefined routes app.use((req, res) => { res.status(404).json({ msg: "Route not found" }); }); return app; }; describe("Error Handling", () => { let app; let authToken; beforeAll(async () => { app = createTestApp(); // Generate auth token authToken = jwt.sign( { user: { id: "test_user_id" } }, process.env.JWT_SECRET || "test_secret" ); }); describe("Authentication Errors", () => { test("should reject requests without token", async () => { const response = await request(app) .get("/api/users/profile") .expect(401); expect(response.body.msg).toBe("No token, authorization denied"); }); test("should reject requests with invalid token", async () => { const response = await request(app) .get("/api/users/profile") .set("x-auth-token", "invalid_token") .expect(401); expect(response.body.msg).toBe("Token is not valid"); }); test("should reject requests with malformed token", async () => { const response = await request(app) .get("/api/users/profile") .set("x-auth-token", "not.a.valid.jwt") .expect(401); expect(response.body.msg).toBe("Token is not valid"); }); test("should reject requests with expired token", async () => { const jwt = require("jsonwebtoken"); const expiredToken = jwt.sign( { user: { id: testUser._id.toString() } }, process.env.JWT_SECRET || "test_secret", { expiresIn: "-1h" } // Expired 1 hour ago ); const response = await request(app) .get("/api/users/profile") .set("x-auth-token", expiredToken) .expect(401); expect(response.body.msg).toBe("Token is not valid"); }); test("should reject requests when user not found", async () => { const jwt = require("jsonwebtoken"); const tokenWithNonExistentUser = jwt.sign( { user: { id: generateTestId() } }, process.env.JWT_SECRET || "test_secret" ); const response = await request(app) .get("/api/users/profile") .set("x-auth-token", tokenWithNonExistentUser) .expect(404); expect(response.body.msg).toBe("User not found"); }); }); describe("Validation Errors", () => { test("should validate required fields in user registration", async () => { const response = await request(app) .post("/api/auth/register") .send({}) .expect(400); expect(response.body.errors).toBeDefined(); expect(response.body.errors.length).toBeGreaterThan(0); const fieldNames = response.body.errors.map(err => err.path); expect(fieldNames).toContain("name"); expect(fieldNames).toContain("email"); expect(fieldNames).toContain("password"); }); test("should validate email format", async () => { const response = await request(app) .post("/api/auth/register") .send({ name: "Test User", email: "invalid-email", password: "password123", }) .expect(400); const emailError = response.body.errors.find(err => err.path === "email"); expect(emailError).toBeDefined(); expect(emailError.msg).toContain("valid email"); }); test("should validate password strength", async () => { const response = await request(app) .post("/api/auth/register") .send({ name: "Test User", email: "test@example.com", password: "123", // Too short }) .expect(400); const passwordError = response.body.errors.find(err => err.path === "password"); expect(passwordError).toBeDefined(); expect(passwordError.msg).toContain("at least 6 characters"); }); test("should validate street creation data", async () => { const response = await request(app) .post("/api/streets") .set("x-auth-token", authToken) .send({}) .expect(400); expect(response.body.errors).toBeDefined(); const fieldNames = response.body.errors.map(err => err.path); expect(fieldNames).toContain("name"); expect(fieldNames).toContain("location"); }); test("should validate GeoJSON location format", async () => { const response = await request(app) .post("/api/streets") .set("x-auth-token", authToken) .send({ name: "Test Street", location: { type: "Point", coordinates: "invalid_coordinates", }, }) .expect(400); expect(response.body.msg).toBeDefined(); }); test("should validate coordinate bounds", async () => { const response = await request(app) .post("/api/streets") .set("x-auth-token", authToken) .send({ name: "Test Street", location: { type: "Point", coordinates: [200, 100], // Invalid coordinates }, }) .expect(400); expect(response.body.msg).toBeDefined(); }); }); describe("Resource Not Found Errors", () => { test("should handle non-existent street", async () => { const nonExistentId = generateTestId(); const response = await request(app) .get(`/api/streets/${nonExistentId}`) .expect(404); expect(response.body.msg).toBe("Street not found"); }); test("should handle non-existent task", async () => { const nonExistentId = generateTestId(); const response = await request(app) .put(`/api/tasks/${nonExistentId}/complete`) .set("x-auth-token", authToken) .expect(404); expect(response.body.msg).toBe("Task not found"); }); test("should handle non-existent event", async () => { const nonExistentId = generateTestId(); const response = await request(app) .put(`/api/events/rsvp/${nonExistentId}`) .set("x-auth-token", authToken) .expect(404); expect(response.body.msg).toBe("Event not found"); }); test("should handle non-existent post", async () => { const nonExistentId = generateTestId(); const response = await request(app) .get(`/api/posts/${nonExistentId}`) .expect(404); expect(response.body.msg).toBe("Post not found"); }); }); describe("Business Logic Errors", () => { let testStreet; beforeEach(async () => { testStreet = generateTestId(); }); test("should prevent duplicate user registration", async () => { const response = await request(app) .post("/api/auth/register") .send({ name: "Another User", email: "test@example.com", // Same email as existing user password: "password123", }) .expect(400); expect(response.body.msg).toContain("already exists"); }); test("should prevent adopting already adopted street", async () => { // First, create and adopt a street const Street = require("../models/Street"); const street = new Street({ name: "Test Street", location: { type: "Point", coordinates: [-74.0060, 40.7128] }, status: "adopted", adoptedBy: { userId: testUser._id, name: testUser.name, }, }); await street.save(); // Try to adopt again const response = await request(app) .put(`/api/streets/adopt/${street._id}`) .set("x-auth-token", authToken) .expect(400); expect(response.body.msg).toBe("Street already adopted"); }); test("should prevent completing already completed task", async () => { const Task = require("../models/Task"); const task = new Task({ title: "Test Task", description: "Test Description", street: { streetId: testStreet }, status: "completed", completedBy: { userId: testUser._id, name: testUser.name, }, }); await task.save(); const response = await request(app) .put(`/api/tasks/${task._id}/complete`) .set("x-auth-token", authToken) .expect(400); expect(response.body.msg).toBe("Task already completed"); }); test("should prevent duplicate event RSVP", async () => { const Event = require("../models/Event"); const event = new Event({ title: "Test Event", description: "Test Description", date: new Date(Date.now() + 86400000), location: "Test Location", participants: [{ userId: testUser._id, name: testUser.name, }], }); await event.save(); const response = await request(app) .put(`/api/events/rsvp/${event._id}`) .set("x-auth-token", authToken) .expect(400); expect(response.body.msg).toBe("Already RSVPed"); }); }); describe("Database Connection Errors", () => { test("should handle database service unavailable", async () => { // Mock CouchDB service to be unavailable const couchdbService = require('../services/couchdbService'); const originalIsReady = couchdbService.isReady; couchdbService.isReady = jest.fn().mockReturnValue(false); const response = await request(app) .get("/api/streets") .expect(500); expect(response.body.msg).toBeDefined(); // Restore original function couchdbService.isReady = originalIsReady; }); test("should handle database operation timeouts", async () => { // Mock a slow CouchDB operation const couchdbService = require('../services/couchdbService'); const originalFindByType = couchdbService.findByType; couchdbService.findByType = jest.fn().mockImplementation(() => { return new Promise((resolve, reject) => { setTimeout(() => reject(new Error("Database timeout")), 100); }); }); const response = await request(app) .get("/api/streets") .expect(500); expect(response.body.msg).toBeDefined(); // Restore original function couchdbService.findByType = originalFindByType; }); }); describe("Rate Limiting Errors", () => { test("should rate limit authentication attempts", async () => { const loginData = { email: "test@example.com", password: "wrongpassword", }; // Make multiple rapid requests const requests = []; for (let i = 0; i < 6; i++) { // Exceeds limit of 5 requests.push( request(app) .post("/api/auth/login") .send(loginData) ); } const responses = await Promise.all(requests); // At least one should be rate limited const rateLimitedResponse = responses.find(res => res.status === 429); expect(rateLimitedResponse).toBeDefined(); expect(rateLimitedResponse.body.error).toContain("Too many authentication attempts"); }); test("should rate limit general API requests", async () => { // Make many rapid requests to exceed general rate limit const requests = []; for (let i = 0; i < 105; i++) { // Exceeds limit of 100 requests.push( request(app) .get("/api/streets") .set("x-auth-token", authToken) ); } const responses = await Promise.all(requests); // At least one should be rate limited const rateLimitedResponse = responses.find(res => res.status === 429); expect(rateLimitedResponse).toBeDefined(); expect(rateLimitedResponse.body.error).toContain("Too many requests"); }); }); describe("Malformed Request Errors", () => { test("should handle invalid JSON", async () => { const response = await request(app) .post("/api/auth/login") .set("Content-Type", "application/json") .send('{"email": "test@example.com", "password": "password123"') // Missing closing brace .expect(400); expect(response.body.msg).toBeDefined(); }); test("should handle invalid query parameters", async () => { const response = await request(app) .get("/api/streets/nearby") .query({ lng: "invalid_longitude", lat: "invalid_latitude", maxDistance: "not_a_number", }) .expect(400); expect(response.body.msg).toBeDefined(); }); test("should handle oversized request body", async () => { const largeData = { content: "x".repeat(1000000), // 1MB of text }; const response = await request(app) .post("/api/posts") .set("x-auth-token", authToken) .send(largeData) .expect(413); // Payload Too Large expect(response.body.msg).toBeDefined(); }); test("should handle unsupported HTTP methods", async () => { const response = await request(app) .patch("/api/auth/login") .expect(404); // Not Found or Method Not Allowed expect(response.body.msg).toBeDefined(); }); }); describe("External Service Errors", () => { test("should handle Cloudinary upload failures", async () => { // Mock Cloudinary failure const cloudinary = require("cloudinary").v2; cloudinary.uploader.upload.mockRejectedValueOnce(new Error("Cloudinary service unavailable")); const response = await request(app) .put("/api/users/profile-picture") .set("x-auth-token", authToken) .attach("profilePicture", Buffer.from("fake image data"), "profile.jpg") .expect(500); expect(response.body.msg).toContain("Error uploading profile picture"); }); test("should handle email service failures", async () => { // Mock email service failure const nodemailer = require("nodemailer"); const mockSendMail = jest.fn().mockRejectedValueOnce(new Error("Email service unavailable")); nodemailer.createTransport.mockReturnValueOnce({ sendMail: mockSendMail, }); const response = await request(app) .post("/api/auth/register") .send({ name: "Test User", email: "newuser@example.com", password: "password123", }) .expect(500); expect(response.body.msg).toBeDefined(); }); }); describe("Error Response Format", () => { test("should return consistent error response format", async () => { const response = await request(app) .get("/api/nonexistent-endpoint") .expect(404); expect(response.body).toHaveProperty("msg"); expect(typeof response.body.msg).toBe("string"); }); test("should include error details for validation errors", async () => { const response = await request(app) .post("/api/auth/register") .send({ name: "", email: "invalid-email", password: "123", }) .expect(400); expect(response.body).toHaveProperty("errors"); expect(Array.isArray(response.body.errors)).toBe(true); expect(response.body.errors[0]).toHaveProperty("path"); expect(response.body.errors[0]).toHaveProperty("msg"); }); test("should sanitize error messages in production", async () => { // Set NODE_ENV to production const originalEnv = process.env.NODE_ENV; process.env.NODE_ENV = "production"; const response = await request(app) .get("/api/streets") .expect(500); // Should not expose internal error details expect(response.body.msg).toBe("Server error"); // Restore original environment process.env.NODE_ENV = originalEnv; }); }); describe("CORS Errors", () => { test("should handle cross-origin requests properly", async () => { const response = await request(app) .options("/api/streets") .set("Origin", "http://localhost:3000") .expect(200); expect(response.headers["access-control-allow-origin"]).toBeDefined(); }); test("should reject requests from unauthorized origins", async () => { // This test depends on CORS configuration // In production, you might want to reject certain origins const response = await request(app) .get("/api/streets") .set("Origin", "http://malicious-site.com") .expect(200); // Currently allows all origins, but could be restricted // If CORS is properly restricted, this would be 401 or 403 }); }); });