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.
- provide/__init__.py +3 -0
- provide/testkit/__init__.py +248 -0
- provide/testkit/archive/__init__.py +24 -0
- provide/testkit/archive/fixtures.py +217 -0
- provide/testkit/cli.py +229 -0
- provide/testkit/common/__init__.py +32 -0
- provide/testkit/common/fixtures.py +234 -0
- provide/testkit/crypto.py +163 -0
- provide/testkit/environment.py +79 -0
- provide/testkit/file/__init__.py +40 -0
- provide/testkit/file/content_fixtures.py +275 -0
- provide/testkit/file/directory_fixtures.py +105 -0
- provide/testkit/file/fixtures.py +49 -0
- provide/testkit/file/special_fixtures.py +141 -0
- provide/testkit/fixtures.py +52 -0
- provide/testkit/harness.py +122 -0
- provide/testkit/hub.py +22 -0
- provide/testkit/logger/__init__.py +39 -0
- provide/testkit/logger/hooks.py +100 -0
- provide/testkit/logger/reset.py +230 -0
- provide/testkit/main.py +22 -0
- provide/testkit/mocking/__init__.py +46 -0
- provide/testkit/mocking/fixtures.py +340 -0
- provide/testkit/process/__init__.py +48 -0
- provide/testkit/process/async_fixtures.py +410 -0
- provide/testkit/process/fixtures.py +54 -0
- provide/testkit/process/subprocess_fixtures.py +208 -0
- provide/testkit/quality/__init__.py +101 -0
- provide/testkit/quality/artifacts.py +360 -0
- provide/testkit/quality/base.py +158 -0
- provide/testkit/quality/cli.py +394 -0
- provide/testkit/quality/complexity/__init__.py +30 -0
- provide/testkit/quality/complexity/analyzer.py +392 -0
- provide/testkit/quality/complexity/fixture.py +196 -0
- provide/testkit/quality/coverage/__init__.py +36 -0
- provide/testkit/quality/coverage/fixture.py +236 -0
- provide/testkit/quality/coverage/reporter.py +150 -0
- provide/testkit/quality/coverage/tracker.py +313 -0
- provide/testkit/quality/decorators.py +380 -0
- provide/testkit/quality/documentation/__init__.py +29 -0
- provide/testkit/quality/documentation/checker.py +361 -0
- provide/testkit/quality/documentation/fixture.py +187 -0
- provide/testkit/quality/profiling/__init__.py +30 -0
- provide/testkit/quality/profiling/fixture.py +332 -0
- provide/testkit/quality/profiling/profiler.py +428 -0
- provide/testkit/quality/report.py +266 -0
- provide/testkit/quality/runner.py +319 -0
- provide/testkit/quality/security/__init__.py +29 -0
- provide/testkit/quality/security/fixture.py +196 -0
- provide/testkit/quality/security/scanner.py +338 -0
- provide/testkit/streams.py +54 -0
- provide/testkit/threading/__init__.py +38 -0
- provide/testkit/threading/basic_fixtures.py +103 -0
- provide/testkit/threading/data_fixtures.py +101 -0
- provide/testkit/threading/execution_fixtures.py +268 -0
- provide/testkit/threading/fixtures.py +50 -0
- provide/testkit/threading/sync_fixtures.py +98 -0
- provide/testkit/time/__init__.py +32 -0
- provide/testkit/time/fixtures.py +416 -0
- provide/testkit/transport/__init__.py +30 -0
- provide/testkit/transport/fixtures.py +278 -0
- provide_testkit-0.0.0.dev0.dist-info/METADATA +145 -0
- provide_testkit-0.0.0.dev0.dist-info/RECORD +66 -0
- provide_testkit-0.0.0.dev0.dist-info/WHEEL +5 -0
- provide_testkit-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- 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-----"""
|