Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions api/tests/unit_tests/controllers/common/test_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from controllers.common.errors import (
BlockedFileExtensionError,
FilenameNotExistsError,
FileTooLargeError,
NoFileUploadedError,
RemoteFileUploadError,
TooManyFilesError,
UnsupportedFileTypeError,
)


class TestFilenameNotExistsError:
def test_defaults(self):
error = FilenameNotExistsError()

assert error.code == 400
assert error.description == "The specified filename does not exist."


class TestRemoteFileUploadError:
def test_defaults(self):
error = RemoteFileUploadError()

assert error.code == 400
assert error.description == "Error uploading remote file."


class TestFileTooLargeError:
def test_defaults(self):
error = FileTooLargeError()

assert error.code == 413
assert error.error_code == "file_too_large"
assert error.description == "File size exceeded. {message}"


class TestUnsupportedFileTypeError:
def test_defaults(self):
error = UnsupportedFileTypeError()

assert error.code == 415
assert error.error_code == "unsupported_file_type"
assert error.description == "File type not allowed."


class TestBlockedFileExtensionError:
def test_defaults(self):
error = BlockedFileExtensionError()

assert error.code == 400
assert error.error_code == "file_extension_blocked"
assert error.description == "The file extension is blocked for security reasons."


class TestTooManyFilesError:
def test_defaults(self):
error = TooManyFilesError()

assert error.code == 400
assert error.error_code == "too_many_files"
assert error.description == "Only one file is allowed."


class TestNoFileUploadedError:
def test_defaults(self):
error = NoFileUploadedError()

assert error.code == 400
assert error.error_code == "no_file_uploaded"
assert error.description == "Please upload your file."
93 changes: 84 additions & 9 deletions api/tests/unit_tests/controllers/common/test_file_response.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,96 @@
from flask import Response
import pytest

from controllers.common.file_response import enforce_download_for_html, is_html_content
from controllers.common.file_response import (
enforce_download_for_html,
is_html_content,
_normalize_mime_type,
)


class TestFileResponseHelpers:
def test_is_html_content_detects_mime_type(self):
class TestNormalizeMimeType:
def test_returns_empty_string_for_none(self):
assert _normalize_mime_type(None) == ""

def test_returns_empty_string_for_empty_string(self):
assert _normalize_mime_type("") == ""

def test_normalizes_mime_type(self):
assert _normalize_mime_type("Text/HTML; Charset=UTF-8") == "text/html"


class TestIsHtmlContent:
def test_detects_html_via_mime_type(self):
mime_type = "text/html; charset=UTF-8"

result = is_html_content(mime_type, filename="file.txt", extension="txt")
result = is_html_content(
mime_type=mime_type,
filename="file.txt",
extension="txt",
)

assert result is True

def test_is_html_content_detects_extension(self):
result = is_html_content("text/plain", filename="report.html", extension=None)
def test_detects_html_via_extension_argument(self):
result = is_html_content(
mime_type="text/plain",
filename=None,
extension="html",
)

assert result is True

def test_enforce_download_for_html_sets_headers(self):
def test_detects_html_via_filename_extension(self):
result = is_html_content(
mime_type="text/plain",
filename="report.html",
extension=None,
)

assert result is True

def test_returns_false_when_no_html_detected_anywhere(self):
"""
Missing negative test:
- MIME type is not HTML
- filename has no HTML extension
- extension argument is not HTML
"""
result = is_html_content(
mime_type="application/json",
filename="data.json",
extension="json",
)

assert result is False

def test_returns_false_when_all_inputs_are_none(self):
result = is_html_content(
mime_type=None,
filename=None,
extension=None,
)

assert result is False


class TestEnforceDownloadForHtml:
def test_sets_attachment_when_filename_missing(self):
response = Response("payload", mimetype="text/html")

updated = enforce_download_for_html(
response,
mime_type="text/html",
filename=None,
extension="html",
)

assert updated is True
assert response.headers["Content-Disposition"] == "attachment"
assert response.headers["Content-Type"] == "application/octet-stream"
assert response.headers["X-Content-Type-Options"] == "nosniff"

def test_sets_headers_when_filename_present(self):
response = Response("payload", mimetype="text/html")

updated = enforce_download_for_html(
Expand All @@ -27,11 +101,12 @@ def test_enforce_download_for_html_sets_headers(self):
)

