use super::{
CanonicalQuery, FormQuery, MAX_REQUEST_TARGET_BYTES, RequestPath, RequestPathError,
RequestQuery, RequestTarget, RequestTargetError, StructuredQueryError,
};
use core::fmt::{self, Write};
macro_rules! valid {
($value:expr) => {{
let result = $value;
assert!(result.is_ok());
let Ok(value) = result else {
return;
};
value
}};
}
#[test]
fn validates_canonical_paths() {
for accepted in [
"/",
"/servers/42",
"/servers/",
"/zones/example.com/rrsets/%40/AAAA",
"/zones/example.com/rrsets/%2A.apps/TXT",
] {
assert_eq!(
RequestPath::new(accepted).map(RequestPath::as_str),
Ok(accepted)
);
}
for (value, error) in [
("", RequestPathError::Empty),
("servers", RequestPathError::NotOriginForm),
("//authority", RequestPathError::NotOriginForm),
("/a//b", RequestPathError::DoubledSlash),
("/a/./b", RequestPathError::DotSegment),
("/a/../b", RequestPathError::DotSegment),
("/a?b", RequestPathError::InvalidByte),
("/a#b", RequestPathError::InvalidByte),
("/a\\b", RequestPathError::InvalidByte),
("/a b", RequestPathError::InvalidByte),
("/a/%", RequestPathError::InvalidPercentTriplet),
("/a/%2", RequestPathError::InvalidPercentTriplet),
("/a/%GG", RequestPathError::InvalidPercentTriplet),
("/a/%2f", RequestPathError::LowercasePercentHex),
("/a/%2F", RequestPathError::EncodedSeparator),
("/a/%5C", RequestPathError::EncodedSeparator),
("/a/%3F", RequestPathError::EncodedSeparator),
("/a/%23", RequestPathError::EncodedControl),
("/a/%00", RequestPathError::EncodedControl),
("/a/%41", RequestPathError::EncodedUnreserved),
("/a/[b]", RequestPathError::InvalidByte),
("/münchen", RequestPathError::InvalidByte),
] {
assert_eq!(RequestPath::new(value), Err(error), "{value}");
}
}
#[test]
fn validates_canonical_and_form_queries_separately() {
for accepted in [
"",
"page=2",
"name=test%20server",
"label=env%3Dprod%26tier%3Dapi",
"flag&empty=&repeat=1&repeat=2",
"utf8=%C3%A4",
] {
assert_eq!(
CanonicalQuery::new(accepted).map(CanonicalQuery::as_str),
Ok(accepted)
);
}
assert_eq!(
CanonicalQuery::new("name=test+server"),
Err(StructuredQueryError::PlusForbidden)
);
assert_eq!(
FormQuery::new("name=test+server").map(FormQuery::as_str),
Ok("name=test+server")
);
for (value, error) in [
("=value", StructuredQueryError::EmptyKey),
("a=1&&b=2", StructuredQueryError::EmptyPair),
("a=1&", StructuredQueryError::EmptyPair),
("a=b=c", StructuredQueryError::MultipleEquals),
("a=%", StructuredQueryError::InvalidPercentTriplet),
("a=%2f", StructuredQueryError::LowercasePercentHex),
("a=%41", StructuredQueryError::EncodedUnreserved),
("a=%23", StructuredQueryError::EncodedControl),
("a=%5C", StructuredQueryError::EncodedControl),
("a b=c", StructuredQueryError::InvalidByte),
("a?b=c", StructuredQueryError::InvalidByte),
("münchen=1", StructuredQueryError::InvalidByte),
] {
assert_eq!(CanonicalQuery::new(value), Err(error), "{value}");
}
}
#[test]
fn preserves_query_order_duplicates_and_value_presence() {
let query = valid!(CanonicalQuery::new("flag&empty=&a=1&a=2"));
let mut pairs = query.pairs();
let values = (pairs.next(), pairs.next(), pairs.next(), pairs.next());
assert!(values.0.is_some() && values.1.is_some() && values.2.is_some() && values.3.is_some());
let (Some(flag), Some(empty), Some(first), Some(second)) = values else {
return;
};
assert_eq!((flag.key(), flag.value()), ("flag", None));
assert_eq!((empty.key(), empty.value()), ("empty", Some("")));
assert_eq!((first.key(), first.value()), ("a", Some("1")));
assert_eq!((second.key(), second.value()), ("a", Some("2")));
assert_eq!(pairs.next(), None);
}
#[test]
fn redacts_query_wrappers_iterators_and_pairs() {
let canonical = valid!(CanonicalQuery::new(
"token=classified%20value&project=alpha"
));
let form = valid!(FormQuery::new("token=classified+value&project=alpha"));
let mut pairs = canonical.pairs();
let first = pairs.next();
assert_debug(&canonical, "CanonicalQuery([redacted])");
assert_debug(&form, "FormQuery([redacted])");
assert_debug(
&RequestQuery::Canonical(canonical),
"Canonical(CanonicalQuery([redacted]))",
);
assert_debug(&RequestQuery::Form(form), "Form(FormQuery([redacted]))");
assert_debug(&pairs, "QueryPairs([redacted])");
assert_debug(&first, "Some(QueryPair([redacted]))");
}
#[test]
fn distinguishes_absent_and_present_empty_queries() {
let absent = valid!(RequestTarget::new("/servers"));
let present = valid!(RequestTarget::new("/servers?"));
let empty = valid!(CanonicalQuery::new(""));
assert_eq!(absent.query(), RequestQuery::Absent);
assert_eq!(absent.query_bytes(), None);
assert_eq!(present.query(), RequestQuery::Canonical(empty));
assert_eq!(present.query_bytes(), Some(b"".as_slice()));
}
#[test]
fn assembles_prefix_only_and_preserves_exact_query_bytes() {
let path = valid!(RequestPath::new("/servers"));
let query = valid!(CanonicalQuery::new("flag&name=test%20server&name=api"));
let mut output = [0xA5; 64];
let target = valid!(RequestTarget::assemble(
path,
RequestQuery::Canonical(query),
&mut output
));
assert_eq!(target.as_str(), "/servers?flag&name=test%20server&name=api");
let initialized_len = target.len();
assert_eq!(target.path(), path);
assert_eq!(target.query_bytes(), Some(query.as_str().as_bytes()));
assert_eq!(target.query(), RequestQuery::Canonical(query));
let tail = output.get(initialized_len..);
assert!(tail.is_some());
let Some(tail) = tail else {
return;
};
assert!(tail.iter().all(|byte| *byte == 0xA5));
}
#[test]
fn assembly_failure_does_not_modify_output() {
let path = valid!(RequestPath::new("/servers"));
let query = valid!(CanonicalQuery::new("page=2"));
let mut output = [0xA5; 8];
let before = output;
assert_eq!(
RequestTarget::assemble(path, RequestQuery::Canonical(query), &mut output),
Err(RequestTargetError::OutputTooSmall)
);
assert_eq!(output, before);
}
#[test]
fn form_targets_retain_their_explicit_dialect() {
let path = valid!(RequestPath::new("/search"));
let query = valid!(FormQuery::new("name=test+server"));
let mut output = [0_u8; 64];
let target = valid!(RequestTarget::assemble(
path,
RequestQuery::Form(query),
&mut output
));
assert_eq!(target.as_str(), "/search?name=test+server");
assert_eq!(target.query(), RequestQuery::Form(query));
}
#[test]
fn complete_target_bound_includes_query_delimiter() {
let mut path = [b'a'; MAX_REQUEST_TARGET_BYTES];
path[0] = b'/';
let path = valid!(core::str::from_utf8(&path));
assert!(RequestTarget::new(path).is_ok());
let mut oversized = [b'a'; MAX_REQUEST_TARGET_BYTES + 1];
oversized[0] = b'/';
let oversized = valid!(core::str::from_utf8(&oversized));
assert_eq!(
RequestTarget::new(oversized),
Err(RequestTargetError::TooLong)
);
let path = valid!(RequestPath::new(path));
let empty = valid!(CanonicalQuery::new(""));
let mut output = [0xA5; MAX_REQUEST_TARGET_BYTES + 1];
assert_eq!(
RequestTarget::assemble(path, RequestQuery::Canonical(empty), &mut output),
Err(RequestTargetError::TooLong)
);
assert!(output.iter().all(|byte| *byte == 0xA5));
}
fn assert_debug(value: &impl fmt::Debug, expected: &str) {
let mut output = DebugBuffer::new();
assert!(write!(&mut output, "{value:?}").is_ok());
assert_eq!(output.as_str(), expected);
}
struct DebugBuffer {
bytes: [u8; 64],
len: usize,
}
impl DebugBuffer {
const fn new() -> Self {
Self {
bytes: [0; 64],
len: 0,
}
}
fn as_str(&self) -> &str {
core::str::from_utf8(self.bytes.get(..self.len).unwrap_or_default()).unwrap_or_default()
}
}
impl Write for DebugBuffer {
fn write_str(&mut self, value: &str) -> fmt::Result {
let end = self.len.checked_add(value.len()).ok_or(fmt::Error)?;
let target = self.bytes.get_mut(self.len..end).ok_or(fmt::Error)?;
target.copy_from_slice(value.as_bytes());
self.len = end;
Ok(())
}
}