const request = require("supertest"); 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"); const { generateTestId } = require('./utils/idGenerator'); // Mock Cloudinary jest.mock("cloudinary", () => ({ v2: { config: jest.fn(), uploader: { upload: jest.fn(), destroy: jest.fn(), }, }, })); describe("File Upload System", () => { let testUser; let authToken; beforeAll(async () => { // Configure test Cloudinary settings cloudinary.config({ cloud_name: "test_cloud", api_key: "test_key", api_secret: "test_secret", }); // 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" ); }); 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 = generateTestId(); }); 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"); }); }); });