nat-engine 1__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 (299) hide show
  1. mannf/__init__.py +33 -0
  2. mannf/__main__.py +10 -0
  3. mannf/_version.py +8 -0
  4. mannf/agents/__init__.py +7 -0
  5. mannf/agents/analyzer_agent.py +9 -0
  6. mannf/agents/base.py +9 -0
  7. mannf/agents/bdi_agent.py +9 -0
  8. mannf/agents/belief_state.py +9 -0
  9. mannf/agents/coordinator_agent.py +9 -0
  10. mannf/agents/executor_agent.py +9 -0
  11. mannf/agents/monitor_agent.py +9 -0
  12. mannf/agents/oracle_agent.py +9 -0
  13. mannf/agents/planner_agent.py +9 -0
  14. mannf/agents/test_agent.py +9 -0
  15. mannf/anomaly/__init__.py +7 -0
  16. mannf/anomaly/enhanced_detector.py +9 -0
  17. mannf/cli.py +9 -0
  18. mannf/core/__init__.py +26 -0
  19. mannf/core/agents/__init__.py +52 -0
  20. mannf/core/agents/accessibility_scanner_agent.py +245 -0
  21. mannf/core/agents/analyzer_agent.py +224 -0
  22. mannf/core/agents/autonomous_loop_agent.py +1086 -0
  23. mannf/core/agents/autonomous_loop_models.py +62 -0
  24. mannf/core/agents/autonomous_run_differ.py +427 -0
  25. mannf/core/agents/base.py +128 -0
  26. mannf/core/agents/bdi_agent.py +330 -0
  27. mannf/core/agents/belief_state.py +202 -0
  28. mannf/core/agents/browser_coordinator_agent.py +224 -0
  29. mannf/core/agents/browser_executor_agent.py +410 -0
  30. mannf/core/agents/coordinator_agent.py +262 -0
  31. mannf/core/agents/executor_agent.py +222 -0
  32. mannf/core/agents/monitor_agent.py +188 -0
  33. mannf/core/agents/oracle_agent.py +150 -0
  34. mannf/core/agents/performance_testing_agent.py +279 -0
  35. mannf/core/agents/planner_agent.py +128 -0
  36. mannf/core/agents/test_agent.py +249 -0
  37. mannf/core/agents/visual_regression_agent.py +311 -0
  38. mannf/core/agents/web_crawler_agent.py +510 -0
  39. mannf/core/agents/worker_pool.py +366 -0
  40. mannf/core/anomaly/__init__.py +14 -0
  41. mannf/core/anomaly/enhanced_detector.py +541 -0
  42. mannf/core/browser/__init__.py +63 -0
  43. mannf/core/browser/accessibility_scanner.py +424 -0
  44. mannf/core/browser/discovery_model.py +178 -0
  45. mannf/core/browser/dom_snapshot.py +349 -0
  46. mannf/core/browser/ingestor_bridge.py +371 -0
  47. mannf/core/browser/performance_metrics.py +217 -0
  48. mannf/core/browser/reflection_analyzer.py +442 -0
  49. mannf/core/browser/scenario_generator.py +1100 -0
  50. mannf/core/browser/security_scenario_generator.py +695 -0
  51. mannf/core/browser/visual_comparer.py +159 -0
  52. mannf/core/diagnostics/__init__.py +28 -0
  53. mannf/core/diagnostics/failure_clusterer.py +211 -0
  54. mannf/core/diagnostics/flake_detector.py +233 -0
  55. mannf/core/diagnostics/root_cause_analyzer.py +273 -0
  56. mannf/core/distributed/__init__.py +16 -0
  57. mannf/core/distributed/endpoint.py +139 -0
  58. mannf/core/distributed/system_under_test.py +207 -0
  59. mannf/core/functional_orchestrator.py +428 -0
  60. mannf/core/messaging/__init__.py +11 -0
  61. mannf/core/messaging/bus.py +113 -0
  62. mannf/core/messaging/messages.py +89 -0
  63. mannf/core/nat_orchestrator.py +342 -0
  64. mannf/core/neural/__init__.py +183 -0
  65. mannf/core/orchestrator.py +272 -0
  66. mannf/core/prioritization/__init__.py +17 -0
  67. mannf/core/prioritization/adaptive_controller.py +509 -0
  68. mannf/core/prioritization/belief_prioritizer.py +231 -0
  69. mannf/core/prioritization/risk_scorer.py +430 -0
  70. mannf/core/reporting/__init__.py +12 -0
  71. mannf/core/reporting/unified_report.py +664 -0
  72. mannf/core/testing/__init__.py +17 -0
  73. mannf/core/testing/adaptive_controller.py +149 -0
  74. mannf/core/testing/models.py +179 -0
  75. mannf/core/validation/__init__.py +10 -0
  76. mannf/core/validation/self_validation_runner.py +180 -0
  77. mannf/dashboard/__init__.py +7 -0
  78. mannf/dashboard/app.py +9 -0
  79. mannf/dashboard/models.py +9 -0
  80. mannf/dashboard/static/index.html +2538 -0
  81. mannf/dashboard/telemetry.py +9 -0
  82. mannf/distributed/__init__.py +7 -0
  83. mannf/distributed/endpoint.py +9 -0
  84. mannf/distributed/system_under_test.py +9 -0
  85. mannf/healing/__init__.py +7 -0
  86. mannf/healing/graphql_schema_diff.py +9 -0
  87. mannf/healing/healer.py +9 -0
  88. mannf/healing/models.py +9 -0
  89. mannf/healing/schema_diff.py +9 -0
  90. mannf/integrations/__init__.py +7 -0
  91. mannf/integrations/auth.py +9 -0
  92. mannf/integrations/graphql_parser.py +9 -0
  93. mannf/integrations/graphql_sut.py +9 -0
  94. mannf/integrations/http_sut.py +9 -0
  95. mannf/integrations/openapi_parser.py +9 -0
  96. mannf/integrations/postman_parser.py +9 -0
  97. mannf/llm/__init__.py +7 -0
  98. mannf/llm/anthropic_provider.py +9 -0
  99. mannf/llm/base.py +9 -0
  100. mannf/llm/config.py +9 -0
  101. mannf/llm/factory.py +9 -0
  102. mannf/llm/openai_provider.py +9 -0
  103. mannf/llm/prompts.py +9 -0
  104. mannf/messaging/__init__.py +7 -0
  105. mannf/messaging/bus.py +9 -0
  106. mannf/messaging/messages.py +9 -0
  107. mannf/nat_orchestrator.py +9 -0
  108. mannf/neural/__init__.py +7 -0
  109. mannf/orchestrator.py +9 -0
  110. mannf/prioritization/__init__.py +7 -0
  111. mannf/prioritization/adaptive_controller.py +9 -0
  112. mannf/prioritization/belief_prioritizer.py +9 -0
  113. mannf/prioritization/risk_scorer.py +9 -0
  114. mannf/product/__init__.py +29 -0
  115. mannf/product/admin/__init__.py +3 -0
  116. mannf/product/admin/routes.py +514 -0
  117. mannf/product/auth/__init__.py +5 -0
  118. mannf/product/auth/saml.py +212 -0
  119. mannf/product/billing/__init__.py +5 -0
  120. mannf/product/billing/audit.py +160 -0
  121. mannf/product/billing/feature_gates.py +180 -0
  122. mannf/product/billing/metering.py +179 -0
  123. mannf/product/billing/notifications.py +181 -0
  124. mannf/product/billing/plans.py +133 -0
  125. mannf/product/billing/rate_limits.py +35 -0
  126. mannf/product/billing/stripe_billing.py +906 -0
  127. mannf/product/billing/tenant_auth.py +233 -0
  128. mannf/product/billing/tenant_manager.py +873 -0
  129. mannf/product/cli.py +3900 -0
  130. mannf/product/cli_admin.py +408 -0
  131. mannf/product/dashboard/__init__.py +61 -0
  132. mannf/product/dashboard/app.py +3567 -0
  133. mannf/product/dashboard/models.py +460 -0
  134. mannf/product/dashboard/static/index.html +6347 -0
  135. mannf/product/dashboard/static/manifest.json +25 -0
  136. mannf/product/dashboard/static/pwa-icon-192.png +0 -0
  137. mannf/product/dashboard/static/pwa-icon-512.png +0 -0
  138. mannf/product/dashboard/static/sw.js +64 -0
  139. mannf/product/dashboard/telemetry.py +547 -0
  140. mannf/product/database.py +145 -0
  141. mannf/product/demo.py +844 -0
  142. mannf/product/doctor.py +509 -0
  143. mannf/product/exporters/__init__.py +65 -0
  144. mannf/product/exporters/azuredevops_exporter.py +257 -0
  145. mannf/product/exporters/base.py +307 -0
  146. mannf/product/exporters/bugzilla_exporter.py +200 -0
  147. mannf/product/exporters/dedup.py +275 -0
  148. mannf/product/exporters/finding_adapter.py +216 -0
  149. mannf/product/exporters/github_exporter.py +197 -0
  150. mannf/product/exporters/gitlab_exporter.py +215 -0
  151. mannf/product/exporters/jira_exporter.py +180 -0
  152. mannf/product/exporters/linear_exporter.py +195 -0
  153. mannf/product/exporters/loader.py +233 -0
  154. mannf/product/exporters/pagerduty_exporter.py +363 -0
  155. mannf/product/exporters/sentry_exporter.py +322 -0
  156. mannf/product/exporters/servicenow_exporter.py +240 -0
  157. mannf/product/exporters/shortcut_exporter.py +231 -0
  158. mannf/product/exporters/webhook_exporter.py +383 -0
  159. mannf/product/formatters/__init__.py +18 -0
  160. mannf/product/formatters/allure_formatter.py +161 -0
  161. mannf/product/formatters/ctrf_formatter.py +149 -0
  162. mannf/product/healing/__init__.py +30 -0
  163. mannf/product/healing/graphql_schema_diff.py +152 -0
  164. mannf/product/healing/healer.py +141 -0
  165. mannf/product/healing/models.py +175 -0
  166. mannf/product/healing/schema_diff.py +251 -0
  167. mannf/product/ingestors/__init__.py +77 -0
  168. mannf/product/ingestors/base.py +256 -0
  169. mannf/product/ingestors/bgstm_ingestor.py +764 -0
  170. mannf/product/ingestors/curl_ingestor.py +1019 -0
  171. mannf/product/ingestors/cypress_ingestor.py +487 -0
  172. mannf/product/ingestors/gherkin_ingestor.py +967 -0
  173. mannf/product/ingestors/graphql_ingestor.py +845 -0
  174. mannf/product/ingestors/grpc_ingestor.py +591 -0
  175. mannf/product/ingestors/har_ingestor.py +976 -0
  176. mannf/product/ingestors/loader.py +284 -0
  177. mannf/product/ingestors/models.py +146 -0
  178. mannf/product/ingestors/openapi_ingestor.py +606 -0
  179. mannf/product/ingestors/playwright_ingestor.py +449 -0
  180. mannf/product/ingestors/postman_ingestor.py +631 -0
  181. mannf/product/ingestors/traffic_ingestor.py +679 -0
  182. mannf/product/ingestors/websocket_ingestor.py +526 -0
  183. mannf/product/integrations/__init__.py +21 -0
  184. mannf/product/integrations/auth.py +190 -0
  185. mannf/product/integrations/graphql_parser.py +436 -0
  186. mannf/product/integrations/graphql_sut.py +247 -0
  187. mannf/product/integrations/grpc_sut.py +469 -0
  188. mannf/product/integrations/http_sut.py +237 -0
  189. mannf/product/integrations/kafka_adapter.py +342 -0
  190. mannf/product/integrations/openapi_parser.py +513 -0
  191. mannf/product/integrations/postman_parser.py +467 -0
  192. mannf/product/integrations/webhook_receiver.py +344 -0
  193. mannf/product/integrations/websocket_sut.py +434 -0
  194. mannf/product/llm/__init__.py +25 -0
  195. mannf/product/llm/anthropic_provider.py +94 -0
  196. mannf/product/llm/base.py +267 -0
  197. mannf/product/llm/config.py +48 -0
  198. mannf/product/llm/factory.py +42 -0
  199. mannf/product/llm/openai_provider.py +93 -0
  200. mannf/product/llm/prompts.py +403 -0
  201. mannf/product/llm/root_cause_service.py +311 -0
  202. mannf/product/llm/test_plan_models.py +78 -0
  203. mannf/product/metrics.py +149 -0
  204. mannf/product/middleware/__init__.py +3 -0
  205. mannf/product/middleware/audit_middleware.py +112 -0
  206. mannf/product/middleware/tenant_isolation.py +114 -0
  207. mannf/product/models.py +347 -0
  208. mannf/product/notifications/__init__.py +24 -0
  209. mannf/product/notifications/dispatcher.py +411 -0
  210. mannf/product/onboarding.py +190 -0
  211. mannf/product/orchestration/__init__.py +39 -0
  212. mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
  213. mannf/product/orchestration/pipeline.py +401 -0
  214. mannf/product/orchestrator.py +987 -0
  215. mannf/product/orchestrator_models.py +269 -0
  216. mannf/product/regression/__init__.py +36 -0
  217. mannf/product/regression/differ.py +172 -0
  218. mannf/product/regression/masking.py +100 -0
  219. mannf/product/regression/models.py +232 -0
  220. mannf/product/regression/recorder.py +124 -0
  221. mannf/product/regression/replayer.py +168 -0
  222. mannf/product/reports/__init__.py +10 -0
  223. mannf/product/reports/pdf.py +132 -0
  224. mannf/product/scheduling/__init__.py +57 -0
  225. mannf/product/scheduling/cron_utils.py +251 -0
  226. mannf/product/scheduling/engine.py +473 -0
  227. mannf/product/scheduling/models.py +86 -0
  228. mannf/product/scheduling/queue.py +894 -0
  229. mannf/product/scheduling/store.py +235 -0
  230. mannf/product/security/__init__.py +21 -0
  231. mannf/product/security/belief_guided.py +143 -0
  232. mannf/product/security/checks/__init__.py +55 -0
  233. mannf/product/security/checks/base.py +69 -0
  234. mannf/product/security/checks/bfla.py +77 -0
  235. mannf/product/security/checks/bola.py +77 -0
  236. mannf/product/security/checks/bopla.py +80 -0
  237. mannf/product/security/checks/broken_auth.py +86 -0
  238. mannf/product/security/checks/graphql_security.py +299 -0
  239. mannf/product/security/checks/inventory.py +70 -0
  240. mannf/product/security/checks/misconfig.py +158 -0
  241. mannf/product/security/checks/resource_consumption.py +70 -0
  242. mannf/product/security/checks/sensitive_flows.py +80 -0
  243. mannf/product/security/checks/ssrf.py +101 -0
  244. mannf/product/security/checks/unsafe_consumption.py +120 -0
  245. mannf/product/security/models.py +92 -0
  246. mannf/product/security/plugin_loader.py +182 -0
  247. mannf/product/security/reporter.py +92 -0
  248. mannf/product/security/scanner.py +183 -0
  249. mannf/product/server.py +6220 -0
  250. mannf/product/setup_wizard.py +873 -0
  251. mannf/product/status.py +404 -0
  252. mannf/product/storage/__init__.py +10 -0
  253. mannf/product/storage/artifact_store.py +343 -0
  254. mannf/product/telemetry.py +300 -0
  255. mannf/product/uninstall.py +169 -0
  256. mannf/product/upgrade.py +139 -0
  257. mannf/product/weights/__init__.py +13 -0
  258. mannf/product/weights/blob_store.py +299 -0
  259. mannf/product/weights/factory.py +42 -0
  260. mannf/product/weights/registry.py +159 -0
  261. mannf/product/weights/store.py +210 -0
  262. mannf/regression/__init__.py +7 -0
  263. mannf/regression/differ.py +9 -0
  264. mannf/regression/masking.py +9 -0
  265. mannf/regression/models.py +9 -0
  266. mannf/regression/recorder.py +9 -0
  267. mannf/regression/replayer.py +9 -0
  268. mannf/security/__init__.py +7 -0
  269. mannf/security/belief_guided.py +9 -0
  270. mannf/security/checks/__init__.py +7 -0
  271. mannf/security/checks/base.py +9 -0
  272. mannf/security/checks/bfla.py +9 -0
  273. mannf/security/checks/bola.py +9 -0
  274. mannf/security/checks/bopla.py +9 -0
  275. mannf/security/checks/broken_auth.py +9 -0
  276. mannf/security/checks/graphql_security.py +9 -0
  277. mannf/security/checks/inventory.py +9 -0
  278. mannf/security/checks/misconfig.py +9 -0
  279. mannf/security/checks/resource_consumption.py +9 -0
  280. mannf/security/checks/sensitive_flows.py +9 -0
  281. mannf/security/checks/ssrf.py +9 -0
  282. mannf/security/checks/unsafe_consumption.py +9 -0
  283. mannf/security/models.py +9 -0
  284. mannf/security/reporter.py +9 -0
  285. mannf/security/scanner.py +9 -0
  286. mannf/server.py +9 -0
  287. mannf/testing/__init__.py +7 -0
  288. mannf/testing/adaptive_controller.py +9 -0
  289. mannf/testing/models.py +9 -0
  290. mannf/weights/__init__.py +7 -0
  291. mannf/weights/registry.py +9 -0
  292. mannf/weights/store.py +9 -0
  293. nat_engine-1.dist-info/METADATA +555 -0
  294. nat_engine-1.dist-info/RECORD +299 -0
  295. nat_engine-1.dist-info/WHEEL +5 -0
  296. nat_engine-1.dist-info/entry_points.txt +4 -0
  297. nat_engine-1.dist-info/licenses/LICENSE +651 -0
  298. nat_engine-1.dist-info/licenses/NOTICE +178 -0
  299. nat_engine-1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,344 @@
