provide-foundation 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 (149) hide show
  1. provide/__init__.py +15 -0
  2. provide/foundation/__init__.py +155 -0
  3. provide/foundation/_version.py +58 -0
  4. provide/foundation/cli/__init__.py +67 -0
  5. provide/foundation/cli/commands/__init__.py +3 -0
  6. provide/foundation/cli/commands/deps.py +71 -0
  7. provide/foundation/cli/commands/logs/__init__.py +63 -0
  8. provide/foundation/cli/commands/logs/generate.py +357 -0
  9. provide/foundation/cli/commands/logs/generate_old.py +569 -0
  10. provide/foundation/cli/commands/logs/query.py +174 -0
  11. provide/foundation/cli/commands/logs/send.py +166 -0
  12. provide/foundation/cli/commands/logs/tail.py +112 -0
  13. provide/foundation/cli/decorators.py +262 -0
  14. provide/foundation/cli/main.py +65 -0
  15. provide/foundation/cli/testing.py +220 -0
  16. provide/foundation/cli/utils.py +210 -0
  17. provide/foundation/config/__init__.py +106 -0
  18. provide/foundation/config/base.py +295 -0
  19. provide/foundation/config/env.py +369 -0
  20. provide/foundation/config/loader.py +311 -0
  21. provide/foundation/config/manager.py +387 -0
  22. provide/foundation/config/schema.py +284 -0
  23. provide/foundation/config/sync.py +281 -0
  24. provide/foundation/config/types.py +78 -0
  25. provide/foundation/config/validators.py +80 -0
  26. provide/foundation/console/__init__.py +29 -0
  27. provide/foundation/console/input.py +364 -0
  28. provide/foundation/console/output.py +178 -0
  29. provide/foundation/context/__init__.py +12 -0
  30. provide/foundation/context/core.py +356 -0
  31. provide/foundation/core.py +20 -0
  32. provide/foundation/crypto/__init__.py +182 -0
  33. provide/foundation/crypto/algorithms.py +111 -0
  34. provide/foundation/crypto/certificates.py +896 -0
  35. provide/foundation/crypto/checksums.py +301 -0
  36. provide/foundation/crypto/constants.py +57 -0
  37. provide/foundation/crypto/hashing.py +265 -0
  38. provide/foundation/crypto/keys.py +188 -0
  39. provide/foundation/crypto/signatures.py +144 -0
  40. provide/foundation/crypto/utils.py +164 -0
  41. provide/foundation/errors/__init__.py +96 -0
  42. provide/foundation/errors/auth.py +73 -0
  43. provide/foundation/errors/base.py +81 -0
  44. provide/foundation/errors/config.py +103 -0
  45. provide/foundation/errors/context.py +299 -0
  46. provide/foundation/errors/decorators.py +484 -0
  47. provide/foundation/errors/handlers.py +360 -0
  48. provide/foundation/errors/integration.py +105 -0
  49. provide/foundation/errors/platform.py +37 -0
  50. provide/foundation/errors/process.py +140 -0
  51. provide/foundation/errors/resources.py +133 -0
  52. provide/foundation/errors/runtime.py +160 -0
  53. provide/foundation/errors/safe_decorators.py +133 -0
  54. provide/foundation/errors/types.py +276 -0
  55. provide/foundation/file/__init__.py +79 -0
  56. provide/foundation/file/atomic.py +157 -0
  57. provide/foundation/file/directory.py +134 -0
  58. provide/foundation/file/formats.py +236 -0
  59. provide/foundation/file/lock.py +175 -0
  60. provide/foundation/file/safe.py +179 -0
  61. provide/foundation/file/utils.py +170 -0
  62. provide/foundation/hub/__init__.py +88 -0
  63. provide/foundation/hub/click_builder.py +310 -0
  64. provide/foundation/hub/commands.py +42 -0
  65. provide/foundation/hub/components.py +640 -0
  66. provide/foundation/hub/decorators.py +244 -0
  67. provide/foundation/hub/info.py +32 -0
  68. provide/foundation/hub/manager.py +446 -0
  69. provide/foundation/hub/registry.py +279 -0
  70. provide/foundation/hub/type_mapping.py +54 -0
  71. provide/foundation/hub/types.py +28 -0
  72. provide/foundation/logger/__init__.py +41 -0
  73. provide/foundation/logger/base.py +22 -0
  74. provide/foundation/logger/config/__init__.py +16 -0
  75. provide/foundation/logger/config/base.py +40 -0
  76. provide/foundation/logger/config/logging.py +394 -0
  77. provide/foundation/logger/config/telemetry.py +188 -0
  78. provide/foundation/logger/core.py +239 -0
  79. provide/foundation/logger/custom_processors.py +172 -0
  80. provide/foundation/logger/emoji/__init__.py +44 -0
  81. provide/foundation/logger/emoji/matrix.py +209 -0
  82. provide/foundation/logger/emoji/sets.py +458 -0
  83. provide/foundation/logger/emoji/types.py +56 -0
  84. provide/foundation/logger/factories.py +56 -0
  85. provide/foundation/logger/processors/__init__.py +13 -0
  86. provide/foundation/logger/processors/main.py +254 -0
  87. provide/foundation/logger/processors/trace.py +113 -0
  88. provide/foundation/logger/ratelimit/__init__.py +31 -0
  89. provide/foundation/logger/ratelimit/limiters.py +294 -0
  90. provide/foundation/logger/ratelimit/processor.py +203 -0
  91. provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
  92. provide/foundation/logger/setup/__init__.py +29 -0
  93. provide/foundation/logger/setup/coordinator.py +138 -0
  94. provide/foundation/logger/setup/emoji_resolver.py +64 -0
  95. provide/foundation/logger/setup/processors.py +85 -0
  96. provide/foundation/logger/setup/testing.py +39 -0
  97. provide/foundation/logger/trace.py +38 -0
  98. provide/foundation/metrics/__init__.py +119 -0
  99. provide/foundation/metrics/otel.py +122 -0
  100. provide/foundation/metrics/simple.py +165 -0
  101. provide/foundation/observability/__init__.py +53 -0
  102. provide/foundation/observability/openobserve/__init__.py +79 -0
  103. provide/foundation/observability/openobserve/auth.py +72 -0
  104. provide/foundation/observability/openobserve/client.py +307 -0
  105. provide/foundation/observability/openobserve/commands.py +357 -0
  106. provide/foundation/observability/openobserve/exceptions.py +41 -0
  107. provide/foundation/observability/openobserve/formatters.py +298 -0
  108. provide/foundation/observability/openobserve/models.py +134 -0
  109. provide/foundation/observability/openobserve/otlp.py +320 -0
  110. provide/foundation/observability/openobserve/search.py +222 -0
  111. provide/foundation/observability/openobserve/streaming.py +235 -0
  112. provide/foundation/platform/__init__.py +44 -0
  113. provide/foundation/platform/detection.py +193 -0
  114. provide/foundation/platform/info.py +157 -0
  115. provide/foundation/process/__init__.py +39 -0
  116. provide/foundation/process/async_runner.py +373 -0
  117. provide/foundation/process/lifecycle.py +406 -0
  118. provide/foundation/process/runner.py +390 -0
  119. provide/foundation/setup/__init__.py +101 -0
  120. provide/foundation/streams/__init__.py +44 -0
  121. provide/foundation/streams/console.py +57 -0
  122. provide/foundation/streams/core.py +65 -0
  123. provide/foundation/streams/file.py +104 -0
  124. provide/foundation/testing/__init__.py +166 -0
  125. provide/foundation/testing/cli.py +227 -0
  126. provide/foundation/testing/crypto.py +163 -0
  127. provide/foundation/testing/fixtures.py +49 -0
  128. provide/foundation/testing/hub.py +23 -0
  129. provide/foundation/testing/logger.py +106 -0
  130. provide/foundation/testing/streams.py +54 -0
  131. provide/foundation/tracer/__init__.py +49 -0
  132. provide/foundation/tracer/context.py +115 -0
  133. provide/foundation/tracer/otel.py +135 -0
  134. provide/foundation/tracer/spans.py +174 -0
  135. provide/foundation/types.py +32 -0
  136. provide/foundation/utils/__init__.py +97 -0
  137. provide/foundation/utils/deps.py +195 -0
  138. provide/foundation/utils/env.py +491 -0
  139. provide/foundation/utils/formatting.py +483 -0
  140. provide/foundation/utils/parsing.py +235 -0
  141. provide/foundation/utils/rate_limiting.py +112 -0
  142. provide/foundation/utils/streams.py +67 -0
  143. provide/foundation/utils/timing.py +93 -0
  144. provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
  145. provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
  146. provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
  147. provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  148. provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
  149. provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,104 @@
