use serde_json::Value;
use qubit_json::{
ErrorPrivacyPolicy,
JsonDecodeErrorKind,
JsonDecodeOptions,
JsonDecodeStage,
JsonTopLevelKind,
LenientJsonDecoder,
};
use crate::fixtures::PublicChoice;
#[test]
fn test_error_display_for_empty_input_uses_message() {
let error = LenientJsonDecoder::default()
.decode_value("")
.expect_err("empty input should return a normalization error");
assert_eq!(error.to_string(), "JSON input is empty after normalization");
assert_eq!(error.privacy_policy(), ErrorPrivacyPolicy::Redacted);
assert_eq!(error.raw_input_bytes(), 0);
assert_eq!(error.normalized_input_bytes(), None);
assert_eq!(error.utf8_valid_up_to(), None);
assert_eq!(error.utf8_error_len(), None);
assert!(std::error::Error::source(&error).is_none());
}
#[test]
fn test_error_exposes_top_level_mismatch_context() {
let error = LenientJsonDecoder::default()
.decode_object::<serde_json::Value>("[]")
.expect_err("top-level array should fail an object contract");
assert_eq!(error.expected_top_level(), Some(JsonTopLevelKind::Object));
assert_eq!(error.actual_top_level(), Some(JsonTopLevelKind::Array));
assert_eq!(error.raw_input_bytes(), 2);
assert_eq!(error.normalized_input_bytes(), Some(2));
assert_eq!(error.privacy_policy(), ErrorPrivacyPolicy::Redacted);
assert_eq!(
error.to_string(),
"Unexpected JSON top-level type: expected object, got array"
);
}
#[test]
fn test_error_exposes_immutable_normalized_diagnostics_without_duplicate_location()
{
let error = LenientJsonDecoder::default()
.decode_value(" {\n")
.expect_err(
"incomplete JSON should fail after whitespace normalization",
);
assert_eq!(error.kind(), JsonDecodeErrorKind::InvalidJson);
assert_eq!(error.stage(), JsonDecodeStage::Parse);
assert_eq!(error.raw_input_bytes(), 4);
assert_eq!(error.normalized_input_bytes(), Some(1));
assert_eq!(error.normalized_line(), Some(1));
assert_eq!(error.normalized_column(), Some(1));
assert_eq!(error.to_string(), error.message());
assert!(std::error::Error::source(&error).is_none());
}
#[test]
fn test_error_source_for_invalid_json_preserves_serde_error() {
let decoder = LenientJsonDecoder::new(
JsonDecodeOptions::default()
.with_error_privacy_policy(ErrorPrivacyPolicy::Detailed),
);
let error = decoder
.decode_value("{")
.expect_err("invalid JSON should preserve the parser source error");
let source = std::error::Error::source(&error)
.expect("invalid JSON errors should expose the serde_json source");
assert!(source.to_string().contains("EOF"));
}
#[test]
fn test_default_error_privacy_redacts_input_derived_serde_details() {
const SECRET: &str = "TOP_SECRET_VALUE";
let error = LenientJsonDecoder::default()
.decode::<PublicChoice>(&format!("\"{SECRET}\""))
.expect_err("an unknown enum variant should fail deserialization");
assert_eq!(error.privacy_policy(), ErrorPrivacyPolicy::Redacted);
assert!(!error.message().contains(SECRET));
assert!(!error.to_string().contains(SECRET));
assert!(!format!("{error:?}").contains(SECRET));
assert!(std::error::Error::source(&error).is_none());
assert_eq!(error.normalized_line(), Some(1));
assert_eq!(error.normalized_column(), Some(18));
}
#[test]
fn test_detailed_error_privacy_preserves_input_derived_serde_details() {
const SECRET: &str = "TOP_SECRET_VALUE";
let decoder = LenientJsonDecoder::new(
JsonDecodeOptions::default()
.with_error_privacy_policy(ErrorPrivacyPolicy::Detailed),
);
let error = decoder
.decode::<PublicChoice>(&format!("\"{SECRET}\""))
.expect_err("an unknown enum variant should fail deserialization");
assert_eq!(error.privacy_policy(), ErrorPrivacyPolicy::Detailed);
assert!(error.message().contains(SECRET));
assert!(error.to_string().contains(SECRET));
assert!(format!("{error:?}").contains(SECRET));
let source = std::error::Error::source(&error)
.expect("detailed errors should retain the serde_json source");
assert!(source.to_string().contains(SECRET));
}
#[test]
fn test_default_invalid_json_error_does_not_expose_serde_source() {
let error = LenientJsonDecoder::default()
.decode_value("{")
.expect_err("invalid JSON should fail parsing");
assert_eq!(error.privacy_policy(), ErrorPrivacyPolicy::Redacted);
assert!(std::error::Error::source(&error).is_none());
}
#[test]
fn test_invalid_utf8_redacted_error_does_not_expose_source() {
let error = LenientJsonDecoder::default()
.decode_slice::<serde_json::Value>(&[0xff])
.expect_err("invalid UTF-8 must fail");
assert_eq!(error.privacy_policy(), ErrorPrivacyPolicy::Redacted);
assert_eq!(error.utf8_valid_up_to(), Some(0));
assert_eq!(error.utf8_error_len(), Some(1));
assert!(std::error::Error::source(&error).is_none());
assert!(!format!("{error:?}").contains("255"));
}
#[test]
fn test_invalid_utf8_exposes_safe_position_diagnostics() {
let definite = LenientJsonDecoder::default()
.decode_slice::<serde_json::Value>(&[b'{', 0xff])
.expect_err("invalid UTF-8 must fail");
assert_eq!(definite.utf8_valid_up_to(), Some(1));
assert_eq!(definite.utf8_error_len(), Some(1));
let incomplete = LenientJsonDecoder::default()
.decode_slice::<serde_json::Value>(&[0xe2, 0x82])
.expect_err("incomplete UTF-8 must fail");
assert_eq!(incomplete.utf8_valid_up_to(), Some(0));
assert_eq!(incomplete.utf8_error_len(), None);
}
#[test]
fn test_invalid_utf8_detailed_error_retains_utf8_source() {
let decoder = LenientJsonDecoder::new(
JsonDecodeOptions::strict()
.with_error_privacy_policy(ErrorPrivacyPolicy::Detailed),
);
let error = decoder
.decode_slice::<serde_json::Value>(&[0xff])
.expect_err("invalid UTF-8 must fail");
assert_eq!(error.utf8_valid_up_to(), Some(0));
assert_eq!(error.utf8_error_len(), Some(1));
let source = std::error::Error::source(&error)
.expect("detailed errors must retain Utf8Error");
assert!(source.downcast_ref::<std::str::Utf8Error>().is_some());
}
#[test]
fn test_normalization_errors_retain_the_configured_privacy_policy() {
let redacted = LenientJsonDecoder::default()
.decode_value("")
.expect_err("empty input should fail normalization");
let detailed = LenientJsonDecoder::new(
JsonDecodeOptions::default()
.with_error_privacy_policy(ErrorPrivacyPolicy::Detailed),
)
.decode_value("")
.expect_err("empty input should fail normalization");
assert_eq!(redacted.privacy_policy(), ErrorPrivacyPolicy::Redacted);
assert_eq!(detailed.privacy_policy(), ErrorPrivacyPolicy::Detailed);
}
#[test]
fn test_error_display_for_deserialize_error_uses_context_message() {
let error = LenientJsonDecoder::default()
.decode::<u64>("\"text\"")
.expect_err("string JSON should not deserialize into u64");
assert_eq!(error.kind(), JsonDecodeErrorKind::Deserialize);
assert_eq!(error.stage(), JsonDecodeStage::Deserialize);
assert_eq!(error.raw_input_bytes(), 6);
assert_eq!(error.normalized_input_bytes(), Some(6));
assert_eq!(
error.to_string(),
"Failed to deserialize JSON value at normalized line 1 column 6"
);
assert!(std::error::Error::source(&error).is_none());
}
#[test]
fn test_error_partial_eq_compares_all_stable_fields() {
let decoder = LenientJsonDecoder::default();
let first = decoder
.decode_value("{\n")
.expect_err("invalid JSON should return parse error");
let second = decoder
.decode_value("{\n")
.expect_err("invalid JSON should return parse error");
assert_eq!(first, second);
let third = decoder
.decode_value("")
.expect_err("empty input should return normalization error");
assert_ne!(first, third);
let detailed = LenientJsonDecoder::new(
JsonDecodeOptions::default()
.with_error_privacy_policy(ErrorPrivacyPolicy::Detailed),
)
.decode_value("")
.expect_err("empty input should return normalization error");
assert_ne!(third, detailed);
let invalid_utf8_at_start = decoder
.decode_slice::<Value>(&[0xff])
.expect_err("invalid UTF-8 should return a decode error");
let invalid_utf8_after_prefix = decoder
.decode_slice::<Value>(&[b'a', 0xff])
.expect_err("invalid UTF-8 should return a decode error");
assert_ne!(invalid_utf8_at_start, invalid_utf8_after_prefix);
}