1
+ # Copyright (C) 2026 Brad Guider
2
+ # This file is part of NAT (Neural Agent Testing Framework).
3
+ # Licensed under the AGPL-3.0. See LICENSE for details.
4
+ # Commercial licensing available — see COMMERCIAL_LICENSE.md.
5
+
6
+ """Mock webhook receiver for event-driven / async API testing.
7
+
8
+ :class:`WebhookReceiver` spins up a lightweight HTTP server that listens for
9
+ incoming webhook payloads. Test cases can:
10
+
11
+ 1. Register a webhook URL by starting the receiver.
12
+ 2. Trigger the external system (via the API under test or a separate trigger).
13
+ 3. Wait for the expected payload to arrive.
14
+ 4. Assert its contents and validate the schema.
15
+
16
+ This enables autonomous testing of outbound notification flows (GitHub webhooks,
17
+ Stripe events, Slack messages, etc.) without requiring a publicly-routable
18
+ server — the receiver binds to a local port only.
19
+
20
+ Usage::
21
+
22
+ receiver = WebhookReceiver(port=8765)
23
+ await receiver.start()
24
+ # ... trigger the webhook ...
25
+ payload = await receiver.wait_for_payload(timeout=10.0)
26
+ await receiver.stop()
27
+
28
+ The receiver also implements the standard ``execute()`` / ``health_check()``
29
+ interface for integration with the NATOrchestrator.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import asyncio
35
+ import json
36
+ import logging
37
+ import time
38
+ import uuid
39
+ from typing import Any, Callable, Dict, List, Optional
40
+
41
+ from mannf.core.distributed.endpoint import Endpoint, EndpointRegistry
42
+ from mannf.core.distributed.system_under_test import SystemUnderTest
43
+ from mannf.core.testing.models import TestCase, TestResult
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+
48
+ class _WebhookHandler:
49
+ """Minimal ASGI-compatible handler that collects incoming webhook payloads."""
50
+
51
+ def __init__(self) -> None:
52
+ self._payloads: asyncio.Queue = asyncio.Queue()
53
+ self._request_count: int = 0
54
+
55
+ @property
56
+ def request_count(self) -> int:
57
+ return self._request_count
58
+
59
+ async def handle(
60
+ self, method: str, path: str, headers: dict, body: bytes
61
+ ) -> tuple[int, dict, bytes]:
62
+ """Process a single incoming HTTP request. Returns (status, headers, body)."""
63
+ self._request_count += 1
64
+
65
+ content_type = (headers.get("content-type") or "").lower()
66
+ payload: Any
67
+ if "json" in content_type:
68
+ try:
69
+ payload = json.loads(body.decode("utf-8"))
70
+ except (json.JSONDecodeError, UnicodeDecodeError):
71
+ payload = body.decode("utf-8", errors="replace")
72
+ else:
73
+ payload = body.decode("utf-8", errors="replace")
74
+
75
+ await self._payloads.put({
76
+ "method": method,
77
+ "path": path,
78
+ "headers": dict(headers),
79
+ "payload": payload,
80
+ "received_at": time.time(),
81
+ })
82
+
83
+ return 200, {"Content-Type": "application/json"}, b'{"ok": true}'
84
+
85
+ async def get_payload(self, timeout: float = 10.0) -> Optional[dict]:
86
+ """Wait up to *timeout* seconds for the next payload and return it."""
87
+ try:
88
+ return await asyncio.wait_for(self._payloads.get(), timeout=timeout)
89
+ except asyncio.TimeoutError:
90
+ return None
91
+
92
+ def get_all_payloads(self) -> list[dict]:
93
+ """Return all payloads received so far without blocking."""
94
+ results: list[dict] = []
95
+ try:
96
+ while True:
97
+ results.append(self._payloads.get_nowait())
98
+ except asyncio.QueueEmpty:
99
+ pass
100
+ return results
101
+
102
+
103
+ class WebhookReceiver(SystemUnderTest):
104
+ """Mock HTTP server that captures and validates incoming webhook payloads.
105
+
106
+ Parameters
107
+ ----------
108
+ port:
109
+ Local port to listen on (default: 0 = OS-assigned).
110
+ host:
111
+ Interface to bind to (default: ``"127.0.0.1"``).
112
+ path_filter:
113
+ Only capture payloads whose path matches this prefix (default: all).
114
+ validators:
115
+ List of callables ``(payload: dict) -> bool`` run against each
116
+ received payload. Used to assert schema or content correctness.
117
+ """
118
+
119
+ def __init__(
120
+ self,
121
+ port: int = 0,
122
+ host: str = "127.0.0.1",
123
+ path_filter: str = "",
124
+ validators: Optional[List[Callable[[dict], bool]]] = None,
125
+ ) -> None:
126
+ self.host = host
127
+ self.requested_port = port
128
+ self.actual_port: int = 0
129
+ self.path_filter = path_filter
130
+ self.validators: List[Callable[[dict], bool]] = validators or []
131
+
132
+ self._handler = _WebhookHandler()
133
+ self._server: Optional[asyncio.Server] = None
134
+ self._running = False
135
+
136
+ self._registry = EndpointRegistry()
137
+ self._registry.register(
138
+ Endpoint(
139
+ name="default",
140
+ host=host,
141
+ port=port or 0,
142
+ protocol="http",
143
+ health_check_path="/health",
144
+ )
145
+ )
146
+
147
+ # ------------------------------------------------------------------
148
+ # Server lifecycle
149
+ # ------------------------------------------------------------------
150
+
151
+ async def start(self) -> None:
152
+ """Start the webhook receiver server."""
153
+ self._server = await asyncio.start_server(
154
+ self._handle_connection, self.host, self.requested_port
155
+ )
156
+ self.actual_port = self._server.sockets[0].getsockname()[1]
157
+ self._running = True
158
+
159
+ # Update registry with actual port
160
+ self._registry.register(
161
+ Endpoint(
162
+ name="default",
163
+ host=self.host,
164
+ port=self.actual_port,
165
+ protocol="http",
166
+ )
167
+ )
168
+ logger.info("WebhookReceiver listening on %s:%d", self.host, self.actual_port)
169
+
170
+ async def stop(self) -> None:
171
+ """Stop the webhook receiver server."""
172
+ if self._server is not None:
173
+ self._server.close()
174
+ await self._server.wait_closed()
175
+ self._server = None
176
+ self._running = False
177
+ logger.info("WebhookReceiver stopped")
178
+
179
+ @property
180
+ def url(self) -> str:
181
+ """Return the base URL of the receiver (e.g. ``http://127.0.0.1:8765``)."""
182
+ return f"http://{self.host}:{self.actual_port}"
183
+
184
+ async def wait_for_payload(self, timeout: float = 10.0) -> Optional[dict]:
185
+ """Block until a payload is received or *timeout* elapses."""
186
+ return await self._handler.get_payload(timeout=timeout)
187
+
188
+ def get_all_payloads(self) -> list[dict]:
189
+ """Return all payloads received so far."""
190
+ return self._handler.get_all_payloads()
191
+
192
+ @property
193
+ def request_count(self) -> int:
194
+ """Total number of HTTP requests received."""
195
+ return self._handler.request_count
196
+
197
+ # ------------------------------------------------------------------
198
+ # Raw TCP handler
199
+ # ------------------------------------------------------------------
200
+
201
+ async def _handle_connection(
202
+ self,
203
+ reader: asyncio.StreamReader,
204
+ writer: asyncio.StreamWriter,
205
+ ) -> None:
206
+ """Parse a minimal HTTP/1.1 request and dispatch to the handler."""
207
+ try:
208
+ raw = await asyncio.wait_for(reader.read(65536), timeout=5.0)
209
+ except asyncio.TimeoutError:
210
+ writer.close()
211
+ return
212
+
213
+ try:
214
+ # Split headers from body
215
+ header_end = raw.find(b"\r\n\r\n")
216
+ if header_end == -1:
217
+ writer.close()
218
+ return
219
+
220
+ header_bytes = raw[:header_end]
221
+ body = raw[header_end + 4:]
222
+
223
+ lines = header_bytes.decode("utf-8", errors="replace").splitlines()
224
+ if not lines:
225
+ writer.close()
226
+ return
227
+
228
+ # Request line
229
+ parts = lines[0].split()
230
+ method = parts[0] if len(parts) > 0 else "GET"
231
+ path = parts[1] if len(parts) > 1 else "/"
232
+
233
+ # Parse headers
234
+ headers: dict = {}
235
+ for line in lines[1:]:
236
+ if ":" in line:
237
+ key, _, val = line.partition(":")
238
+ headers[key.strip().lower()] = val.strip()
239
+
240
+ # Apply path filter
241
+ if self.path_filter and not path.startswith(self.path_filter):
242
+ status, resp_headers, resp_body = 404, {}, b"Not Found"
243
+ else:
244
+ status, resp_headers, resp_body = await self._handler.handle(
245
+ method, path, headers, body
246
+ )
247
+
248
+ # Build minimal HTTP response
249
+ header_lines = [f"HTTP/1.1 {status} OK"]
250
+ for k, v in resp_headers.items():
251
+ header_lines.append(f"{k}: {v}")
252
+ header_lines.append(f"Content-Length: {len(resp_body)}")
253
+ header_lines.append("Connection: close")
254
+ response = "\r\n".join(header_lines) + "\r\n\r\n"
255
+ writer.write(response.encode("utf-8") + resp_body)
256
+ await writer.drain()
257
+ except Exception as exc: # noqa: BLE001
258
+ logger.debug("WebhookReceiver: error handling request: %s", exc)
259
+ finally:
260
+ writer.close()
261
+
262
+ # ------------------------------------------------------------------
263
+ # SystemUnderTest interface
264
+ # ------------------------------------------------------------------
265
+
266
+ @property
267
+ def registry(self) -> EndpointRegistry:
268
+ return self._registry
269
+
270
+ async def execute(self, test_case: TestCase) -> TestResult:
271
+ """Wait for a webhook payload and validate it.
272
+
273
+ ``test_case.target`` is used as a path filter suffix.
274
+
275
+ ``test_case.inputs`` may contain:
276
+
277
+ * ``timeout`` — how long to wait for the payload (default: ``10.0``).
278
+ * ``expected_keys`` — list of top-level JSON keys to verify.
279
+ * ``expected_values`` — dict of ``{key: expected_value}`` pairs to
280
+ check against the received payload.
281
+ """
282
+ timeout = float(test_case.inputs.get("timeout") or 10.0)
283
+ expected_keys: list = test_case.inputs.get("expected_keys") or []
284
+ expected_values: dict = test_case.inputs.get("expected_values") or {}
285
+
286
+ start = time.monotonic()
287
+ record = await self.wait_for_payload(timeout=timeout)
288
+ elapsed_ms = (time.monotonic() - start) * 1000.0
289
+
290
+ if record is None:
291
+ return TestResult(
292
+ test_case_id=test_case.id,
293
+ passed=False,
294
+ actual_output=None,
295
+ execution_time_ms=elapsed_ms,
296
+ error="No webhook payload received within timeout",
297
+ )
298
+
299
+ payload = record.get("payload")
300
+
301
+ # Run validators
302
+ error_msg: Optional[str] = None
303
+ passed = True
304
+
305
+ if expected_keys and isinstance(payload, dict):
306
+ missing = [k for k in expected_keys if k not in payload]
307
+ if missing:
308
+ passed = False
309
+ error_msg = f"Webhook payload missing expected keys: {missing}"
310
+
311
+ if passed and expected_values and isinstance(payload, dict):
312
+ for key, expected_val in expected_values.items():
313
+ actual_val = payload.get(key)
314
+ if actual_val != expected_val:
315
+ passed = False
316
+ error_msg = (
317
+ f"Webhook payload key {key!r}: "
318
+ f"expected {expected_val!r}, got {actual_val!r}"
319
+ )
320
+ break
321
+
322
+ if passed:
323
+ for validator in self.validators:
324
+ try:
325
+ if not validator(payload):
326
+ passed = False
327
+ error_msg = "Custom webhook validator returned False"
328
+ break
329
+ except Exception as exc: # noqa: BLE001
330
+ passed = False
331
+ error_msg = f"Custom webhook validator raised: {exc}"
332
+ break
333
+
334
+ return TestResult(
335
+ test_case_id=test_case.id,
336
+ passed=passed,
337
+ actual_output=payload,
338
+ execution_time_ms=elapsed_ms,
339
+ error=error_msg,
340
+ )
341
+
342
+ async def health_check(self) -> bool:
343
+ """Return True if the server is currently running."""
344
+ return self._running