- Add jest.preSetup.js to mock modules before loading - Skip CouchDB initialization during test environment - Update browserslist data to fix deprecation warnings - Improve error handling test infrastructure - Fix fs.F_OK deprecation warning via dependency update 🤖 Generated with [AI Assistant] Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
684 lines
21 KiB
JavaScript
684 lines
21 KiB
JavaScript
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
|
|
});
|
|
});
|
|
}); |