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.
- mannf/__init__.py +33 -0
- mannf/__main__.py +10 -0
- mannf/_version.py +8 -0
- mannf/agents/__init__.py +7 -0
- mannf/agents/analyzer_agent.py +9 -0
- mannf/agents/base.py +9 -0
- mannf/agents/bdi_agent.py +9 -0
- mannf/agents/belief_state.py +9 -0
- mannf/agents/coordinator_agent.py +9 -0
- mannf/agents/executor_agent.py +9 -0
- mannf/agents/monitor_agent.py +9 -0
- mannf/agents/oracle_agent.py +9 -0
- mannf/agents/planner_agent.py +9 -0
- mannf/agents/test_agent.py +9 -0
- mannf/anomaly/__init__.py +7 -0
- mannf/anomaly/enhanced_detector.py +9 -0
- mannf/cli.py +9 -0
- mannf/core/__init__.py +26 -0
- mannf/core/agents/__init__.py +52 -0
- mannf/core/agents/accessibility_scanner_agent.py +245 -0
- mannf/core/agents/analyzer_agent.py +224 -0
- mannf/core/agents/autonomous_loop_agent.py +1086 -0
- mannf/core/agents/autonomous_loop_models.py +62 -0
- mannf/core/agents/autonomous_run_differ.py +427 -0
- mannf/core/agents/base.py +128 -0
- mannf/core/agents/bdi_agent.py +330 -0
- mannf/core/agents/belief_state.py +202 -0
- mannf/core/agents/browser_coordinator_agent.py +224 -0
- mannf/core/agents/browser_executor_agent.py +410 -0
- mannf/core/agents/coordinator_agent.py +262 -0
- mannf/core/agents/executor_agent.py +222 -0
- mannf/core/agents/monitor_agent.py +188 -0
- mannf/core/agents/oracle_agent.py +150 -0
- mannf/core/agents/performance_testing_agent.py +279 -0
- mannf/core/agents/planner_agent.py +128 -0
- mannf/core/agents/test_agent.py +249 -0
- mannf/core/agents/visual_regression_agent.py +311 -0
- mannf/core/agents/web_crawler_agent.py +510 -0
- mannf/core/agents/worker_pool.py +366 -0
- mannf/core/anomaly/__init__.py +14 -0
- mannf/core/anomaly/enhanced_detector.py +541 -0
- mannf/core/browser/__init__.py +63 -0
- mannf/core/browser/accessibility_scanner.py +424 -0
- mannf/core/browser/discovery_model.py +178 -0
- mannf/core/browser/dom_snapshot.py +349 -0
- mannf/core/browser/ingestor_bridge.py +371 -0
- mannf/core/browser/performance_metrics.py +217 -0
- mannf/core/browser/reflection_analyzer.py +442 -0
- mannf/core/browser/scenario_generator.py +1100 -0
- mannf/core/browser/security_scenario_generator.py +695 -0
- mannf/core/browser/visual_comparer.py +159 -0
- mannf/core/diagnostics/__init__.py +28 -0
- mannf/core/diagnostics/failure_clusterer.py +211 -0
- mannf/core/diagnostics/flake_detector.py +233 -0
- mannf/core/diagnostics/root_cause_analyzer.py +273 -0
- mannf/core/distributed/__init__.py +16 -0
- mannf/core/distributed/endpoint.py +139 -0
- mannf/core/distributed/system_under_test.py +207 -0
- mannf/core/functional_orchestrator.py +428 -0
- mannf/core/messaging/__init__.py +11 -0
- mannf/core/messaging/bus.py +113 -0
- mannf/core/messaging/messages.py +89 -0
- mannf/core/nat_orchestrator.py +342 -0
- mannf/core/neural/__init__.py +183 -0
- mannf/core/orchestrator.py +272 -0
- mannf/core/prioritization/__init__.py +17 -0
- mannf/core/prioritization/adaptive_controller.py +509 -0
- mannf/core/prioritization/belief_prioritizer.py +231 -0
- mannf/core/prioritization/risk_scorer.py +430 -0
- mannf/core/reporting/__init__.py +12 -0
- mannf/core/reporting/unified_report.py +664 -0
- mannf/core/testing/__init__.py +17 -0
- mannf/core/testing/adaptive_controller.py +149 -0
- mannf/core/testing/models.py +179 -0
- mannf/core/validation/__init__.py +10 -0
- mannf/core/validation/self_validation_runner.py +180 -0
- mannf/dashboard/__init__.py +7 -0
- mannf/dashboard/app.py +9 -0
- mannf/dashboard/models.py +9 -0
- mannf/dashboard/static/index.html +2538 -0
- mannf/dashboard/telemetry.py +9 -0
- mannf/distributed/__init__.py +7 -0
- mannf/distributed/endpoint.py +9 -0
- mannf/distributed/system_under_test.py +9 -0
- mannf/healing/__init__.py +7 -0
- mannf/healing/graphql_schema_diff.py +9 -0
- mannf/healing/healer.py +9 -0
- mannf/healing/models.py +9 -0
- mannf/healing/schema_diff.py +9 -0
- mannf/integrations/__init__.py +7 -0
- mannf/integrations/auth.py +9 -0
- mannf/integrations/graphql_parser.py +9 -0
- mannf/integrations/graphql_sut.py +9 -0
- mannf/integrations/http_sut.py +9 -0
- mannf/integrations/openapi_parser.py +9 -0
- mannf/integrations/postman_parser.py +9 -0
- mannf/llm/__init__.py +7 -0
- mannf/llm/anthropic_provider.py +9 -0
- mannf/llm/base.py +9 -0
- mannf/llm/config.py +9 -0
- mannf/llm/factory.py +9 -0
- mannf/llm/openai_provider.py +9 -0
- mannf/llm/prompts.py +9 -0
- mannf/messaging/__init__.py +7 -0
- mannf/messaging/bus.py +9 -0
- mannf/messaging/messages.py +9 -0
- mannf/nat_orchestrator.py +9 -0
- mannf/neural/__init__.py +7 -0
- mannf/orchestrator.py +9 -0
- mannf/prioritization/__init__.py +7 -0
- mannf/prioritization/adaptive_controller.py +9 -0
- mannf/prioritization/belief_prioritizer.py +9 -0
- mannf/prioritization/risk_scorer.py +9 -0
- mannf/product/__init__.py +29 -0
- mannf/product/admin/__init__.py +3 -0
- mannf/product/admin/routes.py +514 -0
- mannf/product/auth/__init__.py +5 -0
- mannf/product/auth/saml.py +212 -0
- mannf/product/billing/__init__.py +5 -0
- mannf/product/billing/audit.py +160 -0
- mannf/product/billing/feature_gates.py +180 -0
- mannf/product/billing/metering.py +179 -0
- mannf/product/billing/notifications.py +181 -0
- mannf/product/billing/plans.py +133 -0
- mannf/product/billing/rate_limits.py +35 -0
- mannf/product/billing/stripe_billing.py +906 -0
- mannf/product/billing/tenant_auth.py +233 -0
- mannf/product/billing/tenant_manager.py +873 -0
- mannf/product/cli.py +3900 -0
- mannf/product/cli_admin.py +408 -0
- mannf/product/dashboard/__init__.py +61 -0
- mannf/product/dashboard/app.py +3567 -0
- mannf/product/dashboard/models.py +460 -0
- mannf/product/dashboard/static/index.html +6347 -0
- mannf/product/dashboard/static/manifest.json +25 -0
- mannf/product/dashboard/static/pwa-icon-192.png +0 -0
- mannf/product/dashboard/static/pwa-icon-512.png +0 -0
- mannf/product/dashboard/static/sw.js +64 -0
- mannf/product/dashboard/telemetry.py +547 -0
- mannf/product/database.py +145 -0
- mannf/product/demo.py +844 -0
- mannf/product/doctor.py +509 -0
- mannf/product/exporters/__init__.py +65 -0
- mannf/product/exporters/azuredevops_exporter.py +257 -0
- mannf/product/exporters/base.py +307 -0
- mannf/product/exporters/bugzilla_exporter.py +200 -0
- mannf/product/exporters/dedup.py +275 -0
- mannf/product/exporters/finding_adapter.py +216 -0
- mannf/product/exporters/github_exporter.py +197 -0
- mannf/product/exporters/gitlab_exporter.py +215 -0
- mannf/product/exporters/jira_exporter.py +180 -0
- mannf/product/exporters/linear_exporter.py +195 -0
- mannf/product/exporters/loader.py +233 -0
- mannf/product/exporters/pagerduty_exporter.py +363 -0
- mannf/product/exporters/sentry_exporter.py +322 -0
- mannf/product/exporters/servicenow_exporter.py +240 -0
- mannf/product/exporters/shortcut_exporter.py +231 -0
- mannf/product/exporters/webhook_exporter.py +383 -0
- mannf/product/formatters/__init__.py +18 -0
- mannf/product/formatters/allure_formatter.py +161 -0
- mannf/product/formatters/ctrf_formatter.py +149 -0
- mannf/product/healing/__init__.py +30 -0
- mannf/product/healing/graphql_schema_diff.py +152 -0
- mannf/product/healing/healer.py +141 -0
- mannf/product/healing/models.py +175 -0
- mannf/product/healing/schema_diff.py +251 -0
- mannf/product/ingestors/__init__.py +77 -0
- mannf/product/ingestors/base.py +256 -0
- mannf/product/ingestors/bgstm_ingestor.py +764 -0
- mannf/product/ingestors/curl_ingestor.py +1019 -0
- mannf/product/ingestors/cypress_ingestor.py +487 -0
- mannf/product/ingestors/gherkin_ingestor.py +967 -0
- mannf/product/ingestors/graphql_ingestor.py +845 -0
- mannf/product/ingestors/grpc_ingestor.py +591 -0
- mannf/product/ingestors/har_ingestor.py +976 -0
- mannf/product/ingestors/loader.py +284 -0
- mannf/product/ingestors/models.py +146 -0
- mannf/product/ingestors/openapi_ingestor.py +606 -0
- mannf/product/ingestors/playwright_ingestor.py +449 -0
- mannf/product/ingestors/postman_ingestor.py +631 -0
- mannf/product/ingestors/traffic_ingestor.py +679 -0
- mannf/product/ingestors/websocket_ingestor.py +526 -0
- mannf/product/integrations/__init__.py +21 -0
- mannf/product/integrations/auth.py +190 -0
- mannf/product/integrations/graphql_parser.py +436 -0
- mannf/product/integrations/graphql_sut.py +247 -0
- mannf/product/integrations/grpc_sut.py +469 -0
- mannf/product/integrations/http_sut.py +237 -0
- mannf/product/integrations/kafka_adapter.py +342 -0
- mannf/product/integrations/openapi_parser.py +513 -0
- mannf/product/integrations/postman_parser.py +467 -0
- mannf/product/integrations/webhook_receiver.py +344 -0
- mannf/product/integrations/websocket_sut.py +434 -0
- mannf/product/llm/__init__.py +25 -0
- mannf/product/llm/anthropic_provider.py +94 -0
- mannf/product/llm/base.py +267 -0
- mannf/product/llm/config.py +48 -0
- mannf/product/llm/factory.py +42 -0
- mannf/product/llm/openai_provider.py +93 -0
- mannf/product/llm/prompts.py +403 -0
- mannf/product/llm/root_cause_service.py +311 -0
- mannf/product/llm/test_plan_models.py +78 -0
- mannf/product/metrics.py +149 -0
- mannf/product/middleware/__init__.py +3 -0
- mannf/product/middleware/audit_middleware.py +112 -0
- mannf/product/middleware/tenant_isolation.py +114 -0
- mannf/product/models.py +347 -0
- mannf/product/notifications/__init__.py +24 -0
- mannf/product/notifications/dispatcher.py +411 -0
- mannf/product/onboarding.py +190 -0
- mannf/product/orchestration/__init__.py +39 -0
- mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
- mannf/product/orchestration/pipeline.py +401 -0
- mannf/product/orchestrator.py +987 -0
- mannf/product/orchestrator_models.py +269 -0
- mannf/product/regression/__init__.py +36 -0
- mannf/product/regression/differ.py +172 -0
- mannf/product/regression/masking.py +100 -0
- mannf/product/regression/models.py +232 -0
- mannf/product/regression/recorder.py +124 -0
- mannf/product/regression/replayer.py +168 -0
- mannf/product/reports/__init__.py +10 -0
- mannf/product/reports/pdf.py +132 -0
- mannf/product/scheduling/__init__.py +57 -0
- mannf/product/scheduling/cron_utils.py +251 -0
- mannf/product/scheduling/engine.py +473 -0
- mannf/product/scheduling/models.py +86 -0
- mannf/product/scheduling/queue.py +894 -0
- mannf/product/scheduling/store.py +235 -0
- mannf/product/security/__init__.py +21 -0
- mannf/product/security/belief_guided.py +143 -0
- mannf/product/security/checks/__init__.py +55 -0
- mannf/product/security/checks/base.py +69 -0
- mannf/product/security/checks/bfla.py +77 -0
- mannf/product/security/checks/bola.py +77 -0
- mannf/product/security/checks/bopla.py +80 -0
- mannf/product/security/checks/broken_auth.py +86 -0
- mannf/product/security/checks/graphql_security.py +299 -0
- mannf/product/security/checks/inventory.py +70 -0
- mannf/product/security/checks/misconfig.py +158 -0
- mannf/product/security/checks/resource_consumption.py +70 -0
- mannf/product/security/checks/sensitive_flows.py +80 -0
- mannf/product/security/checks/ssrf.py +101 -0
- mannf/product/security/checks/unsafe_consumption.py +120 -0
- mannf/product/security/models.py +92 -0
- mannf/product/security/plugin_loader.py +182 -0
- mannf/product/security/reporter.py +92 -0
- mannf/product/security/scanner.py +183 -0
- mannf/product/server.py +6220 -0
- mannf/product/setup_wizard.py +873 -0
- mannf/product/status.py +404 -0
- mannf/product/storage/__init__.py +10 -0
- mannf/product/storage/artifact_store.py +343 -0
- mannf/product/telemetry.py +300 -0
- mannf/product/uninstall.py +169 -0
- mannf/product/upgrade.py +139 -0
- mannf/product/weights/__init__.py +13 -0
- mannf/product/weights/blob_store.py +299 -0
- mannf/product/weights/factory.py +42 -0
- mannf/product/weights/registry.py +159 -0
- mannf/product/weights/store.py +210 -0
- mannf/regression/__init__.py +7 -0
- mannf/regression/differ.py +9 -0
- mannf/regression/masking.py +9 -0
- mannf/regression/models.py +9 -0
- mannf/regression/recorder.py +9 -0
- mannf/regression/replayer.py +9 -0
- mannf/security/__init__.py +7 -0
- mannf/security/belief_guided.py +9 -0
- mannf/security/checks/__init__.py +7 -0
- mannf/security/checks/base.py +9 -0
- mannf/security/checks/bfla.py +9 -0
- mannf/security/checks/bola.py +9 -0
- mannf/security/checks/bopla.py +9 -0
- mannf/security/checks/broken_auth.py +9 -0
- mannf/security/checks/graphql_security.py +9 -0
- mannf/security/checks/inventory.py +9 -0
- mannf/security/checks/misconfig.py +9 -0
- mannf/security/checks/resource_consumption.py +9 -0
- mannf/security/checks/sensitive_flows.py +9 -0
- mannf/security/checks/ssrf.py +9 -0
- mannf/security/checks/unsafe_consumption.py +9 -0
- mannf/security/models.py +9 -0
- mannf/security/reporter.py +9 -0
- mannf/security/scanner.py +9 -0
- mannf/server.py +9 -0
- mannf/testing/__init__.py +7 -0
- mannf/testing/adaptive_controller.py +9 -0
- mannf/testing/models.py +9 -0
- mannf/weights/__init__.py +7 -0
- mannf/weights/registry.py +9 -0
- mannf/weights/store.py +9 -0
- nat_engine-1.dist-info/METADATA +555 -0
- nat_engine-1.dist-info/RECORD +299 -0
- nat_engine-1.dist-info/WHEEL +5 -0
- nat_engine-1.dist-info/entry_points.txt +4 -0
- nat_engine-1.dist-info/licenses/LICENSE +651 -0
- nat_engine-1.dist-info/licenses/NOTICE +178 -0
- nat_engine-1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,247 @@
|
|
|
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
|
+
"""GraphQL System-Under-Test implementation.
|
|
7
|
+
|
|
8
|
+
:class:`GraphQLSUT` introspects a GraphQL endpoint, generates test cases via
|
|
9
|
+
:class:`~mannf.product.integrations.graphql_parser.GraphQLParser`, and executes them
|
|
10
|
+
using ``httpx``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import time
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
|
|
21
|
+
from mannf.product.integrations.auth import AuthStrategy, NoAuth
|
|
22
|
+
from mannf.product.integrations.graphql_parser import GraphQLParser
|
|
23
|
+
from mannf.core.testing.models import TestCase, TestResult
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# Standard GraphQL introspection query
|
|
28
|
+
_INTROSPECTION_QUERY = """
|
|
29
|
+
{
|
|
30
|
+
__schema {
|
|
31
|
+
queryType { name }
|
|
32
|
+
mutationType { name }
|
|
33
|
+
subscriptionType { name }
|
|
34
|
+
types {
|
|
35
|
+
name
|
|
36
|
+
kind
|
|
37
|
+
fields {
|
|
38
|
+
name
|
|
39
|
+
type { name kind ofType { name kind } }
|
|
40
|
+
args { name type { name kind ofType { name kind } } defaultValue }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class GraphQLSUT:
|
|
49
|
+
"""Execute GraphQL test cases against a GraphQL API endpoint.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
endpoint_url:
|
|
54
|
+
Full URL to the GraphQL endpoint (e.g. ``"http://localhost:4000/graphql"``).
|
|
55
|
+
auth:
|
|
56
|
+
Authentication strategy applied before each request.
|
|
57
|
+
Defaults to :class:`~mannf.product.integrations.auth.NoAuth`.
|
|
58
|
+
timeout:
|
|
59
|
+
HTTP request timeout in seconds (default: 10).
|
|
60
|
+
headers:
|
|
61
|
+
Additional HTTP headers added to every request.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
endpoint_url: str,
|
|
67
|
+
auth: Optional[AuthStrategy] = None,
|
|
68
|
+
timeout: float = 10.0,
|
|
69
|
+
headers: Optional[Dict[str, str]] = None,
|
|
70
|
+
recorder: Optional[Any] = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
self.endpoint_url = endpoint_url
|
|
73
|
+
self.auth: AuthStrategy = auth or NoAuth()
|
|
74
|
+
self.timeout = timeout
|
|
75
|
+
self.headers: Dict[str, str] = {
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
**(headers or {}),
|
|
78
|
+
}
|
|
79
|
+
self.recorder = recorder
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------------
|
|
82
|
+
# Core helpers
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
async def _post(self, payload: Any) -> httpx.Response:
|
|
86
|
+
"""POST *payload* to the GraphQL endpoint, applying auth."""
|
|
87
|
+
request_kwargs: Dict[str, Any] = {
|
|
88
|
+
"method": "POST",
|
|
89
|
+
"url": self.endpoint_url,
|
|
90
|
+
"headers": dict(self.headers),
|
|
91
|
+
"json": payload,
|
|
92
|
+
"timeout": self.timeout,
|
|
93
|
+
}
|
|
94
|
+
request_kwargs = await self.auth.apply(request_kwargs)
|
|
95
|
+
|
|
96
|
+
async with httpx.AsyncClient() as client:
|
|
97
|
+
return await client.request(**request_kwargs)
|
|
98
|
+
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
# Public API
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
async def introspect(self) -> dict:
|
|
104
|
+
"""Send a GraphQL introspection query and return the raw result dict."""
|
|
105
|
+
response = await self._post({"query": _INTROSPECTION_QUERY})
|
|
106
|
+
return response.json()
|
|
107
|
+
|
|
108
|
+
async def execute_query(
|
|
109
|
+
self, query: str, variables: Optional[dict] = None
|
|
110
|
+
) -> dict:
|
|
111
|
+
"""Execute a GraphQL query.
|
|
112
|
+
|
|
113
|
+
Returns
|
|
114
|
+
-------
|
|
115
|
+
dict
|
|
116
|
+
Parsed JSON body — typically ``{"data": ..., "errors": [...]}``.
|
|
117
|
+
"""
|
|
118
|
+
payload: Dict[str, Any] = {"query": query}
|
|
119
|
+
if variables:
|
|
120
|
+
payload["variables"] = variables
|
|
121
|
+
response = await self._post(payload)
|
|
122
|
+
return response.json()
|
|
123
|
+
|
|
124
|
+
async def execute_mutation(
|
|
125
|
+
self, mutation: str, variables: Optional[dict] = None
|
|
126
|
+
) -> dict:
|
|
127
|
+
"""Execute a GraphQL mutation.
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
mutation:
|
|
132
|
+
The GraphQL mutation document string.
|
|
133
|
+
variables:
|
|
134
|
+
Optional dict of variable values.
|
|
135
|
+
"""
|
|
136
|
+
return await self.execute_query(mutation, variables)
|
|
137
|
+
|
|
138
|
+
async def generate_test_cases(self) -> List[TestCase]:
|
|
139
|
+
"""Introspect the schema and return generated :class:`~mannf.core.testing.models.TestCase` objects."""
|
|
140
|
+
raw = await self.introspect()
|
|
141
|
+
parser = GraphQLParser(raw)
|
|
142
|
+
schema = parser.parse()
|
|
143
|
+
return parser.generate_test_cases(schema, self.endpoint_url)
|
|
144
|
+
|
|
145
|
+
async def run_test(self, test_case: TestCase) -> TestResult:
|
|
146
|
+
"""Execute a single :class:`~mannf.core.testing.models.TestCase` and return a :class:`~mannf.core.testing.models.TestResult`.
|
|
147
|
+
|
|
148
|
+
A GraphQL response is considered **passing** when:
|
|
149
|
+
|
|
150
|
+
1. The HTTP status code is 200 (or matches ``expected_status_codes``).
|
|
151
|
+
2. The response body contains a ``"data"`` key.
|
|
152
|
+
|
|
153
|
+
If the response has an ``"errors"`` key it is logged as a warning but
|
|
154
|
+
does **not** automatically cause the test to fail.
|
|
155
|
+
"""
|
|
156
|
+
json_payload = test_case.inputs.get("json")
|
|
157
|
+
expected_codes = frozenset(
|
|
158
|
+
test_case.inputs.get("expected_status_codes") or [200]
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
start = time.monotonic()
|
|
162
|
+
try:
|
|
163
|
+
response = await self._post(json_payload)
|
|
164
|
+
elapsed_ms = (time.monotonic() - start) * 1000.0
|
|
165
|
+
|
|
166
|
+
status_ok = response.status_code in expected_codes
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
body: Any = response.json()
|
|
170
|
+
except Exception:
|
|
171
|
+
body = response.text
|
|
172
|
+
|
|
173
|
+
error_msg: Optional[str] = None
|
|
174
|
+
|
|
175
|
+
if not status_ok:
|
|
176
|
+
error_msg = f"Unexpected HTTP status {response.status_code}"
|
|
177
|
+
result = TestResult(
|
|
178
|
+
test_case_id=test_case.id,
|
|
179
|
+
passed=False,
|
|
180
|
+
actual_output=body,
|
|
181
|
+
execution_time_ms=elapsed_ms,
|
|
182
|
+
error=error_msg,
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
# A valid GraphQL response must have a "data" key
|
|
186
|
+
if isinstance(body, dict):
|
|
187
|
+
has_data = "data" in body
|
|
188
|
+
gql_errors = body.get("errors")
|
|
189
|
+
if gql_errors:
|
|
190
|
+
logger.warning(
|
|
191
|
+
"GraphQL errors in response for %s: %s",
|
|
192
|
+
test_case.target,
|
|
193
|
+
gql_errors,
|
|
194
|
+
)
|
|
195
|
+
passed = has_data
|
|
196
|
+
if not passed:
|
|
197
|
+
error_msg = "Response missing 'data' key"
|
|
198
|
+
else:
|
|
199
|
+
# Batch responses are lists — treat as passing when status is OK
|
|
200
|
+
passed = True
|
|
201
|
+
|
|
202
|
+
result = TestResult(
|
|
203
|
+
test_case_id=test_case.id,
|
|
204
|
+
passed=passed,
|
|
205
|
+
actual_output=body,
|
|
206
|
+
execution_time_ms=elapsed_ms,
|
|
207
|
+
error=error_msg,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if self.recorder is not None:
|
|
211
|
+
self.recorder.record(
|
|
212
|
+
test_case_id=test_case.id,
|
|
213
|
+
method="POST",
|
|
214
|
+
url=self.endpoint_url,
|
|
215
|
+
request_headers=dict(self.headers),
|
|
216
|
+
request_body=json_payload,
|
|
217
|
+
query_params=None,
|
|
218
|
+
status_code=response.status_code,
|
|
219
|
+
response_headers=dict(response.headers),
|
|
220
|
+
response_body=body,
|
|
221
|
+
latency_ms=elapsed_ms,
|
|
222
|
+
passed=result.passed,
|
|
223
|
+
error=result.error,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
except httpx.TimeoutException as exc:
|
|
229
|
+
elapsed_ms = (time.monotonic() - start) * 1000.0
|
|
230
|
+
logger.warning("GraphQLSUT: timeout for %s — %s", self.endpoint_url, exc)
|
|
231
|
+
return TestResult(
|
|
232
|
+
test_case_id=test_case.id,
|
|
233
|
+
passed=False,
|
|
234
|
+
actual_output=None,
|
|
235
|
+
execution_time_ms=elapsed_ms,
|
|
236
|
+
error=f"Request timed out: {exc}",
|
|
237
|
+
)
|
|
238
|
+
except httpx.RequestError as exc:
|
|
239
|
+
elapsed_ms = (time.monotonic() - start) * 1000.0
|
|
240
|
+
logger.warning("GraphQLSUT: connection error for %s — %s", self.endpoint_url, exc)
|
|
241
|
+
return TestResult(
|
|
242
|
+
test_case_id=test_case.id,
|
|
243
|
+
passed=False,
|
|
244
|
+
actual_output=None,
|
|
245
|
+
execution_time_ms=elapsed_ms,
|
|
246
|
+
error=f"Connection error: {exc}",
|
|
247
|
+
)
|