const request = require("supertest"); const mongoose = require("mongoose"); const { MongoMemoryServer } = require("mongodb-memory-server"); const app = require("../server"); const User = require("../models/User"); describe("Error Handling", () => { let mongoServer; let testUser; let authToken; beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); const mongoUri = mongoServer.getUri(); await mongoose.connect(mongoUri); // 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(); }); 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: new mongoose.Types.ObjectId().toString() } }, 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 = new mongoose.Types.ObjectId().toString(); 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 = new mongoose.Types.ObjectId().toString(); 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 = new mongoose.Types.ObjectId().toString(); 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 = new mongoose.Types.ObjectId().toString(); 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 = new mongoose.Types.ObjectId(); }); 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 disconnection gracefully", async () => { // Disconnect from database await mongoose.connection.close(); const response = await request(app) .get("/api/streets") .expect(500); expect(response.body.msg).toBeDefined(); // Reconnect for other tests await mongoose.connect(mongoServer.getUri()); }); test("should handle database operation timeouts", async () => { // Mock a slow database operation const originalFind = mongoose.Model.find; mongoose.Model.find = 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 method mongoose.Model.find = originalFind; }); }); 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.mockRejectedValue(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().mockRejectedValue(new Error("Email service unavailable")); nodemailer.createTransport.mockReturnValue({ 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 }); }); });