tests.fixtures
Define fixtures for the test suite.
1"""Define fixtures for the test suite.""" 2 3from __future__ import annotations 4 5import contextlib 6import json 7from pathlib import Path 8from typing import Any, Iterator, TypeVar 9from unittest import mock 10 11import pytest 12import pytest_mock 13from boto3 import Session 14from botocore.exceptions import ClientError 15from botocore.paginate import PageIterator, Paginator 16from botocore.response import StreamingBody 17from mypy_boto3_appconfig import AppConfigClient 18from mypy_boto3_appconfigdata import AppConfigDataClient 19from mypy_boto3_appconfigdata.type_defs import GetLatestConfigurationResponseTypeDef 20from pytest_mock import MockerFixture 21 22from config_ninja import cli, systemd 23 24# pylint: disable=redefined-outer-name 25 26T = TypeVar('T') 27 28MOCK_PYPI_RESPONSE = {'releases': {'1.0': 'ignore', '1.1': 'ignore', '1.2a0': 'ignore'}} 29MOCK_YAML_CONFIG = b""" 30key_0: value_0 31key_1: 1 32key_2: true 33key_3: 34 - 1 35 - 2 36 - 3 37""".strip() 38 39 40class MockFile(mock.MagicMock): 41 """Mock the file object returned by `contextlib.closing`.""" 42 43 mock_bytes: bytes 44 45 def read(self) -> bytes: 46 """Mock the `read` method to return data used in tests.""" 47 return self.mock_bytes 48 49 50def mock_file(mock_bytes: bytes) -> MockFile: 51 """Mock the file object returned by `contextlib.closing`.""" 52 mock_file = MockFile() 53 mock_file.mock_bytes = mock_bytes 54 return mock_file 55 56 57@pytest.fixture 58def _mock_contextlib_closing(mocker: MockerFixture) -> None: # pyright: ignore[reportUnusedFunction] 59 """Mock `contextlib.closing`.""" 60 61 @contextlib.contextmanager 62 def _mocked(request: Any) -> Iterator[Any]: 63 """Pass the input parameter straight through.""" 64 yield request 65 66 mocker.patch('contextlib.closing', new=_mocked) 67 68 69@pytest.fixture 70def _mock_urlopen_for_pypi(mocker: MockerFixture) -> None: # pyright: ignore[reportUnusedFunction] 71 """Mock `urllib.request.urlopen` for PyPI requests.""" 72 73 def _mocked(_: Any) -> MockFile: 74 return mock_file(json.dumps(MOCK_PYPI_RESPONSE).encode('utf-8')) 75 76 mocker.patch('urllib.request.urlopen', new=_mocked) 77 78 79@pytest.fixture 80def mock_appconfig_client() -> AppConfigClient: 81 """Mock the `boto3` client for the `AppConfig` service.""" 82 return mock.MagicMock(name='mock_appconfig_client', spec_set=AppConfigClient) 83 84 85@pytest.fixture 86def _mock_install_io(mocker: MockerFixture) -> None: # pyright: ignore[reportUnusedFunction] 87 """Mock various I/O utilities used by the `install` script.""" 88 mocker.patch('shutil.rmtree') 89 mocker.patch('subprocess.run') 90 mocker.patch('venv.EnvBuilder') 91 mocker.patch('runpy.run_path') 92 93 94@pytest.fixture 95def mock_session(mocker: MockerFixture) -> Session: 96 """Mock the `boto3.Session` class.""" 97 mock_session = mock.MagicMock(name='mock_session', spec_set=Session) 98 mocker.patch('boto3.Session', return_value=mock_session) 99 return mock_session 100 101 102@pytest.fixture 103def mock_session_with_0_ids(mock_appconfig_client: mock.MagicMock, mock_session: mock.MagicMock) -> AppConfigClient: 104 """Mock the `boto3` client for the `AppConfig` service to return no IDs.""" 105 mock_page_iterator = mock.MagicMock(spec_set=PageIterator) 106 mock_page_iterator.search.return_value = [] 107 108 mock_paginator = mock.MagicMock(spec_set=Paginator) 109 mock_paginator.paginate.return_value = mock_page_iterator 110 111 mock_appconfig_client.get_paginator.return_value = mock_paginator 112 mock_session.client.return_value = mock_appconfig_client 113 return mock_session 114 115 116@pytest.fixture 117def mock_session_with_1_id(mock_appconfig_client: mock.MagicMock, mock_session: mock.MagicMock) -> AppConfigClient: 118 """Mock the `boto3` client for the `AppConfig` service to return a single ID.""" 119 mock_page_iterator = mock.MagicMock(name='mock_page_iterator', spec_set=PageIterator) 120 mock_page_iterator.search.return_value = ['id-1'] 121 122 mock_paginator = mock.MagicMock(name='mock_page_iterator', spec_set=Paginator) 123 mock_paginator.paginate.return_value = mock_page_iterator 124 125 mock_appconfig_client.get_paginator.return_value = mock_paginator 126 127 mock_session.client.return_value = mock_appconfig_client 128 return mock_session 129 130 131@pytest.fixture 132def mock_session_with_2_ids(mock_appconfig_client: mock.MagicMock, mock_session: mock.MagicMock) -> AppConfigClient: 133 """Mock the `boto3` client for the `AppConfig` service to return two IDs.""" 134 mock_page_iterator = mock.MagicMock(spec_set=PageIterator) 135 mock_page_iterator.search.return_value = ['id-1', 'id-2'] 136 137 mock_paginator = mock.MagicMock(spec_set=Paginator) 138 mock_paginator.paginate.return_value = mock_page_iterator 139 140 mock_appconfig_client.get_paginator.return_value = mock_paginator 141 142 mock_session.client.return_value = mock_appconfig_client 143 return mock_session 144 145 146@pytest.fixture 147def mock_latest_config() -> GetLatestConfigurationResponseTypeDef: 148 """Mock the response from `get_latest_configuration`.""" 149 mock_config_stream = mock.MagicMock(spec_set=StreamingBody) 150 mock_config_stream.read.return_value = MOCK_YAML_CONFIG 151 return { 152 'NextPollConfigurationToken': 'token', 153 'NextPollIntervalInSeconds': 1, 154 'ContentType': 'application/json', 155 'Configuration': mock_config_stream, 156 'VersionLabel': 'v1', 157 'ResponseMetadata': { 158 'RequestId': '', 159 'HostId': '', 160 'HTTPStatusCode': 200, 161 'HTTPHeaders': {}, 162 'RetryAttempts': 3, 163 }, 164 } 165 166 167@pytest.fixture 168def mock_latest_config_first_empty() -> GetLatestConfigurationResponseTypeDef: 169 """Mock the response from `get_latest_configuration`. 170 171 Return an empty `bytes` on the first iteration, and `MOCK_YAML_CONFIG` on the second. This supports testing 172 `config_ninja.contrib.appconfig.AppConfig`'s response to an empty return value. 173 """ 174 was_called: list[bool] = [] 175 176 def mock_read(*_: Any, **__: Any) -> bytes: 177 if was_called: 178 return MOCK_YAML_CONFIG 179 was_called.append(True) 180 return b'' 181 182 mock_config_stream = mock.MagicMock(spec_set=StreamingBody) 183 mock_config_stream.read = mock_read 184 return { 185 'NextPollConfigurationToken': 'token', 186 'NextPollIntervalInSeconds': 0, 187 'ContentType': 'application/json', 188 'Configuration': mock_config_stream, 189 'VersionLabel': 'v1', 190 'ResponseMetadata': { 191 'RequestId': '', 192 'HostId': '', 193 'HTTPStatusCode': 200, 194 'HTTPHeaders': {}, 195 'RetryAttempts': 3, 196 }, 197 } 198 199 200@pytest.fixture 201def mock_appconfigdata_client(mock_latest_config: mock.MagicMock) -> AppConfigDataClient: 202 """Mock the low-level `boto3` client for the `AppConfigData` service.""" 203 mock_client = mock.MagicMock(name='mock_appconfigdata_client', spec_set=AppConfigDataClient) 204 mock_client.get_latest_configuration.return_value = mock_latest_config 205 return mock_client 206 207 208@pytest.fixture 209def mock_appconfigdata_client_first_empty(mock_latest_config_first_empty: mock.MagicMock) -> AppConfigDataClient: 210 """Mock the low-level `boto3` client for the `AppConfigData` service.""" 211 mock_client = mock.MagicMock(name='mock_appconfigdata_client', spec_set=AppConfigDataClient) 212 mock_client.get_latest_configuration.return_value = mock_latest_config_first_empty 213 return mock_client 214 215 216@pytest.fixture 217def mock_full_session( 218 mock_session_with_1_id: mock.MagicMock, 219 mock_appconfig_client: mock.MagicMock, 220 mock_appconfigdata_client: mock.MagicMock, 221) -> Session: 222 """Mock the `boto3.Session` class with a full AppConfig client.""" 223 224 def client(service: str) -> mock.MagicMock: 225 if service == 'appconfig': 226 return mock_appconfig_client 227 if service == 'appconfigdata': 228 return mock_appconfigdata_client 229 raise ValueError(f'Unknown service: {service}') 230 231 mock_session_with_1_id.client = client 232 return mock_session_with_1_id 233 234 235@pytest.fixture 236def mock_poll_too_early( 237 mock_latest_config: GetLatestConfigurationResponseTypeDef, 238) -> AppConfigDataClient: 239 """Raise a `BadRequestException` when polling for configuration changes.""" 240 mock_client = mock.MagicMock(spec_set=AppConfigDataClient) 241 mock_client.exceptions.BadRequestException = ClientError 242 call_count = 0 243 244 def side_effect(*_: Any, **__: Any) -> GetLatestConfigurationResponseTypeDef: 245 nonlocal call_count 246 call_count += 1 247 if call_count == 1: 248 raise mock_client.exceptions.BadRequestException( 249 { 250 'Error': { 251 'Code': 'BadRequestException', 252 'Message': 'Request too early', 253 }, 254 'ResponseMetadata': {}, 255 }, 256 'GetLatestConfiguration', 257 ) 258 return mock_latest_config 259 260 mock_client.get_latest_configuration.side_effect = side_effect 261 262 return mock_client 263 264 265@pytest.fixture 266def monkeypatch_systemd(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> tuple[Path, Path]: 267 """Monkeypatch various utilities for interfacing with `systemd` and the shell. 268 269 Returns: 270 tuple[pathlib.Path, pathlib.Path]: the patched `SYSTEM_INSTALL_PATH` and `USER_INSTALL_PATH` 271 """ 272 mocker.patch('config_ninja.systemd.sh') 273 mocker.patch.context_manager(systemd, 'sudo') 274 mocker.patch('config_ninja.systemd.sdnotify') 275 276 system_install_path = tmp_path / 'system' 277 user_install_path = tmp_path / 'user' 278 279 monkeypatch.setattr(cli, 'SYSTEMD_AVAILABLE', True) 280 monkeypatch.setattr(systemd, 'SYSTEM_INSTALL_PATH', system_install_path) 281 monkeypatch.setattr(systemd, 'USER_INSTALL_PATH', user_install_path) 282 283 return (system_install_path, user_install_path) 284 285 286@pytest.fixture 287def example_file(tmp_path: Path) -> Path: 288 """Write the test configuration to a file in the temporary directory.""" 289 path = tmp_path / 'example.yaml' 290 path.write_bytes(MOCK_YAML_CONFIG) 291 return path 292 293 294example_file.__doc__ = f"""Write the test configuration to a file in the temporary directory. 295 296```yaml 297{MOCK_YAML_CONFIG.decode('utf-8')} 298``` 299""" 300 301 302@pytest.fixture(autouse=True) 303def mock_logging_basic_config(mocker: pytest_mock.MockerFixture) -> mock.MagicMock: 304 """Mock the `logging.basicConfig` function.""" 305 return mocker.patch('logging.basicConfig')
41class MockFile(mock.MagicMock): 42 """Mock the file object returned by `contextlib.closing`.""" 43 44 mock_bytes: bytes 45 46 def read(self) -> bytes: 47 """Mock the `read` method to return data used in tests.""" 48 return self.mock_bytes
Mock the file object returned by contextlib.closing.
46 def read(self) -> bytes: 47 """Mock the `read` method to return data used in tests.""" 48 return self.mock_bytes
Mock the read method to return data used in tests.
Inherited Members
- unittest.mock.MagicMixin
- MagicMixin
- unittest.mock.MagicMock
- mock_add_spec
- unittest.mock.CallableMixin
- side_effect
- unittest.mock.NonCallableMock
- attach_mock
- return_value
- called
- call_count
- call_args
- call_args_list
- mock_calls
- reset_mock
- configure_mock
- assert_not_called
- assert_called
- assert_called_once
- assert_called_with
- assert_called_once_with
- assert_has_calls
- assert_any_call
51def mock_file(mock_bytes: bytes) -> MockFile: 52 """Mock the file object returned by `contextlib.closing`.""" 53 mock_file = MockFile() 54 mock_file.mock_bytes = mock_bytes 55 return mock_file
Mock the file object returned by contextlib.closing.
80@pytest.fixture 81def mock_appconfig_client() -> AppConfigClient: 82 """Mock the `boto3` client for the `AppConfig` service.""" 83 return mock.MagicMock(name='mock_appconfig_client', spec_set=AppConfigClient)
Mock the boto3 client for the AppConfig service.
95@pytest.fixture 96def mock_session(mocker: MockerFixture) -> Session: 97 """Mock the `boto3.Session` class.""" 98 mock_session = mock.MagicMock(name='mock_session', spec_set=Session) 99 mocker.patch('boto3.Session', return_value=mock_session) 100 return mock_session
Mock the boto3.Session class.
103@pytest.fixture 104def mock_session_with_0_ids(mock_appconfig_client: mock.MagicMock, mock_session: mock.MagicMock) -> AppConfigClient: 105 """Mock the `boto3` client for the `AppConfig` service to return no IDs.""" 106 mock_page_iterator = mock.MagicMock(spec_set=PageIterator) 107 mock_page_iterator.search.return_value = [] 108 109 mock_paginator = mock.MagicMock(spec_set=Paginator) 110 mock_paginator.paginate.return_value = mock_page_iterator 111 112 mock_appconfig_client.get_paginator.return_value = mock_paginator 113 mock_session.client.return_value = mock_appconfig_client 114 return mock_session
Mock the boto3 client for the AppConfig service to return no IDs.
117@pytest.fixture 118def mock_session_with_1_id(mock_appconfig_client: mock.MagicMock, mock_session: mock.MagicMock) -> AppConfigClient: 119 """Mock the `boto3` client for the `AppConfig` service to return a single ID.""" 120 mock_page_iterator = mock.MagicMock(name='mock_page_iterator', spec_set=PageIterator) 121 mock_page_iterator.search.return_value = ['id-1'] 122 123 mock_paginator = mock.MagicMock(name='mock_page_iterator', spec_set=Paginator) 124 mock_paginator.paginate.return_value = mock_page_iterator 125 126 mock_appconfig_client.get_paginator.return_value = mock_paginator 127 128 mock_session.client.return_value = mock_appconfig_client 129 return mock_session
Mock the boto3 client for the AppConfig service to return a single ID.
132@pytest.fixture 133def mock_session_with_2_ids(mock_appconfig_client: mock.MagicMock, mock_session: mock.MagicMock) -> AppConfigClient: 134 """Mock the `boto3` client for the `AppConfig` service to return two IDs.""" 135 mock_page_iterator = mock.MagicMock(spec_set=PageIterator) 136 mock_page_iterator.search.return_value = ['id-1', 'id-2'] 137 138 mock_paginator = mock.MagicMock(spec_set=Paginator) 139 mock_paginator.paginate.return_value = mock_page_iterator 140 141 mock_appconfig_client.get_paginator.return_value = mock_paginator 142 143 mock_session.client.return_value = mock_appconfig_client 144 return mock_session
Mock the boto3 client for the AppConfig service to return two IDs.
147@pytest.fixture 148def mock_latest_config() -> GetLatestConfigurationResponseTypeDef: 149 """Mock the response from `get_latest_configuration`.""" 150 mock_config_stream = mock.MagicMock(spec_set=StreamingBody) 151 mock_config_stream.read.return_value = MOCK_YAML_CONFIG 152 return { 153 'NextPollConfigurationToken': 'token', 154 'NextPollIntervalInSeconds': 1, 155 'ContentType': 'application/json', 156 'Configuration': mock_config_stream, 157 'VersionLabel': 'v1', 158 'ResponseMetadata': { 159 'RequestId': '', 160 'HostId': '', 161 'HTTPStatusCode': 200, 162 'HTTPHeaders': {}, 163 'RetryAttempts': 3, 164 }, 165 }
Mock the response from get_latest_configuration.
168@pytest.fixture 169def mock_latest_config_first_empty() -> GetLatestConfigurationResponseTypeDef: 170 """Mock the response from `get_latest_configuration`. 171 172 Return an empty `bytes` on the first iteration, and `MOCK_YAML_CONFIG` on the second. This supports testing 173 `config_ninja.contrib.appconfig.AppConfig`'s response to an empty return value. 174 """ 175 was_called: list[bool] = [] 176 177 def mock_read(*_: Any, **__: Any) -> bytes: 178 if was_called: 179 return MOCK_YAML_CONFIG 180 was_called.append(True) 181 return b'' 182 183 mock_config_stream = mock.MagicMock(spec_set=StreamingBody) 184 mock_config_stream.read = mock_read 185 return { 186 'NextPollConfigurationToken': 'token', 187 'NextPollIntervalInSeconds': 0, 188 'ContentType': 'application/json', 189 'Configuration': mock_config_stream, 190 'VersionLabel': 'v1', 191 'ResponseMetadata': { 192 'RequestId': '', 193 'HostId': '', 194 'HTTPStatusCode': 200, 195 'HTTPHeaders': {}, 196 'RetryAttempts': 3, 197 }, 198 }
Mock the response from get_latest_configuration.
Return an empty bytes on the first iteration, and MOCK_YAML_CONFIG on the second. This supports testing
config_ninja.contrib.appconfig.AppConfig's response to an empty return value.
201@pytest.fixture 202def mock_appconfigdata_client(mock_latest_config: mock.MagicMock) -> AppConfigDataClient: 203 """Mock the low-level `boto3` client for the `AppConfigData` service.""" 204 mock_client = mock.MagicMock(name='mock_appconfigdata_client', spec_set=AppConfigDataClient) 205 mock_client.get_latest_configuration.return_value = mock_latest_config 206 return mock_client
Mock the low-level boto3 client for the AppConfigData service.
209@pytest.fixture 210def mock_appconfigdata_client_first_empty(mock_latest_config_first_empty: mock.MagicMock) -> AppConfigDataClient: 211 """Mock the low-level `boto3` client for the `AppConfigData` service.""" 212 mock_client = mock.MagicMock(name='mock_appconfigdata_client', spec_set=AppConfigDataClient) 213 mock_client.get_latest_configuration.return_value = mock_latest_config_first_empty 214 return mock_client
Mock the low-level boto3 client for the AppConfigData service.
217@pytest.fixture 218def mock_full_session( 219 mock_session_with_1_id: mock.MagicMock, 220 mock_appconfig_client: mock.MagicMock, 221 mock_appconfigdata_client: mock.MagicMock, 222) -> Session: 223 """Mock the `boto3.Session` class with a full AppConfig client.""" 224 225 def client(service: str) -> mock.MagicMock: 226 if service == 'appconfig': 227 return mock_appconfig_client 228 if service == 'appconfigdata': 229 return mock_appconfigdata_client 230 raise ValueError(f'Unknown service: {service}') 231 232 mock_session_with_1_id.client = client 233 return mock_session_with_1_id
Mock the boto3.Session class with a full AppConfig client.
236@pytest.fixture 237def mock_poll_too_early( 238 mock_latest_config: GetLatestConfigurationResponseTypeDef, 239) -> AppConfigDataClient: 240 """Raise a `BadRequestException` when polling for configuration changes.""" 241 mock_client = mock.MagicMock(spec_set=AppConfigDataClient) 242 mock_client.exceptions.BadRequestException = ClientError 243 call_count = 0 244 245 def side_effect(*_: Any, **__: Any) -> GetLatestConfigurationResponseTypeDef: 246 nonlocal call_count 247 call_count += 1 248 if call_count == 1: 249 raise mock_client.exceptions.BadRequestException( 250 { 251 'Error': { 252 'Code': 'BadRequestException', 253 'Message': 'Request too early', 254 }, 255 'ResponseMetadata': {}, 256 }, 257 'GetLatestConfiguration', 258 ) 259 return mock_latest_config 260 261 mock_client.get_latest_configuration.side_effect = side_effect 262 263 return mock_client
Raise a BadRequestException when polling for configuration changes.
266@pytest.fixture 267def monkeypatch_systemd(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> tuple[Path, Path]: 268 """Monkeypatch various utilities for interfacing with `systemd` and the shell. 269 270 Returns: 271 tuple[pathlib.Path, pathlib.Path]: the patched `SYSTEM_INSTALL_PATH` and `USER_INSTALL_PATH` 272 """ 273 mocker.patch('config_ninja.systemd.sh') 274 mocker.patch.context_manager(systemd, 'sudo') 275 mocker.patch('config_ninja.systemd.sdnotify') 276 277 system_install_path = tmp_path / 'system' 278 user_install_path = tmp_path / 'user' 279 280 monkeypatch.setattr(cli, 'SYSTEMD_AVAILABLE', True) 281 monkeypatch.setattr(systemd, 'SYSTEM_INSTALL_PATH', system_install_path) 282 monkeypatch.setattr(systemd, 'USER_INSTALL_PATH', user_install_path) 283 284 return (system_install_path, user_install_path)
Monkeypatch various utilities for interfacing with systemd and the shell.
Returns:
tuple[pathlib.Path, pathlib.Path]: the patched
SYSTEM_INSTALL_PATHandUSER_INSTALL_PATH
287@pytest.fixture 288def example_file(tmp_path: Path) -> Path: 289 """Write the test configuration to a file in the temporary directory.""" 290 path = tmp_path / 'example.yaml' 291 path.write_bytes(MOCK_YAML_CONFIG) 292 return path
Write the test configuration to a file in the temporary directory.
303@pytest.fixture(autouse=True) 304def mock_logging_basic_config(mocker: pytest_mock.MockerFixture) -> mock.MagicMock: 305 """Mock the `logging.basicConfig` function.""" 306 return mocker.patch('logging.basicConfig')
Mock the logging.basicConfig function.