Repository: home-assistant/core
File: tests/components/aws_s3/conftest.py (Python)

Code before:
                return json.dumps(test_backup.as_dict()).encode()

        client.get_object.return_value = {"Body": MockStream()}
        client.head_bucket.return_value = {}

        create_client.return_value.__aenter__.return_value = client
        yield client


@pytest.fixture
def mock_config_entry(request: pytest.FixtureRequest) -> MockConfigEntry:
    """Return the default mocked config entry.

    Supports optional parametrization via request.param dict.

    Example:
        @pytest.mark.parametrize(
            "mock_config_entry",
            [{"prefix": "backups/home"}, {}],
            indirect=["mock_config_entry"],
        )
    """
    data = CONFIG_ENTRY_DATA.copy()  # Start with default data
    # Check if test parametrizes this fixture
    if hasattr(request, "param"):
        data.update(request.param)  # Add any additional params to data

    return MockConfigEntry(
        entry_id="test",
        title="test",
        domain=DOMAIN,
        data=data,
    )


Review comment (suggestion):
I don't think the indirect paramtrization is very easy to understand.

How about this pattern instead:


```py
@pytest.fixture
def config_entry_extra_data() -> dict[str, Any]:
    """Extra config entry data, override in tests to change defaults."""
    return {}

@pytest.fixture
def mock_config_entry(config_entry_extra_data: dict[str, Any]) -> MockConfigEntry:
    """Return the default mocked config entry."""

    return MockConfigEntry(
        entry_id="test",
        title="test",
        domain=DOMAIN,
        data=CONFIG_ENTRY_DATA | config_entry_extra_data,
    )
```

And in tests:
```py
@pytest.mark.parametrize(
    "config_entry_extra_data",
    [
        {"prefix": "backups/home"},
        {},
    ],
    ids=["with_prefix", "no_prefix"],
)
async def test_agent_list_backups_parametrized(
```

Code after:
"""Common fixtures for the AWS S3 tests."""

from collections.abc import AsyncIterator, Generator
import json
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from homeassistant.components.aws_s3.backup import (
    MULTIPART_MIN_PART_SIZE_BYTES,
    suggested_filenames,
)
from homeassistant.components.aws_s3.const import DOMAIN
from homeassistant.components.backup import AgentBackup

from .const import CONFIG_ENTRY_DATA

from tests.common import MockConfigEntry


@pytest.fixture(
    params=[2**20, MULTIPART_MIN_PART_SIZE_BYTES],
    ids=["small", "large"],
)
def test_backup(request: pytest.FixtureRequest) -> None:
    """Test backup fixture."""
    return AgentBackup(
        addons=[],
        backup_id="23e64aec",
        date="2024-11-22T11:48:48.727189+01:00",
        database_included=True,
        extra_metadata={},
        folders=[],
        homeassistant_included=True,
        homeassistant_version="2024.12.0.dev0",
        name="Core 2024.12.0.dev0",
        protected=False,
        size=request.param,
    )


@pytest.fixture(autouse=True)
def mock_client(test_backup: AgentBackup) -> Generator[AsyncMock]:
    """Mock the S3 client."""
    with patch(
        "aiobotocore.session.AioSession.create_client",
        autospec=True,
        return_value=AsyncMock(),
    ) as create_client:
        client = create_client.return_value

        tar_file, metadata_file = suggested_filenames(test_backup)

        # Mock the paginator for list_objects_v2
        client.get_paginator = MagicMock()
        client.get_paginator.return_value.paginate.return_value.__aiter__.return_value = [
            {"Contents": [{"Key": tar_file}, {"Key": metadata_file}]}
        ]

        client.create_multipart_upload.return_value = {"UploadId": "upload_id"}
        client.upload_part.return_value = {"ETag": "etag"}

        # to simplify this mock, we assume that backup is always "iterated" over, while metadata is always "read" as a whole
        class MockStream:
            async def iter_chunks(self) -> AsyncIterator[bytes]:
                yield b"backup data"

            async def read(self) -> bytes:
                return json.dumps(test_backup.as_dict()).encode()

        client.get_object.return_value = {"Body": MockStream()}
        client.head_bucket.return_value = {}

        create_client.return_value.__aenter__.return_value = client
        yield client


@pytest.fixture
def config_entry_extra_data() -> dict:
    """Extra config entry data, override in tests to change defaults."""
    return {}


@pytest.fixture
def mock_config_entry(config_entry_extra_data: dict) -> MockConfigEntry:
    """Return the default mocked config entry."""
    return MockConfigEntry(
        entry_id="test",
        title="test",
        domain=DOMAIN,
        data=CONFIG_ENTRY_DATA | config_entry_extra_data,
    )