asupersync 0.3.4

Spec-first, cancel-correct, capability-secure async runtime for Rust.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
//! OTLP TLS handshake failure audit test.
//!
//! **AUDIT SCOPE**: Verifies OTLP-Trace exporter behavior when TLS handshake fails
//! due to version mismatch (e.g., collector requires TLS 1.3, client supports TLS 1.2).
//!
//! **OTLP/TLS SPECIFICATION REQUIREMENTS**:
//! - TLS handshake failures MUST fail-fast with clear error message (correct: actionable)
//! - NOT retry forever (waste: burns resources without resolution)
//! - NOT downgrade to plaintext (insecure: violates TLS-required OTLP endpoints)
//! - Error message SHOULD indicate TLS version negotiation failure
//! - Client SHOULD suggest TLS configuration review for resolution
//!
//! **CURRENT BEHAVIOR ANALYSIS**:
//! - HttpClient TLS errors mapped to ClientError::TlsError (http_client.rs:1435)
//! - OTLP exporter treats all request errors as non-retryable (otel.rs:1077)
//! - Results in fail-fast behavior (correct approach)
//! - Error message clarity depends on underlying TLS stack
//!
//! **SECURITY REQUIREMENT**:
//! - Never downgrade HTTPS endpoints to HTTP on TLS failure
//! - Fail-fast prevents accidental plaintext data transmission

#![cfg(test)]

use std::collections::HashMap;
use std::fmt;

/// TLS error fixture types for handshake failure scenarios.
#[derive(Debug, Clone)]
pub enum TlsFailureFixture {
    /// TLS version negotiation failed.
    VersionMismatch {
        client_max: String,
        server_required: String,
    },
    /// Certificate validation failed.
    CertificateError(String),
    /// Protocol negotiation failed.
    ProtocolMismatch,
    /// Generic handshake failure.
    HandshakeTimeout,
}

impl fmt::Display for TlsFailureFixture {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::VersionMismatch {
                client_max,
                server_required,
            } => {
                write!(
                    f,
                    "TLS version mismatch: client supports up to {}, server requires {}",
                    client_max, server_required
                )
            }
            Self::CertificateError(msg) => write!(f, "TLS certificate error: {}", msg),
            Self::ProtocolMismatch => write!(f, "TLS protocol negotiation failed"),
            Self::HandshakeTimeout => write!(f, "TLS handshake timeout"),
        }
    }
}

/// OTLP HTTP client fixture for TLS failure scenarios.
#[derive(Debug)]
pub struct FailingTlsOtlpClient {
    pub endpoint: String,
    pub requests_attempted: Vec<String>,
    pub tls_failures: Vec<TlsFailureFixture>,
    pub should_fail_tls: bool,
    pub failure_type: TlsFailureFixture,
}

impl FailingTlsOtlpClient {
    fn new_with_tls_failure(endpoint: &str, failure_type: TlsFailureFixture) -> Self {
        Self {
            endpoint: endpoint.to_string(),
            requests_attempted: Vec::new(),
            tls_failures: Vec::new(),
            should_fail_tls: true,
            failure_type,
        }
    }

    /// Current behavior: TLS failures become non-retryable errors.
    fn send_request(&mut self, request_body: &[u8]) -> Result<HttpResponseFixture, String> {
        self.requests_attempted.push(format!(
            "POST {} ({} bytes)",
            self.endpoint,
            request_body.len()
        ));

        if self.should_fail_tls {
            // Exercise TLS handshake failure handling.
            self.tls_failures.push(self.failure_type.clone());
            let error_msg = format!("OTLP request failed: TLS error: {}", self.failure_type);
            return Err(error_msg);
        }

        // Success case
        Ok(HttpResponseFixture {
            status: 200,
            headers: HashMap::new(),
            body: b"".to_vec(),
        })
    }

    /// Alternative defective behavior: retry forever on TLS failure.
    fn send_request_with_forever_retry(
        &mut self,
        request_body: &[u8],
        max_attempts: usize,
    ) -> Result<HttpResponseFixture, String> {
        for attempt in 1..=max_attempts {
            self.requests_attempted.push(format!(
                "POST {} attempt {} ({} bytes)",
                self.endpoint,
                attempt,
                request_body.len()
            ));

            if self.should_fail_tls {
                self.tls_failures.push(self.failure_type.clone());
                println!("📊 Attempt {}: TLS handshake failed, retrying...", attempt);
                continue; // DEFECTIVE: retry forever
            }

            return Ok(HttpResponseFixture {
                status: 200,
                headers: HashMap::new(),
                body: b"".to_vec(),
            });
        }

        Err(format!(
            "TLS handshake failed after {} attempts",
            max_attempts
        ))
    }

