File Upload Testing: How to Test File Uploads in Node.js and Python

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 setInputFiles for form-based uploads, drop zone testing

Read more