feat(upload): adaptive 429 backoff, full Gotenberg file types, directory traversal
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
"""Tests for app/utils/allowed_types.py – canonical Gotenberg file-type lists."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.utils.allowed_types import (
|
||||
ALLOWED_EXTENSIONS,
|
||||
ALLOWED_MIME_TYPES,
|
||||
DOCUMENT_MIME_TYPES,
|
||||
IMAGE_MIME_TYPES,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestAllowedTypesStructure:
|
||||
"""Structural sanity checks on the exported sets."""
|
||||
|
||||
def test_allowed_mime_types_is_union(self):
|
||||
"""ALLOWED_MIME_TYPES must equal DOCUMENT_MIME_TYPES ∪ IMAGE_MIME_TYPES."""
|
||||
assert ALLOWED_MIME_TYPES == DOCUMENT_MIME_TYPES | IMAGE_MIME_TYPES
|
||||
|
||||
def test_sets_are_disjoint(self):
|
||||
"""DOCUMENT_MIME_TYPES and IMAGE_MIME_TYPES should not overlap."""
|
||||
assert DOCUMENT_MIME_TYPES.isdisjoint(IMAGE_MIME_TYPES)
|
||||
|
||||
def test_extensions_have_leading_dot(self):
|
||||
"""All entries in ALLOWED_EXTENSIONS must start with '.'."""
|
||||
for ext in ALLOWED_EXTENSIONS:
|
||||
assert ext.startswith("."), f"Extension without leading dot: {ext!r}"
|
||||
|
||||
def test_extensions_are_lowercase(self):
|
||||
"""All entries in ALLOWED_EXTENSIONS must be lower-case."""
|
||||
for ext in ALLOWED_EXTENSIONS:
|
||||
assert ext == ext.lower(), f"Non-lowercase extension: {ext!r}"
|
||||
|
||||
def test_no_empty_entries(self):
|
||||
"""No set should contain empty strings."""
|
||||
for s in (DOCUMENT_MIME_TYPES, IMAGE_MIME_TYPES, ALLOWED_EXTENSIONS):
|
||||
assert "" not in s
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGotenbergCoverageDocuments:
|
||||
"""Verify that every extension Gotenberg handles is present."""
|
||||
|
||||
# These must match OFFICE_EXTENSIONS in app/tasks/convert_to_pdf.py
|
||||
_office_extensions = {
|
||||
".doc",
|
||||
".docx",
|
||||
".docm",
|
||||
".dot",
|
||||
".dotx",
|
||||
".dotm",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".xlsm",
|
||||
".xlsb",
|
||||
".xlt",
|
||||
".xltx",
|
||||
".xlw",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".pptm",
|
||||
".pps",
|
||||
".ppsx",
|
||||
".pot",
|
||||
".potx",
|
||||
".odt",
|
||||
".ods",
|
||||
".odp",
|
||||
".odg",
|
||||
".odf",
|
||||
".rtf",
|
||||
".txt",
|
||||
".csv",
|
||||
}
|
||||
_image_extensions = {
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".gif",
|
||||
".bmp",
|
||||
".tiff",
|
||||
".tif",
|
||||
".webp",
|
||||
".svg",
|
||||
}
|
||||
_html_extensions = {".html", ".htm"}
|
||||
_markdown_extensions = {".md", ".markdown"}
|
||||
|
||||
def test_office_extensions_all_present(self):
|
||||
missing = self._office_extensions - ALLOWED_EXTENSIONS
|
||||
assert not missing, f"Office extensions missing from ALLOWED_EXTENSIONS: {missing}"
|
||||
|
||||
def test_image_extensions_all_present(self):
|
||||
missing = self._image_extensions - ALLOWED_EXTENSIONS
|
||||
assert not missing, f"Image extensions missing from ALLOWED_EXTENSIONS: {missing}"
|
||||
|
||||
def test_html_extensions_present(self):
|
||||
missing = self._html_extensions - ALLOWED_EXTENSIONS
|
||||
assert not missing, f"HTML extensions missing from ALLOWED_EXTENSIONS: {missing}"
|
||||
|
||||
def test_markdown_extensions_present(self):
|
||||
missing = self._markdown_extensions - ALLOWED_EXTENSIONS
|
||||
assert not missing, f"Markdown extensions missing from ALLOWED_EXTENSIONS: {missing}"
|
||||
|
||||
def test_pdf_extension_present(self):
|
||||
assert ".pdf" in ALLOWED_EXTENSIONS
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGotenbergCoverageMimeTypes:
|
||||
"""Verify that key MIME types are present."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mime_type",
|
||||
[
|
||||
"application/pdf",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.ms-powerpoint",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
# OpenDocument
|
||||
"application/vnd.oasis.opendocument.text",
|
||||
"application/vnd.oasis.opendocument.spreadsheet",
|
||||
"application/vnd.oasis.opendocument.presentation",
|
||||
# Images
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/tiff",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
# Text
|
||||
"text/plain",
|
||||
"text/csv",
|
||||
"text/html",
|
||||
"text/markdown",
|
||||
"text/x-markdown",
|
||||
# RTF
|
||||
"application/rtf",
|
||||
"text/rtf",
|
||||
],
|
||||
)
|
||||
def test_mime_type_in_allowed(self, mime_type):
|
||||
assert mime_type in ALLOWED_MIME_TYPES, f"{mime_type} not in ALLOWED_MIME_TYPES"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mime_type",
|
||||
[
|
||||
# New extended Office variants
|
||||
"application/vnd.ms-word.document.macroEnabled.12",
|
||||
"application/vnd.ms-word.template.macroEnabled.12",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.template",
|
||||
"application/vnd.ms-excel.sheet.macroEnabled.12",
|
||||
"application/vnd.ms-excel.sheet.binary.macroEnabled.12",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.template",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
||||
"application/vnd.ms-powerpoint.presentation.macroEnabled.12",
|
||||
"application/vnd.oasis.opendocument.graphics",
|
||||
"application/vnd.oasis.opendocument.formula",
|
||||
],
|
||||
)
|
||||
def test_extended_mime_type_in_allowed(self, mime_type):
|
||||
"""Extended / macro-enabled Office and ODF types must be present."""
|
||||
assert mime_type in ALLOWED_MIME_TYPES, f"{mime_type} not in ALLOWED_MIME_TYPES"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestUploadViewsPassConfig:
|
||||
"""Verify that the upload views inject uploadConfig into their templates."""
|
||||
|
||||
def test_upload_page_contains_upload_config(self, client):
|
||||
"""The /upload page must render window.uploadConfig with numeric values."""
|
||||
response = client.get("/upload")
|
||||
assert response.status_code == 200
|
||||
content = response.text
|
||||
assert "window.uploadConfig" in content
|
||||
# Verify that literal integers are rendered (not the placeholder names)
|
||||
assert "concurrency: 3" in content or "concurrency:" in content
|
||||
assert "queueDelayMs: 500" in content or "queueDelayMs:" in content
|
||||
|
||||
def test_files_page_contains_upload_config(self, client):
|
||||
"""The /files page must render window.uploadConfig with numeric values."""
|
||||
response = client.get("/files")
|
||||
assert response.status_code == 200
|
||||
content = response.text
|
||||
assert "window.uploadConfig" in content
|
||||
assert "concurrency: 3" in content or "concurrency:" in content
|
||||
assert "queueDelayMs: 500" in content or "queueDelayMs:" in content
|
||||
|
||||
def test_upload_config_has_numeric_concurrency(self, client):
|
||||
"""Concurrency in uploadConfig must be a positive integer."""
|
||||
import re
|
||||
|
||||
response = client.get("/upload")
|
||||
assert response.status_code == 200
|
||||
match = re.search(r"concurrency:\s*(\d+)", response.text)
|
||||
assert match is not None, "concurrency integer not found in window.uploadConfig"
|
||||
assert int(match.group(1)) > 0
|
||||
|
||||
def test_upload_config_has_numeric_delay(self, client):
|
||||
"""queueDelayMs in uploadConfig must be a non-negative integer."""
|
||||
import re
|
||||
|
||||
response = client.get("/upload")
|
||||
assert response.status_code == 200
|
||||
match = re.search(r"queueDelayMs:\s*(\d+)", response.text)
|
||||
assert match is not None, "queueDelayMs integer not found in window.uploadConfig"
|
||||
assert int(match.group(1)) >= 0
|
||||
Reference in New Issue
Block a user