    /// Alternative defective behavior: downgrade to plaintext on TLS failure.
    fn send_request_with_plaintext_fallback(
        &mut self,
        request_body: &[u8],
    ) -> Result<HttpResponseFixture, String> {
        self.requests_attempted.push(format!(
            "POST {} ({} bytes)",
            self.endpoint,
            request_body.len()
        ));

        if self.should_fail_tls && self.endpoint.starts_with("https://") {
            self.tls_failures.push(self.failure_type.clone());
            println!("⚠️ TLS handshake failed, falling back to plaintext HTTP");

            // DEFECTIVE: downgrade to HTTP
            let http_endpoint = self.endpoint.replace("https://", "http://");
            self.requests_attempted.push(format!(
                "POST {} fallback ({} bytes)",
                http_endpoint,
                request_body.len()
            ));

            // Exercise a successful plaintext request path (insecure).
            return Ok(HttpResponseFixture {
                status: 200,
                headers: HashMap::new(),
                body: b"".to_vec(),
            });
        }

        Ok(HttpResponseFixture {
            status: 200,
            headers: HashMap::new(),
            body: b"".to_vec(),
        })
    }
}

/// HTTP response fixture.
#[derive(Debug, Clone)]
pub struct HttpResponseFixture {
    pub status: u16,
    pub headers: HashMap<String, String>,
    pub body: Vec<u8>,
}

/// **AUDIT TEST**: Verify TLS version mismatch handling.
///
/// **SCENARIO**: Collector requires TLS 1.3, client supports only TLS 1.2.
/// **REQUIREMENT**: Should fail-fast with clear version mismatch error.
/// **ASSESSMENT**: SOUND - current implementation fails-fast with TLS error.
#[test]
fn audit_tls_version_mismatch_handling() {
    println!("🔍 AUDIT: OTLP TLS version mismatch handling");

    println!("📋 TLS version mismatch scenario:");
    println!("   • Collector endpoint requires TLS 1.3 minimum");
    println!("   • Client supports TLS 1.2 maximum");
    println!("   • Handshake fails during version negotiation");
    println!("   • Expected: Fail-fast with actionable error message");

    let version_mismatch = TlsFailureFixture::VersionMismatch {
        client_max: "TLS 1.2".to_string(),
        server_required: "TLS 1.3".to_string(),
    };

    let mut client = FailingTlsOtlpClient::new_with_tls_failure(
        "https://collector.example.com/v1/traces",
        version_mismatch,
    );

    let test_payload = b"encoded OTLP protobuf payload";

    println!("📊 Test scenario:");
    println!("   Endpoint: {}", client.endpoint);
    println!("   Client TLS: up to TLS 1.2");
    println!("   Server requirement: TLS 1.3+");

    // **CURRENT BEHAVIOR**: Fail-fast (correct)
    println!("📊 Testing current behavior (fail-fast):");
    let result = client.send_request(test_payload);

    println!("   Result: {:?}", result);
    println!("   Requests attempted: {}", client.requests_attempted.len());
    println!("   TLS failures: {}", client.tls_failures.len());

    // Verify fail-fast behavior
    assert!(result.is_err());
    assert_eq!(client.requests_attempted.len(), 1);
    assert_eq!(client.tls_failures.len(), 1);

    let error_msg = result.unwrap_err();
    assert!(error_msg.contains("TLS error"));
    assert!(error_msg.contains("version mismatch"));

    println!("✅ SOUND: Fails fast with TLS version mismatch error");

    // **DEFECTIVE ALTERNATIVE**: Retry forever
    println!("📊 Testing defective retry-forever behavior:");
    let mut retry_client = FailingTlsOtlpClient::new_with_tls_failure(
        "https://collector.example.com/v1/traces",
        TlsFailureFixture::VersionMismatch {
            client_max: "TLS 1.2".to_string(),
            server_required: "TLS 1.3".to_string(),
        },
    );

    let retry_result = retry_client.send_request_with_forever_retry(test_payload, 5);
    println!("   Retry result: {:?}", retry_result);
    println!(
        "   Retry attempts: {}",
        retry_client.requests_attempted.len()
    );
    println!("   TLS failures: {}", retry_client.tls_failures.len());

    assert!(retry_result.is_err());
    assert_eq!(retry_client.requests_attempted.len(), 5);
    assert_eq!(retry_client.tls_failures.len(), 5);

    println!("⚠️  DEFECTIVE: Retry forever wastes resources without resolution");

    println!("🚨 AUDIT CONCLUSION: Current behavior is SOUND");
    println!("   ✅ Fails fast on TLS version mismatch");
    println!("   ✅ Does not retry forever");
    println!("   ✅ Error message includes TLS context");
}

