File Upload Testing: How to Test File Uploads in Node.js and Python
File upload testing covers the entire pipeline: browser-to-server transfer, server-side validation, storage, and post-processing. Each stage needs different testing strategies.
Testing the Upload Endpoint
Node.js with Multer
// routes/upload.js
import multer from 'multer'
import path from 'path'
const upload = multer({
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
fileFilter: (req, file, cb) => {
const allowed = ['.jpg', '.jpeg', '.png', '.pdf']
const ext = path.extname(file.originalname).toLowerCase()
if (allowed.includes(ext)) {
cb(null, true)
} else {
cb(new Error(`File type ${ext} not allowed`))
}
}
})
router.post('/upload', upload.single('file'), async (req, res) => {
const { file } = req
const url = await storage.upload(file.buffer, file.originalname)
res.json({ url, size: file.size, type: file.mimetype })
})// routes/upload.test.js
import request from 'supertest'
import fs from 'fs'
import path from 'path'
import { app } from '../app'
test('uploads valid JPG file', async () => {
const response = await request(app)
.post('/upload')
.set('Authorization', `Bearer ${validToken}`)
.attach('file', Buffer.from('fake-jpg-content'), {
filename: 'test.jpg',
contentType: 'image/jpeg',
})
expect(response.status).toBe(200)
expect(response.body.url).toMatch(/https:\/\//)
expect(response.body.type).toBe('image/jpeg')
})
test('rejects oversized file', async () => {
const bigBuffer = Buffer.alloc(6 * 1024 * 1024) // 6MB
const response = await request(app)
.post('/upload')
.set('Authorization', `Bearer ${validToken}`)
.attach('file', bigBuffer, {
filename: 'big.jpg',
contentType: 'image/jpeg',
})
expect(response.status).toBe(400)
expect(response.body.error).toContain('File too large')
})
test('rejects disallowed file type', async () => {
const response = await request(app)
.post('/upload')
.set('Authorization', `Bearer ${validToken}`)
.attach('file', Buffer.from('<?php echo "hack"; ?>'), {
filename: 'malicious.php',
contentType: 'application/x-php',
})
expect(response.status).toBe(400)
expect(response.body.error).toContain('not allowed')
})
test('validates actual file content, not just extension', async () => {
// File with .jpg extension but PHP content
const response = await request(app)
.post('/upload')
.attach('file', Buffer.from('<?php echo "hack"; ?>'), {
filename: 'image.jpg',
contentType: 'image/jpeg',
})
// Should validate magic bytes, not just extension
expect(response.status).toBe(400)
})Mocking S3 / Object Storage
// services/storage.js
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
const s3 = new S3Client({ region: 'us-east-1' })
export async function upload(buffer, filename) {
const key = `uploads/${Date.now()}-${filename}`
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
Body: buffer,
}))
return `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${key}`
}// Mock S3 in tests
import { mockClient } from 'aws-sdk-client-mock'
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
const s3Mock = mockClient(S3Client)
beforeEach(() => {
s3Mock.reset()
s3Mock.on(PutObjectCommand).resolves({})
})
test('stores file in S3 with correct key format', async () => {
await upload(Buffer.from('content'), 'photo.jpg')
const calls = s3Mock.calls()
expect(calls).toHaveLength(1)
const call = calls[0].args[0].input
expect(call.Bucket).toBe('my-bucket')
expect(call.Key).toMatch(/^uploads\/\d+-photo\.jpg$/)
})Testing Image Processing
// services/image-processor.js
import sharp from 'sharp'
export async function processProfileImage(buffer) {
return sharp(buffer)
.resize(256, 256, { fit: 'cover' })
.webp({ quality: 80 })
.toBuffer()
}
// services/image-processor.test.js
import sharp from 'sharp'
import { processProfileImage } from './image-processor'
test('resizes image to 256x256', async () => {
// Create a test image
const testImage = await sharp({
create: { width: 1200, height: 800, channels: 3, background: { r: 255, g: 0, b: 0 } }
}).jpeg().toBuffer()
const processed = await processProfileImage(testImage)
const metadata = await sharp(processed).metadata()
expect(metadata.width).toBe(256)
expect(metadata.height).toBe(256)
expect(metadata.format).toBe('webp')
})
test('rejects non-image files', async () => {
const notAnImage = Buffer.from('this is not an image')
await expect(processProfileImage(notAnImage))
.rejects.toThrow()
})Playwright File Upload Tests
// e2e/file-upload.spec.ts
import { test, expect } from '@playwright/test'
import path from 'path'
test('uploads profile photo via UI', async ({ page }) => {
await page.goto('/settings/profile')
// Trigger file input
const fileInput = page.locator('input[type="file"]')
await fileInput.setInputFiles(path.join(__dirname, 'fixtures', 'test-avatar.jpg'))
// Submit
await page.click('[data-testid="save-profile"]')
// Verify success
await expect(page.locator('[data-testid="avatar-image"]')).toBeVisible()
await expect(page.locator('[data-testid="upload-success-msg"]')).toHaveText('Photo updated')
})
test('shows error for oversized file', async ({ page }) => {
await page.goto('/settings/profile')
// Create a large buffer and use it as file
const largeBuffer = Buffer.alloc(6 * 1024 * 1024)
await page.locator('input[type="file"]').setInputFiles({
name: 'large.jpg',
mimeType: 'image/jpeg',
buffer: largeBuffer,
})
await page.click('[data-testid="save-profile"]')
await expect(page.locator('[data-testid="upload-error"]')).toContainText('5MB')
})
test('drag and drop file upload', async ({ page }) => {
await page.goto('/documents')
const dropZone = page.locator('[data-testid="drop-zone"]')
await dropZone.dispatchEvent('drop', {
dataTransfer: {
files: [
{ name: 'document.pdf', type: 'application/pdf', size: 1024 }
]
}
})
await expect(page.locator('[data-testid="file-list"]')).toContainText('document.pdf')
})Python: Testing File Uploads with Django
# tests/test_upload.py
from django.test import TestCase
from django.core.files.uploadedfile import SimpleUploadedFile
class FileUploadTests(TestCase):
def test_upload_valid_image(self):
with open('tests/fixtures/test.jpg', 'rb') as f:
image_data = f.read()
file = SimpleUploadedFile('test.jpg', image_data, content_type='image/jpeg')
response = self.client.post('/api/upload/', {'file': file})
self.assertEqual(response.status_code, 200)
self.assertIn('url', response.json())
def test_rejects_oversized_file(self):
big_data = b'0' * (6 * 1024 * 1024) # 6MB
file = SimpleUploadedFile('big.jpg', big_data, content_type='image/jpeg')
response = self.client.post('/api/upload/', {'file': file})
self.assertEqual(response.status_code, 400)
def test_rejects_invalid_type(self):
file = SimpleUploadedFile('script.php', b'<?php echo 1; ?>', content_type='application/x-php')
response = self.client.post('/api/upload/', {'file': file})
self.assertEqual(response.status_code, 400)Summary
File upload testing should cover:
- Happy path: valid files upload and return URLs
- Size limits: oversized files rejected with clear error
- Type validation: disallowed extensions and content types rejected
- Magic byte validation: file content matches claimed type
- Storage mocking: mock S3/GCS to test upload logic without real object storage
- Image processing: validate resize/convert output dimensions and format
- Browser E2E: Playwright
setInputFilesfor form-based uploads, drop zone testing