1
+ #
2
+ # file.py
3
+ #
4
+ """
5
+ File stream management for Foundation.
6
+ Handles file-based logging streams and file operations.
7
+ """
8
+
9
+ import io
10
+ from pathlib import Path
11
+ import sys
12
+
13
+ from provide.foundation.streams.core import (
14
+ _LOG_FILE_HANDLE,
15
+ _PROVIDE_LOG_STREAM,
16
+ _STREAM_LOCK,
17
+ )
18
+ from provide.foundation.utils.streams import get_safe_stderr
19
+
20
+
21
+ def configure_file_logging(log_file_path: str | None) -> None:
22
+ """
23
+ Configure file logging if a path is provided.
24
+
25
+ Args:
26
+ log_file_path: Path to log file, or None to disable file logging
27
+ """
28
+ global _PROVIDE_LOG_STREAM, _LOG_FILE_HANDLE
29
+
30
+ # Import here to avoid circular dependency
31
+ from provide.foundation.streams.core import _is_in_click_testing
32
+
33
+ with _STREAM_LOCK:
34
+ # Don't modify streams if we're in Click testing context
35
+ if _is_in_click_testing():
36
+ return
37
+ # Close existing file handle if it exists
38
+ if _LOG_FILE_HANDLE and _LOG_FILE_HANDLE is not _PROVIDE_LOG_STREAM:
39
+ try:
40
+ _LOG_FILE_HANDLE.close()
41
+ except Exception:
42
+ pass
43
+ _LOG_FILE_HANDLE = None
44
+
45
+ # Check if we're in testing mode
46
+ is_test_stream = _PROVIDE_LOG_STREAM is not sys.stderr and not isinstance(
47
+ _PROVIDE_LOG_STREAM, io.TextIOWrapper
48
+ )
49
+
50
+ if log_file_path:
51
+ try:
52
+ Path(log_file_path).parent.mkdir(parents=True, exist_ok=True)
53
+ _LOG_FILE_HANDLE = open(
54
+ log_file_path, "a", encoding="utf-8", buffering=1
55
+ )
56
+ _PROVIDE_LOG_STREAM = _LOG_FILE_HANDLE
57
+ except Exception as e:
58
+ # Log error to stderr and fall back
59
+ print(f"Failed to open log file {log_file_path}: {e}", file=sys.stderr)
60
+ _PROVIDE_LOG_STREAM = get_safe_stderr()
61
+ elif not is_test_stream:
62
+ _PROVIDE_LOG_STREAM = get_safe_stderr()
63
+
64
+
65
+ def flush_log_streams() -> None:
66
+ """Flush all log streams."""
67
+ global _LOG_FILE_HANDLE
68
+
69
+ with _STREAM_LOCK:
70
+ if _LOG_FILE_HANDLE:
71
+ try:
72
+ _LOG_FILE_HANDLE.flush()
73
+ except Exception as e:
74
+ print(f"Failed to flush log file handle: {e}", file=sys.stderr)
75
+
76
+
77
+ def close_log_streams() -> None:
78
+ """Close file log streams and reset to stderr."""
79
+ global _PROVIDE_LOG_STREAM, _LOG_FILE_HANDLE
80
+
81
+ # Import here to avoid circular dependency
82
+ from provide.foundation.streams.core import _is_in_click_testing
83
+
84
+ with _STREAM_LOCK:
85
+ if _LOG_FILE_HANDLE:
86
+ try:
87
+ _LOG_FILE_HANDLE.close()
88
+ except Exception:
89
+ pass
90
+ _LOG_FILE_HANDLE = None
91
+
92
+ # Don't reset stream to stderr if we're in Click testing context
93
+ if not _is_in_click_testing():
94
+ _PROVIDE_LOG_STREAM = sys.stderr
95
+
96
+
97
+ def reset_streams() -> None:
98
+ """Reset all stream state (for testing)."""
99
+ # Import here to avoid circular dependency
100
+ from provide.foundation.streams.core import _is_in_click_testing
101
+
102
+ # Don't reset streams if we're in Click testing context
103
+ if not _is_in_click_testing():
104
+ close_log_streams()
@@ -0,0 +1,166 @@
1
+ #
2
+ # __init__.py
3
+ #
4
+ """
5
+ Foundation Testing Module.
6
+
7
+ Unified testing utilities for Foundation with automatic context detection and warnings.
8
+ This module consolidates all testing helpers in one place with clear warnings when active.
9
+ """
10
+
11
+ import os
12
+ import sys
13
+ from typing import Any
14
+ import warnings
15
+
16
+
17
+ # Context detection functions
18
+ def _is_testing_context() -> bool:
19
+ """Detect if we're running in a testing context."""
20
+ return (
21
+ "pytest" in sys.modules
22
+ or os.getenv("PYTEST_CURRENT_TEST") is not None
23
+ or "unittest" in sys.modules
24
+ or os.getenv("TESTING") == "true"
25
+ or any(arg.endswith(("pytest", "py.test")) for arg in sys.argv)
26
+ )
27
+
28
+
29
+ def _warn_testing_active() -> None:
30
+ """Issue warning that testing helpers are active."""
31
+ if not os.getenv("FOUNDATION_SUPPRESS_TESTING_WARNINGS"):
32
+ warnings.warn(
33
+ "🚨 Foundation testing helpers are active - production behavior may differ",
34
+ UserWarning,
35
+ stacklevel=3,
36
+ )
37
+
38
+
39
+ def _should_warn() -> bool:
40
+ """Check if we should issue testing warnings."""
41
+ # Don't warn if explicitly suppressed
42
+ if os.getenv("FOUNDATION_SUPPRESS_TESTING_WARNINGS"):
43
+ return False
44
+
45
+ # Don't warn if we're not in a testing context
46
+ if not _is_testing_context():
47
+ return False
48
+
49
+ # Don't warn if already warned (module-level state)
50
+ return not getattr(_should_warn, "_warned", False)
51
+
52
+
53
+ # Issue warning on import if in testing context
54
+ if _should_warn():
55
+ _warn_testing_active()
56
+ _should_warn._warned = True
57
+
58
+
59
+ # Lazy imports to avoid importing testing utilities in production
60
+ def __getattr__(name: str) -> Any:
61
+ """Lazy import testing utilities only when accessed."""
62
+
63
+ # CLI testing utilities
64
+ if name in [
65
+ "MockContext",
66
+ "isolated_cli_runner",
67
+ "temp_config_file",
68
+ "create_test_cli",
69
+ "mock_logger",
70
+ "CliTestCase",
71
+ ]:
72
+ import provide.foundation.testing.cli as cli_module
73
+
74
+ return getattr(cli_module, name)
75
+
76
+ # Logger testing utilities
77
+ elif name in ["reset_foundation_setup_for_testing", "reset_foundation_state"]:
78
+ import provide.foundation.testing.logger as logger_module
79
+
80
+ return getattr(logger_module, name)
81
+
82
+ # Stream testing utilities
83
+ elif name in ["set_log_stream_for_testing"]:
84
+ import provide.foundation.testing.streams as streams_module
85
+
86
+ return getattr(streams_module, name)
87
+
88
+ # Fixture utilities
89
+ elif name in [
90
+ "captured_stderr_for_foundation",
91
+ "setup_foundation_telemetry_for_test",
92
+ ]:
93
+ import provide.foundation.testing.fixtures as fixtures_module
94
+
95
+ return getattr(fixtures_module, name)
96
+
97
+ # Crypto fixtures (many fixtures)
98
+ elif name in [
99
+ "client_cert",
100
+ "server_cert",
101
+ "ca_cert",
102
+ "valid_cert_pem",
103
+ "valid_key_pem",
104
+ "invalid_cert_pem",
105
+ "invalid_key_pem",
106
+ "malformed_cert_pem",
107
+ "empty_cert",
108
+ "temporary_cert_file",
109
+ "temporary_key_file",
110
+ "cert_with_windows_line_endings",
111
+ "cert_with_utf8_bom",
112
+ "cert_with_extra_whitespace",
113
+ "external_ca_pem",
114
+ ]:
115
+ import provide.foundation.testing.crypto as crypto_module
116
+
117
+ return getattr(crypto_module, name)
118
+
119
+ # Hub fixtures
120
+ elif name in ["default_container_directory"]:
121
+ import provide.foundation.testing.hub as hub_module
122
+
123
+ return getattr(hub_module, name)
124
+
125
+ else:
126
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
127
+
128
+
129
+ # Public API - these will be available for import but loaded lazily
130
+ __all__ = [
131
+ # Context detection
132
+ "_is_testing_context",
133
+ # CLI testing
134
+ "MockContext",
135
+ "isolated_cli_runner",
136
+ "temp_config_file",
137
+ "create_test_cli",
138
+ "mock_logger",
139
+ "CliTestCase",
140
+ # Logger testing
141
+ "reset_foundation_setup_for_testing",
142
+ "reset_foundation_state",
143
+ # Stream testing
144
+ "set_log_stream_for_testing",
145
+ # Common fixtures
146
+ "captured_stderr_for_foundation",
147
+ "setup_foundation_telemetry_for_test",
148
+ # Crypto fixtures
149
+ "client_cert",
150
+ "server_cert",
151
+ "ca_cert",
152
+ "valid_cert_pem",
153
+ "valid_key_pem",
154
+ "invalid_cert_pem",
155
+ "invalid_key_pem",
156
+ "malformed_cert_pem",
157
+ "empty_cert",
158
+ "temporary_cert_file",
159
+ "temporary_key_file",
160
+ "cert_with_windows_line_endings",
161
+ "cert_with_utf8_bom",
162
+ "cert_with_extra_whitespace",
163
+ "external_ca_pem",
164
+ # Hub fixtures
165
+ "default_container_directory",
166
+ ]
@@ -0,0 +1,227 @@
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 contextlib import contextmanager
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+ import tempfile
13
+ from typing import Any
14
+ from unittest.mock import MagicMock
15
+
16
+ import click
17
+ from click.testing import CliRunner
18
+
19
+ from provide.foundation.context import Context
20
+ from provide.foundation.logger import get_logger
21
+
22
+ log = get_logger(__name__)
23
+
24
+
25
+ class MockContext(Context):
26
+ """Mock context for testing that tracks method calls."""
27
+
28
+ def __init__(self, **kwargs) -> None:
29
+ """Initialize mock context with tracking."""
30
+ super().__init__(**kwargs)
31
+ self.calls = []
32
+ self.saved_configs = []
33
+ self.loaded_configs = []
34
+
35
+ def save_config(self, path: str | Path) -> None:
36
+ """Track save_config calls."""
37
+ self.saved_configs.append(path)
38
+ super().save_config(path)
39
+
40
+ def load_config(self, path: str | Path) -> None:
41
+ """Track load_config calls."""
42
+ self.loaded_configs.append(path)
43
+ super().load_config(path)
44
+
45
+
46
+ @contextmanager
47
+ def isolated_cli_runner(
48
+ env: dict[str, str] | None = None,
49
+ ):
50
+ """
51
+ Create an isolated test environment for CLI testing.
52
+
53
+ Args:
54
+ env: Environment variables to set
55
+
56
+ Yields:
57
+ CliRunner instance in isolated filesystem
58
+ """
59
+ runner = CliRunner()
60
+
61
+ with runner.isolated_filesystem():
62
+ # Set up environment
63
+ old_env = {}
64
+ if env:
65
+ for key, value in env.items():
66
+ old_env[key] = os.environ.get(key)
67
+ os.environ[key] = value
68
+
69
+ try:
70
+ yield runner
71
+ finally:
72
+ # Restore environment
73
+ for key, old_value in old_env.items():
74
+ if old_value is None:
75
+ os.environ.pop(key, None)
76
+ else:
77
+ os.environ[key] = old_value
78
+
79
+
80
+ @contextmanager
81
+ def temp_config_file(
82
+ content: dict[str, Any] | str,
83
+ format: str = "json",
84
+ ) -> Path:
85
+ """
86
+ Create a temporary configuration file for testing.
87
+
88
+ Args:
89
+ content: Configuration content (dict or string)
90
+ format: File format (json, toml, yaml)
91
+
92
+ Yields:
93
+ Path to temporary config file
94
+ """
95
+ suffix = f".{format}"
96
+
97
+ with tempfile.NamedTemporaryFile(
98
+ mode="w",
99
+ suffix=suffix,
100
+ delete=False,
101
+ ) as f:
102
+ if isinstance(content, dict):
103
+ if format == "json":
104
+ json.dump(content, f, indent=2)
105
+ elif format == "toml":
106
+ try:
107
+ import tomli_w
108
+
109
+ # tomli_w needs the content as a string, not written to file handle
110
+ toml_content = tomli_w.dumps(content)
111
+ f.write(toml_content)
112
+ except ImportError:
113
+ # Fall back to manual formatting
114
+ for key, value in content.items():
115
+ if isinstance(value, str):
116
+ f.write(f'{key} = "{value}"\n')
117
+ else:
118
+ f.write(f"{key} = {value}\n")
119
+ elif format == "yaml":
120
+ try:
121
+ import yaml
122
+
123
+ yaml.safe_dump(content, f)
124
+ except ImportError:
125
+ raise ImportError("PyYAML required for YAML testing")
126
+ else:
127
+ f.write(content)
128
+
129
+ config_path = Path(f.name)
130
+
131
+ try:
132
+ yield config_path
133
+ finally:
134
+ config_path.unlink(missing_ok=True)
135
+
136
+
137
+ def create_test_cli(
138
+ name: str = "test-cli",
139
+ version: str = "1.0.0",
140
+ commands: list[click.Command] | None = None,
141
+ ) -> click.Group:
142
+ """
143
+ Create a test CLI group with standard options.
144
+
145
+ Args:
146
+ name: CLI name
147
+ version: CLI version
148
+ commands: Optional list of commands to add
149
+
150
+ Returns:
151
+ Click Group configured for testing
152
+ """
153
+ from provide.foundation.cli.decorators import standard_options
154
+
155
+ @click.group(name=name)
156
+ @standard_options
157
+ @click.pass_context
158
+ def cli(ctx, **kwargs) -> None:
159
+ """Test CLI for testing."""
160
+ ctx.obj = Context(**{k: v for k, v in kwargs.items() if v is not None})
161
+
162
+ if commands:
163
+ for cmd in commands:
164
+ cli.add_command(cmd)
165
+
166
+ return cli
167
+
168
+
169
+ def mock_logger():
170
+ """
171
+ Create a mock logger for testing.
172
+
173
+ Returns:
174
+ MagicMock with common logger methods
175
+ """
176
+ mock = MagicMock()
177
+ mock.debug = MagicMock()
178
+ mock.info = MagicMock()
179
+ mock.warning = MagicMock()
180
+ mock.error = MagicMock()
181
+ mock.critical = MagicMock()
182
+ return mock
183
+
184
+
185
+ class CliTestCase:
186
+ """Base class for CLI test cases with common utilities."""
187
+
188
+ def setup_method(self) -> None:
189
+ """Set up test case."""
190
+ self.runner = CliRunner()
191
+ self.temp_files = []
192
+
193
+ def teardown_method(self) -> None:
194
+ """Clean up test case."""
195
+ for path in self.temp_files:
196
+ if path.exists():
197
+ path.unlink()
198
+
199
+ def invoke(self, *args, **kwargs):
200
+ """Invoke CLI command."""
201
+ return self.runner.invoke(*args, **kwargs)
202
+
203
+ def create_temp_file(self, content: str = "", suffix: str = "") -> Path:
204
+ """Create a temporary file that will be cleaned up."""
205
+ with tempfile.NamedTemporaryFile(
206
+ mode="w",
207
+ suffix=suffix,
208
+ delete=False,
209
+ ) as f:
210
+ f.write(content)
211
+ path = Path(f.name)
212
+
213
+ self.temp_files.append(path)
214
+ return path
215
+
216
+ def assert_json_output(self, result, expected: dict[str, Any]) -> None:
217
+ """Assert that output is valid JSON matching expected."""
218
+ try:
219
+ output = json.loads(result.output)
220
+ except json.JSONDecodeError as e:
221
+ raise AssertionError(f"Output is not valid JSON: {e}\n{result.output}")
222
+
223
+ for key, value in expected.items():
224
+ assert key in output, f"Key '{key}' not in output"
225
+ assert output[key] == value, (
226
+ f"Value mismatch for '{key}': {output[key]} != {value}"
227
+ )
@@ -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():
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():
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():
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):
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):
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-----"""
@@ -0,0 +1,49 @@
1
+ """
2
+ Common Test Fixtures for Foundation.
3
+
4
+ Provides pytest fixtures for capturing output, setting up telemetry,
5
+ and other common testing scenarios across the Foundation test suite.
6
+ """
7
+
8
+ from collections.abc import Callable, Generator
9
+ import io
10
+ from typing import TextIO
11
+
12
+ import pytest
13
+
14
+ from provide.foundation import TelemetryConfig, setup_telemetry
15
+ from provide.foundation.testing.streams import set_log_stream_for_testing
16
+
17
+
18
+ @pytest.fixture
19
+ def captured_stderr_for_foundation() -> Generator[TextIO]:
20
+ """
21
+ Fixture to capture stderr output from Foundation's logging system.
22
+
23
+ It redirects Foundation's log stream to an `io.StringIO` buffer, yields the buffer
24
+ to the test, and then restores the original stream.
25
+ """
26
+ current_test_stream = io.StringIO()
27
+ set_log_stream_for_testing(current_test_stream)
28
+ yield current_test_stream
29
+ set_log_stream_for_testing(None)
30
+ current_test_stream.close()
31
+
32
+
33
+ @pytest.fixture
34
+ def setup_foundation_telemetry_for_test(
35
+ captured_stderr_for_foundation: TextIO,
36
+ ) -> Callable[[TelemetryConfig | None], None]:
37
+ """
38
+ Fixture providing a function to set up Foundation Telemetry for tests.
39
+
40
+ This fixture captures stderr via `captured_stderr_for_foundation`
41
+ and provides a callable to configure telemetry with custom settings.
42
+ """
43
+
44
+ def _setup(config: TelemetryConfig | None = None) -> None:
45
+ if config is None:
46
+ config = TelemetryConfig()
47
+ setup_telemetry(config)
48
+
49
+ return _setup