#[cfg(feature = "redact")]
use std::collections::HashMap;
#[cfg(feature = "big-decimal")]
use std::str::FromStr;
#[cfg(feature = "big-decimal")]
use bigdecimal::BigDecimal;
#[cfg(feature = "chrono")]
use chrono::NaiveDate;
#[cfg(feature = "big-integer")]
use num_bigint::BigInt;
#[cfg(feature = "converter")]
use qubit_datatype::AdmittedConversion;
#[cfg(feature = "converter")]
use qubit_datatype::DataConversionError;
#[cfg(feature = "converter")]
use qubit_datatype::DataConversionTarget;
#[cfg(feature = "converter")]
use qubit_datatype::DataType;
#[cfg(feature = "converter")]
use qubit_datatype::DataTypeOf;
#[cfg(feature = "redact")]
use qubit_redact::MaskPolicy;
#[cfg(feature = "redact")]
use qubit_redact::Redact;
#[cfg(feature = "redact")]
use qubit_redact::RedactionCompletion;
#[cfg(feature = "redact")]
use qubit_redact::RedactionPolicy;
#[cfg(feature = "redact")]
use qubit_redact::Redactor;
#[cfg(feature = "redact")]
use qubit_redact::Sensitivity;
#[cfg(feature = "redact")]
fn redacted_text<T: Redact>(value: &T, policy: &RedactionPolicy) -> String {
Redactor::new(policy.clone())
.redact_text(value)
.into_complete_text()
.expect("test output must be complete")
.into_string()
}
#[cfg(any(
feature = "converter",
feature = "chrono",
feature = "big-integer",
feature = "big-decimal",
feature = "url",
feature = "json",
feature = "redact",
))]
use qubit_value::MultiValues;
#[cfg(feature = "redact")]
use qubit_value::NamedValue;
#[cfg(any(
feature = "converter",
feature = "chrono",
feature = "big-integer",
feature = "big-decimal",
feature = "url",
feature = "json",
feature = "redact",
))]
use qubit_value::Value;
#[cfg(any(
feature = "converter",
feature = "chrono",
feature = "big-integer",
feature = "big-decimal",
feature = "url",
feature = "json"
))]
use qubit_value::ValueContainer;
#[cfg(any(
feature = "converter",
feature = "chrono",
feature = "big-integer",
feature = "big-decimal",
feature = "url",
feature = "json"
))]
use qubit_value::ValueWirePayloadV1;
#[cfg(any(
feature = "converter",
feature = "chrono",
feature = "big-integer",
feature = "big-decimal",
feature = "url",
feature = "json",
))]
#[cfg(feature = "url")]
use url::Url;
#[cfg(any(
feature = "converter",
feature = "chrono",
feature = "big-integer",
feature = "big-decimal",
feature = "url",
feature = "json",
))]
fn assert_wire_serialization(value: impl Into<ValueContainer>) {
let wire = ValueWirePayloadV1::try_from(value.into()).expect("construct wire payload");
let _encoded = serde_json::to_string(&wire).expect("serialize wire payload");
#[cfg(feature = "json")]
{
let decoded = ValueWirePayloadV1::decode_json_slice(_encoded.as_bytes()).expect("deserialize wire payload");
assert_eq!(decoded, wire);
}
}
#[cfg(feature = "converter")]
#[test]
fn test_converter_feature_converts_core_values() {
let scalar = ValueContainer::from(42_i32);
let collection = ValueContainer::from(vec![43_i32, 44]);
assert_eq!(scalar.to_first::<i64>().expect("convert scalar"), 42);
assert_eq!(collection.to_list::<i64>().expect("convert collection"), vec![43, 44]);
assert_wire_serialization(collection);
}
#[cfg(feature = "converter")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Port(u16);
#[cfg(feature = "converter")]
impl DataTypeOf for Port {
const DATA_TYPE: DataType = DataType::UInt16;
}
#[cfg(feature = "converter")]
impl DataConversionTarget for Port {
fn convert(input: AdmittedConversion<'_, '_, '_>) -> Result<Self, DataConversionError> {
input.convert::<u16>().map(Self)
}
}
#[cfg(feature = "converter")]
#[test]
fn test_converter_feature_accepts_target_side_extension() {
assert_eq!(Value::from("8080").to::<Port>().unwrap(), Port(8080));
assert_eq!(
MultiValues::from(vec!["8080", "8081"]).to_list::<Port>().unwrap(),
vec![Port(8080), Port(8081)]
);
assert_eq!(ValueContainer::from("8082").to_first::<Port>().unwrap(), Port(8082));
}
#[cfg(feature = "chrono")]
#[test]
fn test_chrono_feature_preserves_values_and_wire_payloads() {
let date = NaiveDate::from_ymd_opt(2026, 7, 15).expect("valid date");
let scalar = Value::Date(date);
let collection = MultiValues::Date(vec![date]);
assert_eq!(scalar.get::<NaiveDate>().expect("read date"), date);
assert_eq!(collection.get_dates().expect("read dates"), &[date]);
assert_wire_serialization(scalar);
assert_wire_serialization(collection);
}
#[cfg(feature = "big-integer")]
#[test]
fn test_big_integer_feature_preserves_values_and_wire_payloads() {
let integer = BigInt::from(123_456_789_i64);
let integer_value = Value::BigInteger(integer.clone());
let integers = MultiValues::BigInteger(vec![integer.clone()]);
assert_eq!(integer_value.get::<BigInt>().expect("read big integer"), integer);
assert_eq!(integers.get_bigintegers().expect("read big integers"), &[integer]);
assert_wire_serialization(integer_value);
assert_wire_serialization(integers);
}
#[cfg(feature = "big-decimal")]
#[test]
fn test_big_decimal_feature_preserves_values_and_wire_payloads() {
let decimal = BigDecimal::from_str("123.4500").expect("valid decimal");
let decimal_value = Value::BigDecimal(decimal.clone());
let decimals = MultiValues::BigDecimal(vec![decimal.clone()]);
assert_eq!(decimal_value.get::<BigDecimal>().expect("read big decimal"), decimal);
assert_eq!(decimals.get_bigdecimals().expect("read big decimals"), &[decimal]);
assert_wire_serialization(decimal_value);
assert_wire_serialization(decimals);
}
#[cfg(feature = "url")]
#[test]
fn test_url_feature_preserves_values_and_wire_payloads() {
let url = Url::parse("https://example.com/path?q=1").expect("valid URL");
let scalar = Value::new(url.clone());
let collection = MultiValues::Url(vec![url.clone()]);
assert_eq!(scalar.get::<Url>().expect("read URL"), url);
assert_eq!(collection.get_urls().expect("read URLs"), &[url]);
assert_wire_serialization(scalar);
assert_wire_serialization(collection);
}
#[cfg(feature = "json")]
#[test]
fn test_json_feature_preserves_values_and_wire_payloads() {
let json = serde_json::json!({"nested": [true, 42]});
let scalar = Value::Json(json.clone());
let collection = MultiValues::Json(vec![json.clone()]);
assert_eq!(scalar.get::<serde_json::Value>().expect("read JSON value"), json);
assert_eq!(collection.get_jsons().expect("read JSON values"), &[json]);
assert_wire_serialization(scalar);
assert_wire_serialization(collection);
}
#[cfg(feature = "redact")]
#[test]
fn test_redact_feature_masks_sensitive_string_map_entries() {
let value = Value::StringMap(HashMap::from([
("api_key".to_owned(), "raw-secret".to_owned()),
("label".to_owned(), "visible".to_owned()),
]));
let policy = RedactionPolicy::builder()
.fields(|fields| {
fields.raise("api_key", Sensitivity::Secret);
})
.expect("the test builder input should be valid")
.build()
.expect("policy should build");
let output = redacted_text(&value, &policy);
assert!(!output.contains("raw-secret"));
assert!(output.contains("visible"));
}
#[cfg(feature = "redact")]
#[test]
fn test_redact_feature_masks_sensitive_named_non_strings_as_opaque_values() {
let value = NamedValue::new("secret_number", Value::Int32(12345));
let policy = RedactionPolicy::builder()
.fields(|fields| {
fields
.raise("secret_number", Sensitivity::Low)
.mask(Sensitivity::Low, MaskPolicy::preserve_edges(1, 1, "OPAQUE", 0));
})
.expect("the test policy should be valid")
.build()
.expect("policy should build");
let output = redacted_text(&value, &policy);
assert!(!output.contains("12345"), "{output}");
assert!(output.contains("OPAQUE"), "{output}");
}
#[cfg(all(feature = "redact", feature = "json"))]
#[test]
fn test_redact_feature_recursively_masks_sensitive_json_object_entries() {
let value = Value::Json(serde_json::json!({
"profile": {
"api_key": "nested-secret",
"label": "visible"
},
"items": [
{ "token": "array-secret" },
"unkeyed-value"
]
}));
let policy = RedactionPolicy::builder()
.fields(|fields| {
fields
.raise("api_key", Sensitivity::Secret)
.raise("token", Sensitivity::Secret);
})
.expect("the test builder input should be valid")
.build()
.expect("policy should build");
let debug = redacted_text(&value, &policy);
let display = debug.clone();
assert!(!debug.contains("nested-secret"));
assert!(!debug.contains("array-secret"));
assert!(debug.contains("visible"));
assert!(debug.contains("unkeyed-value"));
assert!(!display.contains("nested-secret"));
assert!(!display.contains("array-secret"));
assert!(!display.contains('\n'));
}
#[cfg(feature = "redact")]
#[test]
fn test_redact_feature_stops_before_unadmitted_collection_elements() {
let values = MultiValues::String(vec!["visible".to_owned(), "must-not-be-formatted".to_owned()]);
let policy = RedactionPolicy::builder()
.limits(|limits| {
limits.max_nodes(64).max_collection_items(1).max_depth(8);
})
.expect("the test domain limits should build a policy")
.build()
.expect("the test domain limits should build a policy");
let result = Redactor::new(policy).redact_text(&values);
assert_eq!(result.summary().completion(), RedactionCompletion::Truncated);
let output = result.text().as_str();
assert!(output.contains("visible"), "{output}");
assert!(!output.contains("must-not-be-formatted"), "{output}");
assert!(output.contains("<truncated>"), "{output}");
}
#[cfg(all(feature = "converter", feature = "chrono"))]
#[test]
fn test_converter_chrono_features_convert_text_to_date() {
let expected = NaiveDate::from_ymd_opt(2026, 7, 15).expect("valid date");
assert_eq!(
Value::from("2026-07-15")
.to::<NaiveDate>()
.expect("convert text to date"),
expected
);
}
#[cfg(all(feature = "converter", feature = "big-integer"))]
#[test]
fn test_converter_big_integer_features_convert_text_to_big_integer() {
assert_eq!(
Value::from("123456789")
.to::<BigInt>()
.expect("convert text to big integer"),
BigInt::from(123_456_789_i64)
);
}
#[cfg(all(feature = "converter", feature = "big-decimal"))]
#[test]
fn test_converter_big_decimal_features_convert_text_to_big_decimal() {
assert_eq!(
Value::from("123.4500")
.to::<BigDecimal>()
.expect("convert text to big decimal"),
BigDecimal::from_str("123.4500").expect("valid decimal")
);
}
#[cfg(all(feature = "converter", feature = "url"))]
#[test]
fn test_converter_url_features_convert_text_to_url() {
let expected = Url::parse("https://example.com/path").expect("valid URL");
assert_eq!(
Value::from("https://example.com/path")
.to::<Url>()
.expect("convert text to URL"),
expected
);
}
#[cfg(all(feature = "converter", feature = "json"))]
#[test]
fn test_converter_json_features_convert_text_to_json() {
assert_eq!(
Value::from(r#"{"answer":42}"#)
.to::<serde_json::Value>()
.expect("convert text to JSON"),
serde_json::json!({"answer": 42})
);
}
#[cfg(all(feature = "converter", feature = "json"))]
#[test]
fn test_natural_json_feature_combination_preserves_shape() {
let scalar = ValueContainer::from("one");
let collection = ValueContainer::from(vec!["one".to_owned()]);
assert_eq!(
scalar.to_json_value().expect("scalar projection"),
serde_json::json!("one")
);
assert_eq!(
collection.to_json_value().expect("collection projection"),
serde_json::json!(["one"])
);
}