provide-testkit 0.0.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. provide/__init__.py +3 -0
  2. provide/testkit/__init__.py +248 -0
  3. provide/testkit/archive/__init__.py +24 -0
  4. provide/testkit/archive/fixtures.py +217 -0
  5. provide/testkit/cli.py +229 -0
  6. provide/testkit/common/__init__.py +32 -0
  7. provide/testkit/common/fixtures.py +234 -0
  8. provide/testkit/crypto.py +163 -0
  9. provide/testkit/environment.py +79 -0
  10. provide/testkit/file/__init__.py +40 -0
  11. provide/testkit/file/content_fixtures.py +275 -0
  12. provide/testkit/file/directory_fixtures.py +105 -0
  13. provide/testkit/file/fixtures.py +49 -0
  14. provide/testkit/file/special_fixtures.py +141 -0
  15. provide/testkit/fixtures.py +52 -0
  16. provide/testkit/harness.py +122 -0
  17. provide/testkit/hub.py +22 -0
  18. provide/testkit/logger/__init__.py +39 -0
  19. provide/testkit/logger/hooks.py +100 -0
  20. provide/testkit/logger/reset.py +230 -0
  21. provide/testkit/main.py +22 -0
  22. provide/testkit/mocking/__init__.py +46 -0
  23. provide/testkit/mocking/fixtures.py +340 -0
  24. provide/testkit/process/__init__.py +48 -0
  25. provide/testkit/process/async_fixtures.py +410 -0
  26. provide/testkit/process/fixtures.py +54 -0
  27. provide/testkit/process/subprocess_fixtures.py +208 -0
  28. provide/testkit/quality/__init__.py +101 -0
  29. provide/testkit/quality/artifacts.py +360 -0
  30. provide/testkit/quality/base.py +158 -0
  31. provide/testkit/quality/cli.py +394 -0
  32. provide/testkit/quality/complexity/__init__.py +30 -0
  33. provide/testkit/quality/complexity/analyzer.py +392 -0
  34. provide/testkit/quality/complexity/fixture.py +196 -0
  35. provide/testkit/quality/coverage/__init__.py +36 -0
  36. provide/testkit/quality/coverage/fixture.py +236 -0
  37. provide/testkit/quality/coverage/reporter.py +150 -0
  38. provide/testkit/quality/coverage/tracker.py +313 -0
  39. provide/testkit/quality/decorators.py +380 -0
  40. provide/testkit/quality/documentation/__init__.py +29 -0
  41. provide/testkit/quality/documentation/checker.py +361 -0
  42. provide/testkit/quality/documentation/fixture.py +187 -0
  43. provide/testkit/quality/profiling/__init__.py +30 -0
  44. provide/testkit/quality/profiling/fixture.py +332 -0
  45. provide/testkit/quality/profiling/profiler.py +428 -0
  46. provide/testkit/quality/report.py +266 -0
  47. provide/testkit/quality/runner.py +319 -0
  48. provide/testkit/quality/security/__init__.py +29 -0
  49. provide/testkit/quality/security/fixture.py +196 -0
  50. provide/testkit/quality/security/scanner.py +338 -0
  51. provide/testkit/streams.py +54 -0
  52. provide/testkit/threading/__init__.py +38 -0
  53. provide/testkit/threading/basic_fixtures.py +103 -0
  54. provide/testkit/threading/data_fixtures.py +101 -0
  55. provide/testkit/threading/execution_fixtures.py +268 -0
  56. provide/testkit/threading/fixtures.py +50 -0
  57. provide/testkit/threading/sync_fixtures.py +98 -0
  58. provide/testkit/time/__init__.py +32 -0
  59. provide/testkit/time/fixtures.py +416 -0
  60. provide/testkit/transport/__init__.py +30 -0
  61. provide/testkit/transport/fixtures.py +278 -0
  62. provide_testkit-0.0.0.dev0.dist-info/METADATA +145 -0
  63. provide_testkit-0.0.0.dev0.dist-info/RECORD +66 -0
  64. provide_testkit-0.0.0.dev0.dist-info/WHEEL +5 -0
  65. provide_testkit-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  66. provide_testkit-0.0.0.dev0.dist-info/top_level.txt +1 -0
