1use serde::{Deserialize, Serialize};
8use std::fmt;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub enum ConnectMethodTestVerdict {
13 Pass,
14 Fail,
15 ExpectedFailure, Skipped,
17}
18
19impl fmt::Display for ConnectMethodTestVerdict {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 match self {
22 Self::Pass => write!(f, "PASS"),
23 Self::Fail => write!(f, "FAIL"),
24 Self::ExpectedFailure => write!(f, "XFAIL"),
25 Self::Skipped => write!(f, "SKIP"),
26 }
27 }
28}
29
30#[derive(Debug, Clone)]
32pub struct ConnectMethodConformanceCase {
33 pub id: String,
34 pub description: String,
35 pub connect_request: ConnectRequest,
36 pub expected_response_status: Option<u16>,
37 pub should_establish_tunnel: bool,
38}
39
40#[derive(Debug, Clone)]
42pub struct ConnectRequest {
43 pub authority: String,
45 pub headers: Vec<(String, String)>,
47 pub tunnel_test_data: Vec<u8>,
49}
50
51#[derive(Debug, Clone, Serialize)]
53pub struct ConnectMethodTestResult {
54 pub case_id: String,
55 pub verdict: ConnectMethodTestVerdict,
56 pub error: Option<String>,
57 pub asupersync_response_status: Option<u16>,
58 pub h2_response_status: Option<u16>,
59 pub asupersync_tunnel_established: bool,
60 pub h2_tunnel_established: bool,
61 pub response_status_match: bool,
62 pub tunnel_behavior_match: bool,
63 pub test_duration_ms: u64,
64}
65
66#[derive(Debug, Clone, Serialize)]
68pub struct ConnectMethodComplianceSummary {
69 pub passed: usize,
70 pub failed: usize,
71 pub expected_failures: usize,
72 pub skipped: usize,
73 pub total: usize,
74 pub compliance_score: f64,
75}
76
77#[derive(Debug, Clone, Serialize)]
79pub struct ConnectMethodComplianceReport {
80 pub test_run_id: String,
81 pub timestamp: String,
82 pub total_cases: usize,
83 pub results: Vec<ConnectMethodTestResult>,
84 pub summary: ConnectMethodComplianceSummary,
85}
86
87pub struct ConnectMethodConformanceTester {
89 pub test_cases: Vec<ConnectMethodConformanceCase>,
90}
91
92impl Default for ConnectMethodConformanceTester {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98impl ConnectMethodConformanceTester {
99 pub fn new() -> Self {
101 Self {
102 test_cases: Self::create_test_cases(),
103 }
104 }
105
106 fn create_test_cases() -> Vec<ConnectMethodConformanceCase> {
108 vec![
109 ConnectMethodConformanceCase {
110 id: "CONNECT-001".to_string(),
111 description: "Basic CONNECT to HTTPS endpoint".to_string(),
112 connect_request: ConnectRequest {
113 authority: "example.com:443".to_string(),
114 headers: vec![("User-Agent".to_string(), "test-client/1.0".to_string())],
115 tunnel_test_data: b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".to_vec(),
116 },
117 expected_response_status: Some(200),
118 should_establish_tunnel: true,
119 },
120 ConnectMethodConformanceCase {
121 id: "CONNECT-002".to_string(),
122 description: "CONNECT to non-standard port".to_string(),
123 connect_request: ConnectRequest {
124 authority: "test.example.org:8080".to_string(),
125 headers: vec![(
126 "Proxy-Authorization".to_string(),
127 "Basic dGVzdDp0ZXN0".to_string(),
128 )],
129 tunnel_test_data: b"PING".to_vec(),
130 },
131 expected_response_status: Some(200),
132 should_establish_tunnel: true,
133 },
134 ConnectMethodConformanceCase {
135 id: "CONNECT-003".to_string(),
136 description: "CONNECT with IPv4 address".to_string(),
137 connect_request: ConnectRequest {
138 authority: "192.168.1.100:80".to_string(),
139 headers: vec![],
140 tunnel_test_data: b"Hello, tunnel!".to_vec(),
141 },
142 expected_response_status: Some(200),
143 should_establish_tunnel: true,
144 },
145 ConnectMethodConformanceCase {
146 id: "CONNECT-004".to_string(),
147 description: "CONNECT with IPv6 address".to_string(),
148 connect_request: ConnectRequest {
149 authority: "[2001:db8::1]:443".to_string(),
150 headers: vec![("X-Forwarded-For".to_string(), "203.0.113.1".to_string())],
151 tunnel_test_data: b"IPv6 tunnel test".to_vec(),
152 },
153 expected_response_status: Some(200),
154 should_establish_tunnel: true,
155 },
156 ConnectMethodConformanceCase {
157 id: "CONNECT-005".to_string(),
158 description: "CONNECT to blocked/forbidden endpoint".to_string(),
159 connect_request: ConnectRequest {
160 authority: "blocked.example.com:443".to_string(),
161 headers: vec![],
162 tunnel_test_data: Vec::new(),
163 },
164 expected_response_status: Some(403),
165 should_establish_tunnel: false,
166 },
167 ConnectMethodConformanceCase {
168 id: "CONNECT-006".to_string(),
169 description: "CONNECT with invalid authority format".to_string(),
170 connect_request: ConnectRequest {
171 authority: "invalid-authority-no-port".to_string(),
172 headers: vec![],
173 tunnel_test_data: Vec::new(),
174 },
175 expected_response_status: Some(400),
176 should_establish_tunnel: false,
177 },
178 ConnectMethodConformanceCase {
179 id: "CONNECT-007".to_string(),
180 description: "CONNECT tunnel bidirectional data flow".to_string(),
181 connect_request: ConnectRequest {
182 authority: "echo.example.com:9090".to_string(),
183 headers: vec![("X-Protocol".to_string(), "websocket".to_string())],
184 tunnel_test_data: b"ECHO_REQUEST\nHello, bidirectional tunnel!\n".to_vec(),
185 },
186 expected_response_status: Some(200),
187 should_establish_tunnel: true,
188 },
189 ConnectMethodConformanceCase {
190 id: "CONNECT-008".to_string(),
191 description: "CONNECT with large authority string".to_string(),
192 connect_request: ConnectRequest {
193 authority: format!("{}:443", "a".repeat(253)), headers: vec![],
195 tunnel_test_data: b"Large authority test".to_vec(),
196 },
197 expected_response_status: Some(200),
198 should_establish_tunnel: true,
199 },
200 ConnectMethodConformanceCase {
201 id: "CONNECT-009".to_string(),
202 description: "CONNECT with timeout scenario".to_string(),
203 connect_request: ConnectRequest {
204 authority: "slow.example.com:443".to_string(),
205 headers: vec![("X-Timeout".to_string(), "30".to_string())],
206 tunnel_test_data: b"Timeout test".to_vec(),
207 },
208 expected_response_status: Some(200),
209 should_establish_tunnel: true,
210 },
211 ConnectMethodConformanceCase {
212 id: "CONNECT-010".to_string(),
213 description: "CONNECT tunnel termination handling".to_string(),
214 connect_request: ConnectRequest {
215 authority: "term.example.com:443".to_string(),
216 headers: vec![],
217 tunnel_test_data: b"CLOSE_TUNNEL\n".to_vec(),
218 },
219 expected_response_status: Some(200),
220 should_establish_tunnel: true,
221 },
222 ]
223 }
224
225 pub async fn run_all_tests(&mut self) -> ConnectMethodComplianceReport {
227 let test_run_id = uuid::Uuid::new_v4().to_string();
228 let timestamp = chrono::Utc::now().to_rfc3339();
229 let total_cases = self.test_cases.len();
230 let mut results = Vec::new();
231
232 for test_case in &self.test_cases {
233 let result = self.run_single_test(test_case).await;
234 results.push(result);
235 }
236
237 let summary = self.compute_summary(&results);
238
239 ConnectMethodComplianceReport {
240 test_run_id,
241 timestamp,
242 total_cases,
243 results,
244 summary,
245 }
246 }
247
248 async fn run_single_test(
250 &self,
251 test_case: &ConnectMethodConformanceCase,
252 ) -> ConnectMethodTestResult {
253 let start_time = std::time::Instant::now();
254
255 let asupersync_result = self.test_connect_with_asupersync(test_case).await;
257
258 let h2_result = self.test_connect_with_h2(test_case).await;
260
261 let duration = start_time.elapsed();
262 let test_duration_ms = duration.as_millis() as u64;
263
264 if let Some(error) = backend_unwired_error(&asupersync_result, &h2_result) {
265 return Self::skipped_backend_result(test_case, error, test_duration_ms);
266 }
267
268 match (asupersync_result, h2_result) {
269 (Ok((asupersync_status, asupersync_tunnel)), Ok((h2_status, h2_tunnel))) => {
270 let response_status_match = asupersync_status == h2_status;
271 let tunnel_behavior_match = asupersync_tunnel == h2_tunnel;
272
273 let verdict = if response_status_match && tunnel_behavior_match {
275 let status_correct = test_case
277 .expected_response_status
278 .is_none_or(|expected| asupersync_status == expected);
279 let tunnel_correct = asupersync_tunnel == test_case.should_establish_tunnel;
280
281 if status_correct && tunnel_correct {
282 ConnectMethodTestVerdict::Pass
283 } else {
284 ConnectMethodTestVerdict::Fail
285 }
286 } else {
287 ConnectMethodTestVerdict::Fail
288 };
289
290 ConnectMethodTestResult {
291 case_id: test_case.id.clone(),
292 verdict,
293 error: None,
294 asupersync_response_status: Some(asupersync_status),
295 h2_response_status: Some(h2_status),
296 asupersync_tunnel_established: asupersync_tunnel,
297 h2_tunnel_established: h2_tunnel,
298 response_status_match,
299 tunnel_behavior_match,
300 test_duration_ms,
301 }
302 }
303 (Err(e), _) | (_, Err(e)) => ConnectMethodTestResult {
304 case_id: test_case.id.clone(),
305 verdict: ConnectMethodTestVerdict::Fail,
306 error: Some(e),
307 asupersync_response_status: None,
308 h2_response_status: None,
309 asupersync_tunnel_established: false,
310 h2_tunnel_established: false,
311 response_status_match: false,
312 tunnel_behavior_match: false,
313 test_duration_ms,
314 },
315 }
316 }
317
318 fn skipped_backend_result(
319 test_case: &ConnectMethodConformanceCase,
320 error: String,
321 test_duration_ms: u64,
322 ) -> ConnectMethodTestResult {
323 ConnectMethodTestResult {
324 case_id: test_case.id.clone(),
325 verdict: ConnectMethodTestVerdict::Skipped,
326 error: Some(error),
327 asupersync_response_status: None,
328 h2_response_status: None,
329 asupersync_tunnel_established: false,
330 h2_tunnel_established: false,
331 response_status_match: false,
332 tunnel_behavior_match: false,
333 test_duration_ms,
334 }
335 }
336
337 async fn test_connect_with_asupersync(
339 &self,
340 test_case: &ConnectMethodConformanceCase,
341 ) -> Result<(u16, bool), String> {
342 Err(format!(
343 "asupersync HTTP/2 CONNECT backend not wired; refusing to synthesize comparison result for {}",
344 test_case.id
345 ))
346 }
347
348 async fn test_connect_with_h2(
350 &self,
351 test_case: &ConnectMethodConformanceCase,
352 ) -> Result<(u16, bool), String> {
353 Err(format!(
354 "h2 HTTP/2 CONNECT backend not wired; refusing to synthesize comparison result for {}",
355 test_case.id
356 ))
357 }
358
359 fn compute_summary(
361 &self,
362 results: &[ConnectMethodTestResult],
363 ) -> ConnectMethodComplianceSummary {
364 let passed = results
365 .iter()
366 .filter(|r| r.verdict == ConnectMethodTestVerdict::Pass)
367 .count();
368 let failed = results
369 .iter()
370 .filter(|r| r.verdict == ConnectMethodTestVerdict::Fail)
371 .count();
372 let expected_failures = results
373 .iter()
374 .filter(|r| r.verdict == ConnectMethodTestVerdict::ExpectedFailure)
375 .count();
376 let skipped = results
377 .iter()
378 .filter(|r| r.verdict == ConnectMethodTestVerdict::Skipped)
379 .count();
380 let total = results.len();
381
382 let compliance_score = if total > 0 {
383 (passed + expected_failures) as f64 / total as f64
384 } else {
385 0.0
386 };
387
388 ConnectMethodComplianceSummary {
389 passed,
390 failed,
391 expected_failures,
392 skipped,
393 total,
394 compliance_score,
395 }
396 }
397
398 pub fn generate_markdown_report(&self, report: &ConnectMethodComplianceReport) -> String {
400 let mut md = String::new();
401
402 md.push_str("# HTTP/2 CONNECT Method Handling Conformance Report\n\n");
403
404 md.push_str(&format!("**Test Run ID:** {}\n", report.test_run_id));
405 md.push_str(&format!("**Timestamp:** {}\n", report.timestamp));
406 md.push_str(&format!("**Total Test Cases:** {}\n\n", report.total_cases));
407
408 md.push_str("## Summary\n\n");
409 md.push_str(&format!("- ✅ **Passed:** {}\n", report.summary.passed));
410 md.push_str(&format!("- ❌ **Failed:** {}\n", report.summary.failed));
411 md.push_str(&format!(
412 "- ⚠️ **Expected Failures:** {}\n",
413 report.summary.expected_failures
414 ));
415 md.push_str(&format!("- ⏭️ **Skipped:** {}\n", report.summary.skipped));
416 md.push_str(&format!(
417 "- 🎯 **Compliance Score:** {:.1}%\n\n",
418 report.summary.compliance_score * 100.0
419 ));
420
421 md.push_str("## Test Results\n\n");
422 md.push_str(
423 "| Test ID | Description | Verdict | Asupersync Status | H2 Status | Tunnel Match |\n",
424 );
425 md.push_str(
426 "|---------|-------------|---------|-------------------|-----------|-------------|\n",
427 );
428
429 for result in &report.results {
430 let tunnel_icon = if result.tunnel_behavior_match {
431 "✅"
432 } else {
433 "❌"
434 };
435 md.push_str(&format!(
436 "| {} | {} | {} | {} | {} | {} |\n",
437 result.case_id,
438 self.test_cases
439 .iter()
440 .find(|case| case.id == result.case_id)
441 .map(|case| case.description.as_str())
442 .unwrap_or("Unknown"),
443 result.verdict,
444 result
445 .asupersync_response_status
446 .map(|s| s.to_string())
447 .unwrap_or_else(|| "Error".to_string()),
448 result
449 .h2_response_status
450 .map(|s| s.to_string())
451 .unwrap_or_else(|| "Error".to_string()),
452 tunnel_icon
453 ));
454 }
455
456 md.push_str("\n## Failed Tests\n\n");
457 let failed_tests: Vec<_> = report
458 .results
459 .iter()
460 .filter(|r| r.verdict == ConnectMethodTestVerdict::Fail)
461 .collect();
462
463 if failed_tests.is_empty() {
464 md.push_str("No tests failed.\n\n");
465 } else {
466 for result in failed_tests {
467 md.push_str(&format!("### {}\n\n", result.case_id));
468 if let Some(error) = &result.error {
469 md.push_str(&format!("**Error:** {}\n\n", error));
470 }
471 md.push_str(&format!(
472 "**Response Status:** asupersync={:?}, h2={:?}\n",
473 result.asupersync_response_status, result.h2_response_status
474 ));
475 md.push_str(&format!(
476 "**Tunnel Established:** asupersync={}, h2={}\n\n",
477 result.asupersync_tunnel_established, result.h2_tunnel_established
478 ));
479 }
480 }
481
482 md.push_str("\n## Skipped Tests\n\n");
483 let skipped_tests: Vec<_> = report
484 .results
485 .iter()
486 .filter(|r| r.verdict == ConnectMethodTestVerdict::Skipped)
487 .collect();
488
489 if skipped_tests.is_empty() {
490 md.push_str("No tests were skipped.\n\n");
491 } else {
492 for result in skipped_tests {
493 md.push_str(&format!("### {}\n\n", result.case_id));
494 if let Some(error) = &result.error {
495 md.push_str(&format!("**Reason:** {}\n\n", error));
496 }
497 }
498 }
499
500 md.push_str("---\n");
501 md.push_str(&format!(
502 "*Generated by asupersync conformance tester at {}*\n",
503 chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
504 ));
505
506 md
507 }
508}
509
510fn backend_unwired_error(
511 asupersync_result: &Result<(u16, bool), String>,
512 h2_result: &Result<(u16, bool), String>,
513) -> Option<String> {
514 let mut errors = Vec::new();
515
516 if let Err(error) = asupersync_result
517 && is_backend_unwired(error)
518 {
519 errors.push(error.as_str());
520 }
521
522 if let Err(error) = h2_result
523 && is_backend_unwired(error)
524 {
525 errors.push(error.as_str());
526 }
527
528 if errors.is_empty() {
529 None
530 } else {
531 Some(errors.join("; "))
532 }
533}
534
535fn is_backend_unwired(error: &str) -> bool {
536 error.contains("backend not wired") && error.contains("refusing to synthesize")
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 #[tokio::test]
544 async fn connect_harness_does_not_report_synthetic_passes() {
545 let mut tester = ConnectMethodConformanceTester::new();
546 let total_cases = tester.test_cases.len();
547
548 let report = tester.run_all_tests().await;
549
550 assert_eq!(report.summary.total, total_cases);
551 assert_eq!(report.summary.passed, 0);
552 assert_eq!(report.summary.failed, 0);
553 assert_eq!(report.summary.expected_failures, 0);
554 assert_eq!(report.summary.skipped, total_cases);
555 assert_eq!(report.summary.compliance_score, 0.0);
556 assert!(
557 report
558 .results
559 .iter()
560 .all(|result| result.verdict == ConnectMethodTestVerdict::Skipped)
561 );
562 assert!(
563 report
564 .results
565 .iter()
566 .all(|result| { result.error.as_deref().is_some_and(is_backend_unwired) })
567 );
568 }
569
570 #[tokio::test]
571 async fn markdown_report_explains_skipped_unwired_backends() {
572 let mut tester = ConnectMethodConformanceTester::new();
573 let report = tester.run_all_tests().await;
574
575 let markdown = tester.generate_markdown_report(&report);
576
577 assert!(markdown.contains("## Skipped Tests"));
578 assert!(markdown.contains("asupersync HTTP/2 CONNECT backend not wired"));
579 assert!(markdown.contains("h2 HTTP/2 CONNECT backend not wired"));
580 assert!(markdown.contains("refusing to synthesize comparison result"));
581 }
582}