تم الانتهاء من آخر دفعة تحسينات على المشروع، وتشمل:
تحويل لوحة الإدارة الداخلية من secret header إلى session auth حقيقي مع صلاحيات admin. إضافة دعم إدارة الأدوار من داخل لوحة الإدارة نفسها، مع حماية الحسابات المعتمدة عبر INTERNAL_ADMIN_EMAILS. تحسين بيانات المستخدم في الواجهة والباكند لتشمل role وis_allowlisted_admin. إضافة اختبار frontend مخصص لصفحة /internal/admin بدل الاعتماد فقط على build واختبار routes. تحسين إضافي في الأداء عبر إزالة الاعتماد على pdfjs-dist/pdf.worker في عدّ صفحات PDF واستبداله بمسار أخف باستخدام pdf-lib. تحسين تقسيم الـ chunks في build لتقليل أثر الحزم الكبيرة وفصل أجزاء مثل network, icons, pdf-core, وeditor. التحقق الذي تم: نجاح build للواجهة. نجاح اختبار صفحة الإدارة الداخلية في frontend. نجاح اختبارات auth/admin في backend. نجاح full backend suite مسبقًا مع EXIT:0. ولو تريد نسخة أقصر جدًا، استخدم هذه: آخر التحديثات: تم تحسين نظام الإدارة الداخلية ليعتمد على صلاحيات وجلسات حقيقية بدل secret header، مع إضافة إدارة أدوار من لوحة admin نفسها، وإضافة اختبارات frontend مخصصة للوحة، وتحسين أداء الواجهة عبر إزالة pdf.worker وتحسين تقسيم الـ chunks في build. جميع الاختبارات والتحققات الأساسية المطلوبة نجح
This commit is contained in:
460
backend/tests/test_phase2_tools.py
Normal file
460
backend/tests/test_phase2_tools.py
Normal file
@@ -0,0 +1,460 @@
|
||||
"""Tests for Phase 2 routes — PDF Conversion, PDF Extra, Image Extra, Barcode."""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _barcode_available():
|
||||
"""Check if python-barcode is installed."""
|
||||
try:
|
||||
import barcode # noqa: F401
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Helpers
|
||||
# =========================================================================
|
||||
|
||||
def _make_pdf():
|
||||
"""Minimal valid PDF bytes."""
|
||||
return (
|
||||
b"%PDF-1.4\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n"
|
||||
b"2 0 obj<</Type/Pages/Count 1/Kids[3 0 R]>>endobj\n"
|
||||
b"3 0 obj<</Type/Page/MediaBox[0 0 612 792]/Parent 2 0 R>>endobj\n"
|
||||
b"xref\n0 4\n0000000000 65535 f \n0000000009 00000 n \n"
|
||||
b"0000000058 00000 n \n0000000115 00000 n \n"
|
||||
b"trailer<</Root 1 0 R/Size 4>>\nstartxref\n190\n%%EOF"
|
||||
)
|
||||
|
||||
|
||||
def _make_png():
|
||||
"""Minimal valid PNG bytes."""
|
||||
return (
|
||||
b"\x89PNG\r\n\x1a\n"
|
||||
b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
|
||||
b"\x08\x02\x00\x00\x00\x90wS\xde"
|
||||
b"\x00\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01\x00\x05"
|
||||
b"\x18\xd8N\x00\x00\x00\x00IEND\xaeB`\x82"
|
||||
)
|
||||
|
||||
|
||||
def _mock_route(monkeypatch, route_module, task_name, validator_name='validate_actor_file'):
|
||||
"""Mock validate + generate_safe_path + celery task for a route module."""
|
||||
mock_task = MagicMock()
|
||||
mock_task.id = 'mock-task-id'
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
save_path = os.path.join(tmp_dir, 'mock_file')
|
||||
|
||||
monkeypatch.setattr(
|
||||
f'app.routes.{route_module}.validate_actor_file',
|
||||
lambda f, allowed_types, actor: ('test_file', 'pdf'),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
f'app.routes.{route_module}.generate_safe_path',
|
||||
lambda ext, folder_type: ('mock-task-id', save_path),
|
||||
)
|
||||
mock_delay = MagicMock(return_value=mock_task)
|
||||
monkeypatch.setattr(f'app.routes.{route_module}.{task_name}.delay', mock_delay)
|
||||
return mock_task, mock_delay
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# PDF Convert Routes — /api/convert
|
||||
# =========================================================================
|
||||
|
||||
class TestPdfToPptx:
|
||||
def test_no_file(self, client):
|
||||
resp = client.post('/api/convert/pdf-to-pptx')
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_success(self, client, monkeypatch):
|
||||
_, mock_delay = _mock_route(monkeypatch, 'pdf_convert', 'pdf_to_pptx_task')
|
||||
resp = client.post('/api/convert/pdf-to-pptx', data={
|
||||
'file': (io.BytesIO(_make_pdf()), 'test.pdf'),
|
||||
}, content_type='multipart/form-data')
|
||||
assert resp.status_code == 202
|
||||
data = resp.get_json()
|
||||
assert data['task_id'] == 'mock-task-id'
|
||||
mock_delay.assert_called_once()
|
||||
|
||||
|
||||
class TestExcelToPdf:
|
||||
def test_no_file(self, client):
|
||||
resp = client.post('/api/convert/excel-to-pdf')
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_success(self, client, monkeypatch):
|
||||
mock_task = MagicMock()
|
||||
mock_task.id = 'mock-task-id'
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
save_path = os.path.join(tmp_dir, 'mock.xlsx')
|
||||
|
||||
monkeypatch.setattr(
|
||||
'app.routes.pdf_convert.validate_actor_file',
|
||||
lambda f, allowed_types, actor: ('test.xlsx', 'xlsx'),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
'app.routes.pdf_convert.generate_safe_path',
|
||||
lambda ext, folder_type: ('mock-task-id', save_path),
|
||||
)
|
||||
mock_delay = MagicMock(return_value=mock_task)
|
||||
monkeypatch.setattr('app.routes.pdf_convert.excel_to_pdf_task.delay', mock_delay)
|
||||
|
||||
# Create a file with xlsx content type
|
||||
resp = client.post('/api/convert/excel-to-pdf', data={
|
||||
'file': (io.BytesIO(b'PK\x03\x04' + b'\x00' * 100), 'test.xlsx'),
|
||||
}, content_type='multipart/form-data')
|
||||
assert resp.status_code == 202
|
||||
assert resp.get_json()['task_id'] == 'mock-task-id'
|
||||
|
||||
|
||||
class TestPptxToPdf:
|
||||
def test_no_file(self, client):
|
||||
resp = client.post('/api/convert/pptx-to-pdf')
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_success(self, client, monkeypatch):
|
||||
mock_task = MagicMock()
|
||||
mock_task.id = 'mock-task-id'
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
save_path = os.path.join(tmp_dir, 'mock.pptx')
|
||||
|
||||
monkeypatch.setattr(
|
||||
'app.routes.pdf_convert.validate_actor_file',
|
||||
lambda f, allowed_types, actor: ('test.pptx', 'pptx'),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
'app.routes.pdf_convert.generate_safe_path',
|
||||
lambda ext, folder_type: ('mock-task-id', save_path),
|
||||
)
|
||||
mock_delay = MagicMock(return_value=mock_task)
|
||||
monkeypatch.setattr('app.routes.pdf_convert.pptx_to_pdf_task.delay', mock_delay)
|
||||
|
||||
resp = client.post('/api/convert/pptx-to-pdf', data={
|
||||
'file': (io.BytesIO(b'PK\x03\x04' + b'\x00' * 100), 'test.pptx'),
|
||||
}, content_type='multipart/form-data')
|
||||
assert resp.status_code == 202
|
||||
assert resp.get_json()['task_id'] == 'mock-task-id'
|
||||
|
||||
|
||||
class TestSignPdf:
|
||||
def test_no_files(self, client):
|
||||
resp = client.post('/api/convert/sign')
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_missing_signature(self, client):
|
||||
resp = client.post('/api/convert/sign', data={
|
||||
'file': (io.BytesIO(_make_pdf()), 'test.pdf'),
|
||||
}, content_type='multipart/form-data')
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_success(self, client, monkeypatch):
|
||||
mock_task = MagicMock()
|
||||
mock_task.id = 'mock-task-id'
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
|
||||
monkeypatch.setattr(
|
||||
'app.routes.pdf_convert.validate_actor_file',
|
||||
lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
'app.routes.pdf_convert.generate_safe_path',
|
||||
lambda ext, folder_type: ('mock-task-id', os.path.join(tmp_dir, f'mock.{ext}')),
|
||||
)
|
||||
mock_delay = MagicMock(return_value=mock_task)
|
||||
monkeypatch.setattr('app.routes.pdf_convert.sign_pdf_task.delay', mock_delay)
|
||||
|
||||
resp = client.post('/api/convert/sign', data={
|
||||
'file': (io.BytesIO(_make_pdf()), 'test.pdf'),
|
||||
'signature': (io.BytesIO(_make_png()), 'sig.png'),
|
||||
'page': '1',
|
||||
}, content_type='multipart/form-data')
|
||||
assert resp.status_code == 202
|
||||
assert resp.get_json()['task_id'] == 'mock-task-id'
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# PDF Extra Routes — /api/pdf-tools
|
||||
# =========================================================================
|
||||
|
||||
class TestCropPdf:
|
||||
def test_no_file(self, client):
|
||||
resp = client.post('/api/pdf-tools/crop')
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_success(self, client, monkeypatch):
|
||||
_, mock_delay = _mock_route(monkeypatch, 'pdf_extra', 'crop_pdf_task')
|
||||
resp = client.post('/api/pdf-tools/crop', data={
|
||||
'file': (io.BytesIO(_make_pdf()), 'test.pdf'),
|
||||
'left': '10', 'right': '10', 'top': '20', 'bottom': '20',
|
||||
}, content_type='multipart/form-data')
|
||||
assert resp.status_code == 202
|
||||
assert resp.get_json()['task_id'] == 'mock-task-id'
|
||||
mock_delay.assert_called_once()
|
||||
|
||||
|
||||
class TestFlattenPdf:
|
||||
def test_no_file(self, client):
|
||||
resp = client.post('/api/pdf-tools/flatten')
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_success(self, client, monkeypatch):
|
||||
_, mock_delay = _mock_route(monkeypatch, 'pdf_extra', 'flatten_pdf_task')
|
||||
resp = client.post('/api/pdf-tools/flatten', data={
|
||||
'file': (io.BytesIO(_make_pdf()), 'test.pdf'),
|
||||
}, content_type='multipart/form-data')
|
||||
assert resp.status_code == 202
|
||||
mock_delay.assert_called_once()
|
||||
|
||||
|
||||
class TestRepairPdf:
|
||||
def test_no_file(self, client):
|
||||
resp = client.post('/api/pdf-tools/repair')
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_success(self, client, monkeypatch):
|
||||
_, mock_delay = _mock_route(monkeypatch, 'pdf_extra', 'repair_pdf_task')
|
||||
resp = client.post('/api/pdf-tools/repair', data={
|
||||
'file': (io.BytesIO(_make_pdf()), 'test.pdf'),
|
||||
}, content_type='multipart/form-data')
|
||||
assert resp.status_code == 202
|
||||
mock_delay.assert_called_once()
|
||||
|
||||
|
||||
class TestEditMetadata:
|
||||
def test_no_file(self, client):
|
||||
resp = client.post('/api/pdf-tools/metadata')
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_success(self, client, monkeypatch):
|
||||
_, mock_delay = _mock_route(monkeypatch, 'pdf_extra', 'edit_metadata_task')
|
||||
resp = client.post('/api/pdf-tools/metadata', data={
|
||||
'file': (io.BytesIO(_make_pdf()), 'test.pdf'),
|
||||
'title': 'Test Title',
|
||||
'author': 'Test Author',
|
||||
}, content_type='multipart/form-data')
|
||||
assert resp.status_code == 202
|
||||
mock_delay.assert_called_once()
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Image Extra Routes — /api/image
|
||||
# =========================================================================
|
||||
|
||||
class TestImageCrop:
|
||||
def test_no_file(self, client):
|
||||
resp = client.post('/api/image/crop')
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_success(self, client, monkeypatch):
|
||||
mock_task = MagicMock()
|
||||
mock_task.id = 'mock-task-id'
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
save_path = os.path.join(tmp_dir, 'mock.png')
|
||||
|
||||
monkeypatch.setattr(
|
||||
'app.routes.image_extra.validate_actor_file',
|
||||
lambda f, allowed_types, actor: ('test.png', 'png'),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
'app.routes.image_extra.generate_safe_path',
|
||||
lambda ext, folder_type: ('mock-task-id', save_path),
|
||||
)
|
||||
mock_delay = MagicMock(return_value=mock_task)
|
||||
monkeypatch.setattr('app.routes.image_extra.crop_image_task.delay', mock_delay)
|
||||
|
||||
resp = client.post('/api/image/crop', data={
|
||||
'file': (io.BytesIO(_make_png()), 'test.png'),
|
||||
'left': '0', 'top': '0', 'right': '100', 'bottom': '100',
|
||||
}, content_type='multipart/form-data')
|
||||
assert resp.status_code == 202
|
||||
assert resp.get_json()['task_id'] == 'mock-task-id'
|
||||
|
||||
|
||||
class TestImageRotateFlip:
|
||||
def test_no_file(self, client):
|
||||
resp = client.post('/api/image/rotate-flip')
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_success(self, client, monkeypatch):
|
||||
mock_task = MagicMock()
|
||||
mock_task.id = 'mock-task-id'
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
save_path = os.path.join(tmp_dir, 'mock.png')
|
||||
|
||||
monkeypatch.setattr(
|
||||
'app.routes.image_extra.validate_actor_file',
|
||||
lambda f, allowed_types, actor: ('test.png', 'png'),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
'app.routes.image_extra.generate_safe_path',
|
||||
lambda ext, folder_type: ('mock-task-id', save_path),
|
||||
)
|
||||
mock_delay = MagicMock(return_value=mock_task)
|
||||
monkeypatch.setattr('app.routes.image_extra.rotate_flip_image_task.delay', mock_delay)
|
||||
|
||||
resp = client.post('/api/image/rotate-flip', data={
|
||||
'file': (io.BytesIO(_make_png()), 'test.png'),
|
||||
'rotation': '90',
|
||||
'flip_horizontal': 'true',
|
||||
}, content_type='multipart/form-data')
|
||||
assert resp.status_code == 202
|
||||
assert resp.get_json()['task_id'] == 'mock-task-id'
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Barcode Routes — /api/barcode
|
||||
# =========================================================================
|
||||
|
||||
class TestBarcodeGenerate:
|
||||
def test_no_data(self, client):
|
||||
resp = client.post('/api/barcode/generate',
|
||||
data=json.dumps({}),
|
||||
content_type='application/json')
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_success_json(self, client, monkeypatch):
|
||||
mock_task = MagicMock()
|
||||
mock_task.id = 'mock-task-id'
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
|
||||
monkeypatch.setattr(
|
||||
'app.routes.barcode.generate_safe_path',
|
||||
lambda ext, folder_type: ('mock-task-id', os.path.join(tmp_dir, 'mock.png')),
|
||||
)
|
||||
mock_delay = MagicMock(return_value=mock_task)
|
||||
monkeypatch.setattr('app.routes.barcode.generate_barcode_task.delay', mock_delay)
|
||||
|
||||
resp = client.post('/api/barcode/generate',
|
||||
data=json.dumps({'data': '12345', 'barcode_type': 'code128'}),
|
||||
content_type='application/json')
|
||||
assert resp.status_code == 202
|
||||
assert resp.get_json()['task_id'] == 'mock-task-id'
|
||||
|
||||
def test_invalid_barcode_type(self, client):
|
||||
resp = client.post('/api/barcode/generate',
|
||||
data=json.dumps({'data': '12345', 'type': 'invalid_type'}),
|
||||
content_type='application/json')
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Service unit tests
|
||||
# =========================================================================
|
||||
|
||||
class TestBarcodeService:
|
||||
@pytest.mark.skipif(
|
||||
not _barcode_available(),
|
||||
reason='python-barcode not installed'
|
||||
)
|
||||
def test_generate_barcode(self, app):
|
||||
from app.services.barcode_service import generate_barcode
|
||||
with app.app_context():
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
output_path = os.path.join(tmp_dir, 'test_barcode')
|
||||
result = generate_barcode('12345678', 'code128', output_path, 'png')
|
||||
assert 'output_path' in result
|
||||
assert os.path.exists(result['output_path'])
|
||||
|
||||
def test_invalid_barcode_type(self, app):
|
||||
from app.services.barcode_service import generate_barcode, BarcodeGenerationError
|
||||
with app.app_context():
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
output_path = os.path.join(tmp_dir, 'test_barcode')
|
||||
with pytest.raises(BarcodeGenerationError):
|
||||
generate_barcode('12345', 'nonexistent_type', output_path, 'png')
|
||||
|
||||
|
||||
class TestPdfExtraService:
|
||||
def test_edit_metadata(self, app):
|
||||
from app.services.pdf_extra_service import edit_pdf_metadata
|
||||
with app.app_context():
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
input_path = os.path.join(tmp_dir, 'input.pdf')
|
||||
output_path = os.path.join(tmp_dir, 'output.pdf')
|
||||
with open(input_path, 'wb') as f:
|
||||
f.write(_make_pdf())
|
||||
edit_pdf_metadata(input_path, output_path, title='Test Title', author='Test Author')
|
||||
assert os.path.exists(output_path)
|
||||
assert os.path.getsize(output_path) > 0
|
||||
|
||||
def test_flatten_pdf(self, app):
|
||||
from app.services.pdf_extra_service import flatten_pdf
|
||||
with app.app_context():
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
input_path = os.path.join(tmp_dir, 'input.pdf')
|
||||
output_path = os.path.join(tmp_dir, 'output.pdf')
|
||||
with open(input_path, 'wb') as f:
|
||||
f.write(_make_pdf())
|
||||
flatten_pdf(input_path, output_path)
|
||||
assert os.path.exists(output_path)
|
||||
|
||||
def test_repair_pdf(self, app):
|
||||
from app.services.pdf_extra_service import repair_pdf
|
||||
with app.app_context():
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
input_path = os.path.join(tmp_dir, 'input.pdf')
|
||||
output_path = os.path.join(tmp_dir, 'output.pdf')
|
||||
with open(input_path, 'wb') as f:
|
||||
f.write(_make_pdf())
|
||||
repair_pdf(input_path, output_path)
|
||||
assert os.path.exists(output_path)
|
||||
|
||||
def test_crop_pdf(self, app):
|
||||
from app.services.pdf_extra_service import crop_pdf
|
||||
with app.app_context():
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
input_path = os.path.join(tmp_dir, 'input.pdf')
|
||||
output_path = os.path.join(tmp_dir, 'output.pdf')
|
||||
with open(input_path, 'wb') as f:
|
||||
f.write(_make_pdf())
|
||||
crop_pdf(input_path, output_path, margin_left=10, margin_right=10, margin_top=10, margin_bottom=10)
|
||||
assert os.path.exists(output_path)
|
||||
|
||||
|
||||
class TestImageExtraService:
|
||||
def test_rotate_flip(self, app):
|
||||
from app.services.image_extra_service import rotate_flip_image
|
||||
from PIL import Image
|
||||
with app.app_context():
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
input_path = os.path.join(tmp_dir, 'input.png')
|
||||
output_path = os.path.join(tmp_dir, 'output.png')
|
||||
img = Image.new('RGB', (100, 100), color='red')
|
||||
img.save(input_path)
|
||||
rotate_flip_image(input_path, output_path, rotation=90)
|
||||
assert os.path.exists(output_path)
|
||||
result = Image.open(output_path)
|
||||
assert result.size == (100, 100)
|
||||
|
||||
def test_crop_image(self, app):
|
||||
from app.services.image_extra_service import crop_image
|
||||
from PIL import Image
|
||||
with app.app_context():
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
input_path = os.path.join(tmp_dir, 'input.png')
|
||||
output_path = os.path.join(tmp_dir, 'output.png')
|
||||
img = Image.new('RGB', (200, 200), color='blue')
|
||||
img.save(input_path)
|
||||
crop_image(input_path, output_path, left=10, top=10, right=100, bottom=100)
|
||||
assert os.path.exists(output_path)
|
||||
result = Image.open(output_path)
|
||||
assert result.size == (90, 90)
|
||||
|
||||
def test_crop_invalid_coords(self, app):
|
||||
from app.services.image_extra_service import crop_image, ImageExtraError
|
||||
from PIL import Image
|
||||
with app.app_context():
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
input_path = os.path.join(tmp_dir, 'input.png')
|
||||
output_path = os.path.join(tmp_dir, 'output.png')
|
||||
img = Image.new('RGB', (100, 100), color='blue')
|
||||
img.save(input_path)
|
||||
with __import__('pytest').raises(ImageExtraError):
|
||||
crop_image(input_path, output_path, left=100, top=0, right=50, bottom=100)
|
||||
Reference in New Issue
Block a user