Files
SaaS-PDF/backend/tests/test_pdf_tools.py
Your Name d7f6228d7f الميزات: إضافة أدوات جديدة لمعالجة ملفات PDF، تشمل التلخيص والترجمة واستخراج الجداول.
- تفعيل مكون SummarizePdf لإنشاء ملخصات PDF باستخدام الذكاء الاصطناعي.

- تفعيل مكون TranslatePdf لترجمة محتوى PDF إلى لغات متعددة.

- تفعيل مكون TableExtractor لاستخراج الجداول من ملفات PDF.

- تحديث الصفحة الرئيسية والتوجيه ليشمل الأدوات الجديدة.

- إضافة ترجمات للأدوات الجديدة باللغات الإنجليزية والعربية والفرنسية.

- توسيع أنواع واجهة برمجة التطبيقات (API) لدعم الميزات الجديدة المتعلقة بمعالجة ملفات PDF. --feat: Initialize frontend with React, Vite, and Tailwind CSS

- Set up main entry point for React application.
- Create About, Home, NotFound, Privacy, and Terms pages with SEO support.
- Implement API service for file uploads and task management.
- Add global styles using Tailwind CSS.
- Create utility functions for SEO and text processing.
- Configure Vite for development and production builds.
- Set up Nginx configuration for serving frontend and backend.
- Add scripts for cleanup of expired files and sitemap generation.
- Implement deployment script for production environment.
2026-03-08 05:49:09 +02:00

634 lines
23 KiB
Python