assert updated is True
assert "attachment" in response.headers["Content-Disposition"]
assert response.headers["Content-Disposition"].startswith("attachment")
assert "unsafe.html" in response.headers["Content-Disposition"]
assert response.headers["Content-Type"] == "application/octet-stream"
assert response.headers["X-Content-Type-Options"] == "nosniff"

def test_enforce_download_for_html_no_change_for_non_html(self):
def test_does_not_modify_response_for_non_html_content(self):
response = Response("payload", mimetype="text/plain")

updated = enforce_download_for_html(
Expand Down
174 changes: 174 additions & 0 deletions api/tests/unit_tests/controllers/common/test_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import importlib
import sys
from uuid import UUID

import httpx
import pytest

from controllers.common import helpers
from controllers.common.helpers import FileInfo, guess_file_info_from_response


def make_response(
url="https://example.com/file.txt",
headers=None,
content=None,
):
return httpx.Response(
200,
request=httpx.Request("GET", url),
headers=headers or {},
content=content or b"",
)


class TestGuessFileInfoFromResponse:
def test_filename_from_url(self):
response = make_response(
url="https://example.com/test.pdf",
content=b"Hello World",
)

info = guess_file_info_from_response(response)

assert info.filename == "test.pdf"
assert info.extension == ".pdf"
assert info.mimetype == "application/pdf"

def test_filename_from_content_disposition(self):
headers = {
"Content-Disposition": 'attachment; filename=myfile.csv',
"Content-Type": "text/csv",
}
response = make_response(
url="https://example.com/",
headers=headers,
content=b"Hello World",
)

info = guess_file_info_from_response(response)

assert info.filename == "myfile.csv"
assert info.extension == ".csv"
assert info.mimetype == "text/csv"

@pytest.mark.parametrize(
("magic_available", "expected_ext"),
[
(True, "txt"),
(False, "bin"),
],
)
def test_generated_filename_when_missing(self, monkeypatch, magic_available, expected_ext):
if magic_available:
if helpers.magic is None:
pytest.skip("python-magic is not installed, cannot run 'magic_available=True' test variant")
else:
monkeypatch.setattr(helpers, "magic", None)

response = make_response(
url="https://example.com/",
content=b"Hello World",
)

info = guess_file_info_from_response(response)

name, ext = info.filename.split(".")
UUID(name)
assert ext == expected_ext

def test_mimetype_from_header_when_unknown(self):
headers = {"Content-Type": "application/json"}
response = make_response(
url="https://example.com/file.unknown",
headers=headers,
content=b'{"a": 1}',
)

info = guess_file_info_from_response(response)

assert info.mimetype == "application/json"

def test_extension_added_when_missing(self):
headers = {"Content-Type": "image/png"}
response = make_response(
url="https://example.com/image",
headers=headers,
content=b"fakepngdata",
)

info = guess_file_info_from_response(response)

assert info.extension == ".png"
assert info.filename.endswith(".png")

def test_content_length_used_as_size(self):
headers = {
"Content-Length": "1234",
"Content-Type": "text/plain",
}
response = make_response(
url="https://example.com/a.txt",
headers=headers,
content=b"a" * 1234,
)

info = guess_file_info_from_response(response)

assert info.size == 1234

def test_size_minus_one_when_header_missing(self):
response = make_response(url="https://example.com/a.txt")

info = guess_file_info_from_response(response)

assert info.size == -1

def test_fallback_to_bin_extension(self):
headers = {"Content-Type": "application/octet-stream"}
response = make_response(
url="https://example.com/download",
headers=headers,
content=b"\x00\x01\x02\x03",
)

info = guess_file_info_from_response(response)

assert info.extension == ".bin"
assert info.filename.endswith(".bin")

def test_return_type(self):
response = make_response()

info = guess_file_info_from_response(response)

assert isinstance(info, FileInfo)


class TestMagicImportWarnings:
@pytest.mark.parametrize(
("platform_name", "expected_message"),
[
("Windows", "pip install python-magic-bin"),
("Darwin", "brew install libmagic"),
("Linux", "sudo apt-get install libmagic1"),
("Other", "install `libmagic`"),
],
)
def test_magic_import_warning_per_platform(
self,
monkeypatch,
platform_name,
expected_message,
):
sys.modules.pop("magic", None)

monkeypatch.setitem(sys.modules, "magic", None)

monkeypatch.setattr(helpers.platform, "system", lambda: platform_name)

with pytest.warns(UserWarning, match="To use python-magic") as warning:
importlib.reload(helpers)

assert expected_message in str(warning[0].message)
assert helpers.magic is None
Loading