From 32fb361ff8e5acec67e6352a8ef7465ed08c652e Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Thu, 13 Nov 2025 15:30:15 +0300 Subject: [PATCH 1/6] migrate to python-magic --- .dockerignore | 15 +++++++++ .github/workflows/ci.yml | 54 +++++++++++++++++++++++++++++++ .github/workflows/publish.yml | 17 ++++++++++ .github/workflows/workflow.yml | 20 ------------ Dockerfile | 27 ++++++++++++++++ Justfile | 15 +++++++-- README.md | 2 +- docker-compose.yml | 11 +++++++ pyproject.toml | 17 ++++++++-- safe_s3_storage/file_validator.py | 13 +++----- tests/conftest.py | 2 +- tests/test_file_validator.py | 16 ++++----- tests/test_s3_service.py | 8 ++--- 13 files changed, 169 insertions(+), 48 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish.yml delete mode 100644 .github/workflows/workflow.yml create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4caaa77 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +.env +.coverage +.gitignore +.idea +.mypy_cache +.ruff_cache +.vscode +.git +.pytest_cache +.DS_Store +*.yml +Dockerfile +**/__pycache__ +.hypothesis +.venv diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d4f6d68 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: main + +on: + push: + branches: + - main + pull_request: {} + +concurrency: + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: extractions/setup-just@v2 + - uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: "**/pyproject.toml" + - run: uv python install 3.10 + - run: just install lint-ci + + pytest: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + steps: + - name: install libmagic + run: sudo apt install -y libmagic-dev + - uses: actions/checkout@v4 + - uses: extractions/setup-just@v2 + - uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: "**/pyproject.toml" + - run: uv python install ${{ matrix.python-version }} + - run: just install + - run: just test . --cov=. --cov-report xml + - uses: codecov/codecov-action@v4.0.1 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + files: ./coverage.xml + flags: unittests + name: codecov-${{ matrix.python-version }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..b637272 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,17 @@ +name: Publish Package + +on: + release: + types: + - published + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: extractions/setup-just@v2 + - uses: astral-sh/setup-uv@v3 + - run: just publish + env: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml deleted file mode 100644 index 894d465..0000000 --- a/.github/workflows/workflow.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: CI Pipeline - -on: - push: - branches: - - main - pull_request: - branches: - - main - release: - types: - - published - -jobs: - ci: - uses: community-of-python/community-workflow/.github/workflows/preset.yml@main - with: - python-version: '["3.10","3.11","3.12","3.13"]' - os: '["ubuntu-latest"]' - secrets: inherit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0040201 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.13-slim + +# required for python-magic +RUN apt update \ + && apt install -y --no-install-recommends \ + libmagic-dev \ + && apt clean \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv +RUN useradd --no-create-home --gid root runner + +ENV UV_PYTHON_PREFERENCE=only-system +ENV UV_NO_CACHE=true + +WORKDIR /code + +COPY pyproject.toml . +COPY uv.lock . + +RUN uv sync --all-extras --frozen --no-install-project + +COPY . . + +RUN chown -R runner:root /code && chmod -R g=u /code + +USER runner diff --git a/Justfile b/Justfile index 51148c6..97655ec 100644 --- a/Justfile +++ b/Justfile @@ -1,5 +1,17 @@ default: install lint test +down: + docker compose down --remove-orphans + +sh: + docker compose run --service-ports application bash + +test *args: down && down + docker compose run application uv run --no-sync pytest {{ args }} + +build: + docker compose build application + install: uv lock --upgrade uv sync --frozen --all-groups @@ -16,9 +28,6 @@ lint-ci: uv run --group lint ruff check --no-fix uv run --group lint mypy . -test *args: - uv run pytest {{ args }} - publish: rm -rf dist uv version $GITHUB_REF_NAME diff --git a/README.md b/README.md index f306662..373d899 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # safe-s3-storage -S3 tools for uploading files to S3 safely (antivirus check, etc) as well as downloading and deleting files. +S3 tools for uploading files to S3 safely (antivirus check, etc.) as well as downloading and deleting files. ## How To Use diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1dfe0d8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +services: + application: + build: + context: . + dockerfile: ./Dockerfile + restart: always + volumes: + - .:/code + - /code/.venv + stdin_open: true + tty: true diff --git a/pyproject.toml b/pyproject.toml index 23a6c61..0efa1be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ keywords = ["s3", "kaspersky", "antivirus", "upload"] classifiers = [ "Natural Language :: English", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries", @@ -22,12 +23,22 @@ dependencies = [ "pydantic", "pyvips", "pyvips-binary", - "magika", + "python-magic", ] [dependency-groups] -dev = ["anyio", "faker", "pytest", "pytest-cov"] -lint = [{ include-group = "dev" }, "auto-typing-final", "mypy", "ruff"] +dev = [ + "anyio", + "faker", + "pytest", + "pytest-cov", +] +lint = [ + { include-group = "dev" }, + "auto-typing-final", + "mypy", + "ruff", +] [build-system] requires = ["uv_build"] diff --git a/safe_s3_storage/file_validator.py b/safe_s3_storage/file_validator.py index 3a6af6c..297193c 100644 --- a/safe_s3_storage/file_validator.py +++ b/safe_s3_storage/file_validator.py @@ -1,9 +1,10 @@ import dataclasses import enum +import mimetypes import typing +import magic import pyvips # type: ignore[import-untyped] -from magika import Magika from safe_s3_storage import exceptions from safe_s3_storage.kaspersky_scan_engine import KasperskyScanEngineClient @@ -49,13 +50,9 @@ class FileValidator: excluded_conversion_formats: list[str] | None = None def _validate_mime_type(self, *, file_name: str, file_content: bytes) -> str: - mime_type_prediction: typing.Final = Magika().identify_bytes(file_content) - if mime_type_prediction.output.is_text and file_name.endswith(".txt"): - mime_type = "text/plain" - elif mime_type_prediction.dl.extensions: - mime_type = mime_type_prediction.dl.mime_type - else: - mime_type = mime_type_prediction.output.mime_type + mime_type = magic.from_buffer(file_content, mime=True) + if mime_type == "application/octet-stream" and (mime_type_by_name := mimetypes.guess_type(file_name)[0]): + mime_type = mime_type_by_name if self.allowed_mime_types is None or mime_type in self.allowed_mime_types: return mime_type diff --git a/tests/conftest.py b/tests/conftest.py index 2205b29..55d7905 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,7 @@ def anyio_backend() -> str: return "asyncio" -MIME_OCTET_STREAM: typing.Final = "application/octet-stream" +JS_MIME_TYPE: typing.Final = "text/javascript" def generate_binary_content(faker: faker.Faker) -> bytes: diff --git a/tests/test_file_validator.py b/tests/test_file_validator.py index 804e51a..3092d54 100644 --- a/tests/test_file_validator.py +++ b/tests/test_file_validator.py @@ -18,7 +18,7 @@ KasperskyScanEngineResponse, KasperskyScanEngineScanResult, ) -from tests.conftest import MIME_OCTET_STREAM, generate_binary_content +from tests.conftest import JS_MIME_TYPE, generate_binary_content @pytest.fixture @@ -86,7 +86,7 @@ async def test_fails_to_validate_mime_type(self, faker: faker.Faker) -> None: async def test_fails_to_validate_file_size(self, faker: faker.Faker) -> None: with pytest.raises(exceptions.TooLargeFileError): - await FileValidator(allowed_mime_types=[MIME_OCTET_STREAM], max_file_size_bytes=0).validate_file( + await FileValidator(allowed_mime_types=[JS_MIME_TYPE], max_file_size_bytes=0).validate_file( file_name=faker.file_name(), file_content=generate_binary_content(faker) ) @@ -148,13 +148,13 @@ async def test_ok_not_image(self, faker: faker.Faker, binary: bool) -> None: file_content: typing.Final = generate_binary_content(faker) if binary else faker.pystr().encode() validated_file: typing.Final = await FileValidator( - allowed_mime_types=[MIME_OCTET_STREAM if binary else "text/plain"] + allowed_mime_types=[JS_MIME_TYPE if binary else "text/plain"] ).validate_file(file_name=file_name, file_content=file_content) assert validated_file.file_name == file_name assert validated_file.file_content == file_content assert validated_file.file_size == len(file_content) - assert validated_file.mime_type == MIME_OCTET_STREAM if binary else "text/plain" + assert validated_file.mime_type == JS_MIME_TYPE if binary else "text/plain" @pytest.mark.parametrize("ok_response", [True, False]) async def test_antivirus_skips_images(self, faker: faker.Faker, png_file: bytes, ok_response: bool) -> None: @@ -168,8 +168,8 @@ async def test_antivirus_fails_on_files(self, faker: faker.Faker) -> None: with pytest.raises(exceptions.KasperskyScanEngineThreatDetectedError): await FileValidator( kaspersky_scan_engine=get_mocked_kaspersky_scan_engine_client(faker=faker, ok_response=False), - allowed_mime_types=[MIME_OCTET_STREAM], - ).validate_file(file_name=faker.file_name(), file_content=generate_binary_content(faker)) + allowed_mime_types=[JS_MIME_TYPE], + ).validate_file(file_name="test.js", file_content=generate_binary_content(faker)) async def test_antivirus_fails_on_images(self, faker: faker.Faker, png_file: bytes) -> None: with pytest.raises(exceptions.KasperskyScanEngineThreatDetectedError): @@ -181,8 +181,8 @@ async def test_antivirus_fails_on_images(self, faker: faker.Faker, png_file: byt async def test_antivirus_passes_on_files(self, faker: faker.Faker) -> None: await FileValidator( kaspersky_scan_engine=get_mocked_kaspersky_scan_engine_client(faker=faker, ok_response=True), - allowed_mime_types=[MIME_OCTET_STREAM], - ).validate_file(file_name=faker.file_name(), file_content=generate_binary_content(faker)) + allowed_mime_types=[JS_MIME_TYPE], + ).validate_file(file_name="test.js", file_content=generate_binary_content(faker)) async def test_antivirus_passes_on_images(self, faker: faker.Faker, png_file: bytes) -> None: await FileValidator( diff --git a/tests/test_s3_service.py b/tests/test_s3_service.py index 1b8530c..c5c45a1 100644 --- a/tests/test_s3_service.py +++ b/tests/test_s3_service.py @@ -8,7 +8,7 @@ from safe_s3_storage.exceptions import FailedToReplaceS3BaseUrlWithProxyBaseUrlError, InvalidS3PathError from safe_s3_storage.file_validator import FileValidator from safe_s3_storage.s3_service import S3Service, UploadedFile -from tests.conftest import MIME_OCTET_STREAM, generate_binary_content +from tests.conftest import JS_MIME_TYPE, generate_binary_content class TestS3ServiceUpload: @@ -19,7 +19,7 @@ async def test_ok_upload(self, faker: faker.Faker) -> None: file_content: typing.Final = generate_binary_content(faker) file_original_name_key: typing.Final = faker.pystr() - validated_file: typing.Final = await FileValidator(allowed_mime_types=[MIME_OCTET_STREAM]).validate_file( + validated_file: typing.Final = await FileValidator(allowed_mime_types=[JS_MIME_TYPE]).validate_file( file_name=file_name, file_content=file_content ) uploaded_file: typing.Final = await S3Service(s3_client=s3_client_mock).upload_file( @@ -33,14 +33,14 @@ async def test_ok_upload(self, faker: faker.Faker) -> None: file_content=file_content, file_name=file_name, file_size=len(file_content), - mime_type=MIME_OCTET_STREAM, + mime_type=JS_MIME_TYPE, s3_path=f"{bucket_name}/{file_name}", ) s3_client_mock.put_object.assert_called_once_with( Body=file_content, Bucket=bucket_name, Key=file_name, - ContentType=MIME_OCTET_STREAM, + ContentType=JS_MIME_TYPE, Metadata={file_original_name_key: file_name}, ) From 478ef04e3b78c480c8e50fc845b7b58e95853e20 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Thu, 13 Nov 2025 15:33:26 +0300 Subject: [PATCH 2/6] fix coverage settings --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0efa1be..32235e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,7 @@ lines-after-imports = 2 "tests/*.py" = ["S101", "S311"] [tool.pytest.ini_options] -addopts = "--cov=." +addopts = "--cov=. --cov-report term-missing" [tool.coverage.report] skip_covered = true From 88afaa673d4c3d8ef6bfeaaee682c1996a326a12 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Thu, 13 Nov 2025 15:34:56 +0300 Subject: [PATCH 3/6] fix coverage settings --- .github/workflows/ci.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4f6d68..1cb8861 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,11 +44,4 @@ jobs: cache-dependency-glob: "**/pyproject.toml" - run: uv python install ${{ matrix.python-version }} - run: just install - - run: just test . --cov=. --cov-report xml - - uses: codecov/codecov-action@v4.0.1 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - with: - files: ./coverage.xml - flags: unittests - name: codecov-${{ matrix.python-version }} + - run: just test From 4b46fc9cec1f7d3ea6111af2e2218ceb2d1f6b7b Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Thu, 13 Nov 2025 15:38:11 +0300 Subject: [PATCH 4/6] fix ci --- .github/workflows/ci.yml | 9 ++++++++- Justfile | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1cb8861..f5ae9ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,4 +44,11 @@ jobs: cache-dependency-glob: "**/pyproject.toml" - run: uv python install ${{ matrix.python-version }} - run: just install - - run: just test + - run: just test-ci . --cov=. --cov-report xml + - uses: codecov/codecov-action@v4.0.1 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + files: ./coverage.xml + flags: unittests + name: codecov-${{ matrix.python-version }} diff --git a/Justfile b/Justfile index 97655ec..51176a7 100644 --- a/Justfile +++ b/Justfile @@ -28,6 +28,9 @@ lint-ci: uv run --group lint ruff check --no-fix uv run --group lint mypy . +test-ci *args: + uv run pytest {{ args }} + publish: rm -rf dist uv version $GITHUB_REF_NAME From 2ba78784f65b3bf641b1cae56f7aeffa9e6ee192 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Thu, 13 Nov 2025 15:39:44 +0300 Subject: [PATCH 5/6] fix settings --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 32235e8..0efa1be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,7 @@ lines-after-imports = 2 "tests/*.py" = ["S101", "S311"] [tool.pytest.ini_options] -addopts = "--cov=. --cov-report term-missing" +addopts = "--cov=." [tool.coverage.report] skip_covered = true From dbdfbbf789fa107cda6c9f78d7df24cf8c2fad75 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Thu, 13 Nov 2025 15:54:38 +0300 Subject: [PATCH 6/6] remove extension mime type definition --- safe_s3_storage/file_validator.py | 5 +---- tests/conftest.py | 2 +- tests/test_file_validator.py | 18 +++++++++--------- tests/test_s3_service.py | 8 ++++---- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/safe_s3_storage/file_validator.py b/safe_s3_storage/file_validator.py index 297193c..16172ff 100644 --- a/safe_s3_storage/file_validator.py +++ b/safe_s3_storage/file_validator.py @@ -1,6 +1,5 @@ import dataclasses import enum -import mimetypes import typing import magic @@ -50,9 +49,7 @@ class FileValidator: excluded_conversion_formats: list[str] | None = None def _validate_mime_type(self, *, file_name: str, file_content: bytes) -> str: - mime_type = magic.from_buffer(file_content, mime=True) - if mime_type == "application/octet-stream" and (mime_type_by_name := mimetypes.guess_type(file_name)[0]): - mime_type = mime_type_by_name + mime_type: typing.Final = magic.from_buffer(file_content, mime=True) if self.allowed_mime_types is None or mime_type in self.allowed_mime_types: return mime_type diff --git a/tests/conftest.py b/tests/conftest.py index 55d7905..2205b29 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,7 @@ def anyio_backend() -> str: return "asyncio" -JS_MIME_TYPE: typing.Final = "text/javascript" +MIME_OCTET_STREAM: typing.Final = "application/octet-stream" def generate_binary_content(faker: faker.Faker) -> bytes: diff --git a/tests/test_file_validator.py b/tests/test_file_validator.py index 3092d54..4505ce4 100644 --- a/tests/test_file_validator.py +++ b/tests/test_file_validator.py @@ -18,7 +18,7 @@ KasperskyScanEngineResponse, KasperskyScanEngineScanResult, ) -from tests.conftest import JS_MIME_TYPE, generate_binary_content +from tests.conftest import MIME_OCTET_STREAM, generate_binary_content @pytest.fixture @@ -86,7 +86,7 @@ async def test_fails_to_validate_mime_type(self, faker: faker.Faker) -> None: async def test_fails_to_validate_file_size(self, faker: faker.Faker) -> None: with pytest.raises(exceptions.TooLargeFileError): - await FileValidator(allowed_mime_types=[JS_MIME_TYPE], max_file_size_bytes=0).validate_file( + await FileValidator(allowed_mime_types=[MIME_OCTET_STREAM], max_file_size_bytes=0).validate_file( file_name=faker.file_name(), file_content=generate_binary_content(faker) ) @@ -129,7 +129,7 @@ async def test_ok_image( == _IMAGE_CONVERSION_FORMAT_TO_MIME_TYPE_AND_EXTENSION_MAP[image_conversion_format][0] ) - @pytest.mark.parametrize("file_content", ["'", "test'", "abracadabra", "python script"]) + @pytest.mark.parametrize("file_content", ["test'", "abracadabra", "python script"]) async def test_txt_file_validate(self, file_content: str) -> None: file_name: typing.Final = "file_name.txt" @@ -148,13 +148,13 @@ async def test_ok_not_image(self, faker: faker.Faker, binary: bool) -> None: file_content: typing.Final = generate_binary_content(faker) if binary else faker.pystr().encode() validated_file: typing.Final = await FileValidator( - allowed_mime_types=[JS_MIME_TYPE if binary else "text/plain"] + allowed_mime_types=[MIME_OCTET_STREAM if binary else "text/plain"] ).validate_file(file_name=file_name, file_content=file_content) assert validated_file.file_name == file_name assert validated_file.file_content == file_content assert validated_file.file_size == len(file_content) - assert validated_file.mime_type == JS_MIME_TYPE if binary else "text/plain" + assert validated_file.mime_type == MIME_OCTET_STREAM if binary else "text/plain" @pytest.mark.parametrize("ok_response", [True, False]) async def test_antivirus_skips_images(self, faker: faker.Faker, png_file: bytes, ok_response: bool) -> None: @@ -168,8 +168,8 @@ async def test_antivirus_fails_on_files(self, faker: faker.Faker) -> None: with pytest.raises(exceptions.KasperskyScanEngineThreatDetectedError): await FileValidator( kaspersky_scan_engine=get_mocked_kaspersky_scan_engine_client(faker=faker, ok_response=False), - allowed_mime_types=[JS_MIME_TYPE], - ).validate_file(file_name="test.js", file_content=generate_binary_content(faker)) + allowed_mime_types=[MIME_OCTET_STREAM], + ).validate_file(file_name=faker.file_name(), file_content=generate_binary_content(faker)) async def test_antivirus_fails_on_images(self, faker: faker.Faker, png_file: bytes) -> None: with pytest.raises(exceptions.KasperskyScanEngineThreatDetectedError): @@ -181,8 +181,8 @@ async def test_antivirus_fails_on_images(self, faker: faker.Faker, png_file: byt async def test_antivirus_passes_on_files(self, faker: faker.Faker) -> None: await FileValidator( kaspersky_scan_engine=get_mocked_kaspersky_scan_engine_client(faker=faker, ok_response=True), - allowed_mime_types=[JS_MIME_TYPE], - ).validate_file(file_name="test.js", file_content=generate_binary_content(faker)) + allowed_mime_types=[MIME_OCTET_STREAM], + ).validate_file(file_name=faker.file_name(), file_content=generate_binary_content(faker)) async def test_antivirus_passes_on_images(self, faker: faker.Faker, png_file: bytes) -> None: await FileValidator( diff --git a/tests/test_s3_service.py b/tests/test_s3_service.py index c5c45a1..1b8530c 100644 --- a/tests/test_s3_service.py +++ b/tests/test_s3_service.py @@ -8,7 +8,7 @@ from safe_s3_storage.exceptions import FailedToReplaceS3BaseUrlWithProxyBaseUrlError, InvalidS3PathError from safe_s3_storage.file_validator import FileValidator from safe_s3_storage.s3_service import S3Service, UploadedFile -from tests.conftest import JS_MIME_TYPE, generate_binary_content +from tests.conftest import MIME_OCTET_STREAM, generate_binary_content class TestS3ServiceUpload: @@ -19,7 +19,7 @@ async def test_ok_upload(self, faker: faker.Faker) -> None: file_content: typing.Final = generate_binary_content(faker) file_original_name_key: typing.Final = faker.pystr() - validated_file: typing.Final = await FileValidator(allowed_mime_types=[JS_MIME_TYPE]).validate_file( + validated_file: typing.Final = await FileValidator(allowed_mime_types=[MIME_OCTET_STREAM]).validate_file( file_name=file_name, file_content=file_content ) uploaded_file: typing.Final = await S3Service(s3_client=s3_client_mock).upload_file( @@ -33,14 +33,14 @@ async def test_ok_upload(self, faker: faker.Faker) -> None: file_content=file_content, file_name=file_name, file_size=len(file_content), - mime_type=JS_MIME_TYPE, + mime_type=MIME_OCTET_STREAM, s3_path=f"{bucket_name}/{file_name}", ) s3_client_mock.put_object.assert_called_once_with( Body=file_content, Bucket=bucket_name, Key=file_name, - ContentType=JS_MIME_TYPE, + ContentType=MIME_OCTET_STREAM, Metadata={file_original_name_key: file_name}, )