provide/testkit/cli.py ADDED
@@ -0,0 +1,229 @@
1
+ """
2
+ CLI Testing Utilities for Foundation.
3
+
4
+ Provides comprehensive testing support for CLI applications including
5
+ context mocking, isolated runners, and configuration helpers.
6
+ """
7
+
8
+ from collections.abc import Generator
9
+ from contextlib import contextmanager
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import click
16
+ from click.testing import CliRunner, Result
17
+ import pytest
18
+
19
+ from provide.foundation.context import CLIContext
20
+ from provide.foundation.file import temp_file as foundation_temp_file
21
+ from provide.foundation.logger import get_logger
22
+
23
+ log = get_logger(__name__)
24
+
25
+
26
+ class MockContext(CLIContext):
27
+ """Mock context for testing that tracks method calls."""
28
+
29
+ def __init__(self, **kwargs: Any) -> None:
30
+ """Initialize mock context with tracking."""
31
+ super().__init__(**kwargs)
32
+ self.calls = []
33
+ self.saved_configs = []
34
+ self.loaded_configs = []
35
+
36
+ def save_config(self, path: str | Path) -> None:
37
+ """Track save_config calls."""
38
+ self.saved_configs.append(path)
39
+ super().save_config(path)
40
+
41
+ def load_config(self, path: str | Path) -> None:
42
+ """Track load_config calls."""
43
+ self.loaded_configs.append(path)
44
+ super().load_config(path)
45
+
46
+
47
+ @contextmanager
48
+ def isolated_cli_runner(
49
+ env: dict[str, str] | None = None,
50
+ ) -> Generator[CliRunner, None, None]:
51
+ """
52
+ Create an isolated test environment for CLI testing.
53
+
54
+ Args:
55
+ env: Environment variables to set
56
+
57
+ Yields:
58
+ CliRunner instance in isolated filesystem
59
+ """
60
+ runner = CliRunner()
61
+
62
+ with runner.isolated_filesystem():
63
+ # Set up environment
64
+ old_env = {}
65
+ if env:
66
+ for key, value in env.items():
67
+ old_env[key] = os.environ.get(key)
68
+ os.environ[key] = value
69
+
70
+ try:
71
+ yield runner
72
+ finally:
73
+ # Restore environment
74
+ for key, old_value in old_env.items():
75
+ if old_value is None:
76
+ os.environ.pop(key, None)
77
+ else:
78
+ os.environ[key] = old_value
79
+
80
+
81
+ @contextmanager
82
+ def temp_config_file(
83
+ content: dict[str, Any] | str,
84
+ format: str = "json",
85
+ ) -> Path:
86
+ """
87
+ Create a temporary configuration file for testing.
88
+
89
+ Args:
90
+ content: Configuration content (dict or string)
91
+ format: File format (json, toml, yaml)
92
+
93
+ Yields:
94
+ Path to temporary config file
95
+ """
96
+ suffix = f".{format}"
97
+
98
+ with foundation_temp_file(suffix=suffix, text=True, cleanup=False) as config_path:
99
+ with open(config_path, "w") as f:
100
+ if isinstance(content, dict):
101
+ if format == "json":
102
+ json.dump(content, f, indent=2)
103
+ elif format == "toml":
104
+ try:
105
+ import tomli_w
106
+
107
+ # tomli_w needs the content as a string, not written to file handle
108
+ toml_content = tomli_w.dumps(content)
109
+ f.write(toml_content)
110
+ except ImportError:
111
+ # Fall back to manual formatting
112
+ for key, value in content.items():
113
+ if isinstance(value, str):
114
+ f.write(f'{key} = "{value}"\n')
115
+ elif isinstance(value, bool):
116
+ # TOML uses lowercase for booleans
117
+ f.write(f"{key} = {str(value).lower()}\n")
118
+ else:
119
+ f.write(f"{key} = {value}\n")
120
+ elif format == "yaml":
121
+ try:
122
+ import yaml
123
+
124
+ yaml.safe_dump(content, f)
125
+ except ImportError:
126
+ raise ImportError("PyYAML required for YAML testing")
127
+ else:
128
+ f.write(content)
129
+
130
+ try:
131
+ yield config_path
132
+ finally:
133
+ config_path.unlink(missing_ok=True)
134
+
135
+
136
+ def create_test_cli(
137
+ name: str = "test-cli",
138
+ version: str = "1.0.0",
139
+ commands: list[click.Command] | None = None,
140
+ ) -> click.Group:
141
+ """
142
+ Create a test CLI group with standard options.
143
+
144
+ Args:
145
+ name: CLI name
146
+ version: CLI version
147
+ commands: Optional list of commands to add
148
+
149
+ Returns:
150
+ Click Group configured for testing
151
+ """
152
+ from provide.foundation.cli.decorators import standard_options
153
+
154
+ @click.group(name=name)
155
+ @standard_options
156
+ @click.pass_context
157
+ def cli(ctx: click.Context, **kwargs: Any) -> None:
158
+ """Test CLI for testing."""
159
+ ctx.obj = CLIContext(**{k: v for k, v in kwargs.items() if v is not None})
160
+
161
+ if commands:
162
+ for cmd in commands:
163
+ cli.add_command(cmd)
164
+
165
+ return cli
166
+
167
+
168
+ class CliTestCase:
169
+ """Base class for CLI test cases with common utilities."""
170
+
171
+ def setup_method(self) -> None:
172
+ """Set up test case."""
173
+ self.runner = CliRunner()
174
+ self.temp_files = []
175
+
176
+ def teardown_method(self) -> None:
177
+ """Clean up test case."""
178
+ for path in self.temp_files:
179
+ if path.exists():
180
+ path.unlink()
181
+
182
+ def invoke(self, *args: Any, **kwargs: Any) -> Result:
183
+ """Invoke CLI command."""
184
+ return self.runner.invoke(*args, **kwargs)
185
+
186
+ def create_temp_file(self, content: str = "", suffix: str = "") -> Path:
187
+ """Create a temporary file that will be cleaned up."""
188
+ with foundation_temp_file(suffix=suffix, text=True, cleanup=False) as path:
189
+ path.write_text(content)
190
+
191
+ self.temp_files.append(path)
192
+ return path
193
+
194
+ def assert_json_output(self, result: Result, expected: dict[str, Any]) -> None:
195
+ """Assert that output is valid JSON matching expected."""
196
+ try:
197
+ output = json.loads(result.output)
198
+ except json.JSONDecodeError as e:
199
+ raise AssertionError(f"Output is not valid JSON: {e}\n{result.output}")
200
+
201
+ for key, value in expected.items():
202
+ assert key in output, f"Key '{key}' not in output"
203
+ assert output[key] == value, f"Value mismatch for '{key}': {output[key]} != {value}"
204
+
205
+
206
+ @pytest.fixture
207
+ def click_testing_mode() -> Generator[None, None, None]:
208
+ """
209
+ Pytest fixture to enable Click testing mode.
210
+
211
+ Sets CLICK_TESTING=1 environment variable for the duration of the test,
212
+ then restores the original value. This fixture makes it easy to enable
213
+ Click testing mode without manual environment variable management.
214
+
215
+ Usage:
216
+ def test_my_cli(click_testing_mode):
217
+ # Test CLI code here - CLICK_TESTING is automatically set
218
+ pass
219
+ """
220
+ original_value = os.environ.get("CLICK_TESTING")
221
+ os.environ["CLICK_TESTING"] = "1"
222
+
223
+ try:
224
+ yield
225
+ finally:
226
+ if original_value is None:
227
+ os.environ.pop("CLICK_TESTING", None)
228
+ else:
229
+ os.environ["CLICK_TESTING"] = original_value
@@ -0,0 +1,32 @@
1
+ """
2
+ Common testing fixtures for the provide-io ecosystem.
3
+
4
+ Standard mock objects and fixtures that are used across multiple modules
5
+ in any project that depends on provide.foundation.
6
+ """
7
+
8
+ from provide.testkit.common.fixtures import (
9
+ mock_cache,
10
+ mock_config_source,
11
+ mock_database,
12
+ mock_event_emitter,
13
+ mock_file_system,
14
+ mock_http_config,
15
+ mock_metrics_collector,
16
+ mock_subprocess,
17
+ mock_telemetry_config,
18
+ mock_transport,
19
+ )
20
+
21
+ __all__ = [
22
+ "mock_cache",
23
+ "mock_config_source",
24
+ "mock_database",
25
+ "mock_event_emitter",
26
+ "mock_file_system",
27
+ "mock_http_config",
28
+ "mock_metrics_collector",
29
+ "mock_subprocess",
30
+ "mock_telemetry_config",
31
+ "mock_transport",
32
+ ]
@@ -0,0 +1,234 @@
1
+ """
2
+ Common Mock Objects and Fixtures.
3
+
4
+ Reusable mock objects for configuration, logging, and other common
5
+ testing scenarios across the provide-io ecosystem.
6
+ """
7
+
8
+ from typing import Any
9
+ from unittest.mock import Mock, PropertyMock
10
+
11
+ import pytest
12
+
13
+ from provide.foundation import TelemetryConfig
14
+ from provide.foundation.logger.config.logging import LoggingConfig
15
+
16
+
17
+ @pytest.fixture
18
+ def mock_http_config() -> Any:
19
+ """
20
+ Standard HTTP configuration for testing.
21
+
22
+ Returns:
23
+ HTTPConfig with common test settings.
24
+ """
25
+ from provide.foundation.transport.config import HTTPConfig
26
+
27
+ return HTTPConfig(
28
+ timeout=30.0,
29
+ max_retries=3,
30
+ retry_backoff_factor=0.5,
31
+ verify_ssl=True,
32
+ pool_connections=10,
33
+ pool_maxsize=100,
34
+ follow_redirects=True,
35
+ http2=True,
36
+ max_redirects=5,
37
+ )
38
+
39
+
40
+ @pytest.fixture
41
+ def mock_telemetry_config() -> TelemetryConfig:
42
+ """
43
+ Standard telemetry configuration for testing.
44
+
45
+ Returns:
46
+ TelemetryConfig with debug logging enabled.
47
+ """
48
+ return TelemetryConfig(
49
+ logging=LoggingConfig(default_level="DEBUG"),
50
+ globally_disabled=False,
51
+ service_name="test_service",
52
+ service_version="1.0.0",
53
+ )
54
+
55
+
56
+ @pytest.fixture
57
+ def mock_config_source() -> Mock:
58
+ """
59
+ Mock configuration source for testing config loading.
60
+
61
+ Returns:
62
+ Mock that simulates a configuration source.
63
+ """
64
+ source = Mock()
65
+ source.load = Mock(return_value={"key": "value", "nested": {"key": "value"}})
66
+ source.exists = Mock(return_value=True)
67
+ source.reload = Mock()
68
+ source.watch = Mock()
69
+ source.priority = 100
70
+ source.name = "mock_source"
71
+
72
+ return source
73
+
74
+
75
+ @pytest.fixture
76
+ def mock_event_emitter():
77
+ """
78
+ Mock event emitter for testing event-driven components.
79
+
80
+ Returns:
81
+ Mock with emit, on, off methods.
82
+ """
83
+ emitter = Mock()
84
+ emitter.emit = Mock()
85
+ emitter.on = Mock()
86
+ emitter.off = Mock()
87
+ emitter.once = Mock()
88
+ emitter.listeners = Mock(return_value=[])
89
+ emitter.remove_all_listeners = Mock()
90
+
91
+ return emitter
92
+
93
+
94
+ @pytest.fixture
95
+ def mock_transport():
96
+ """
97
+ Mock transport for testing network operations.
98
+
99
+ Returns:
100
+ Mock transport with request/response methods.
101
+ """
102
+ transport = Mock()
103
+ transport.request = Mock(return_value={"status": 200, "data": {}})
104
+ transport.get = Mock(return_value={"status": 200, "data": {}})
105
+ transport.post = Mock(return_value={"status": 200, "data": {}})
106
+ transport.put = Mock(return_value={"status": 200, "data": {}})
107
+ transport.delete = Mock(return_value={"status": 204})
108
+ transport.close = Mock()
109
+
110
+ return transport
111
+
112
+
113
+ @pytest.fixture
114
+ def mock_metrics_collector():
115
+ """
116
+ Mock metrics collector for testing instrumentation.
117
+
118
+ Returns:
119
+ Mock with common metrics methods.
120
+ """
121
+ collector = Mock()
122
+ collector.increment = Mock()
123
+ collector.decrement = Mock()
124
+ collector.gauge = Mock()
125
+ collector.histogram = Mock()
126
+ collector.timer = Mock()
127
+ collector.flush = Mock()
128
+
129
+ # Add context manager support for timing
130
+ timer_cm = Mock()
131
+ timer_cm.__enter__ = Mock(return_value=timer_cm)
132
+ timer_cm.__exit__ = Mock(return_value=None)
133
+ collector.timer.return_value = timer_cm
134
+
135
+ return collector
136
+
137
+
138
+ @pytest.fixture
139
+ def mock_cache():
140
+ """
141
+ Mock cache for testing caching behavior.
142
+
143
+ Returns:
144
+ Mock with get, set, delete, clear methods.
145
+ """
146
+ cache_data = {}
147
+
148
+ cache = Mock()
149
+ cache.get = Mock(side_effect=lambda k, default=None: cache_data.get(k, default))
150
+ cache.set = Mock(side_effect=lambda k, v, ttl=None: cache_data.update({k: v}))
151
+ cache.delete = Mock(side_effect=lambda k: cache_data.pop(k, None))
152
+ cache.clear = Mock(side_effect=cache_data.clear)
153
+ cache.exists = Mock(side_effect=lambda k: k in cache_data)
154
+ cache.keys = Mock(return_value=list(cache_data.keys()))
155
+
156
+ # Store reference to data for test assertions
157
+ cache._data = cache_data
158
+
159
+ return cache
160
+
161
+
162
+ @pytest.fixture
163
+ def mock_database():
164
+ """
165
+ Mock database connection for testing.
166
+
167
+ Returns:
168
+ Mock with execute, fetch, commit, rollback methods.
169
+ """
170
+ db = Mock()
171
+ db.execute = Mock(return_value=Mock(rowcount=1))
172
+ db.fetch = Mock(return_value=[])
173
+ db.fetchone = Mock(return_value=None)
174
+ db.fetchall = Mock(return_value=[])
175
+ db.commit = Mock()
176
+ db.rollback = Mock()
177
+ db.close = Mock()
178
+ db.is_connected = PropertyMock(return_value=True)
179
+
180
+ # Add context manager support
181
+ db.__enter__ = Mock(return_value=db)
182
+ db.__exit__ = Mock(return_value=None)
183
+
184
+ return db
185
+
186
+
187
+ @pytest.fixture
188
+ def mock_file_system():
189
+ """
190
+ Mock file system operations.
191
+
192
+ Returns:
193
+ Mock with read, write, exists, delete methods.
194
+ """
195
+ fs = Mock()
196
+ fs.read = Mock(return_value=b"content")
197
+ fs.write = Mock()
198
+ fs.exists = Mock(return_value=True)
199
+ fs.delete = Mock()
200
+ fs.mkdir = Mock()
201
+ fs.rmdir = Mock()
202
+ fs.list = Mock(return_value=[])
203
+ fs.stat = Mock(return_value=Mock(st_size=1024, st_mtime=0))
204
+
205
+ return fs
206
+
207
+
208
+ @pytest.fixture
209
+ def mock_subprocess():
210
+ """
211
+ Mock subprocess for testing command execution.
212
+
213
+ Returns:
214
+ Mock with run, Popen methods.
215
+ """
216
+ subprocess = Mock()
217
+
218
+ # Mock run method
219
+ result = Mock()
220
+ result.returncode = 0
221
+ result.stdout = "output"
222
+ result.stderr = ""
223
+ subprocess.run = Mock(return_value=result)
224
+
225
+ # Mock Popen
226
+ process = Mock()
227
+ process.communicate = Mock(return_value=("output", ""))
228
+ process.returncode = 0
229
+ process.pid = 12345
230
+ process.poll = Mock(return_value=0)
231
+ process.wait = Mock(return_value=0)
232
+ subprocess.Popen = Mock(return_value=process)
233
+
234
+ return subprocess
@@ -0,0 +1,163 @@
1
+ """
2
+ Crypto Testing Fixtures for Foundation.
3
+
4
+ Provides comprehensive pytest fixtures for testing certificate functionality,
5
+ including valid/invalid certificates, keys, chains, and edge cases.
6
+ """
7
+
8
+ import pytest
9
+
10
+ from provide.foundation import logger
11
+ from provide.foundation.crypto import Certificate
12
+
13
+
14
+ @pytest.fixture(scope="module")
15
+ def client_cert() -> Certificate:
16
+ """Create a client certificate for testing."""
17
+ cert_pem = """-----BEGIN CERTIFICATE-----
18
+ MIIB+jCCAYGgAwIBAgIJAPsxOr78BIU0MAoGCCqGSM49BAMEMCgxEjAQBgNVBAoM
19
+ CUhhc2hpQ29ycDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDIwNTIzMTkzN1oX
20
+ DTI2MDIwNTIzMTkzN1owKDESMBAGA1UECgwJSGFzaGlDb3JwMRIwEAYDVQQDDAls
21
+ b2NhbGhvc3QwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARCi3SNYYDpSeScRM52tFYr
22
+ URzsPOE/ad8BzvpvL+mfy1c5oHQhh6KPnxpoo1WyDJGYplwPTGS68DvvWmolrPAt
23
+ C7I7r7spgyJS1358E5fA2NWk9/YPaiUzK2gsyrL9dKajdzB1MA8GA1UdEwEB/wQF
24
+ MAMBAf8wFAYDVR0RBA0wC4IJbG9jYWxob3N0MB0GA1UdJQQWMBQGCCsGAQUFBwMC
25
+ BggrBgEFBQcDATAOBgNVHQ8BAf8EBAMCA6gwHQYDVR0OBBYEFOwuttXPh5kTPSpX
26
+ a2ex0+VKjlpaMAoGCCqGSM49BAMEA2cAMGQCMGbN17Zt1GxZ41cXTaQOKuv/BIQd
27
+ nkaRz51XrITKaULNie4bgW6gT94cTUFQ9SNwEAIwOpmKeZqYG9WHcqol4QEUmMVM
28
+ MY3jxMiLpb9Mt/ysstXmsrQY7UoLu+c6zfKwyTEJ
29
+ -----END CERTIFICATE-----"""
30
+
31
+ key_pem = """-----BEGIN EC PRIVATE KEY-----
32
+ MIGkAgEBBDAkxo19KczdciRiJjOWEKGY5mH9s1D0aUS5XBdvktcaonIOdqNrkCt1
33
+ BC5YjEAVLNWgBwYFK4EEACKhZANiAARCi3SNYYDpSeScRM52tFYrURzsPOE/ad8B
34
+ zvpvL+mfy1c5oHQhh6KPnxpoo1WyDJGYplwPTGS68DvvWmolrPAtC7I7r7spgyJS
35
+ 1358E5fA2NWk9/YPaiUzK2gsyrL9dKY=
36
+ -----END EC PRIVATE KEY-----"""
37
+
38
+ logger.debug(f"Created CLIENT_CERT fixture: {cert_pem[:30]}...")
39
+ return Certificate(cert_pem_or_uri=cert_pem, key_pem_or_uri=key_pem)
40
+
41
+
42
+ @pytest.fixture(scope="module")
43
+ def server_cert() -> Certificate:
44
+ """Create a server certificate for testing."""
45
+ cert_pem = """-----BEGIN CERTIFICATE-----
46
+ MIIB+jCCAYGgAwIBAgIJAKrIoEQw7N9LMAoGCCqGSM49BAMEMCgxEjAQBgNVBAoM
47
+ CUhhc2hpQ29ycDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDIwNTIzMTkzN1oX
48
+ DTI2MDIwNTIzMTkzN1owKDESMBAGA1UECgwJSGFzaGlDb3JwMRIwEAYDVQQDDAls
49
+ b2NhbGhvc3QwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARMxEVmGX3a4IWPOAJ2MX2s
50
+ 2Wj3KZ0Io5EwUPMkxknGheO2e55qeHp/tkEFzYt9AH8du1xJLKKFbsGV5q9vipGN
51
+ x5XMbj2RMdH5VXHTAdc/bLFFy9kybQqo300Rv6ViW2KjdzB1MA8GA1UdEwEB/wQF
52
+ MAMBAf8wFAYDVR0RBA0wC4IJbG9jYWxob3N0MB0GA1UdJQQWMBQGCCsGAQUFBwMC
53
+ BggrBgEFBQcDATAOBgNVHQ8BAf8EBAMCA6gwHQYDVR0OBBYEFJy7Iz7whfiALYDB
54
+ TsM+IHXb1E8+MAoGCCqGSM49BAMEA2cAMGQCMFwxBS3lZSUprvrNGfJL83oGVY97
55
+ emQpHy/SEWpHBK8awn1XeTf+ZAwLaxc3K+AKqwIwPwIbIlmstd69zAYMFNHtzceN
56
+ XOzBx35sWRw92gr/hbE4hYeDBqEUwstSFNZ6MZu0
57
+ -----END CERTIFICATE-----"""
58
+
59
+ key_pem = """-----BEGIN EC PRIVATE KEY-----
60
+ MIGkAgEBBDDZ1MORWFVI0HtgKv+zZys/5e1HVmfcs4bwdp3VEsuwS6an3gTwGnSP
61
+ Ce+bI6f/TvGgBwYFK4EEACKhZANiAARMxEVmGX3a4IWPOAJ2MX2s2Wj3KZ0Io5Ew
62
+ UPMkxknGheO2e55qeHp/tkEFzYt9AH8du1xJLKKFbsGV5q9vipGNx5XMbj2RMdH5
63
+ VXHTAdc/bLFFy9kybQqo300Rv6ViW2I=
64
+ -----END EC PRIVATE KEY-----"""
65
+
66
+ logger.debug(f"Created SERVER_CERT fixture: {cert_pem[:30]}...")
67
+ return Certificate(cert_pem_or_uri=cert_pem, key_pem_or_uri=key_pem)
68
+
69
+
70
+ @pytest.fixture(scope="module")
71
+ def ca_cert() -> Certificate:
72
+ """Create a self-signed CA certificate for testing."""
73
+ return Certificate.create_ca(
74
+ common_name="Test CA", organization_name="Test Organization", validity_days=365
75
+ )
76
+
77
+
78
+ @pytest.fixture(scope="module")
79
+ def valid_key_pem(client_cert: Certificate) -> str:
80
+ """Get a valid key PEM from the client cert fixture."""
81
+ return client_cert.key
82
+
83
+
84
+ @pytest.fixture
85
+ def valid_cert_pem(client_cert: Certificate) -> str:
86
+ """Get a valid certificate PEM from the client cert fixture."""
87
+ return client_cert.cert
88
+
89
+
90
+ @pytest.fixture
91
+ def invalid_key_pem() -> str:
92
+ """Returns an invalid PEM key."""
93
+ return "INVALID KEY DATA"
94
+
95
+
96
+ @pytest.fixture
97
+ def invalid_cert_pem() -> str:
98
+ """Returns an invalid PEM certificate."""
99
+ return "INVALID CERTIFICATE DATA"
100
+
101
+
102
+ @pytest.fixture
103
+ def malformed_cert_pem() -> str:
104
+ """Returns a PEM certificate with incorrect headers."""
105
+ return "-----BEGIN CERT-----\nMALFORMED DATA\n-----END CERT-----"
106
+
107
+
108
+ @pytest.fixture
109
+ def empty_cert() -> str:
110
+ """Returns an empty certificate string."""
111
+ return ""
112
+
113
+
114
+ @pytest.fixture
115
+ def temporary_cert_file(tmp_path, client_cert) -> str:
116
+ """Creates a temporary file containing the client certificate."""
117
+ cert_file = tmp_path / "client_cert.pem"
118
+ cert_file.write_text(client_cert.cert)
119
+ return f"file://{cert_file}"
120
+
121
+
122
+ @pytest.fixture
123
+ def temporary_key_file(tmp_path, client_cert) -> str:
124
+ """Creates a temporary file containing the client private key."""
125
+ key_file = tmp_path / "client_key.pem"
126
+ key_file.write_text(client_cert.key)
127
+ return f"file://{key_file}"
128
+
129
+
130
+ @pytest.fixture
131
+ def cert_with_windows_line_endings(client_cert) -> str:
132
+ """Returns a certificate PEM with Windows line endings."""
133
+ return client_cert.cert.replace("\n", "\r\n")
134
+
135
+
136
+ @pytest.fixture
137
+ def cert_with_utf8_bom(client_cert) -> str:
138
+ """Returns a certificate PEM with UTF-8 BOM."""
139
+ return "\ufeff" + client_cert.cert
140
+
141
+
142
+ @pytest.fixture
143
+ def cert_with_extra_whitespace(client_cert) -> str:
144
+ """Returns a certificate PEM with extra whitespace."""
145
+ return f" {client_cert.cert} \n\n "
146
+
147
+
148
+ @pytest.fixture(scope="module")
149
+ def external_ca_pem() -> str:
150
+ """Provides an externally generated CA certificate PEM."""
151
+ return """-----BEGIN CERTIFICATE-----
152
+ MIIB4TCCAYegAwIBAgIJAPZ9vcVfR8AdMAoGCCqGSM49BAMCMFExCzAJBgNVBAYT
153
+ AlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FuIEZyYW5jaXNjbzEOMAwGA1UE
154
+ CgwFTXlPcmcxEzARBgNVBAMMCkV4dGVybmFsIENBMB4XDTI0MDgwMjEwNTgwMVoX
155
+ DTM0MDczMDEwNTgwMVowUTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMREwDwYD
156
+ VQQHDAhTYW5EaWVnbzEOMAwGA1UECgwFTXlPcmcxEzARBgNVBAMMCkV4dGVybmFs
157
+ IENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEgyF5Y8upm+M3ZzO8P4n7q2sS+L4c
158
+ mhl5XGg3vIOwFf7lG8XZCgJ6Xy4t1t8oD3zY0m9X8H8Z4YhY7K6b7c8Y7Xv6Y9fV
159
+ Q8M7Jg9nJ0x5c1N40zQwZzKjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E
160
+ BTADAQH/MB0GA1UdDgQWBBTGX00Gq7b09y/0C9eK0XgJp0mY7DAKBggqhkjOPQQD
161
+ AgNJADBGAiEAx1xH/b83/u5t7r29a/THZnFjQ7pvT2N0L4hG4BgGgXACIQD02W2+
162
+ MHB78ZWM+JOgikYj99qD6nLp0nkMyGmkSC7RYg==
163
+ -----END CERTIFICATE-----"""