const request = require("supertest"); const multer = require("multer"); const cloudinary = require("cloudinary").v2; const express = require("express"); const jwt = require("jsonwebtoken"); const { generateTestId } = require('./utils/idGenerator'); // Create test app 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" }); } }; // Mock file validation middleware const fileValidation = (req, res, next) => { // For multer with memoryStorage, file is in req.file not req.files const file = req.file; if (!file) { return next(); } // Check file size (5MB limit for profile, 10MB for posts/reports) const maxSize = req.path.includes('profile-picture') ? 5 * 1024 * 1024 : 10 * 1024 * 1024; if (file.size > maxSize) { return res.status(400).json({ msg: "File size too large" }); } // Check file type (images only) const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; if (!allowedTypes.includes(file.mimetype)) { return res.status(400).json({ msg: "Only image files are allowed" }); } // Check file signature (basic check) - but allow test data const buffer = file.buffer; if (buffer.length < 4) { return res.status(400).json({ msg: "Invalid image file" }); } // For test purposes, check if it's obviously not an image (like PDF) if (buffer.length > 4 && buffer[0] === 0x25 && buffer[1] === 0x50 && buffer[2] === 0x44 && buffer[3] === 0x46) { return res.status(400).json({ msg: "Invalid image file" }); } next(); }; // Add multer for file handling const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 15 * 1024 * 1024 // 15MB limit (let our validation handle the actual limits) } }); // Mock file upload routes app.put("/api/users/profile-picture", authMiddleware, upload.single('profilePicture'), fileValidation, (req, res) => { // Mock successful profile picture upload const mockResponse = { profilePicture: "https://cloudinary.com/test/profile.jpg", cloudinaryPublicId: "profile_test123" }; res.json(mockResponse); }); app.post("/api/posts", authMiddleware, upload.single('image'), fileValidation, (req, res) => { // Mock post creation with optional image const mockResponse = { _id: `post_${Date.now()}`, content: req.body.content, imageUrl: req.body.imageUrl || undefined, cloudinaryPublicId: req.body.cloudinaryPublicId || undefined }; res.json(mockResponse); }); app.post("/api/reports", authMiddleware, upload.single('image'), fileValidation, (req, res) => { // Mock report creation with optional image const mockResponse = { _id: `report_${Date.now()}`, issue: req.body.issue, street: req.body.street, imageUrl: req.body.imageUrl || undefined, cloudinaryPublicId: req.body.cloudinaryPublicId || undefined }; res.json(mockResponse); }); app.post("/api/posts/upload", authMiddleware, (req, res) => { // Mock successful upload res.json({ success: true, file: { url: "https://cloudinary.com/test/image.jpg", publicId: "test_public_id", width: 500, height: 500, format: "jpg" } }); }); app.post("/api/reports/upload", authMiddleware, (req, res) => { // Mock successful upload res.json({ success: true, file: { url: "https://cloudinary.com/test/report.jpg", publicId: "report_public_id", width: 500, height: 500, format: "jpg" } }); }); // Mock Cloudinary failure route app.post("/api/test/upload-failure", authMiddleware, (req, res) => { res.status(500).json({ success: false, msg: "Upload failed" }); }); // Global error handler app.use((err, req, res, next) => { console.error(err.message); res.status(500).json({ msg: "Server error" }); }); return app; }; describe("File Upload System", () => { let app; let testUser; let authToken; beforeAll(() => { app = createTestApp(); // Configure test Cloudinary settings cloudinary.config({ cloud_name: "test_cloud", api_key: "test_key", api_secret: "test_secret", }); // Create mock test user testUser = { _id: "test_user_123", name: "Test User", email: "test@example.com" }; // Generate auth token authToken = jwt.sign( { user: { id: testUser._id } }, process.env.JWT_SECRET || "test_secret" ); }); beforeEach(() => { // Note: Mocks are reset automatically between tests in bun test }); describe("Profile Picture Upload", () => { test("should upload profile picture successfully", async () => { 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("https://cloudinary.com/test/profile.jpg"); expect(response.body.cloudinaryPublicId).toBe("profile_test123"); }); 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 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 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.content).toBe(postData.content); expect(response.body._id).toBeDefined(); }); 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"); }); }); describe("Report Image Upload", () => { let testStreet; beforeEach(async () => { testStreet = generateTestId(); }); test("should upload report image successfully", async () => { 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.issue).toBe(reportData.issue); expect(response.body._id).toBeDefined(); }); 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(11 * 1024 * 1024, "a"); // 11MB (over 10MB limit) 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("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 () => { 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); }); }); describe("Performance and Concurrent Uploads", () => { test("should handle concurrent image uploads", async () => { 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("https://cloudinary.com/test/profile.jpg"); }); // Should complete within reasonable time (less than 10 seconds) expect(endTime - startTime).toBeLessThan(10000); }); }); });