/// **AUDIT TEST**: Verify no plaintext downgrade on TLS failure.
///
/// **SCENARIO**: TLS handshake fails for HTTPS OTLP endpoint.
/// **REQUIREMENT**: Must NOT downgrade to plaintext HTTP (security violation).
/// **ASSESSMENT**: SOUND - current implementation maintains HTTPS requirement.
#[test]
fn audit_no_plaintext_downgrade_on_tls_failure() {
    println!("🔍 AUDIT: OTLP TLS failure plaintext downgrade protection");

    println!("📋 Security requirement:");
    println!("   • HTTPS OTLP endpoints must never downgrade to HTTP");
    println!("   • TLS failures should fail-fast, not fallback");
    println!("   • Prevents accidental plaintext telemetry transmission");
    println!("   • Maintains data confidentiality and integrity");

    let tls_timeout = TlsFailureFixture::HandshakeTimeout;

    // **CURRENT BEHAVIOR**: No downgrade (correct)
    println!("📊 Testing current behavior (no downgrade):");
    let mut secure_client = FailingTlsOtlpClient::new_with_tls_failure(
        "https://secure-collector.company.com/v1/traces",
        tls_timeout.clone(),
    );

    let result = secure_client.send_request(b"sensitive telemetry data");

    println!("   HTTPS result: {:?}", result);
    println!(
        "   Requests attempted: {}",
        secure_client.requests_attempted.len()
    );

    assert!(result.is_err());
    assert_eq!(secure_client.requests_attempted.len(), 1);

    // Verify no HTTP fallback occurred
    assert!(secure_client.requests_attempted[0].contains("https://"));

    println!("✅ SOUND: HTTPS endpoint failure does not trigger HTTP fallback");

    // **DEFECTIVE ALTERNATIVE**: Plaintext downgrade
    println!("📊 Testing defective plaintext downgrade behavior:");
    let mut insecure_client = FailingTlsOtlpClient::new_with_tls_failure(
        "https://secure-collector.company.com/v1/traces",
        tls_timeout,
    );

    let downgrade_result =
        insecure_client.send_request_with_plaintext_fallback(b"sensitive telemetry data");
    println!("   Downgrade result: {:?}", downgrade_result);
    println!(
        "   Requests attempted: {}",
        insecure_client.requests_attempted.len()
    );

    // This verifies the security violation.
    assert!(downgrade_result.is_ok()); // Succeeded via HTTP
    assert_eq!(insecure_client.requests_attempted.len(), 2);
    assert!(insecure_client.requests_attempted[0].contains("https://"));
    assert!(insecure_client.requests_attempted[1].contains("http://")); // INSECURE!

    println!("⚠️  DEFECTIVE: Plaintext downgrade exposes sensitive telemetry data");

    println!("🚨 SECURITY AUDIT: Current behavior is SOUND");
    println!("   ✅ No plaintext downgrade on TLS failure");
    println!("   ✅ Maintains HTTPS-only data transmission");
    println!("   ✅ Fails closed for security");
}

