use crate::model::paths::{
item::PathItem,
method::Method,
operation::Operation,
template::{InvalidPathTemplate, PathTemplate},
};
#[test]
fn a_template_exposes_its_variables_in_order() {
let template = PathTemplate::parse("/users/{id}/posts/{postId}").expect("valid");
assert_eq!(template.variables(), ["id", "postId"]);
}
#[test]
fn a_template_without_variables_is_valid() {
let template = PathTemplate::parse("/health").expect("valid");
assert!(template.variables().is_empty());
assert_eq!(template.normalized(), "/health");
}
#[test]
fn templates_must_begin_with_a_slash() {
assert!(matches!(
PathTemplate::parse("users/{id}"),
Err(InvalidPathTemplate::MissingLeadingSlash(_))
));
}
#[test]
fn braces_must_balance() {
assert!(matches!(
PathTemplate::parse("/users/{id"),
Err(InvalidPathTemplate::UnbalancedBraces(_))
));
assert!(matches!(
PathTemplate::parse("/users/id}"),
Err(InvalidPathTemplate::UnbalancedBraces(_))
));
assert!(matches!(
PathTemplate::parse("/users/{{id}"),
Err(InvalidPathTemplate::UnbalancedBraces(_))
));
}
#[test]
fn an_empty_expression_is_rejected() {
assert!(matches!(
PathTemplate::parse("/users/{}"),
Err(InvalidPathTemplate::EmptyExpression(_))
));
}
#[test]
fn a_variable_may_not_repeat_within_one_template() {
assert!(matches!(
PathTemplate::parse("/a/{id}/b/{id}"),
Err(InvalidPathTemplate::DuplicateVariable { .. })
));
}
#[test]
fn query_strings_and_fragments_are_not_paths() {
assert!(matches!(
PathTemplate::parse("/users?page=1"),
Err(InvalidPathTemplate::NotAPath(_))
));
assert!(matches!(
PathTemplate::parse("/users#top"),
Err(InvalidPathTemplate::NotAPath(_))
));
}
#[test]
fn literal_segments_accept_every_character_the_grammar_allows() {
let template =
PathTemplate::parse("/a-b.c_d~e/f!g$h&i'j(k)l*m+n,o;p=q/r:s@t/%2Fu").expect("valid");
assert!(template.variables().is_empty());
}
#[test]
fn literal_segments_reject_characters_outside_pchar() {
for raw in [
"/users/a b",
"/users/a<b",
"/users/a>b",
"/users/a\"b",
"/users/a\\b",
"/users/a^b",
"/users/a`b",
"/users/a|b",
"/café",
] {
assert!(
matches!(
PathTemplate::parse(raw),
Err(InvalidPathTemplate::IllegalLiteralCharacter { .. })
),
"`{raw}` should not parse"
);
}
}
#[test]
fn a_percent_must_introduce_an_encoded_triple() {
for raw in ["/users/%", "/users/%2", "/users/%zz", "/users/%2z"] {
assert!(
matches!(
PathTemplate::parse(raw),
Err(InvalidPathTemplate::MalformedPercentEncoding(_))
),
"`{raw}` should not parse"
);
}
}
#[test]
fn a_variable_name_may_hold_anything_but_a_brace() {
let template = PathTemplate::parse("/assets/{*path}").expect("valid");
assert_eq!(template.variables(), ["*path"]);
let spaced = PathTemplate::parse("/users/{user id}").expect("valid");
assert_eq!(spaced.variables(), ["user id"]);
}
#[test]
fn a_path_segment_is_never_empty() {
for raw in ["//", "//users", "/users//posts", "/users//"] {
assert!(
matches!(
PathTemplate::parse(raw),
Err(InvalidPathTemplate::EmptySegment(_))
),
"`{raw}` should not parse"
);
}
}
#[test]
fn the_root_and_a_trailing_slash_are_still_paths() {
assert_eq!(PathTemplate::parse("/").expect("valid").as_str(), "/");
assert_eq!(
PathTemplate::parse("/users/").expect("valid").as_str(),
"/users/"
);
assert_eq!(
PathTemplate::parse("/users/{id}/").expect("valid").as_str(),
"/users/{id}/"
);
}
#[test]
fn a_slash_inside_an_expression_does_not_open_a_segment() {
let template = PathTemplate::parse("/files/{a/b}").expect("valid");
assert_eq!(template.variables(), ["a/b"]);
}
#[test]
fn a_stray_closing_brace_reads_as_unbalanced_wherever_it_is() {
assert!(matches!(
PathTemplate::parse("/a}/b/{id}"),
Err(InvalidPathTemplate::UnbalancedBraces(_))
));
assert!(matches!(
PathTemplate::parse("/{id}/a}"),
Err(InvalidPathTemplate::UnbalancedBraces(_))
));
}
#[test]
fn templates_differing_only_in_variable_name_normalize_alike() {
let left = PathTemplate::parse("/pets/{petId}").expect("valid");
let right = PathTemplate::parse("/pets/{name}").expect("valid");
assert_ne!(left, right);
assert_eq!(left.normalized(), right.normalized());
}
#[test]
fn prefixing_concatenates_and_revalidates() {
let template = PathTemplate::parse("/users/{id}").expect("valid");
let prefixed = template.with_prefix("/v1").expect("valid");
assert_eq!(prefixed.as_str(), "/v1/users/{id}");
assert!(matches!(
template.with_prefix("/tenants/{id}"),
Err(InvalidPathTemplate::DuplicateVariable { .. })
));
}
#[test]
fn a_trailing_slash_on_the_prefix_does_not_double_up() {
let template = PathTemplate::parse("/users").expect("valid");
assert_eq!(
template.with_prefix("/v1/").expect("valid").as_str(),
"/v1/users"
);
}
#[test]
fn operations_are_addressed_by_method() {
let item = PathItem::new().with_operation(Method::Get, Operation::new("listUsers"));
assert!(item.operation(Method::Get).is_some());
assert!(item.operation(Method::Post).is_none());
assert_eq!(item.operations().count(), 1);
}
#[test]
fn the_method_list_is_complete() {
assert_eq!(Method::all(), EVERY_METHOD);
}
const EVERY_METHOD: &[Method] = &[
Method::Get,
Method::Put,
Method::Post,
Method::Delete,
Method::Options,
Method::Head,
Method::Patch,
Method::Trace,
#[cfg(feature = "openapi32")]
Method::Query,
];
fn spellings(method: Method) -> (&'static str, &'static str) {
match method {
Method::Get => ("GET", "get"),
Method::Put => ("PUT", "put"),
Method::Post => ("POST", "post"),
Method::Delete => ("DELETE", "delete"),
Method::Options => ("OPTIONS", "options"),
Method::Head => ("HEAD", "head"),
Method::Patch => ("PATCH", "patch"),
Method::Trace => ("TRACE", "trace"),
#[cfg(feature = "openapi32")]
Method::Query => ("QUERY", "query"),
}
}
#[test]
fn every_method_carries_the_two_spellings_the_specification_gives_it() {
for &method in EVERY_METHOD {
let (token, field) = spellings(method);
assert_eq!(method.as_wire_str(), token, "{method:?} as a method token");
assert_eq!(method.to_string(), token, "{method:?} rendered");
assert_eq!(
Method::from_wire_str(token),
Some(method),
"{token} parsed back"
);
assert_eq!(
serde_json::to_value(method).expect("a method is representable"),
serde_json::Value::String(field.to_owned()),
"{method:?} as a Path Item field"
);
assert_eq!(
serde_json::from_value::<Method>(serde_json::Value::String(field.to_owned()))
.expect("a field name the model writes, the model reads"),
method,
"{field} read back"
);
}
}
#[test]
fn wire_spellings_are_case_sensitive_and_closed() {
assert_eq!(Method::from_wire_str("get"), None);
assert_eq!(Method::from_wire_str("PROPFIND"), None);
assert_eq!(Method::from_wire_str(""), None);
}
#[cfg(feature = "http")]
#[test]
fn every_method_round_trips_through_the_http_crate() {
for described in Method::all() {
let wire: http::Method = (*described).into();
assert_eq!(wire.as_str(), described.as_wire_str());
assert_eq!(
Method::try_from(&wire).expect("what we just wrote, we read"),
*described
);
}
}
#[cfg(feature = "http")]
#[test]
fn a_method_with_no_path_item_field_is_refused() {
let extension = http::Method::from_bytes(b"PROPFIND").expect("a valid method token");
let error = Method::try_from(&extension).expect_err("no Path Item field names it");
assert_eq!(error.method, "PROPFIND");
assert!(error.to_string().contains("additionalOperations"));
}