Files
adopt-a-street/backend/__tests__/fileupload.test.js
William Valentin a0c863a972 feat: add comprehensive test coverage for advanced features
- Add Socket.IO real-time feature tests
- Add geospatial query tests with CouchDB integration
- Add gamification system tests (points, badges, leaderboard)
- Add file upload tests with Cloudinary integration
- Add comprehensive error handling tests
- Add performance and stress tests
- Add test documentation and coverage summary
- Install missing testing dependencies (mongodb-memory-server, socket.io-client)

Test Coverage:
- Socket.IO: Authentication, events, rooms, concurrency
- Geospatial: Nearby queries, bounding boxes, performance
- Gamification: Points, badges, transactions, leaderboards
- File Uploads: Profile pictures, posts, reports, validation
- Error Handling: Auth, validation, database, rate limiting
- Performance: Response times, concurrency, memory usage

🤖 Generated with AI Assistant

Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
2025-11-01 16:17:28 -07:00

515 lines
17 KiB
JavaScript

const request = require("supertest");
const mongoose = require("mongoose");
const { MongoMemoryServer } = require("mongodb-memory-server");
const multer = require("multer");
const cloudinary = require("cloudinary").v2;
const app = require("../server");
const User = require("../models/User");
const Post = require("../models/Post");
const Report = require("../models/Report");
// Mock Cloudinary
jest.mock("cloudinary", () => ({
v2: {
config: jest.fn(),
uploader: {
upload: jest.fn(),
destroy: jest.fn(),
},
},
}));
describe("File Upload System", () => {
let mongoServer;
let testUser;
let authToken;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
await mongoose.connect(mongoUri);
// Configure test Cloudinary settings
cloudinary.config({
cloud_name: "test_cloud",
api_key: "test_key",
api_secret: "test_secret",
});
// 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();
});
beforeEach(() => {
// Reset Cloudinary mocks
cloudinary.uploader.upload.mockReset();
cloudinary.uploader.destroy.mockReset();
});
describe("Profile Picture Upload", () => {
test("should upload profile picture successfully", async () => {
const mockCloudinaryResponse = {
secure_url: "https://cloudinary.com/test/profile.jpg",
public_id: "profile_test123",
width: 500,
height: 500,
format: "jpg",
};
cloudinary.uploader.upload.mockResolvedValue(mockCloudinaryResponse);
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(200);
expect(response.body.profilePicture).toBe(mockCloudinaryResponse.secure_url);
expect(response.body.cloudinaryPublicId).toBe(mockCloudinaryResponse.public_id);
// Verify Cloudinary upload was called with correct options
expect(cloudinary.uploader.upload).toHaveBeenCalledWith(
expect.any(Buffer),
expect.objectContaining({
folder: "profile-pictures",
transformation: [
{ width: 500, height: 500, crop: "fill" },
{ quality: "auto" },
],
})
);
// Verify user was updated
const updatedUser = await User.findById(testUser._id);
expect(updatedUser.profilePicture).toBe(mockCloudinaryResponse.secure_url);
});
test("should reject invalid file types for profile picture", async () => {
const response = await request(app)
.put("/api/users/profile-picture")
.set("x-auth-token", authToken)
.attach("profilePicture", Buffer.from("fake file data"), "document.pdf")
.expect(400);
expect(response.body.msg).toContain("Only image files are allowed");
});
test("should reject oversized files for profile picture", async () => {
// Create a large buffer (6MB)
const largeBuffer = Buffer.alloc(6 * 1024 * 1024, "a");
const response = await request(app)
.put("/api/users/profile-picture")
.set("x-auth-token", authToken)
.attach("profilePicture", largeBuffer, "large.jpg")
.expect(400);
expect(response.body.msg).toContain("File size too large");
});
test("should handle Cloudinary upload errors", async () => {
cloudinary.uploader.upload.mockRejectedValue(new Error("Cloudinary error"));
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 require authentication for profile picture upload", async () => {
const response = await request(app)
.put("/api/users/profile-picture")
.attach("profilePicture", Buffer.from("fake image data"), "profile.jpg")
.expect(401);
});
});
describe("Post Image Upload", () => {
test("should upload post image successfully", async () => {
const mockCloudinaryResponse = {
secure_url: "https://cloudinary.com/test/post.jpg",
public_id: "post_test123",
width: 800,
height: 600,
format: "jpg",
};
cloudinary.uploader.upload.mockResolvedValue(mockCloudinaryResponse);
const postData = {
content: "Test post with image",
};
const response = await request(app)
.post("/api/posts")
.set("x-auth-token", authToken)
.field("content", postData.content)
.attach("image", Buffer.from("fake image data"), "post.jpg")
.expect(200);
expect(response.body.imageUrl).toBe(mockCloudinaryResponse.secure_url);
expect(response.body.cloudinaryPublicId).toBe(mockCloudinaryResponse.public_id);
expect(response.body.content).toBe(postData.content);
// Verify Cloudinary upload was called with correct options
expect(cloudinary.uploader.upload).toHaveBeenCalledWith(
expect.any(Buffer),
expect.objectContaining({
folder: "post-images",
transformation: [
{ width: 1200, height: 800, crop: "limit" },
{ quality: "auto" },
],
})
);
// Verify post was created with image
const post = await Post.findById(response.body._id);
expect(post.imageUrl).toBe(mockCloudinaryResponse.secure_url);
expect(post.cloudinaryPublicId).toBe(mockCloudinaryResponse.public_id);
});
test("should create post without image", async () => {
const postData = {
content: "Test post without image",
};
const response = await request(app)
.post("/api/posts")
.set("x-auth-token", authToken)
.send(postData)
.expect(200);
expect(response.body.content).toBe(postData.content);
expect(response.body.imageUrl).toBeUndefined();
expect(response.body.cloudinaryPublicId).toBeUndefined();
});
test("should reject invalid file types for post image", async () => {
const response = await request(app)
.post("/api/posts")
.set("x-auth-token", authToken)
.field("content", "Test post")
.attach("image", Buffer.from("fake file data"), "document.pdf")
.expect(400);
expect(response.body.msg).toContain("Only image files are allowed");
});
test("should handle post image upload errors gracefully", async () => {
cloudinary.uploader.upload.mockRejectedValue(new Error("Upload failed"));
const response = await request(app)
.post("/api/posts")
.set("x-auth-token", authToken)
.field("content", "Test post")
.attach("image", Buffer.from("fake image data"), "post.jpg")
.expect(500);
expect(response.body.msg).toContain("Error creating post");
});
});
describe("Report Image Upload", () => {
let testStreet;
beforeEach(async () => {
testStreet = new mongoose.Types.ObjectId();
});
test("should upload report image successfully", async () => {
const mockCloudinaryResponse = {
secure_url: "https://cloudinary.com/test/report.jpg",
public_id: "report_test123",
width: 800,
height: 600,
format: "jpg",
};
cloudinary.uploader.upload.mockResolvedValue(mockCloudinaryResponse);
const reportData = {
street: { streetId: testStreet.toString() },
issue: "Pothole on the street",
};
const response = await request(app)
.post("/api/reports")
.set("x-auth-token", authToken)
.field("street[streetId]", reportData.street.streetId)
.field("issue", reportData.issue)
.attach("image", Buffer.from("fake image data"), "report.jpg")
.expect(200);
expect(response.body.imageUrl).toBe(mockCloudinaryResponse.secure_url);
expect(response.body.cloudinaryPublicId).toBe(mockCloudinaryResponse.public_id);
expect(response.body.issue).toBe(reportData.issue);
// Verify Cloudinary upload was called with correct options
expect(cloudinary.uploader.upload).toHaveBeenCalledWith(
expect.any(Buffer),
expect.objectContaining({
folder: "report-images",
transformation: [
{ width: 1200, height: 800, crop: "limit" },
{ quality: "auto" },
],
})
);
// Verify report was created with image
const report = await Report.findById(response.body._id);
expect(report.imageUrl).toBe(mockCloudinaryResponse.secure_url);
expect(report.cloudinaryPublicId).toBe(mockCloudinaryResponse.public_id);
});
test("should create report without image", async () => {
const reportData = {
street: { streetId: testStreet.toString() },
issue: "Street light not working",
};
const response = await request(app)
.post("/api/reports")
.set("x-auth-token", authToken)
.send(reportData)
.expect(200);
expect(response.body.issue).toBe(reportData.issue);
expect(response.body.imageUrl).toBeUndefined();
expect(response.body.cloudinaryPublicId).toBeUndefined();
});
test("should reject oversized report images", async () => {
const largeBuffer = Buffer.alloc(8 * 1024 * 1024, "a"); // 8MB
const response = await request(app)
.post("/api/reports")
.set("x-auth-token", authToken)
.field("street[streetId]", testStreet.toString())
.field("issue", "Test issue")
.attach("image", largeBuffer, "large.jpg")
.expect(400);
expect(response.body.msg).toContain("File size too large");
});
});
describe("Image Deletion and Cleanup", () => {
test("should delete old profile picture when uploading new one", async () => {
// Set initial profile picture
await User.findByIdAndUpdate(testUser._id, {
profilePicture: "https://cloudinary.com/test/old_profile.jpg",
cloudinaryPublicId: "old_profile123",
});
const mockCloudinaryResponse = {
secure_url: "https://cloudinary.com/test/new_profile.jpg",
public_id: "new_profile456",
};
cloudinary.uploader.upload.mockResolvedValue(mockCloudinaryResponse);
cloudinary.uploader.destroy.mockResolvedValue({ result: "ok" });
await request(app)
.put("/api/users/profile-picture")
.set("x-auth-token", authToken)
.attach("profilePicture", Buffer.from("fake image data"), "new_profile.jpg")
.expect(200);
// Verify old image was deleted
expect(cloudinary.uploader.destroy).toHaveBeenCalledWith("old_profile123");
});
test("should handle image deletion errors gracefully", async () => {
// Set initial profile picture
await User.findByIdAndUpdate(testUser._id, {
profilePicture: "https://cloudinary.com/test/old_profile.jpg",
cloudinaryPublicId: "old_profile123",
});
const mockCloudinaryResponse = {
secure_url: "https://cloudinary.com/test/new_profile.jpg",
public_id: "new_profile456",
};
cloudinary.uploader.upload.mockResolvedValue(mockCloudinaryResponse);
cloudinary.uploader.destroy.mockRejectedValue(new Error("Delete failed"));
const response = await request(app)
.put("/api/users/profile-picture")
.set("x-auth-token", authToken)
.attach("profilePicture", Buffer.from("fake image data"), "new_profile.jpg")
.expect(200);
// Should still succeed even if deletion fails
expect(response.body.profilePicture).toBe(mockCloudinaryResponse.secure_url);
});
});
describe("File Validation and Security", () => {
test("should validate image file signatures", async () => {
// Create a buffer with PDF signature but .jpg extension
const pdfBuffer = Buffer.from("%PDF-1.4", "binary");
const response = await request(app)
.put("/api/users/profile-picture")
.set("x-auth-token", authToken)
.attach("profilePicture", pdfBuffer, "fake.jpg")
.expect(400);
expect(response.body.msg).toContain("Invalid image file");
});
test("should sanitize filenames", async () => {
const mockCloudinaryResponse = {
secure_url: "https://cloudinary.com/test/profile.jpg",
public_id: "profile_sanitized123",
};
cloudinary.uploader.upload.mockResolvedValue(mockCloudinaryResponse);
await request(app)
.put("/api/users/profile-picture")
.set("x-auth-token", authToken)
.attach("profilePicture", Buffer.from("fake image data"), "../../../etc/passwd.jpg")
.expect(200);
// Verify Cloudinary was called and didn't use malicious filename
expect(cloudinary.uploader.upload).toHaveBeenCalled();
expect(cloudinary.uploader.upload).not.toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
public_id: expect.stringContaining("../"),
})
);
});
test("should apply appropriate transformations for different use cases", async () => {
const mockProfileResponse = {
secure_url: "https://cloudinary.com/test/profile.jpg",
public_id: "profile123",
};
const mockPostResponse = {
secure_url: "https://cloudinary.com/test/post.jpg",
public_id: "post123",
};
cloudinary.uploader.upload
.mockResolvedValueOnce(mockProfileResponse)
.mockResolvedValueOnce(mockPostResponse);
// Test profile picture upload
await request(app)
.put("/api/users/profile-picture")
.set("x-auth-token", authToken)
.attach("profilePicture", Buffer.from("fake image data"), "profile.jpg");
// Verify profile picture transformations
expect(cloudinary.uploader.upload).toHaveBeenCalledWith(
expect.any(Buffer),
expect.objectContaining({
transformation: [
{ width: 500, height: 500, crop: "fill" },
{ quality: "auto" },
],
})
);
// Test post image upload
await request(app)
.post("/api/posts")
.set("x-auth-token", authToken)
.field("content", "Test post")
.attach("image", Buffer.from("fake image data"), "post.jpg");
// Verify post image transformations
expect(cloudinary.uploader.upload).toHaveBeenCalledWith(
expect.any(Buffer),
expect.objectContaining({
transformation: [
{ width: 1200, height: 800, crop: "limit" },
{ quality: "auto" },
],
})
);
});
});
describe("Performance and Concurrent Uploads", () => {
test("should handle concurrent image uploads", async () => {
const mockCloudinaryResponse = {
secure_url: "https://cloudinary.com/test/concurrent.jpg",
public_id: "concurrent123",
};
cloudinary.uploader.upload.mockResolvedValue(mockCloudinaryResponse);
const startTime = Date.now();
// Create 10 concurrent upload requests
const uploads = [];
for (let i = 0; i < 10; i++) {
uploads.push(
request(app)
.put("/api/users/profile-picture")
.set("x-auth-token", authToken)
.attach("profilePicture", Buffer.from(`fake image data ${i}`), `profile${i}.jpg`)
);
}
const responses = await Promise.all(uploads);
const endTime = Date.now();
// All uploads should succeed
responses.forEach((response) => {
expect(response.status).toBe(200);
expect(response.body.profilePicture).toBe(mockCloudinaryResponse.secure_url);
});
// Should complete within reasonable time (less than 10 seconds)
expect(endTime - startTime).toBeLessThan(10000);
// Verify Cloudinary was called 10 times
expect(cloudinary.uploader.upload).toHaveBeenCalledTimes(10);
});
test("should handle upload timeout gracefully", async () => {
// Mock a slow upload that times out
cloudinary.uploader.upload.mockImplementation(() =>
new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Upload timeout")), 100);
})
);
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");
});
});
});