/// **AUDIT TEST**: Verify TLS error message actionability.
///
/// **SCENARIO**: Various TLS handshake failures with different root causes.
/// **REQUIREMENT**: Error messages should guide users to resolution.
/// **ASSESSMENT**: Message quality depends on underlying TLS implementation.
#[test]
fn audit_tls_error_message_actionability() {
    println!("🔍 AUDIT: OTLP TLS error message actionability");

    println!("📋 Actionable error message requirements:");
    println!("   • Identify TLS as the failure point");
    println!("   • Indicate specific failure type when possible");
    println!("   • Guide user toward configuration changes");
    println!("   • Distinguish TLS from network/DNS failures");

    let test_cases = vec![
        (
            "Version mismatch",
            TlsFailureFixture::VersionMismatch {
                client_max: "TLS 1.2".to_string(),
                server_required: "TLS 1.3".to_string(),
            },
        ),
        (
            "Certificate error",
            TlsFailureFixture::CertificateError("certificate has expired".to_string()),
        ),
        ("Protocol mismatch", TlsFailureFixture::ProtocolMismatch),
        ("Handshake timeout", TlsFailureFixture::HandshakeTimeout),
    ];

    println!("📊 Testing TLS error message quality:");

    for (scenario, error_type) in test_cases {
        println!("   Scenario: {}", scenario);

        let mut client = FailingTlsOtlpClient::new_with_tls_failure(
            "https://collector.example.com/v1/traces",
            error_type,
        );

        let result = client.send_request(b"test payload");

        if let Err(error_msg) = result {
            println!("     Error: {}", error_msg);

            // Verify error message quality
            assert!(
                error_msg.contains("TLS error"),
                "Should identify TLS as failure point"
            );
            assert!(
                error_msg.contains("OTLP request failed"),
                "Should indicate OTLP context"
            );

            // Check for actionable information
            let has_actionable_info = error_msg.contains("version")
                || error_msg.contains("certificate")
                || error_msg.contains("protocol")
                || error_msg.contains("timeout");

            if !has_actionable_info {
                println!("     ⚠️ Warning: Error message lacks specific diagnostic information");
            } else {
                println!("     ✅ Contains specific failure information");
            }
        }
    }

    println!("📊 Error message enhancement opportunities:");
    println!("   • Current: Generic 'TLS error' prefix");
    println!("   • Enhancement: Include TLS configuration guidance");
    println!("   • Example: 'TLS error: version mismatch. Try upgrading client TLS version.'");
    println!("   • Example: 'TLS error: certificate validation failed. Check CA configuration.'");

    println!("✅ SOUND: TLS errors fail-fast with error context");
    println!("📌 IMPROVEMENT: Error messages could include configuration guidance");
}

/// **AUDIT TEST**: Verify current OTLP exporter TLS error classification.
///
/// **SCENARIO**: Document how TLS failures integrate with OTLP error handling.
/// **REQUIREMENT**: TLS failures should be non-retryable per OTLP best practice.
/// **ASSESSMENT**: SOUND - TLS errors classified as non-retryable.
#[test]
fn audit_otlp_tls_error_classification() {
    println!("🔍 AUDIT: OTLP TLS error classification in retry logic");

    println!("📋 Current OTLP error handling (otel.rs):");
    println!("   • Line 1077: .map_err(|e| OtlpError::non_retryable(...))");
    println!("   • TLS errors from HttpClient become non-retryable");
    println!("   • No retry attempts on TLS handshake failure");
    println!("   • Consistent with OTLP best practice");

    println!("📊 TLS error classification analysis:");
    println!("   ✅ Version mismatch: non-retryable (correct - config change needed)");
    println!("   ✅ Certificate error: non-retryable (correct - cert/CA fix needed)");
    println!("   ✅ Protocol error: non-retryable (correct - protocol config needed)");
    println!("   ✅ Handshake timeout: non-retryable (correct - not transient)");

    println!("📊 Comparison with other error types:");
    println!("   • 502/503/504: retryable (server-side, transient)");
    println!("   • DNS errors: non-retryable (config issue)");
    println!("   • TLS errors: non-retryable (config/compatibility issue)");
    println!("   • Connection refused: retryable (service might restart)");

    // Exercise the classification.
    fn classify_otlp_error(error_type: &str) -> &'static str {
        match error_type {
            "TLS error" => "non_retryable", // Current behavior (correct)
            "502 Bad Gateway" => "retryable",
            "503 Service Unavailable" => "retryable",
            "DNS resolution failed" => "non_retryable",
            _ => "unknown",
        }
    }

    let error_types = [
        "TLS error",
        "502 Bad Gateway",
        "503 Service Unavailable",
        "DNS resolution failed",
    ];

    println!("📊 OTLP error classification matrix:");
    for error_type in error_types {
        let classification = classify_otlp_error(error_type);
        println!("   {}: {}", error_type, classification);
    }

    // Verify TLS errors are non-retryable
    assert_eq!(classify_otlp_error("TLS error"), "non_retryable");

    println!("✅ SOUND: TLS errors correctly classified as non-retryable");
    println!("   • Prevents wasteful retry loops");
    println!("   • Forces users to fix configuration issues");
    println!("   • Aligned with OTLP specification guidance");

    println!("🚨 AUDIT CONCLUSION: TLS handshake failure handling is SOUND");
    println!("   Current: Fail-fast with clear TLS error context");
    println!("   Security: No plaintext downgrade");
    println!("   Performance: No wasteful retry loops");
}