use std::fmt;
use serde::Serialize;
use crate::error::CompatError;
use crate::warnings::CompatWarning;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticCode {
UnsupportedProtocol,
UnsupportedTransportWrapper,
UnsupportedFlag,
UnsupportedPlatform,
UnsupportedSecuritySensitiveLegacyFeature,
InvalidUriSyntax,
InvalidChainComposition,
MissingTarget,
MissingCredential,
InvalidCipherMethod,
BindFailure,
PrivilegeCapabilityMissing,
ExternalDependencyMissing,
RulefileError,
InvalidRegexPattern,
FancyRegexBackend,
UriPreservedUnsupportedComponent,
H2HandshakeFailure,
H2ConnectRejected,
H2StreamReset,
H2GoawayReceived,
H2PoolExhausted,
H2FlowControlStall,
H2AuthFailure,
H2UnsupportedCleartext,
H2TlsAlpnMismatch,
}
impl fmt::Display for DiagnosticCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::UnsupportedProtocol => "unsupported_protocol",
Self::UnsupportedTransportWrapper => "unsupported_transport_wrapper",
Self::UnsupportedFlag => "unsupported_flag",
Self::UnsupportedPlatform => "unsupported_platform",
Self::UnsupportedSecuritySensitiveLegacyFeature => {
"unsupported_security_sensitive_legacy_feature"
}
Self::InvalidUriSyntax => "invalid_uri_syntax",
Self::InvalidChainComposition => "invalid_chain_composition",
Self::MissingTarget => "missing_target",
Self::MissingCredential => "missing_credential",
Self::InvalidCipherMethod => "invalid_cipher_method",
Self::BindFailure => "bind_failure",
Self::PrivilegeCapabilityMissing => "privilege_capability_missing",
Self::ExternalDependencyMissing => "external_dependency_missing",
Self::RulefileError => "rulefile_error",
Self::InvalidRegexPattern => "invalid_regex_pattern",
Self::FancyRegexBackend => "fancy_regex_backend",
Self::UriPreservedUnsupportedComponent => "uri_preserved_unsupported_component",
Self::H2HandshakeFailure => "h2_handshake_failure",
Self::H2ConnectRejected => "h2_connect_rejected",
Self::H2StreamReset => "h2_stream_reset",
Self::H2GoawayReceived => "h2_goaway_received",
Self::H2PoolExhausted => "h2_pool_exhausted",
Self::H2FlowControlStall => "h2_flow_control_stall",
Self::H2AuthFailure => "h2_auth_failure",
Self::H2UnsupportedCleartext => "h2_unsupported_cleartext",
Self::H2TlsAlpnMismatch => "h2_tls_alpn_mismatch",
};
f.write_str(label)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct StructuredDiagnostic {
pub code: DiagnosticCode,
#[serde(skip_serializing_if = "Option::is_none")]
pub feature_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tier: Option<String>,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub suggestion: Option<String>,
}
impl fmt::Display for StructuredDiagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {}", self.code, self.message)?;
if let Some(ref tier) = self.tier {
write!(f, " (tier: {})", tier)?;
}
if let Some(ref suggestion) = self.suggestion {
write!(f, " — suggestion: {}", suggestion)?;
}
Ok(())
}
}
impl From<CompatError> for StructuredDiagnostic {
fn from(err: CompatError) -> Self {
match err {
CompatError::UnsupportedProtocol(proto) => StructuredDiagnostic {
code: DiagnosticCode::UnsupportedProtocol,
feature_id: None,
tier: Some("unsupported".to_string()),
message: format!("unsupported protocol: {}", proto),
suggestion: Some("use http, socks4, socks5, trojan, or ss".to_string()),
},
CompatError::UnsupportedFeature { feature, detail } => {
let (code, tier, suggestion) = classify_unsupported_feature(feature);
StructuredDiagnostic {
code,
feature_id: Some(feature.to_string()),
tier: Some(tier.to_string()),
message: detail,
suggestion: suggestion.map(String::from),
}
}
CompatError::InvalidUri { message } => StructuredDiagnostic {
code: DiagnosticCode::InvalidUriSyntax,
feature_id: None,
tier: None,
message,
suggestion: None,
},
CompatError::InvalidArgs { message } => StructuredDiagnostic {
code: DiagnosticCode::InvalidUriSyntax,
feature_id: None,
tier: None,
message,
suggestion: None,
},
CompatError::ConfigValidation { message } => StructuredDiagnostic {
code: DiagnosticCode::InvalidChainComposition,
feature_id: None,
tier: None,
message,
suggestion: None,
},
CompatError::MissingArgument(flag) => StructuredDiagnostic {
code: DiagnosticCode::MissingTarget,
feature_id: None,
tier: None,
message: format!("missing required argument: {}", flag),
suggestion: None,
},
}
}
}
impl From<&CompatWarning> for StructuredDiagnostic {
fn from(warn: &CompatWarning) -> Self {
match warn.category {
"unknown-flag" => StructuredDiagnostic {
code: DiagnosticCode::UnsupportedFlag,
feature_id: None,
tier: Some("unsupported".to_string()),
message: warn.message.clone(),
suggestion: None,
},
"direct-mode" => StructuredDiagnostic {
code: DiagnosticCode::MissingTarget,
feature_id: None,
tier: Some("compatible_with_warning".to_string()),
message: warn.message.clone(),
suggestion: Some("add -r <upstream-uri> for proxied connections".to_string()),
},
"credential-in-toml" => StructuredDiagnostic {
code: DiagnosticCode::MissingCredential,
feature_id: None,
tier: Some("compatible_with_warning".to_string()),
message: warn.message.clone(),
suggestion: Some(
"use secret sources or environment variables for credentials".to_string(),
),
},
"verbose-mode" => StructuredDiagnostic {
code: DiagnosticCode::UnsupportedFlag,
feature_id: Some("verbose".to_string()),
tier: Some("compatible_with_warning".to_string()),
message: warn.message.clone(),
suggestion: Some("set RUST_LOG=debug".to_string()),
},
"debug-mode" => StructuredDiagnostic {
code: DiagnosticCode::UnsupportedFlag,
feature_id: Some("debug".to_string()),
tier: Some("compatible_with_warning".to_string()),
message: warn.message.clone(),
suggestion: Some(
"Eggress selects debug-level default tracing; set RUST_LOG explicitly to override it"
.to_string(),
),
},
"scheduler" => StructuredDiagnostic {
code: DiagnosticCode::UnsupportedFlag,
feature_id: Some("scheduler".to_string()),
tier: Some("compatible_with_warning".to_string()),
message: warn.message.clone(),
suggestion: Some(
"use first-available, round-robin, or least-connections".to_string(),
),
},
"alive-check" => StructuredDiagnostic {
code: DiagnosticCode::UnsupportedFlag,
feature_id: Some("alive".to_string()),
tier: Some("native_equivalent".to_string()),
message: warn.message.clone(),
suggestion: Some("configure health probes in eggress TOML".to_string()),
},
"ul-no-listener" => StructuredDiagnostic {
code: DiagnosticCode::MissingTarget,
feature_id: None,
tier: Some("compatible_with_warning".to_string()),
message: warn.message.clone(),
suggestion: None,
},
"pac-serving" => StructuredDiagnostic {
code: DiagnosticCode::UnsupportedFlag,
feature_id: Some("pac".to_string()),
tier: Some("compatible_with_warning".to_string()),
message: warn.message.clone(),
suggestion: Some(
"configure PAC serving in eggress TOML admin.pac block".to_string(),
),
},
"test-mode" => StructuredDiagnostic {
code: DiagnosticCode::UnsupportedFlag,
feature_id: Some("test".to_string()),
tier: Some("native_equivalent".to_string()),
message: warn.message.clone(),
suggestion: Some("use 'eggress upstream test -c <config>'".to_string()),
},
"system-proxy" => StructuredDiagnostic {
code: DiagnosticCode::UnsupportedFlag,
feature_id: Some("sys".to_string()),
tier: Some("compatible_with_warning".to_string()),
message: warn.message.clone(),
suggestion: Some(
"compatibility mode restores prior settings after --sys".to_string(),
),
},
"auth-timeout" => StructuredDiagnostic {
code: DiagnosticCode::UnsupportedFlag,
feature_id: Some("auth".to_string()),
tier: Some("compatible_with_warning".to_string()),
message: warn.message.clone(),
suggestion: Some(
"configure listener credentials to enable source-IP reuse".to_string(),
),
},
"log-file" => StructuredDiagnostic {
code: DiagnosticCode::UnsupportedFlag,
feature_id: Some("log".to_string()),
tier: Some("compatible_with_warning".to_string()),
message: warn.message.clone(),
suggestion: Some(
"redirect stderr with shell redirection for file logging".to_string(),
),
},
"reuse-port" => StructuredDiagnostic {
code: DiagnosticCode::UnsupportedFlag,
feature_id: Some("reuse".to_string()),
tier: Some("native_equivalent".to_string()),
message: warn.message.clone(),
suggestion: Some("SO_REUSEPORT applied to listener sockets".to_string()),
},
"get-static-content" => StructuredDiagnostic {
code: DiagnosticCode::UnsupportedFlag,
feature_id: Some("get".to_string()),
tier: Some("native_equivalent".to_string()),
message: warn.message.clone(),
suggestion: Some(
"configure the same PATH,FILE pair as static content in the Eggress admin server"
.to_string(),
),
},
"rulefile-read" | "rulefile-parse" | "rulefile-partial" => StructuredDiagnostic {
code: DiagnosticCode::RulefileError,
feature_id: Some("rulefile".to_string()),
tier: Some("compatible_with_warning".to_string()),
message: warn.message.clone(),
suggestion: Some(
"configure rules in eggress TOML [[rules]] with structured matchers"
.to_string(),
),
},
"chain-unsupported-hop" => StructuredDiagnostic {
code: DiagnosticCode::UnsupportedProtocol,
feature_id: Some("chain".to_string()),
tier: Some("unsupported".to_string()),
message: warn.message.clone(),
suggestion: Some(
"remove unsupported hops or use multi-r flag for alternatives".to_string(),
),
},
"chain-backward-composition" => StructuredDiagnostic {
code: DiagnosticCode::UnsupportedProtocol,
feature_id: Some("chain".to_string()),
tier: Some("unsupported".to_string()),
message: warn.message.clone(),
suggestion: Some(
"use single-hop backward (+in) or split into separate -r flags".to_string(),
),
},
_ => StructuredDiagnostic {
code: DiagnosticCode::UnsupportedFlag,
feature_id: None,
tier: None,
message: warn.message.clone(),
suggestion: None,
},
}
}
}
pub fn h2_diagnostic(code: DiagnosticCode, message: impl Into<String>) -> StructuredDiagnostic {
StructuredDiagnostic {
code,
feature_id: Some("h2".to_string()),
tier: Some("drop_in".to_string()),
message: message.into(),
suggestion: None,
}
}
pub fn h2_diagnostic_with_suggestion(
code: DiagnosticCode,
message: impl Into<String>,
suggestion: impl Into<String>,
) -> StructuredDiagnostic {
StructuredDiagnostic {
code,
feature_id: Some("h2".to_string()),
tier: Some("drop_in".to_string()),
message: message.into(),
suggestion: Some(suggestion.into()),
}
}
impl CompatWarning {
pub fn diagnostic_code(&self) -> DiagnosticCode {
StructuredDiagnostic::from(self).code
}
}
fn classify_unsupported_feature(
feature: &'static str,
) -> (DiagnosticCode, &'static str, Option<&'static str>) {
match feature {
"daemon" | "backward-jump-chain" | "backward-tls" => (
DiagnosticCode::UnsupportedFlag,
"unsupported",
Some("configure this via eggress TOML"),
),
"system-proxy" => (
DiagnosticCode::UnsupportedFlag,
"compatible_with_warning",
Some("compatibility mode restores prior settings after --sys"),
),
"auth-timeout" => (
DiagnosticCode::UnsupportedFlag,
"compatible_with_warning",
Some("configure listener credentials to enable source-IP reuse"),
),
"chain-unsupported-hop" | "chain-backward-composition" => (
DiagnosticCode::UnsupportedProtocol,
"unsupported",
Some("remove unsupported hops or use multi-r flag for alternatives"),
),
"ssr-listener" | "ssr-upstream" | "ssr-udp" => (
DiagnosticCode::UnsupportedSecuritySensitiveLegacyFeature,
"intentional_non_parity",
Some("use standard Shadowsocks (ss://) with AEAD methods"),
),
"ssh-listener" => (
DiagnosticCode::UnsupportedProtocol,
"intentional_non_parity",
Some("SSH is upstream-only; enable the ssh feature for pproxy-compatible transport or use OpenSSH dynamic forwarding (ssh -D)"),
),
"ssh-upstream" => (
DiagnosticCode::UnsupportedProtocol,
"intentional_non_parity",
Some("SSH upstream transport requires the optional ssh feature"),
),
"unix-upstream" | "redir-upstream"
| "direct-listener" => (DiagnosticCode::UnsupportedProtocol, "unsupported", None),
"socks4-bind" => (
DiagnosticCode::UnsupportedProtocol,
"unsupported",
Some("SOCKS4 BIND is not implemented; pproxy also does not implement SOCKS4 BIND"),
),
"socks5-bind" => (
DiagnosticCode::UnsupportedProtocol,
"unsupported",
Some("SOCKS5 BIND is not implemented; pproxy also does not implement SOCKS5 BIND"),
),
"udp-http-transport" | "udp-https-transport" => (
DiagnosticCode::UnsupportedProtocol,
"unsupported",
Some("use direct://, socks5://, or ss:// for UDP upstreams"),
),
"udp-socks4-transport" | "udp-socks4a-transport" => (
DiagnosticCode::UnsupportedProtocol,
"unsupported",
Some("SOCKS4 does not support UDP; use socks5:// for UDP upstreams"),
),
"udp-trojan-transport" => (
DiagnosticCode::UnsupportedProtocol,
"unsupported",
Some("Trojan does not support UDP; use direct://, socks5://, or ss://"),
),
"udp-multihop" => (
DiagnosticCode::UnsupportedProtocol,
"unsupported",
Some("UDP multi-hop chains are not supported; use single-hop upstreams"),
),
"trojan-no-password" => (
DiagnosticCode::UnsupportedProtocol,
"unsupported",
Some("provide a password in the Trojan URI: trojan://password@host:port"),
),
"scheme" => (
DiagnosticCode::UnsupportedProtocol,
"unsupported",
Some("use a recognized protocol scheme"),
),
"legacy-cipher" => (
DiagnosticCode::InvalidCipherMethod,
"unsupported",
Some("enable the optional legacy-crypto feature or use an AEAD method: aes-128-gcm, aes-192-gcm, aes-256-gcm, chacha20-ietf-poly1305"),
),
_ => (DiagnosticCode::UnsupportedFlag, "unsupported", None),
}
}
pub fn classify_unsupported_feature_code(feature: &str) -> DiagnosticCode {
classify_unsupported_feature_inner(feature)
}
pub fn classify_unsupported_feature_tier(feature: &'static str) -> &'static str {
let (_, tier, _) = classify_unsupported_feature(feature);
tier
}
fn classify_unsupported_feature_inner(feature: &str) -> DiagnosticCode {
match feature {
"daemon" | "backward-jump-chain" | "backward-tls" => DiagnosticCode::UnsupportedFlag,
"system-proxy" | "auth-timeout" => DiagnosticCode::UnsupportedFlag,
"chain-unsupported-hop" | "chain-backward-composition" => {
DiagnosticCode::UnsupportedProtocol
}
"ssr-listener" | "ssr-upstream" | "ssr-udp" => {
DiagnosticCode::UnsupportedSecuritySensitiveLegacyFeature
}
"trojan-listener" | "ssh-listener" | "ssh-upstream" | "unix-upstream"
| "redir-upstream" | "direct-listener" => DiagnosticCode::UnsupportedProtocol,
"socks4-bind" | "socks5-bind" => DiagnosticCode::UnsupportedProtocol,
"udp-http-transport"
| "udp-https-transport"
| "udp-socks4-transport"
| "udp-socks4a-transport"
| "udp-trojan-transport"
| "udp-multihop" => DiagnosticCode::UnsupportedProtocol,
"scheme" => DiagnosticCode::UnsupportedProtocol,
"legacy-cipher" => DiagnosticCode::InvalidCipherMethod,
_ => DiagnosticCode::UnsupportedFlag,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::CompatError;
use crate::warnings::CompatWarning;
#[test]
fn diagnostic_code_display_is_snake_case() {
assert_eq!(
DiagnosticCode::UnsupportedProtocol.to_string(),
"unsupported_protocol"
);
assert_eq!(
DiagnosticCode::UnsupportedSecuritySensitiveLegacyFeature.to_string(),
"unsupported_security_sensitive_legacy_feature"
);
assert_eq!(
DiagnosticCode::InvalidUriSyntax.to_string(),
"invalid_uri_syntax"
);
assert_eq!(DiagnosticCode::MissingTarget.to_string(), "missing_target");
assert_eq!(
DiagnosticCode::MissingCredential.to_string(),
"missing_credential"
);
assert_eq!(
DiagnosticCode::InvalidCipherMethod.to_string(),
"invalid_cipher_method"
);
assert_eq!(DiagnosticCode::BindFailure.to_string(), "bind_failure");
assert_eq!(
DiagnosticCode::PrivilegeCapabilityMissing.to_string(),
"privilege_capability_missing"
);
assert_eq!(
DiagnosticCode::ExternalDependencyMissing.to_string(),
"external_dependency_missing"
);
}
#[test]
fn diagnostic_code_serializes_to_snake_case() {
let code = DiagnosticCode::UnsupportedProtocol;
let json = serde_json::to_string(&code).unwrap();
assert_eq!(json, "\"unsupported_protocol\"");
}
#[test]
fn structured_diagnostic_display_includes_code_and_message() {
let diag = StructuredDiagnostic {
code: DiagnosticCode::UnsupportedProtocol,
feature_id: None,
tier: Some("unsupported".to_string()),
message: "unsupported protocol: ftp".to_string(),
suggestion: None,
};
let s = diag.to_string();
assert!(s.contains("[unsupported_protocol]"));
assert!(s.contains("unsupported protocol: ftp"));
assert!(s.contains("tier: unsupported"));
}
#[test]
fn structured_diagnostic_display_includes_suggestion() {
let diag = StructuredDiagnostic {
code: DiagnosticCode::InvalidCipherMethod,
feature_id: None,
tier: Some("intentional_non_parity".to_string()),
message: "legacy cipher".to_string(),
suggestion: Some("use AEAD".to_string()),
};
let s = diag.to_string();
assert!(s.contains("suggestion: use AEAD"));
}
#[test]
fn from_unsupported_protocol_error() {
let err = CompatError::UnsupportedProtocol("ftp".to_string());
let diag = StructuredDiagnostic::from(err);
assert_eq!(diag.code, DiagnosticCode::UnsupportedProtocol);
assert_eq!(diag.tier.as_deref(), Some("unsupported"));
assert!(diag.suggestion.is_some());
}
#[test]
fn from_unsupported_feature_daemon() {
let err = CompatError::unsupported("daemon", "--daemon not supported");
let diag = StructuredDiagnostic::from(err);
assert_eq!(diag.code, DiagnosticCode::UnsupportedFlag);
assert_eq!(diag.feature_id.as_deref(), Some("daemon"));
}
#[test]
fn from_unsupported_feature_ssr() {
let err = CompatError::unsupported("ssr-listener", "SSR not supported");
let diag = StructuredDiagnostic::from(err);
assert_eq!(
diag.code,
DiagnosticCode::UnsupportedSecuritySensitiveLegacyFeature
);
assert_eq!(diag.tier.as_deref(), Some("intentional_non_parity"));
}
#[test]
fn from_unsupported_feature_legacy_cipher() {
let err = CompatError::unsupported("legacy-cipher", "aes-128-ctr not supported");
let diag = StructuredDiagnostic::from(err);
assert_eq!(diag.code, DiagnosticCode::InvalidCipherMethod);
}
#[test]
fn from_invalid_uri_error() {
let err = CompatError::InvalidUri {
message: "bad host".to_string(),
};
let diag = StructuredDiagnostic::from(err);
assert_eq!(diag.code, DiagnosticCode::InvalidUriSyntax);
}
#[test]
fn from_invalid_args_error() {
let err = CompatError::InvalidArgs {
message: "no listener".to_string(),
};
let diag = StructuredDiagnostic::from(err);
assert_eq!(diag.code, DiagnosticCode::InvalidUriSyntax);
}
#[test]
fn from_config_validation_error() {
let err = CompatError::ConfigValidation {
message: "conflict".to_string(),
};
let diag = StructuredDiagnostic::from(err);
assert_eq!(diag.code, DiagnosticCode::InvalidChainComposition);
}
#[test]
fn from_missing_argument_error() {
let err = CompatError::MissingArgument("-l".to_string());
let diag = StructuredDiagnostic::from(err);
assert_eq!(diag.code, DiagnosticCode::MissingTarget);
}
#[test]
fn from_unknown_flag_warning() {
let warn = CompatWarning {
category: "unknown-flag",
message: "unrecognized flag '--foo'".to_string(),
};
let diag = StructuredDiagnostic::from(&warn);
assert_eq!(diag.code, DiagnosticCode::UnsupportedFlag);
}
#[test]
fn from_direct_mode_warning() {
let warn = CompatWarning {
category: "direct-mode",
message: "no upstream".to_string(),
};
let diag = StructuredDiagnostic::from(&warn);
assert_eq!(diag.code, DiagnosticCode::MissingTarget);
assert!(diag.suggestion.is_some());
}
#[test]
fn from_credential_warning() {
let warn = CompatWarning {
category: "credential-in-toml",
message: "plaintext creds".to_string(),
};
let diag = StructuredDiagnostic::from(&warn);
assert_eq!(diag.code, DiagnosticCode::MissingCredential);
}
#[test]
fn from_verbose_warning() {
let warn = CompatWarning {
category: "verbose-mode",
message: "use RUST_LOG".to_string(),
};
let diag = StructuredDiagnostic::from(&warn);
assert_eq!(diag.code, DiagnosticCode::UnsupportedFlag);
assert_eq!(diag.feature_id.as_deref(), Some("verbose"));
assert_eq!(diag.tier.as_deref(), Some("compatible_with_warning"));
}
#[test]
fn from_debug_warning_uses_debug_feature_and_tier() {
let warn = CompatWarning {
category: "debug-mode",
message: "debug tracing difference".to_string(),
};
let diag = StructuredDiagnostic::from(&warn);
assert_eq!(diag.feature_id.as_deref(), Some("debug"));
assert_eq!(diag.tier.as_deref(), Some("compatible_with_warning"));
}
#[test]
fn touched_warning_categories_have_consistent_tiers() {
let categories = [
("debug-mode", "debug"),
("verbose-mode", "verbose"),
("pac-serving", "pac"),
("get-static-content", "get"),
("test-mode", "test"),
];
for (category, feature_id) in categories {
let warn = CompatWarning {
category,
message: "test diagnostic".to_string(),
};
let diagnostic = StructuredDiagnostic::from(&warn);
assert_eq!(diagnostic.feature_id.as_deref(), Some(feature_id));
assert_eq!(
diagnostic.tier.as_deref(),
Some(crate::manifest_tier_for_category(category).as_str()),
"tier mismatch for {category}"
);
}
}
#[test]
fn from_unknown_category_warning() {
let warn = CompatWarning {
category: "some-new-category",
message: "something happened".to_string(),
};
let diag = StructuredDiagnostic::from(&warn);
assert_eq!(diag.code, DiagnosticCode::UnsupportedFlag);
}
#[test]
fn warning_diagnostic_code_method() {
let warn = CompatWarning {
category: "direct-mode",
message: "no upstream".to_string(),
};
assert_eq!(warn.diagnostic_code(), DiagnosticCode::MissingTarget);
}
#[test]
fn structured_diagnostic_json_roundtrip() {
let diag = StructuredDiagnostic {
code: DiagnosticCode::UnsupportedProtocol,
feature_id: Some("ssh-upstream".to_string()),
tier: Some("unsupported".to_string()),
message: "SSH not supported".to_string(),
suggestion: None,
};
let json = serde_json::to_value(&diag).unwrap();
assert_eq!(json["code"], "unsupported_protocol");
assert_eq!(json["feature_id"], "ssh-upstream");
assert_eq!(json["tier"], "unsupported");
assert_eq!(json["message"], "SSH not supported");
assert!(json.get("suggestion").is_none());
}
#[test]
fn all_diagnostic_codes_serialize() {
let codes = [
DiagnosticCode::UnsupportedProtocol,
DiagnosticCode::UnsupportedTransportWrapper,
DiagnosticCode::UnsupportedFlag,
DiagnosticCode::UnsupportedPlatform,
DiagnosticCode::UnsupportedSecuritySensitiveLegacyFeature,
DiagnosticCode::InvalidUriSyntax,
DiagnosticCode::InvalidChainComposition,
DiagnosticCode::MissingTarget,
DiagnosticCode::MissingCredential,
DiagnosticCode::InvalidCipherMethod,
DiagnosticCode::BindFailure,
DiagnosticCode::PrivilegeCapabilityMissing,
DiagnosticCode::ExternalDependencyMissing,
DiagnosticCode::RulefileError,
DiagnosticCode::InvalidRegexPattern,
DiagnosticCode::FancyRegexBackend,
DiagnosticCode::UriPreservedUnsupportedComponent,
DiagnosticCode::H2HandshakeFailure,
DiagnosticCode::H2ConnectRejected,
DiagnosticCode::H2StreamReset,
DiagnosticCode::H2GoawayReceived,
DiagnosticCode::H2PoolExhausted,
DiagnosticCode::H2FlowControlStall,
DiagnosticCode::H2AuthFailure,
DiagnosticCode::H2UnsupportedCleartext,
DiagnosticCode::H2TlsAlpnMismatch,
];
for code in &codes {
let json = serde_json::to_string(code).unwrap();
assert!(json.starts_with('"'));
assert!(json.ends_with('"'));
}
}
#[test]
fn structured_diagnostic_from_unsupported_protocol_never_leaks_credentials() {
let err = CompatError::UnsupportedProtocol("ssh".to_string());
let diag = StructuredDiagnostic::from(err);
assert!(!diag.message.contains("@"));
assert!(diag.suggestion.is_some());
}
#[test]
fn structured_diagnostic_from_unsupported_feature_never_leaks_credentials() {
let err = CompatError::unsupported("daemon", "--daemon not supported");
let diag = StructuredDiagnostic::from(err);
assert_eq!(diag.code, DiagnosticCode::UnsupportedFlag);
assert!(!diag.message.contains("@"));
}
#[test]
fn structured_diagnostic_from_invalid_uri_never_leaks_credentials() {
let err = CompatError::InvalidUri {
message: "missing port in endpoint".to_string(),
};
let diag = StructuredDiagnostic::from(err);
assert_eq!(diag.code, DiagnosticCode::InvalidUriSyntax);
assert!(!diag.message.contains("@"));
}
#[test]
fn structured_diagnostic_json_excludes_optional_none_fields() {
let diag = StructuredDiagnostic {
code: DiagnosticCode::UnsupportedProtocol,
feature_id: None,
tier: None,
message: "test".to_string(),
suggestion: None,
};
let json = serde_json::to_value(&diag).unwrap();
assert!(json.get("feature_id").is_none());
assert!(json.get("tier").is_none());
assert!(json.get("suggestion").is_none());
}
#[test]
fn compat_warning_display_never_leaks_credentials() {
let warn = CompatWarning {
category: "credential-in-toml",
message: "Listener 'pproxy-local-0' has plaintext credentials in generated TOML"
.to_string(),
};
let display = warn.to_string();
assert!(display.contains("[credential-in-toml]"));
assert!(!display.contains("@"));
}
#[test]
fn h2_diagnostic_code_display() {
assert_eq!(
DiagnosticCode::H2HandshakeFailure.to_string(),
"h2_handshake_failure"
);
assert_eq!(
DiagnosticCode::H2ConnectRejected.to_string(),
"h2_connect_rejected"
);
assert_eq!(DiagnosticCode::H2StreamReset.to_string(), "h2_stream_reset");
assert_eq!(
DiagnosticCode::H2GoawayReceived.to_string(),
"h2_goaway_received"
);
assert_eq!(
DiagnosticCode::H2PoolExhausted.to_string(),
"h2_pool_exhausted"
);
assert_eq!(
DiagnosticCode::H2FlowControlStall.to_string(),
"h2_flow_control_stall"
);
assert_eq!(DiagnosticCode::H2AuthFailure.to_string(), "h2_auth_failure");
assert_eq!(
DiagnosticCode::H2UnsupportedCleartext.to_string(),
"h2_unsupported_cleartext"
);
assert_eq!(
DiagnosticCode::H2TlsAlpnMismatch.to_string(),
"h2_tls_alpn_mismatch"
);
}
#[test]
fn h2_diagnostic_helper() {
let diag = h2_diagnostic(DiagnosticCode::H2HandshakeFailure, "handshake failed");
assert_eq!(diag.code, DiagnosticCode::H2HandshakeFailure);
assert_eq!(diag.feature_id.as_deref(), Some("h2"));
assert_eq!(diag.tier.as_deref(), Some("drop_in"));
assert_eq!(diag.message, "handshake failed");
assert!(diag.suggestion.is_none());
}
#[test]
fn h2_diagnostic_with_suggestion_helper() {
let diag = h2_diagnostic_with_suggestion(
DiagnosticCode::H2TlsAlpnMismatch,
"ALPN mismatch",
"use h2 ALPN",
);
assert_eq!(diag.code, DiagnosticCode::H2TlsAlpnMismatch);
assert_eq!(diag.feature_id.as_deref(), Some("h2"));
assert_eq!(diag.tier.as_deref(), Some("drop_in"));
assert_eq!(diag.message, "ALPN mismatch");
assert_eq!(diag.suggestion.as_deref(), Some("use h2 ALPN"));
}
#[test]
fn diagnostic_code_display_never_contains_credentials() {
let codes = [
DiagnosticCode::UnsupportedProtocol,
DiagnosticCode::UnsupportedFlag,
DiagnosticCode::InvalidUriSyntax,
DiagnosticCode::MissingTarget,
DiagnosticCode::MissingCredential,
DiagnosticCode::InvalidCipherMethod,
DiagnosticCode::BindFailure,
];
for code in &codes {
let display = code.to_string();
assert!(
!display.contains("@"),
"code display contains @: {}",
display
);
}
}
}