"""Tests for ALL PDF tools routes — Merge, Split, Rotate, Page Numbers, PDF↔Images, Watermark, Protect, Unlock."""
import io
import os
import tempfile
from unittest.mock import patch, MagicMock
# =========================================================================
# Helper: create mock for validate_file + celery task
# =========================================================================
def _mock_validate_and_task(monkeypatch, task_module_path, task_name):
"""Shared helper: mock validate_file to pass, mock the celery task,
and ensure file.save() works by using a real temp directory."""
mock_task = MagicMock()
mock_task.id = 'mock-task-id'
# Create a real temp dir so file.save() works
tmp_dir = tempfile.mkdtemp()
save_path = os.path.join(tmp_dir, 'mock.pdf')
# Mock file validator to accept any file
monkeypatch.setattr(
'app.routes.pdf_tools.validate_actor_file',
lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
)
monkeypatch.setattr(
'app.routes.pdf_tools.generate_safe_path',
lambda ext, folder_type: ('mock-task-id', save_path),
)
# Mock the celery task delay
mock_delay = MagicMock(return_value=mock_task)
monkeypatch.setattr(f'app.routes.pdf_tools.{task_name}.delay', mock_delay)
return mock_task, mock_delay
# =========================================================================
# 1. Merge PDFs — POST /api/pdf-tools/merge
# =========================================================================
class TestMergePdfs:
def test_merge_no_files(self, client):
"""Should return 400 when no files provided."""
response = client.post('/api/pdf-tools/merge')
assert response.status_code == 400
data = response.get_json()
assert 'error' in data
def test_merge_single_file(self, client):
"""Should return 400 when only one file provided."""
data = {'files': (io.BytesIO(b'%PDF-1.4 test'), 'test.pdf')}
response = client.post(
'/api/pdf-tools/merge',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 400
assert 'at least 2' in response.get_json()['error'].lower()
def test_merge_success(self, client, monkeypatch):
"""Should return 202 with task_id when valid PDFs provided."""
mock_task = MagicMock()
mock_task.id = 'merge-task-id'
monkeypatch.setattr(
'app.routes.pdf_tools.validate_actor_file',
lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
)
monkeypatch.setattr(
'app.routes.pdf_tools.merge_pdfs_task.delay',
MagicMock(return_value=mock_task),
)
# Mock os.makedirs and FileStorage.save so nothing touches disk
monkeypatch.setattr('app.routes.pdf_tools.os.makedirs', lambda *a, **kw: None)
monkeypatch.setattr(
'werkzeug.datastructures.file_storage.FileStorage.save',
lambda self, dst, buffer_size=16384: None,
)
data = {
'files': [
(io.BytesIO(b'%PDF-1.4 file1'), 'a.pdf'),
(io.BytesIO(b'%PDF-1.4 file2'), 'b.pdf'),
]
}
response = client.post(
'/api/pdf-tools/merge',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 202
body = response.get_json()
assert body['task_id'] == 'merge-task-id'
assert 'message' in body
def test_merge_too_many_files(self, client, monkeypatch):
"""Should return 400 when more than 20 files provided."""
monkeypatch.setattr(
'app.routes.pdf_tools.validate_actor_file',
lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
)
data = {
'files': [
(io.BytesIO(b'%PDF-1.4'), f'file{i}.pdf')
for i in range(21)
]
}
response = client.post(
'/api/pdf-tools/merge',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 400
assert '20' in response.get_json()['error']
# =========================================================================
# 2. Split PDF — POST /api/pdf-tools/split
# =========================================================================
class TestSplitPdf:
def test_split_no_file(self, client):
"""Should return 400 when no file provided."""
response = client.post('/api/pdf-tools/split')
assert response.status_code == 400
data = response.get_json()
assert data['error'] == 'No file provided.'
def test_split_success_all_mode(self, client, monkeypatch):
"""Should accept file and return 202 with mode=all."""
mock_task, mock_delay = _mock_validate_and_task(
monkeypatch, 'app.routes.pdf_tools', 'split_pdf_task'
)
data = {
'file': (io.BytesIO(b'%PDF-1.4 test'), 'test.pdf'),
'mode': 'all',
}
response = client.post(
'/api/pdf-tools/split',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 202
body = response.get_json()
assert body['task_id'] == 'mock-task-id'
def test_split_success_range_mode(self, client, monkeypatch):
"""Should accept file with mode=range and pages."""
mock_task, mock_delay = _mock_validate_and_task(
monkeypatch, 'app.routes.pdf_tools', 'split_pdf_task'
)
data = {
'file': (io.BytesIO(b'%PDF-1.4 test'), 'test.pdf'),
'mode': 'range',
'pages': '1,3,5-8',
}
response = client.post(
'/api/pdf-tools/split',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 202
mock_delay.assert_called_once()
# Verify pages parameter was passed
call_args = mock_delay.call_args
assert call_args[0][4] == '1,3,5-8' # pages arg
def test_split_range_mode_requires_pages(self, client, monkeypatch):
"""Should return 400 when range mode is selected without pages."""
monkeypatch.setattr(
'app.routes.pdf_tools.validate_actor_file',
lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
)
data = {
'file': (io.BytesIO(b'%PDF-1.4 test'), 'test.pdf'),
'mode': 'range',
}
response = client.post(
'/api/pdf-tools/split',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 400
assert 'specify which pages to extract' in response.get_json()['error'].lower()
# =========================================================================
# 3. Rotate PDF — POST /api/pdf-tools/rotate
# =========================================================================
class TestRotatePdf:
def test_rotate_no_file(self, client):
response = client.post('/api/pdf-tools/rotate')
assert response.status_code == 400
def test_rotate_invalid_degrees(self, client, monkeypatch):
"""Should reject invalid rotation angles."""
monkeypatch.setattr(
'app.routes.pdf_tools.validate_actor_file',
lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
)
data = {
'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
'rotation': '45',
}
response = client.post(
'/api/pdf-tools/rotate',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 400
assert '90, 180, or 270' in response.get_json()['error']
def test_rotate_success(self, client, monkeypatch):
mock_task, mock_delay = _mock_validate_and_task(
monkeypatch, 'app.routes.pdf_tools', 'rotate_pdf_task'
)
data = {
'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
'rotation': '90',
'pages': 'all',
}
response = client.post(
'/api/pdf-tools/rotate',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 202
assert response.get_json()['task_id'] == 'mock-task-id'
# =========================================================================
# 4. Page Numbers — POST /api/pdf-tools/page-numbers
# =========================================================================
class TestAddPageNumbers:
def test_page_numbers_no_file(self, client):
response = client.post('/api/pdf-tools/page-numbers')
assert response.status_code == 400
def test_page_numbers_success(self, client, monkeypatch):
mock_task, mock_delay = _mock_validate_and_task(
monkeypatch, 'app.routes.pdf_tools', 'add_page_numbers_task'
)
data = {
'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
'position': 'bottom-center',
'start_number': '1',
}
response = client.post(
'/api/pdf-tools/page-numbers',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 202
def test_page_numbers_invalid_position_defaults(self, client, monkeypatch):
"""Invalid position should default to bottom-center."""
mock_task, mock_delay = _mock_validate_and_task(
monkeypatch, 'app.routes.pdf_tools', 'add_page_numbers_task'
)
data = {
'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
'position': 'invalid-position',
}
response = client.post(
'/api/pdf-tools/page-numbers',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 202
# Should have used default 'bottom-center'
call_args = mock_delay.call_args[0]
assert call_args[3] == 'bottom-center'
# =========================================================================
# 5. PDF to Images — POST /api/pdf-tools/pdf-to-images
# =========================================================================
class TestPdfToImages:
def test_pdf_to_images_no_file(self, client):
response = client.post('/api/pdf-tools/pdf-to-images')
assert response.status_code == 400
def test_pdf_to_images_success(self, client, monkeypatch):
mock_task, mock_delay = _mock_validate_and_task(
monkeypatch, 'app.routes.pdf_tools', 'pdf_to_images_task'
)
data = {
'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
'format': 'png',
'dpi': '200',
}
response = client.post(
'/api/pdf-tools/pdf-to-images',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 202
def test_pdf_to_images_invalid_format_defaults(self, client, monkeypatch):
"""Invalid format should default to png."""
mock_task, mock_delay = _mock_validate_and_task(
monkeypatch, 'app.routes.pdf_tools', 'pdf_to_images_task'
)
data = {
'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
'format': 'bmp',
}
response = client.post(
'/api/pdf-tools/pdf-to-images',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 202
call_args = mock_delay.call_args[0]
assert call_args[3] == 'png' # default format
# =========================================================================
# 6. Images to PDF — POST /api/pdf-tools/images-to-pdf
# =========================================================================
class TestImagesToPdf:
def test_images_to_pdf_no_files(self, client):
response = client.post('/api/pdf-tools/images-to-pdf')
assert response.status_code == 400
def test_images_to_pdf_success(self, client, monkeypatch):
mock_task = MagicMock()
mock_task.id = 'images-task-id'
monkeypatch.setattr(
'app.routes.pdf_tools.validate_actor_file',
lambda f, allowed_types, actor: ('test.png', 'png'),
)
monkeypatch.setattr(
'app.routes.pdf_tools.images_to_pdf_task.delay',
MagicMock(return_value=mock_task),
)
# Mock os.makedirs and FileStorage.save so nothing touches disk
monkeypatch.setattr('app.routes.pdf_tools.os.makedirs', lambda *a, **kw: None)
monkeypatch.setattr(
'werkzeug.datastructures.file_storage.FileStorage.save',
lambda self, dst, buffer_size=16384: None,
)
data = {
'files': [
(io.BytesIO(b'\x89PNG\r\n'), 'img1.png'),
(io.BytesIO(b'\x89PNG\r\n'), 'img2.png'),
]
}
response = client.post(
'/api/pdf-tools/images-to-pdf',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 202
assert response.get_json()['task_id'] == 'images-task-id'
def test_images_to_pdf_too_many(self, client, monkeypatch):
monkeypatch.setattr(
'app.routes.pdf_tools.validate_actor_file',
lambda f, allowed_types, actor: ('test.png', 'png'),
)
data = {
'files': [
(io.BytesIO(b'\x89PNG\r\n'), f'img{i}.png')
for i in range(51)
]
}
response = client.post(
'/api/pdf-tools/images-to-pdf',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 400
assert '50' in response.get_json()['error']
# =========================================================================
# 7. Watermark PDF — POST /api/pdf-tools/watermark
# =========================================================================
class TestWatermarkPdf:
def test_watermark_no_file(self, client):
response = client.post('/api/pdf-tools/watermark')
assert response.status_code == 400
def test_watermark_no_text(self, client, monkeypatch):
monkeypatch.setattr(
'app.routes.pdf_tools.validate_actor_file',
lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
)
data = {
'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
'text': '',
}
response = client.post(
'/api/pdf-tools/watermark',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 400
assert 'required' in response.get_json()['error'].lower()
def test_watermark_text_too_long(self, client, monkeypatch):
monkeypatch.setattr(
'app.routes.pdf_tools.validate_actor_file',
lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
)
data = {
'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
'text': 'x' * 101,
}
response = client.post(
'/api/pdf-tools/watermark',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 400
assert '100' in response.get_json()['error']
def test_watermark_success(self, client, monkeypatch):
mock_task, mock_delay = _mock_validate_and_task(
monkeypatch, 'app.routes.pdf_tools', 'watermark_pdf_task'
)
data = {
'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
'text': 'CONFIDENTIAL',
'opacity': '0.5',
}
response = client.post(
'/api/pdf-tools/watermark',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 202
# =========================================================================
# 8. Protect PDF — POST /api/pdf-tools/protect
# =========================================================================
class TestProtectPdf:
def test_protect_no_file(self, client):
response = client.post('/api/pdf-tools/protect')
assert response.status_code == 400
def test_protect_no_password(self, client, monkeypatch):
monkeypatch.setattr(
'app.routes.pdf_tools.validate_actor_file',
lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
)
data = {
'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
'password': '',
}
response = client.post(
'/api/pdf-tools/protect',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 400
assert 'required' in response.get_json()['error'].lower()
def test_protect_short_password(self, client, monkeypatch):
monkeypatch.setattr(
'app.routes.pdf_tools.validate_actor_file',
lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
)
data = {
'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
'password': 'abc',
}
response = client.post(
'/api/pdf-tools/protect',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 400
assert '4 characters' in response.get_json()['error']
def test_protect_success(self, client, monkeypatch):
mock_task, mock_delay = _mock_validate_and_task(
monkeypatch, 'app.routes.pdf_tools', 'protect_pdf_task'
)
data = {
'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
'password': 'secret1234',
}
response = client.post(
'/api/pdf-tools/protect',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 202
# =========================================================================
# 9. Unlock PDF — POST /api/pdf-tools/unlock
# =========================================================================
class TestUnlockPdf:
def test_unlock_no_file(self, client):
response = client.post('/api/pdf-tools/unlock')
assert response.status_code == 400
def test_unlock_no_password(self, client, monkeypatch):
monkeypatch.setattr(
'app.routes.pdf_tools.validate_actor_file',
lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
)
data = {
'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
'password': '',
}
response = client.post(
'/api/pdf-tools/unlock',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 400
def test_unlock_success(self, client, monkeypatch):
mock_task, mock_delay = _mock_validate_and_task(
monkeypatch, 'app.routes.pdf_tools', 'unlock_pdf_task'
)
data = {
'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
'password': 'mypassword',
}
response = client.post(
'/api/pdf-tools/unlock',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 202
# =========================================================================
# 9. Remove Watermark — POST /api/pdf-tools/remove-watermark
# =========================================================================
class TestRemoveWatermark:
def test_no_file(self, client):
"""Should return 400 when no file provided."""
response = client.post('/api/pdf-tools/remove-watermark')
assert response.status_code == 400
def test_success(self, client, monkeypatch):
"""Should return 202 with task_id on valid PDF."""
_mock_validate_and_task(
monkeypatch, 'app.routes.pdf_tools', 'remove_watermark_task'
)
data = {'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf')}
response = client.post(
'/api/pdf-tools/remove-watermark',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 202
# =========================================================================
# 10. Reorder PDF — POST /api/pdf-tools/reorder
# =========================================================================
class TestReorderPdf:
def test_no_file(self, client):
"""Should return 400 when no file provided."""
response = client.post('/api/pdf-tools/reorder')
assert response.status_code == 400
def test_no_page_order(self, client, monkeypatch):
"""Should return 400 when no page_order provided."""
monkeypatch.setattr(
'app.routes.pdf_tools.validate_actor_file',
lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
)
data = {'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf')}
response = client.post(
'/api/pdf-tools/reorder',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 400
def test_success(self, client, monkeypatch):
"""Should return 202 with task_id on valid request."""
_mock_validate_and_task(
monkeypatch, 'app.routes.pdf_tools', 'reorder_pdf_task'
)
data = {
'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
'page_order': '3,1,2',
}
response = client.post(
'/api/pdf-tools/reorder',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 202
# =========================================================================
# 11. Extract Pages — POST /api/pdf-tools/extract-pages
# =========================================================================
class TestExtractPages:
def test_no_file(self, client):
"""Should return 400 when no file provided."""
response = client.post('/api/pdf-tools/extract-pages')
assert response.status_code == 400
def test_no_pages(self, client, monkeypatch):
"""Should return 400 when no pages param provided."""
monkeypatch.setattr(
'app.routes.pdf_tools.validate_actor_file',
lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
)
data = {'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf')}
response = client.post(
'/api/pdf-tools/extract-pages',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 400
def test_success(self, client, monkeypatch):
"""Should return 202 with task_id on valid request."""
_mock_validate_and_task(
monkeypatch, 'app.routes.pdf_tools', 'extract_pages_task'
)
data = {
'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
'pages': '1,3,5-8',
}
response = client.post(
'/api/pdf-tools/extract-pages',
data=data,
content_type='multipart/form-data',
)
assert response.status_code == 202