Files
adopt-a-street/backend/__tests__/errorhandling.test.js
William Valentin 6070474404 feat: Complete CouchDB test infrastructure migration for route tests
- Fixed 5/7 route test suites (auth, events, reports, rewards, streets)
- Updated Jest configuration with global CouchDB mocks
- Created comprehensive test helper utilities with proper ID generation
- Fixed pagination response format expectations (.data property)
- Added proper model method mocks (populate, save, toJSON, etc.)
- Resolved ID validation issues for different entity types
- Implemented proper CouchDB service method mocking
- Updated test helpers to generate valid IDs matching validator patterns

Remaining work:
- posts.test.js: needs model mocking and response format fixes
- tasks.test.js: needs Task model constructor fixes and mocking

🤖 Generated with [AI Assistant]

Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
2025-11-02 22:57:08 -08:00

557 lines
17 KiB
JavaScript

// Mock CouchDB service before importing anything else
jest.mock('../services/couchdbService', () => ({
initialize: jest.fn().mockResolvedValue(true),
isReady: jest.fn().mockReturnValue(true),
create: jest.fn(),
getById: jest.fn(),
find: jest.fn(),
createDocument: jest.fn(),
updateDocument: jest.fn(),
deleteDocument: jest.fn(),
findByType: jest.fn().mockResolvedValue([]),
findUserById: jest.fn(),
findUserByEmail: jest.fn(),
update: jest.fn(),
getDocument: jest.fn(),
}));
const request = require("supertest");
const app = require("../server");
const User = require("../models/User");
const { generateTestId } = require('./utils/idGenerator');
describe("Error Handling", () => {
let testUser;
let authToken;
beforeAll(async () => {
// 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"
);
});
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.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
});
});
});