use qubit_argument::{
ArgumentError,
ArgumentErrorKind,
LengthConstraint,
LengthMetric,
StringArgument,
};
#[cfg(feature = "regex")]
use qubit_argument::PatternExpectation;
#[cfg(feature = "regex")]
use regex::Regex;
fn assert_structured_error(
error: ArgumentError,
expected_path: &str,
expected_kind: ArgumentErrorKind,
) {
assert_eq!(error.path().as_str(), expected_path);
assert_eq!(error.kind(), &expected_kind);
}
#[test]
fn test_require_non_blank_preserves_owned_string() {
let value = String::from("qubit");
let validated: String =
value.require_non_blank("name").expect("name is non-blank");
assert_eq!(validated, "qubit");
}
#[test]
fn test_require_non_blank_preserves_borrowed_str() {
let value: &str = "qubit";
let validated: &str =
value.require_non_blank("name").expect("name is non-blank");
assert_eq!(validated, value);
}
#[test]
fn test_require_non_blank_rejects_unicode_whitespace() {
for value in ["", "\u{2003}\u{3000}", " \t\n"] {
let error = value
.require_non_blank("name")
.expect_err("whitespace-only input must be blank");
assert_structured_error(error, "name", ArgumentErrorKind::Blank);
}
}
#[test]
fn test_byte_len_and_char_count_have_distinct_semantics() {
let value = "汉😀";
assert!(value.require_byte_len("value", 7).is_ok());
assert!(value.require_char_count("value", 2).is_ok());
}
#[test]
fn test_require_byte_len_returns_exact_length_error() {
assert_eq!(
"abc"
.require_byte_len("value", 3)
.expect("three ASCII bytes satisfy the constraint"),
"abc",
);
let error = "ab"
.require_byte_len("value", 3)
.expect_err("two bytes do not satisfy an exact length of three");
assert_structured_error(
error,
"value",
ArgumentErrorKind::Length {
actual: 2,
constraint: LengthConstraint::Exact(3),
metric: LengthMetric::Bytes,
},
);
}
#[test]
fn test_require_byte_len_at_least_returns_minimum_length_error() {
assert_eq!(
"abc"
.require_byte_len_at_least("value", 3)
.expect("three bytes satisfy a minimum of three"),
"abc",
);
let error = "ab"
.require_byte_len_at_least("value", 3)
.expect_err("two bytes are below the minimum");
assert_structured_error(
error,
"value",
ArgumentErrorKind::Length {
actual: 2,
constraint: LengthConstraint::AtLeast(3),
metric: LengthMetric::Bytes,
},
);
}
#[test]
fn test_require_byte_len_at_most_returns_maximum_length_error() {
assert_eq!(
"abc"
.require_byte_len_at_most("value", 3)
.expect("three bytes satisfy a maximum of three"),
"abc",
);
let error = "abcd"
.require_byte_len_at_most("value", 3)
.expect_err("four bytes exceed the maximum");
assert_structured_error(
error,
"value",
ArgumentErrorKind::Length {
actual: 4,
constraint: LengthConstraint::AtMost(3),
metric: LengthMetric::Bytes,
},
);
}
#[test]
fn test_require_byte_len_in_returns_range_length_errors() {
assert_eq!(
"abc"
.require_byte_len_in("value", 3, 4)
.expect("three bytes lie on the inclusive lower boundary"),
"abc",
);
for value in ["ab", "abcde"] {
let error = value
.require_byte_len_in("value", 3, 4)
.expect_err("byte length lies outside the range");
assert_structured_error(
error,
"value",
ArgumentErrorKind::Length {
actual: value.len(),
constraint: LengthConstraint::InRange { min: 3, max: 4 },
metric: LengthMetric::Bytes,
},
);
}
}
#[test]
fn test_require_byte_len_in_rejects_invalid_constraint_first() {
let error = "x"
.require_byte_len_in("value", 3, 1)
.expect_err("a reversed byte-length range is invalid");
assert_structured_error(
error,
"value",
ArgumentErrorKind::InvalidLengthConstraint {
constraint: LengthConstraint::InRange { min: 3, max: 1 },
metric: LengthMetric::Bytes,
},
);
}
#[test]
fn test_require_char_count_returns_exact_length_error() {
assert_eq!(
"汉😀"
.require_char_count("value", 2)
.expect("the input has two Unicode scalar values"),
"汉😀",
);
let error = "汉😀"
.require_char_count("value", 3)
.expect_err("two scalar values do not satisfy a count of three");
assert_structured_error(
error,
"value",
ArgumentErrorKind::Length {
actual: 2,
constraint: LengthConstraint::Exact(3),
metric: LengthMetric::UnicodeScalars,
},
);
}
#[test]
fn test_require_char_count_at_least_returns_minimum_length_error() {
assert_eq!(
"汉😀"
.require_char_count_at_least("value", 2)
.expect("two scalar values satisfy a minimum of two"),
"汉😀",
);
let error = "汉"
.require_char_count_at_least("value", 2)
.expect_err("one scalar value is below the minimum");
assert_structured_error(
error,
"value",
ArgumentErrorKind::Length {
actual: 1,
constraint: LengthConstraint::AtLeast(2),
metric: LengthMetric::UnicodeScalars,
},
);
}
#[test]
fn test_require_char_count_at_most_returns_maximum_length_error() {
assert_eq!(
"汉😀"
.require_char_count_at_most("value", 2)
.expect("two scalar values satisfy a maximum of two"),
"汉😀",
);
let error = "汉😀x"
.require_char_count_at_most("value", 2)
.expect_err("three scalar values exceed the maximum");
assert_structured_error(
error,
"value",
ArgumentErrorKind::Length {
actual: 3,
constraint: LengthConstraint::AtMost(2),
metric: LengthMetric::UnicodeScalars,
},
);
}
#[test]
fn test_require_char_count_in_returns_range_length_errors() {
assert_eq!(
"汉😀"
.require_char_count_in("value", 1, 2)
.expect("two scalar values lie on the inclusive upper boundary"),
"汉😀",
);
for value in ["", "汉😀x"] {
let error = value
.require_char_count_in("value", 1, 2)
.expect_err("scalar count lies outside the range");
assert_structured_error(
error,
"value",
ArgumentErrorKind::Length {
actual: value.chars().count(),
constraint: LengthConstraint::InRange { min: 1, max: 2 },
metric: LengthMetric::UnicodeScalars,
},
);
}
}
#[test]
fn test_require_char_count_in_rejects_invalid_constraint_first() {
let error = "x"
.require_char_count_in("value", 3, 1)
.expect_err("a reversed scalar-count range is invalid");
assert_structured_error(
error,
"value",
ArgumentErrorKind::InvalidLengthConstraint {
constraint: LengthConstraint::InRange { min: 3, max: 1 },
metric: LengthMetric::UnicodeScalars,
},
);
}
#[test]
fn test_length_methods_accept_empty_string_at_zero() {
assert_eq!(
"".require_byte_len("value", 0)
.expect("the empty string has zero bytes"),
"",
);
assert_eq!(
"".require_char_count("value", 0)
.expect("the empty string has zero scalar values"),
"",
);
}
#[test]
fn test_core_string_validation_chain_preserves_ownership() {
let validated: String = String::from("汉😀")
.require_non_blank("value")
.and_then(|value| value.require_byte_len("value", 7))
.and_then(|value| value.require_byte_len_at_least("value", 7))
.and_then(|value| value.require_byte_len_at_most("value", 7))
.and_then(|value| value.require_byte_len_in("value", 7, 7))
.and_then(|value| value.require_char_count("value", 2))
.and_then(|value| value.require_char_count_at_least("value", 2))
.and_then(|value| value.require_char_count_at_most("value", 2))
.and_then(|value| value.require_char_count_in("value", 2, 2))
.expect("every constraint accepts the owned value");
assert_eq!(validated, "汉😀");
}
#[test]
fn test_string_error_does_not_expose_input() {
let secret = "secret-token-value";
let error = secret
.require_byte_len_at_most("token", 4)
.expect_err("secret is too long");
assert!(!format!("{error:?}").contains(secret));
assert!(!error.to_string().contains(secret));
}
#[cfg(feature = "regex")]
#[test]
fn test_require_match_returns_pattern_error_without_input() {
let pattern = Regex::new("^[a-z]+$").expect("test pattern is valid");
let secret = "123-secret-token";
let error = secret
.require_match("name", &pattern)
.expect_err("digits and punctuation must fail");
assert_structured_error(
error.clone(),
"name",
ArgumentErrorKind::Pattern {
pattern: String::from("^[a-z]+$"),
expectation: PatternExpectation::Match,
},
);
assert!(!format!("{error:?}").contains(secret));
assert!(!error.to_string().contains(secret));
}
#[cfg(feature = "regex")]
#[test]
fn test_require_not_match_returns_pattern_error() {
let pattern = Regex::new("^[0-9]+$").expect("test pattern is valid");
let error = "123"
.require_not_match("identifier", &pattern)
.expect_err("digits match the forbidden pattern");
assert_structured_error(
error,
"identifier",
ArgumentErrorKind::Pattern {
pattern: String::from("^[0-9]+$"),
expectation: PatternExpectation::NoMatch,
},
);
}
#[cfg(feature = "regex")]
#[test]
fn test_require_match_preserves_owned_string() {
let pattern = Regex::new("^[a-z]+$").expect("test pattern is valid");
let validated: String = String::from("qubit")
.require_match("name", &pattern)
.and_then(|value| {
value.require_not_match(
"name",
&Regex::new("^[0-9]+$").expect("test pattern is valid"),
)
})
.expect("the owned string satisfies both pattern constraints");
assert_eq!(validated, "qubit");
}
#[cfg(feature = "regex")]
#[test]
fn test_pattern_methods_preserve_borrowed_str() {
let value: &str = "qubit";
let validated = value
.require_match(
"name",
&Regex::new("^[a-z]+$").expect("test pattern is valid"),
)
.and_then(|matched| {
matched.require_not_match(
"name",
&Regex::new("^[0-9]+$").expect("test pattern is valid"),
)
})
.expect("the borrowed string satisfies both pattern constraints");
assert!(std::ptr::eq(validated, value));
}