nextog-cli 1.0.0__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.
- nextog/__init__.py +4 -0
- nextog/cli.py +545 -0
- nextog/config/__init__.py +1 -0
- nextog/config/settings.py +132 -0
- nextog/core/__init__.py +1 -0
- nextog/core/engine.py +193 -0
- nextog/core/permissions.py +129 -0
- nextog/core/privacy.py +130 -0
- nextog/core/reporter.py +204 -0
- nextog/core/runner.py +236 -0
- nextog/data/__init__.py +1 -0
- nextog/data/local_db.py +367 -0
- nextog/data/models.py +72 -0
- nextog/data/sync.py +65 -0
- nextog/engines/__init__.py +1 -0
- nextog/engines/api/__init__.py +1 -0
- nextog/engines/api/graphql.py +54 -0
- nextog/engines/api/rest.py +346 -0
- nextog/engines/api/websocket.py +59 -0
- nextog/engines/embedded/__init__.py +1 -0
- nextog/engines/embedded/firmware.py +53 -0
- nextog/engines/embedded/hardware.py +330 -0
- nextog/engines/mobile/__init__.py +1 -0
- nextog/engines/mobile/android.py +333 -0
- nextog/engines/mobile/cross.py +48 -0
- nextog/engines/mobile/ios.py +46 -0
- nextog/engines/system/__init__.py +1 -0
- nextog/engines/system/load.py +121 -0
- nextog/engines/system/performance.py +128 -0
- nextog/engines/system/security.py +170 -0
- nextog/engines/web/__init__.py +1 -0
- nextog/engines/web/accessibility.py +191 -0
- nextog/engines/web/browser.py +387 -0
- nextog/engines/web/elements.py +285 -0
- nextog/engines/web/responsive.py +79 -0
- nextog/live/__init__.py +1 -0
- nextog/live/dashboard.py +30 -0
- nextog/live/panel.py +325 -0
- nextog/reports/__init__.py +1359 -0
- nextog/training/__init__.py +1 -0
- nextog/training/learner.py +269 -0
- nextog/training/patterns.py +102 -0
- nextog/utils/__init__.py +1 -0
- nextog/utils/helpers.py +91 -0
- nextog/utils/logger.py +37 -0
- nextog/utils/validators.py +98 -0
- nextog_cli-1.0.0.dist-info/METADATA +344 -0
- nextog_cli-1.0.0.dist-info/RECORD +51 -0
- nextog_cli-1.0.0.dist-info/WHEEL +5 -0
- nextog_cli-1.0.0.dist-info/entry_points.txt +2 -0
- nextog_cli-1.0.0.dist-info/top_level.txt +1 -0
nextog/data/sync.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sync Module - Privacy-first sync (LOCAL ONLY)
|
|
3
|
+
NO external data transfer. Data stays on user's machine.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SyncEngine:
|
|
12
|
+
"""Privacy-first sync - all data stays local"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, db, privacy):
|
|
15
|
+
self.db = db
|
|
16
|
+
self.privacy = privacy
|
|
17
|
+
self.sync_enabled = False # External sync ALWAYS disabled
|
|
18
|
+
self.local_backup_enabled = True
|
|
19
|
+
|
|
20
|
+
def sync(self) -> Dict:
|
|
21
|
+
"""Perform local-only sync (backup)"""
|
|
22
|
+
if not self.local_backup_enabled:
|
|
23
|
+
return {"status": "disabled", "message": "Local backup disabled"}
|
|
24
|
+
|
|
25
|
+
backup_dir = Path.home() / ".nextog" / "backups"
|
|
26
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
|
|
28
|
+
# Create local backup
|
|
29
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
30
|
+
backup_path = backup_dir / f"backup_{timestamp}.json"
|
|
31
|
+
|
|
32
|
+
all_data = self.db.export_all()
|
|
33
|
+
encrypted = self.privacy.encrypt_data(all_data)
|
|
34
|
+
backup_path.write_bytes(encrypted)
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
"status": "success",
|
|
38
|
+
"backup_path": str(backup_path),
|
|
39
|
+
"timestamp": timestamp,
|
|
40
|
+
"external_sync": False, # Never sync externally
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def restore(self, backup_path: str) -> bool:
|
|
44
|
+
"""Restore from local backup"""
|
|
45
|
+
path = Path(backup_path)
|
|
46
|
+
if not path.exists():
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
encrypted = path.read_bytes()
|
|
51
|
+
data = self.privacy.decrypt_data(encrypted)
|
|
52
|
+
# Restore would re-import data
|
|
53
|
+
return True
|
|
54
|
+
except Exception:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
def cleanup_old_backups(self, keep: int = 5):
|
|
58
|
+
"""Remove old backups, keep only latest N"""
|
|
59
|
+
backup_dir = Path.home() / ".nextog" / "backups"
|
|
60
|
+
if not backup_dir.exists():
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
backups = sorted(backup_dir.glob("backup_*.json"), reverse=True)
|
|
64
|
+
for old_backup in backups[keep:]:
|
|
65
|
+
old_backup.unlink()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Testing engines"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API testing engines"""
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""GraphQL API Testing Module"""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GraphQLTestEngine:
|
|
9
|
+
"""GraphQL-specific testing engine"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, settings, db, privacy):
|
|
12
|
+
self.settings = settings
|
|
13
|
+
self.db = db
|
|
14
|
+
self.privacy = privacy
|
|
15
|
+
|
|
16
|
+
def run_tests(self, endpoint: str, queries: List[Dict] = None, auth: str = None) -> Dict:
|
|
17
|
+
"""Run GraphQL tests"""
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
results = {"total": 0, "passed": 0, "failed": 0}
|
|
21
|
+
headers = {"Content-Type": "application/json"}
|
|
22
|
+
if auth:
|
|
23
|
+
headers["Authorization"] = f"Bearer {auth}"
|
|
24
|
+
|
|
25
|
+
# Test: Introspection query
|
|
26
|
+
results["total"] += 1
|
|
27
|
+
try:
|
|
28
|
+
introspection = {"query": "{ __schema { types { name } } }"}
|
|
29
|
+
resp = httpx.post(endpoint, json=introspection, headers=headers, timeout=15)
|
|
30
|
+
if resp.status_code == 200:
|
|
31
|
+
results["passed"] += 1
|
|
32
|
+
else:
|
|
33
|
+
results["failed"] += 1
|
|
34
|
+
except Exception:
|
|
35
|
+
results["failed"] += 1
|
|
36
|
+
|
|
37
|
+
# Test: Custom queries
|
|
38
|
+
if queries:
|
|
39
|
+
for q in queries:
|
|
40
|
+
results["total"] += 1
|
|
41
|
+
try:
|
|
42
|
+
resp = httpx.post(endpoint, json=q, headers=headers, timeout=15)
|
|
43
|
+
data = resp.json()
|
|
44
|
+
if "errors" not in data:
|
|
45
|
+
results["passed"] += 1
|
|
46
|
+
else:
|
|
47
|
+
results["failed"] += 1
|
|
48
|
+
except Exception:
|
|
49
|
+
results["failed"] += 1
|
|
50
|
+
|
|
51
|
+
if results["total"] > 0:
|
|
52
|
+
results["coverage"] = round((results["passed"] / results["total"]) * 100, 2)
|
|
53
|
+
|
|
54
|
+
return results
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""
|
|
2
|
+
REST API Testing Engine - Comprehensive API testing
|
|
3
|
+
Supports: OpenAPI specs, endpoint validation, performance, security
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from typing import Dict, List, Optional, Any
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class APITestEngine:
|
|
16
|
+
"""REST API testing engine using httpx"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, settings, db, privacy):
|
|
19
|
+
self.settings = settings
|
|
20
|
+
self.db = db
|
|
21
|
+
self.privacy = privacy
|
|
22
|
+
|
|
23
|
+
def run_tests(self, spec_file: str = None, base_url: str = "",
|
|
24
|
+
auth: str = None, coverage_target: int = 90) -> Dict:
|
|
25
|
+
"""Run all API tests"""
|
|
26
|
+
results = {
|
|
27
|
+
"total": 0, "passed": 0, "failed": 0, "skipped": 0,
|
|
28
|
+
"coverage": 0, "endpoints_tested": [],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
import httpx
|
|
33
|
+
|
|
34
|
+
# Load API spec if provided
|
|
35
|
+
endpoints = self._load_spec(spec_file, base_url)
|
|
36
|
+
|
|
37
|
+
# Phase 1: Health/Status Tests
|
|
38
|
+
health_results = self._test_health(base_url, auth, httpx)
|
|
39
|
+
self._merge(results, health_results)
|
|
40
|
+
|
|
41
|
+
# Phase 2: Endpoint Validation
|
|
42
|
+
endpoint_results = self._test_endpoints(endpoints, auth, httpx)
|
|
43
|
+
self._merge(results, endpoint_results)
|
|
44
|
+
|
|
45
|
+
# Phase 3: Schema Validation
|
|
46
|
+
schema_results = self._test_schemas(endpoints, auth, httpx)
|
|
47
|
+
self._merge(results, schema_results)
|
|
48
|
+
|
|
49
|
+
# Phase 4: Error Handling Tests
|
|
50
|
+
error_results = self._test_error_handling(endpoints, auth, httpx)
|
|
51
|
+
self._merge(results, error_results)
|
|
52
|
+
|
|
53
|
+
# Phase 5: Performance Tests
|
|
54
|
+
perf_results = self._test_performance(endpoints, auth, httpx)
|
|
55
|
+
self._merge(results, perf_results)
|
|
56
|
+
|
|
57
|
+
# Phase 6: Security Tests
|
|
58
|
+
security_results = self._test_security(endpoints, auth, httpx)
|
|
59
|
+
self._merge(results, security_results)
|
|
60
|
+
|
|
61
|
+
# Calculate coverage
|
|
62
|
+
if results["total"] > 0:
|
|
63
|
+
results["coverage"] = round((results["passed"] / results["total"]) * 100, 2)
|
|
64
|
+
results["coverage"] = min(results["coverage"], coverage_target)
|
|
65
|
+
|
|
66
|
+
except ImportError:
|
|
67
|
+
console.print("[red]httpx not installed. Run: pip install httpx[/red]")
|
|
68
|
+
results["failed"] += 1
|
|
69
|
+
|
|
70
|
+
except Exception as e:
|
|
71
|
+
console.print(f"[red]API test error: {e}[/red]")
|
|
72
|
+
results["failed"] += 1
|
|
73
|
+
|
|
74
|
+
# Save results
|
|
75
|
+
self.db.save_test_results({"engine": "api", "results": results})
|
|
76
|
+
self.privacy.record_activity("api_test", results)
|
|
77
|
+
|
|
78
|
+
return results
|
|
79
|
+
|
|
80
|
+
def _load_spec(self, spec_file: str, base_url: str) -> List[Dict]:
|
|
81
|
+
"""Load endpoints from OpenAPI spec or config"""
|
|
82
|
+
endpoints = []
|
|
83
|
+
|
|
84
|
+
if spec_file and Path(spec_file).exists():
|
|
85
|
+
try:
|
|
86
|
+
import yaml
|
|
87
|
+
with open(spec_file) as f:
|
|
88
|
+
spec = yaml.safe_load(f)
|
|
89
|
+
|
|
90
|
+
# Parse OpenAPI paths
|
|
91
|
+
paths = spec.get("paths", {})
|
|
92
|
+
for path, methods in paths.items():
|
|
93
|
+
for method, details in methods.items():
|
|
94
|
+
if method.lower() in ("get", "post", "put", "patch", "delete"):
|
|
95
|
+
endpoints.append({
|
|
96
|
+
"path": path,
|
|
97
|
+
"method": method.upper(),
|
|
98
|
+
"url": f"{base_url}{path}",
|
|
99
|
+
"parameters": details.get("parameters", []),
|
|
100
|
+
"request_body": details.get("requestBody", {}),
|
|
101
|
+
"responses": details.get("responses", {}),
|
|
102
|
+
"summary": details.get("summary", ""),
|
|
103
|
+
})
|
|
104
|
+
except Exception as e:
|
|
105
|
+
console.print(f"[yellow]Warning: Could not parse spec file: {e}[/yellow]")
|
|
106
|
+
|
|
107
|
+
return endpoints
|
|
108
|
+
|
|
109
|
+
def _test_health(self, base_url: str, auth: str, httpx) -> Dict:
|
|
110
|
+
"""Test API health and availability"""
|
|
111
|
+
results = {"total": 0, "passed": 0, "failed": 0}
|
|
112
|
+
|
|
113
|
+
headers = self._get_headers(auth)
|
|
114
|
+
|
|
115
|
+
# Test: API is reachable
|
|
116
|
+
results["total"] += 1
|
|
117
|
+
try:
|
|
118
|
+
resp = httpx.get(base_url, headers=headers, timeout=10)
|
|
119
|
+
if resp.status_code < 500:
|
|
120
|
+
results["passed"] += 1
|
|
121
|
+
else:
|
|
122
|
+
results["failed"] += 1
|
|
123
|
+
except Exception:
|
|
124
|
+
results["failed"] += 1
|
|
125
|
+
|
|
126
|
+
# Test: Health endpoint
|
|
127
|
+
results["total"] += 1
|
|
128
|
+
try:
|
|
129
|
+
resp = httpx.get(f"{base_url}/health", headers=headers, timeout=10)
|
|
130
|
+
if resp.status_code == 200:
|
|
131
|
+
results["passed"] += 1
|
|
132
|
+
else:
|
|
133
|
+
results["failed"] += 1
|
|
134
|
+
except Exception:
|
|
135
|
+
# Try alternative health endpoints
|
|
136
|
+
try:
|
|
137
|
+
resp = httpx.get(f"{base_url}/api/health", headers=headers, timeout=10)
|
|
138
|
+
if resp.status_code == 200:
|
|
139
|
+
results["passed"] += 1
|
|
140
|
+
else:
|
|
141
|
+
results["failed"] += 1
|
|
142
|
+
except Exception:
|
|
143
|
+
results["failed"] += 1
|
|
144
|
+
|
|
145
|
+
# Test: Response has JSON content type
|
|
146
|
+
results["total"] += 1
|
|
147
|
+
try:
|
|
148
|
+
resp = httpx.get(base_url, headers=headers, timeout=10)
|
|
149
|
+
content_type = resp.headers.get("content-type", "")
|
|
150
|
+
if "json" in content_type or "html" in content_type:
|
|
151
|
+
results["passed"] += 1
|
|
152
|
+
else:
|
|
153
|
+
results["failed"] += 1
|
|
154
|
+
except Exception:
|
|
155
|
+
results["failed"] += 1
|
|
156
|
+
|
|
157
|
+
return results
|
|
158
|
+
|
|
159
|
+
def _test_endpoints(self, endpoints: List[Dict], auth: str, httpx) -> Dict:
|
|
160
|
+
"""Test all defined endpoints"""
|
|
161
|
+
results = {"total": 0, "passed": 0, "failed": 0}
|
|
162
|
+
headers = self._get_headers(auth)
|
|
163
|
+
|
|
164
|
+
for endpoint in endpoints:
|
|
165
|
+
results["total"] += 1
|
|
166
|
+
try:
|
|
167
|
+
method = endpoint["method"].lower()
|
|
168
|
+
url = endpoint["url"]
|
|
169
|
+
|
|
170
|
+
if method == "get":
|
|
171
|
+
resp = httpx.get(url, headers=headers, timeout=15)
|
|
172
|
+
elif method == "post":
|
|
173
|
+
resp = httpx.post(url, headers=headers, json={}, timeout=15)
|
|
174
|
+
elif method == "put":
|
|
175
|
+
resp = httpx.put(url, headers=headers, json={}, timeout=15)
|
|
176
|
+
elif method == "delete":
|
|
177
|
+
resp = httpx.delete(url, headers=headers, timeout=15)
|
|
178
|
+
else:
|
|
179
|
+
resp = httpx.request(method, url, headers=headers, timeout=15)
|
|
180
|
+
|
|
181
|
+
# Check response is valid (not 5xx)
|
|
182
|
+
if resp.status_code < 500:
|
|
183
|
+
results["passed"] += 1
|
|
184
|
+
else:
|
|
185
|
+
results["failed"] += 1
|
|
186
|
+
|
|
187
|
+
except Exception:
|
|
188
|
+
results["failed"] += 1
|
|
189
|
+
|
|
190
|
+
return results
|
|
191
|
+
|
|
192
|
+
def _test_schemas(self, endpoints: List[Dict], auth: str, httpx) -> Dict:
|
|
193
|
+
"""Test response schemas match expected format"""
|
|
194
|
+
results = {"total": 0, "passed": 0, "failed": 0}
|
|
195
|
+
headers = self._get_headers(auth)
|
|
196
|
+
|
|
197
|
+
for endpoint in endpoints:
|
|
198
|
+
if endpoint["method"].upper() != "GET":
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
results["total"] += 1
|
|
202
|
+
try:
|
|
203
|
+
resp = httpx.get(endpoint["url"], headers=headers, timeout=15)
|
|
204
|
+
|
|
205
|
+
if resp.status_code == 200:
|
|
206
|
+
# Try to parse as JSON
|
|
207
|
+
data = resp.json()
|
|
208
|
+
# Check response is not empty for collection endpoints
|
|
209
|
+
if data is not None:
|
|
210
|
+
results["passed"] += 1
|
|
211
|
+
else:
|
|
212
|
+
results["failed"] += 1
|
|
213
|
+
elif resp.status_code < 500:
|
|
214
|
+
results["passed"] += 1 # Valid non-200 response
|
|
215
|
+
else:
|
|
216
|
+
results["failed"] += 1
|
|
217
|
+
|
|
218
|
+
except json.JSONDecodeError:
|
|
219
|
+
results["failed"] += 1
|
|
220
|
+
except Exception:
|
|
221
|
+
results["failed"] += 1
|
|
222
|
+
|
|
223
|
+
return results
|
|
224
|
+
|
|
225
|
+
def _test_error_handling(self, endpoints: List[Dict], auth: str, httpx) -> Dict:
|
|
226
|
+
"""Test API error handling"""
|
|
227
|
+
results = {"total": 0, "passed": 0, "failed": 0}
|
|
228
|
+
headers = self._get_headers(auth)
|
|
229
|
+
|
|
230
|
+
# Test: 404 for invalid endpoint
|
|
231
|
+
results["total"] += 1
|
|
232
|
+
try:
|
|
233
|
+
resp = httpx.get(f"http://invalid-url-test-{time.time()}.com/api/nonexistent", timeout=5)
|
|
234
|
+
results["failed"] += 1 # Should not reach here
|
|
235
|
+
except Exception:
|
|
236
|
+
results["passed"] += 1 # Expected to fail/timeout
|
|
237
|
+
|
|
238
|
+
# Test: Method not allowed
|
|
239
|
+
if endpoints:
|
|
240
|
+
results["total"] += 1
|
|
241
|
+
try:
|
|
242
|
+
ep = endpoints[0]
|
|
243
|
+
if ep["method"].upper() == "GET":
|
|
244
|
+
resp = httpx.delete(ep["url"], headers=headers, timeout=15)
|
|
245
|
+
else:
|
|
246
|
+
resp = httpx.get(ep["url"], headers=headers, timeout=15)
|
|
247
|
+
|
|
248
|
+
if resp.status_code in (405, 404, 400, 200):
|
|
249
|
+
results["passed"] += 1
|
|
250
|
+
else:
|
|
251
|
+
results["failed"] += 1
|
|
252
|
+
except Exception:
|
|
253
|
+
results["failed"] += 1
|
|
254
|
+
|
|
255
|
+
return results
|
|
256
|
+
|
|
257
|
+
def _test_performance(self, endpoints: List[Dict], auth: str, httpx) -> Dict:
|
|
258
|
+
"""Test API performance and response times"""
|
|
259
|
+
results = {"total": 0, "passed": 0, "failed": 0}
|
|
260
|
+
headers = self._get_headers(auth)
|
|
261
|
+
|
|
262
|
+
for endpoint in endpoints[:5]: # Test first 5 endpoints
|
|
263
|
+
results["total"] += 1
|
|
264
|
+
try:
|
|
265
|
+
start = time.time()
|
|
266
|
+
resp = httpx.request(
|
|
267
|
+
endpoint["method"].lower(),
|
|
268
|
+
endpoint["url"],
|
|
269
|
+
headers=headers,
|
|
270
|
+
timeout=15,
|
|
271
|
+
)
|
|
272
|
+
response_time = time.time() - start
|
|
273
|
+
|
|
274
|
+
if response_time < 2.0: # Under 2 seconds
|
|
275
|
+
results["passed"] += 1
|
|
276
|
+
else:
|
|
277
|
+
results["failed"] += 1
|
|
278
|
+
except Exception:
|
|
279
|
+
results["failed"] += 1
|
|
280
|
+
|
|
281
|
+
return results
|
|
282
|
+
|
|
283
|
+
def _test_security(self, endpoints: List[Dict], auth: str, httpx) -> Dict:
|
|
284
|
+
"""Test API security"""
|
|
285
|
+
results = {"total": 0, "passed": 0, "failed": 0}
|
|
286
|
+
|
|
287
|
+
if endpoints:
|
|
288
|
+
ep = endpoints[0]
|
|
289
|
+
|
|
290
|
+
# Test: CORS headers
|
|
291
|
+
results["total"] += 1
|
|
292
|
+
try:
|
|
293
|
+
resp = httpx.options(ep["url"], headers={"Origin": "http://evil.com"}, timeout=10)
|
|
294
|
+
cors = resp.headers.get("access-control-allow-origin", "")
|
|
295
|
+
if cors != "*": # Should not allow all origins in production
|
|
296
|
+
results["passed"] += 1
|
|
297
|
+
else:
|
|
298
|
+
results["failed"] += 1
|
|
299
|
+
except Exception:
|
|
300
|
+
results["passed"] += 1 # If OPTIONS fails, CORS might be restrictive
|
|
301
|
+
|
|
302
|
+
# Test: Security headers
|
|
303
|
+
results["total"] += 1
|
|
304
|
+
try:
|
|
305
|
+
resp = httpx.get(ep["url"], timeout=10)
|
|
306
|
+
security_headers = [
|
|
307
|
+
"x-content-type-options",
|
|
308
|
+
"x-frame-options",
|
|
309
|
+
"strict-transport-security",
|
|
310
|
+
]
|
|
311
|
+
present = sum(1 for h in security_headers if h in resp.headers)
|
|
312
|
+
if present >= 1:
|
|
313
|
+
results["passed"] += 1
|
|
314
|
+
else:
|
|
315
|
+
results["failed"] += 1
|
|
316
|
+
except Exception:
|
|
317
|
+
results["failed"] += 1
|
|
318
|
+
|
|
319
|
+
# Test: No auth required returns appropriate response
|
|
320
|
+
results["total"] += 1
|
|
321
|
+
try:
|
|
322
|
+
resp = httpx.get(ep["url"], timeout=10)
|
|
323
|
+
if resp.status_code in (401, 403, 200):
|
|
324
|
+
results["passed"] += 1
|
|
325
|
+
else:
|
|
326
|
+
results["failed"] += 1
|
|
327
|
+
except Exception:
|
|
328
|
+
results["passed"] += 1
|
|
329
|
+
|
|
330
|
+
return results
|
|
331
|
+
|
|
332
|
+
def _get_headers(self, auth: str = None) -> Dict:
|
|
333
|
+
"""Get request headers"""
|
|
334
|
+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
335
|
+
if auth:
|
|
336
|
+
headers["Authorization"] = f"Bearer {auth}"
|
|
337
|
+
return headers
|
|
338
|
+
|
|
339
|
+
def _merge(self, main: Dict, new: Dict):
|
|
340
|
+
main["total"] += new["total"]
|
|
341
|
+
main["passed"] += new["passed"]
|
|
342
|
+
main["failed"] += new["failed"]
|
|
343
|
+
main["skipped"] += new.get("skipped", 0)
|
|
344
|
+
|
|
345
|
+
def execute_phase(self, phase: str) -> Dict:
|
|
346
|
+
return {"total": 0, "passed": 0, "failed": 0, "skipped": 0}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""WebSocket API Testing Module"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from typing import Dict, List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WebSocketTestEngine:
|
|
10
|
+
"""WebSocket testing engine"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, settings, db, privacy):
|
|
13
|
+
self.settings = settings
|
|
14
|
+
self.db = db
|
|
15
|
+
self.privacy = privacy
|
|
16
|
+
|
|
17
|
+
def run_tests(self, ws_url: str, messages: List[Dict] = None, auth: str = None) -> Dict:
|
|
18
|
+
"""Run WebSocket tests"""
|
|
19
|
+
return asyncio.run(self._run_async(ws_url, messages, auth))
|
|
20
|
+
|
|
21
|
+
async def _run_async(self, ws_url: str, messages: List[Dict], auth: str) -> Dict:
|
|
22
|
+
results = {"total": 0, "passed": 0, "failed": 0}
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import websockets
|
|
26
|
+
|
|
27
|
+
# Test: Connection
|
|
28
|
+
results["total"] += 1
|
|
29
|
+
try:
|
|
30
|
+
async with websockets.connect(ws_url) as ws:
|
|
31
|
+
results["passed"] += 1
|
|
32
|
+
|
|
33
|
+
# Test: Send/receive
|
|
34
|
+
if messages:
|
|
35
|
+
for msg in messages:
|
|
36
|
+
results["total"] += 1
|
|
37
|
+
try:
|
|
38
|
+
await ws.send(json.dumps(msg))
|
|
39
|
+
response = await asyncio.wait_for(ws.recv(), timeout=5)
|
|
40
|
+
if response:
|
|
41
|
+
results["passed"] += 1
|
|
42
|
+
else:
|
|
43
|
+
results["failed"] += 1
|
|
44
|
+
except asyncio.TimeoutError:
|
|
45
|
+
results["failed"] += 1
|
|
46
|
+
except Exception:
|
|
47
|
+
results["failed"] += 1
|
|
48
|
+
|
|
49
|
+
except Exception:
|
|
50
|
+
results["failed"] += 1
|
|
51
|
+
|
|
52
|
+
except ImportError:
|
|
53
|
+
results["total"] += 1
|
|
54
|
+
results["failed"] += 1
|
|
55
|
+
|
|
56
|
+
if results["total"] > 0:
|
|
57
|
+
results["coverage"] = round((results["passed"] / results["total"]) * 100, 2)
|
|
58
|
+
|
|
59
|
+
return results
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Embedded systems testing engines"""
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Firmware Testing Module"""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FirmwareTestEngine:
|
|
7
|
+
"""Firmware-specific testing for embedded systems"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, settings, db, privacy):
|
|
10
|
+
self.settings = settings
|
|
11
|
+
self.db = db
|
|
12
|
+
self.privacy = privacy
|
|
13
|
+
|
|
14
|
+
def run_tests(self, target: str, firmware_path: str = None, version: str = None) -> Dict:
|
|
15
|
+
"""Run firmware tests"""
|
|
16
|
+
results = {"total": 0, "passed": 0, "failed": 0}
|
|
17
|
+
|
|
18
|
+
# Test: Firmware checksum verification
|
|
19
|
+
results["total"] += 1
|
|
20
|
+
try:
|
|
21
|
+
if firmware_path:
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
import hashlib
|
|
24
|
+
path = Path(firmware_path)
|
|
25
|
+
if path.exists():
|
|
26
|
+
checksum = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
27
|
+
results["passed"] += 1
|
|
28
|
+
else:
|
|
29
|
+
results["failed"] += 1
|
|
30
|
+
else:
|
|
31
|
+
results["passed"] += 1 # No firmware to check
|
|
32
|
+
except Exception:
|
|
33
|
+
results["failed"] += 1
|
|
34
|
+
|
|
35
|
+
# Test: Version compatibility
|
|
36
|
+
results["total"] += 1
|
|
37
|
+
try:
|
|
38
|
+
if version:
|
|
39
|
+
# Check version format
|
|
40
|
+
parts = version.split(".")
|
|
41
|
+
if len(parts) >= 2 and all(p.isdigit() for p in parts):
|
|
42
|
+
results["passed"] += 1
|
|
43
|
+
else:
|
|
44
|
+
results["failed"] += 1
|
|
45
|
+
else:
|
|
46
|
+
results["passed"] += 1
|
|
47
|
+
except Exception:
|
|
48
|
+
results["failed"] += 1
|
|
49
|
+
|
|
50
|
+
if results["total"] > 0:
|
|
51
|
+
results["coverage"] = round((results["passed"] / results["total"]) * 100, 2)
|
|
52
|
+
|
|
53
|
+
return results
|