#![cfg(all(feature = "macros", feature = "json"))]
use std::collections::BTreeSet;
use kynos::{
Router,
error::rejection::RangeRejection,
extract::{body::binary::Binary, media::OctetStream},
middleware::limits::{BodySize, Timeout},
openapi::Document,
response::{
range::{Range, Ranged},
status::NoContent,
},
router::group::Group,
};
#[kynos::get("/alpha")]
async fn alpha() -> NoContent {
NoContent
}
#[kynos::get("/beta")]
async fn beta() -> NoContent {
NoContent
}
#[kynos::get("/gamma")]
async fn gamma() -> NoContent {
NoContent
}
fn declaring(document: &Document, status: &str) -> BTreeSet<String> {
document
.paths
.items
.iter()
.filter(|(_, item)| {
item.operations()
.any(|(_, operation)| operation.responses.responses.contains_key(status))
})
.map(|(path, _)| path.clone())
.collect()
}
fn paths(names: &[&str]) -> BTreeSet<String> {
names.iter().map(|name| (*name).to_owned()).collect()
}
#[test]
fn a_router_scoped_interceptor_declares_on_every_operation_either_side_of_it() {
let document = Router::<()>::new()
.mount(kynos::routes![alpha])
.intercept(BodySize::new(1024))
.mount(kynos::routes![beta])
.openapi()
.expect("a describable router");
assert_eq!(declaring(&document, "413"), paths(&["/alpha", "/beta"]));
}
#[test]
fn a_group_scoped_interceptor_declares_on_its_group_alone() {
let document = Router::<()>::new()
.mount(kynos::routes![alpha])
.group(
Group::<()>::new("/inner")
.intercept(BodySize::new(1024))
.mount(kynos::routes![beta]),
)
.openapi()
.expect("a describable router");
assert_eq!(declaring(&document, "413"), paths(&["/inner/beta"]));
}
#[test]
fn an_endpoint_scoped_interceptor_declares_on_its_endpoint_alone() {
let document = Router::<()>::new()
.mount((
kynos::routes![alpha],
kynos::routes![beta].0.intercept(BodySize::new(1024)),
))
.openapi()
.expect("a describable router");
assert_eq!(declaring(&document, "413"), paths(&["/beta"]));
}
#[test]
fn an_operation_under_two_scopes_declares_what_each_contributes() {
let document = Router::<()>::new()
.intercept(BodySize::new(1024))
.mount(kynos::routes![alpha])
.group(
Group::<()>::new("/inner")
.intercept(Timeout::new(std::time::Duration::from_secs(1)))
.mount(kynos::routes![beta]),
)
.mount(kynos::routes![gamma])
.openapi()
.expect("a describable router");
assert_eq!(
declaring(&document, "413"),
paths(&["/alpha", "/inner/beta", "/gamma"])
);
assert_eq!(declaring(&document, "408"), paths(&["/inner/beta"]));
}
#[test]
fn a_nested_routers_interceptor_stays_with_what_that_router_held() {
let inner = Router::<()>::new()
.intercept(BodySize::new(1024))
.mount(kynos::routes![beta]);
let document = Router::<()>::new()
.mount(kynos::routes![alpha])
.nest("/v1", inner)
.openapi()
.expect("a describable router");
assert_eq!(declaring(&document, "413"), paths(&["/v1/beta"]));
}
#[test]
fn a_merged_routers_interceptor_stays_with_what_that_router_held() {
let other = Router::<()>::new()
.intercept(BodySize::new(1024))
.mount(kynos::routes![beta]);
let document = Router::<()>::new()
.mount(kynos::routes![alpha])
.merge(other)
.openapi()
.expect("a describable router");
assert_eq!(declaring(&document, "413"), paths(&["/beta"]));
}
#[test]
fn nothing_declares_a_limits_status_when_no_limit_is_mounted() {
let document = Router::<()>::new()
.mount(kynos::routes![alpha, beta, gamma])
.openapi()
.expect("a describable router");
assert!(declaring(&document, "413").is_empty());
assert!(declaring(&document, "408").is_empty());
}
#[kynos::get("/recordings/current")]
async fn recording(
range: Range<Binary<OctetStream>>,
) -> Result<Ranged<Binary<OctetStream>>, RangeRejection> {
range.apply(Binary::new(&b"0123456789"[..]))
}
#[kynos::get("/recordings/whole")]
async fn whole_recording() -> Binary<OctetStream> {
Binary::new(&b"0123456789"[..])
}
#[kynos::get("/recordings/preview")]
async fn preview_recording(range: Range<Binary<OctetStream>>) -> Binary<OctetStream> {
let _ = range;
Binary::new(&b"0123456789"[..])
}
fn operation(document: &Document, path: &str) -> kynos::openapi::Operation {
let item = document.paths.items.get(path).expect("a described path");
let (_, operation) = item.operations().next().expect("one operation");
operation.clone()
}
fn declared_headers(operation: &kynos::openapi::Operation, status: &str) -> Option<Vec<String>> {
let kynos::openapi::RefOr::Item(response) = operation.responses.responses.get(status)? else {
panic!("{status} is described as a `$ref`");
};
Some(response.headers.keys().cloned().collect())
}
#[test]
fn a_ranged_operation_declares_the_field_it_reads() {
let document = Router::<()>::new()
.mount(kynos::routes![
recording,
whole_recording,
preview_recording
])
.openapi()
.expect("a describable router");
let ranged = operation(&document, "/recordings/current");
let declared = ranged.parameters.iter().find(
|parameter| matches!(parameter, kynos::openapi::RefOr::Item(item) if item.name == "Range"),
);
let Some(kynos::openapi::RefOr::Item(parameter)) = declared else {
panic!("the `Range` field is declared, and inline rather than as a `$ref`");
};
assert_eq!(parameter.location, kynos::openapi::ParameterIn::Header);
assert_ne!(parameter.required, Some(true));
let kynos::openapi::Schema::Object(schema) = parameter.schema().expect("a schema") else {
panic!("described by a boolean schema");
};
let pattern = schema.pattern.clone().expect("a pattern");
assert!(pattern.starts_with("^bytes="), "{pattern}");
let whole = operation(&document, "/recordings/whole");
assert!(whole.parameters.is_empty());
}
#[test]
fn a_ranged_operation_declares_each_field_on_the_statuses_that_carry_it() {
let document = Router::<()>::new()
.mount(kynos::routes![
recording,
whole_recording,
preview_recording
])
.openapi()
.expect("a describable router");
let ranged = operation(&document, "/recordings/current");
let statuses: BTreeSet<&str> = ranged
.responses
.responses
.keys()
.map(String::as_str)
.collect();
assert_eq!(statuses, BTreeSet::from(["200", "206", "416"]));
assert_eq!(
declared_headers(&ranged, "200"),
Some(vec!["Accept-Ranges".to_owned()])
);
assert_eq!(
declared_headers(&ranged, "206"),
Some(vec!["Accept-Ranges".to_owned(), "Content-Range".to_owned()])
);
assert_eq!(
declared_headers(&ranged, "416"),
Some(vec!["Content-Range".to_owned()])
);
}
#[test]
fn an_operation_that_does_not_range_advertises_no_unit() {
let document = Router::<()>::new()
.mount(kynos::routes![
recording,
whole_recording,
preview_recording
])
.openapi()
.expect("a describable router");
let whole = operation(&document, "/recordings/whole");
assert_eq!(
whole
.responses
.responses
.keys()
.map(String::as_str)
.collect::<BTreeSet<_>>(),
BTreeSet::from(["200"])
);
assert_eq!(declared_headers(&whole, "200"), Some(Vec::new()));
}
#[test]
fn an_operation_that_reads_a_range_but_cannot_fail_declares_no_416() {
let document = Router::<()>::new()
.mount(kynos::routes![
recording,
whole_recording,
preview_recording
])
.openapi()
.expect("a describable router");
let preview = operation(&document, "/recordings/preview");
assert!(
preview
.parameters
.iter()
.any(|parameter| matches!(parameter, kynos::openapi::RefOr::Item(item) if item.name == "Range")),
"the field it reads is still declared"
);
assert_eq!(
preview
.responses
.responses
.keys()
.map(String::as_str)
.collect::<BTreeSet<_>>(),
BTreeSet::from(["200"])
);
assert_eq!(declared_headers(&preview, "200"), Some(Vec::new()));
let ranged = operation(&document, "/recordings/current");
assert!(ranged.responses.responses.contains_key("416"));
}
#[deprecated(note = "use `beta`")]
#[kynos::get("/retired")]
async fn retired() -> NoContent {
NoContent
}
#[test]
fn a_deprecated_handler_marks_its_operation() {
#[allow(deprecated)]
let document = Router::<()>::new()
.mount(kynos::routes![retired, alpha])
.openapi()
.expect("the fixture describes itself");
assert_eq!(
operation(&document, "/retired").deprecated,
Some(true),
"a deprecated handler reached the document unmarked"
);
assert_eq!(operation(&document, "/alpha").deprecated, None);
}