- 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>
515 lines
17 KiB
JavaScript
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");
|
|
});
|
|
});
|
|
}); |