use super::RustApi;
use crate::extract::{FromRequestParts, State};
use crate::path_params::PathParams;
use crate::request::Request;
use crate::router::{get, post, Router};
use bytes::Bytes;
use http::Method;
use proptest::prelude::*;
#[test]
fn state_is_available_via_extractor() {
let app = RustApi::new().state(123u32);
let router = app.into_router();
let req = http::Request::builder()
.method(Method::GET)
.uri("/test")
.body(())
.unwrap();
let (parts, _) = req.into_parts();
let request = Request::new(
parts,
crate::request::BodyVariant::Buffered(Bytes::new()),
router.state_ref(),
PathParams::new(),
);
let State(value) = State::<u32>::from_request_parts(&request).unwrap();
assert_eq!(value, 123u32);
}
#[test]
fn test_path_param_type_inference_integer() {
use super::helpers::infer_path_param_schema;
let int_params = [
"page",
"limit",
"offset",
"count",
"item_count",
"year",
"month",
"day",
"index",
"position",
];
for name in int_params {
let schema = infer_path_param_schema(name);
match schema {
rustapi_openapi::SchemaRef::Inline(v) => {
assert_eq!(
v.get("type").and_then(|v| v.as_str()),
Some("integer"),
"Expected '{}' to be inferred as integer",
name
);
}
_ => panic!("Expected inline schema for '{}'", name),
}
}
}
#[test]
fn test_path_param_type_inference_uuid() {
use super::helpers::infer_path_param_schema;
let uuid_params = ["uuid", "user_uuid", "sessionUuid"];
for name in uuid_params {
let schema = infer_path_param_schema(name);
match schema {
rustapi_openapi::SchemaRef::Inline(v) => {
assert_eq!(
v.get("type").and_then(|v| v.as_str()),
Some("string"),
"Expected '{}' to be inferred as string",
name
);
assert_eq!(
v.get("format").and_then(|v| v.as_str()),
Some("uuid"),
"Expected '{}' to have uuid format",
name
);
}
_ => panic!("Expected inline schema for '{}'", name),
}
}
}
#[test]
fn test_path_param_type_inference_string() {
use super::helpers::infer_path_param_schema;
let string_params = [
"name", "slug", "code", "token", "username", "id", "user_id", "userId", "postId",
];
for name in string_params {
let schema = infer_path_param_schema(name);
match schema {
rustapi_openapi::SchemaRef::Inline(v) => {
assert_eq!(
v.get("type").and_then(|v| v.as_str()),
Some("string"),
"Expected '{}' to be inferred as string",
name
);
assert!(
v.get("format").is_none()
|| v.get("format").and_then(|v| v.as_str()) != Some("uuid"),
"Expected '{}' to NOT have uuid format",
name
);
}
_ => panic!("Expected inline schema for '{}'", name),
}
}
}
#[test]
fn test_schema_type_to_openapi_schema() {
use super::helpers::schema_type_to_openapi_schema;
let uuid_schema = schema_type_to_openapi_schema("uuid");
match uuid_schema {
rustapi_openapi::SchemaRef::Inline(v) => {
assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("uuid"));
}
_ => panic!("Expected inline schema for uuid"),
}
for schema_type in ["integer", "int", "int64", "i64"] {
let schema = schema_type_to_openapi_schema(schema_type);
match schema {
rustapi_openapi::SchemaRef::Inline(v) => {
assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int64"));
}
_ => panic!("Expected inline schema for {}", schema_type),
}
}
let int32_schema = schema_type_to_openapi_schema("int32");
match int32_schema {
rustapi_openapi::SchemaRef::Inline(v) => {
assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int32"));
}
_ => panic!("Expected inline schema for int32"),
}
for schema_type in ["number", "float"] {
let schema = schema_type_to_openapi_schema(schema_type);
match schema {
rustapi_openapi::SchemaRef::Inline(v) => {
assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("number"));
}
_ => panic!("Expected inline schema for {}", schema_type),
}
}
for schema_type in ["boolean", "bool"] {
let schema = schema_type_to_openapi_schema(schema_type);
match schema {
rustapi_openapi::SchemaRef::Inline(v) => {
assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("boolean"));
}
_ => panic!("Expected inline schema for {}", schema_type),
}
}
let string_schema = schema_type_to_openapi_schema("string");
match string_schema {
rustapi_openapi::SchemaRef::Inline(v) => {
assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
}
_ => panic!("Expected inline schema for string"),
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_nested_routes_in_openapi_spec(
prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
has_param in any::<bool>(),
) {
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix_segments.join("/"));
let mut route_path = format!("/{}", route_segments.join("/"));
if has_param {
route_path.push_str("/{id}");
}
let nested_router = Router::new().route(&route_path, get(handler));
let app = RustApi::new().nest(&prefix, nested_router);
let expected_openapi_path = format!("{}{}", prefix, route_path);
let spec = app.openapi_spec();
prop_assert!(
spec.paths.contains_key(&expected_openapi_path),
"Expected OpenAPI path '{}' not found. Available paths: {:?}",
expected_openapi_path,
spec.paths.keys().collect::<Vec<_>>()
);
let path_item = spec.paths.get(&expected_openapi_path).unwrap();
prop_assert!(
path_item.get.is_some(),
"GET operation should exist for path '{}'",
expected_openapi_path
);
}
#[test]
fn prop_multiple_methods_preserved_in_openapi(
prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
) {
async fn get_handler() -> &'static str { "get" }
async fn post_handler() -> &'static str { "post" }
let prefix = format!("/{}", prefix_segments.join("/"));
let route_path = format!("/{}", route_segments.join("/"));
let get_route_path = format!("{}/get", route_path);
let post_route_path = format!("{}/post", route_path);
let nested_router = Router::new()
.route(&get_route_path, get(get_handler))
.route(&post_route_path, post(post_handler));
let app = RustApi::new().nest(&prefix, nested_router);
let expected_get_path = format!("{}{}", prefix, get_route_path);
let expected_post_path = format!("{}{}", prefix, post_route_path);
let spec = app.openapi_spec();
prop_assert!(
spec.paths.contains_key(&expected_get_path),
"Expected OpenAPI path '{}' not found",
expected_get_path
);
prop_assert!(
spec.paths.contains_key(&expected_post_path),
"Expected OpenAPI path '{}' not found",
expected_post_path
);
let get_path_item = spec.paths.get(&expected_get_path).unwrap();
prop_assert!(
get_path_item.get.is_some(),
"GET operation should exist for path '{}'",
expected_get_path
);
let post_path_item = spec.paths.get(&expected_post_path).unwrap();
prop_assert!(
post_path_item.post.is_some(),
"POST operation should exist for path '{}'",
expected_post_path
);
}
#[test]
fn prop_path_params_in_openapi_after_nesting(
prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
param_name in "[a-z][a-z0-9]{0,5}",
) {
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix_segments.join("/"));
let route_path = format!("/{{{}}}", param_name);
let nested_router = Router::new().route(&route_path, get(handler));
let app = RustApi::new().nest(&prefix, nested_router);
let expected_openapi_path = format!("{}{}", prefix, route_path);
let spec = app.openapi_spec();
prop_assert!(
spec.paths.contains_key(&expected_openapi_path),
"Expected OpenAPI path '{}' not found",
expected_openapi_path
);
let path_item = spec.paths.get(&expected_openapi_path).unwrap();
let get_op = path_item.get.as_ref().unwrap();
prop_assert!(
!get_op.parameters.is_empty(),
"Operation should have parameters for path '{}'",
expected_openapi_path
);
let params = &get_op.parameters;
let has_param = params.iter().any(|p| p.name == param_name && p.location == "path");
prop_assert!(
has_param,
"Path parameter '{}' should exist in operation parameters. Found: {:?}",
param_name,
params.iter().map(|p| &p.name).collect::<Vec<_>>()
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_rustapi_nest_delegates_to_router_nest(
prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
has_param in any::<bool>(),
) {
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix_segments.join("/"));
let mut route_path = format!("/{}", route_segments.join("/"));
if has_param {
route_path.push_str("/{id}");
}
let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
let nested_router_for_router = Router::new().route(&route_path, get(handler));
let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
let rustapi_router = rustapi_app.into_router();
let router_app = Router::new().nest(&prefix, nested_router_for_router);
let rustapi_routes = rustapi_router.registered_routes();
let router_routes = router_app.registered_routes();
prop_assert_eq!(
rustapi_routes.len(),
router_routes.len(),
"RustApi and Router should have same number of routes"
);
for (path, info) in router_routes {
prop_assert!(
rustapi_routes.contains_key(path),
"Route '{}' from Router should exist in RustApi routes",
path
);
let rustapi_info = rustapi_routes.get(path).unwrap();
prop_assert_eq!(
&info.path, &rustapi_info.path,
"Display paths should match for route '{}'",
path
);
prop_assert_eq!(
info.methods.len(), rustapi_info.methods.len(),
"Method count should match for route '{}'",
path
);
}
}
#[test]
fn prop_rustapi_nest_includes_routes_in_openapi(
prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
has_param in any::<bool>(),
) {
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix_segments.join("/"));
let mut route_path = format!("/{}", route_segments.join("/"));
if has_param {
route_path.push_str("/{id}");
}
let nested_router = Router::new().route(&route_path, get(handler));
let app = RustApi::new().nest(&prefix, nested_router);
let expected_openapi_path = format!("{}{}", prefix, route_path);
let spec = app.openapi_spec();
prop_assert!(
spec.paths.contains_key(&expected_openapi_path),
"Expected OpenAPI path '{}' not found. Available paths: {:?}",
expected_openapi_path,
spec.paths.keys().collect::<Vec<_>>()
);
let path_item = spec.paths.get(&expected_openapi_path).unwrap();
prop_assert!(
path_item.get.is_some(),
"GET operation should exist for path '{}'",
expected_openapi_path
);
}
#[test]
fn prop_rustapi_nest_route_matching_identical(
prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
param_value in "[a-z0-9]{1,10}",
) {
use crate::router::RouteMatch;
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix_segments.join("/"));
let route_path = format!("/{}/{{id}}", route_segments.join("/"));
let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
let nested_router_for_router = Router::new().route(&route_path, get(handler));
let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
let rustapi_router = rustapi_app.into_router();
let router_app = Router::new().nest(&prefix, nested_router_for_router);
let full_path = format!("{}/{}/{}", prefix, route_segments.join("/"), param_value);
let rustapi_match = rustapi_router.match_route(&full_path, &Method::GET);
let router_match = router_app.match_route(&full_path, &Method::GET);
match (rustapi_match, router_match) {
(RouteMatch::Found { params: rustapi_params, .. }, RouteMatch::Found { params: router_params, .. }) => {
prop_assert_eq!(
rustapi_params.len(),
router_params.len(),
"Parameter count should match"
);
for (key, value) in &router_params {
prop_assert!(
rustapi_params.contains_key(key),
"RustApi should have parameter '{}'",
key
);
prop_assert_eq!(
rustapi_params.get(key).unwrap(),
value,
"Parameter '{}' value should match",
key
);
}
}
(rustapi_result, router_result) => {
prop_assert!(
false,
"Both should return Found, but RustApi returned {:?} and Router returned {:?}",
match rustapi_result {
RouteMatch::Found { .. } => "Found",
RouteMatch::NotFound => "NotFound",
RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
},
match router_result {
RouteMatch::Found { .. } => "Found",
RouteMatch::NotFound => "NotFound",
RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
}
);
}
}
}
}
#[test]
fn test_openapi_operations_propagated_during_nesting() {
async fn list_users() -> &'static str {
"list users"
}
async fn get_user() -> &'static str {
"get user"
}
async fn create_user() -> &'static str {
"create user"
}
let users_router = Router::new()
.route("/", get(list_users))
.route("/create", post(create_user))
.route("/{id}", get(get_user));
let app = RustApi::new().nest("/api/v1/users", users_router);
let spec = app.openapi_spec();
assert!(
spec.paths.contains_key("/api/v1/users"),
"Should have /api/v1/users path"
);
let users_path = spec.paths.get("/api/v1/users").unwrap();
assert!(users_path.get.is_some(), "Should have GET operation");
assert!(
spec.paths.contains_key("/api/v1/users/create"),
"Should have /api/v1/users/create path"
);
let create_path = spec.paths.get("/api/v1/users/create").unwrap();
assert!(create_path.post.is_some(), "Should have POST operation");
assert!(
spec.paths.contains_key("/api/v1/users/{id}"),
"Should have /api/v1/users/{{id}} path"
);
let user_path = spec.paths.get("/api/v1/users/{id}").unwrap();
assert!(
user_path.get.is_some(),
"Should have GET operation for user by id"
);
let get_user_op = user_path.get.as_ref().unwrap();
assert!(!get_user_op.parameters.is_empty(), "Should have parameters");
let params = &get_user_op.parameters;
assert!(
params
.iter()
.any(|p| p.name == "id" && p.location == "path"),
"Should have 'id' path parameter"
);
}
#[test]
fn test_openapi_spec_empty_without_routes() {
let app = RustApi::new();
let spec = app.openapi_spec();
assert!(
spec.paths.is_empty(),
"OpenAPI spec should have no paths without routes"
);
}
#[test]
fn test_rustapi_nest_delegates_to_router_nest() {
use crate::router::RouteMatch;
async fn list_users() -> &'static str {
"list users"
}
async fn get_user() -> &'static str {
"get user"
}
async fn create_user() -> &'static str {
"create user"
}
let users_router = Router::new()
.route("/", get(list_users))
.route("/create", post(create_user))
.route("/{id}", get(get_user));
let app = RustApi::new().nest("/api/v1/users", users_router);
let router = app.into_router();
let routes = router.registered_routes();
assert_eq!(routes.len(), 3, "Should have 3 routes registered");
assert!(
routes.contains_key("/api/v1/users"),
"Should have /api/v1/users route"
);
assert!(
routes.contains_key("/api/v1/users/create"),
"Should have /api/v1/users/create route"
);
assert!(
routes.contains_key("/api/v1/users/:id"),
"Should have /api/v1/users/:id route"
);
match router.match_route("/api/v1/users", &Method::GET) {
RouteMatch::Found { params, .. } => {
assert!(params.is_empty(), "Root route should have no params");
}
_ => panic!("GET /api/v1/users should be found"),
}
match router.match_route("/api/v1/users/create", &Method::POST) {
RouteMatch::Found { params, .. } => {
assert!(params.is_empty(), "Create route should have no params");
}
_ => panic!("POST /api/v1/users/create should be found"),
}
match router.match_route("/api/v1/users/123", &Method::GET) {
RouteMatch::Found { params, .. } => {
assert_eq!(
params.get("id"),
Some(&"123".to_string()),
"Should extract id param"
);
}
_ => panic!("GET /api/v1/users/123 should be found"),
}
match router.match_route("/api/v1/users", &Method::DELETE) {
RouteMatch::MethodNotAllowed { allowed } => {
assert!(allowed.contains(&Method::GET), "Should allow GET");
}
_ => panic!("DELETE /api/v1/users should return MethodNotAllowed"),
}
}
#[test]
fn test_rustapi_nest_includes_routes_in_openapi_spec() {
async fn list_items() -> &'static str {
"list items"
}
async fn get_item() -> &'static str {
"get item"
}
let items_router = Router::new()
.route("/", get(list_items))
.route("/{item_id}", get(get_item));
let app = RustApi::new().nest("/api/items", items_router);
let spec = app.openapi_spec();
assert!(
spec.paths.contains_key("/api/items"),
"Should have /api/items in OpenAPI"
);
assert!(
spec.paths.contains_key("/api/items/{item_id}"),
"Should have /api/items/{{item_id}} in OpenAPI"
);
let list_path = spec.paths.get("/api/items").unwrap();
assert!(
list_path.get.is_some(),
"Should have GET operation for /api/items"
);
let get_path = spec.paths.get("/api/items/{item_id}").unwrap();
assert!(
get_path.get.is_some(),
"Should have GET operation for /api/items/{{item_id}}"
);
let get_op = get_path.get.as_ref().unwrap();
assert!(!get_op.parameters.is_empty(), "Should have parameters");
let params = &get_op.parameters;
assert!(
params
.iter()
.any(|p| p.name == "item_id" && p.location == "path"),
"Should have 'item_id' path parameter"
);
}
struct HotReloadEnvGuard {
previous: Option<String>,
}
impl HotReloadEnvGuard {
fn set(value: Option<&str>) -> Self {
let previous = std::env::var("RUSTAPI_HOT_RELOAD").ok();
match value {
Some(v) => std::env::set_var("RUSTAPI_HOT_RELOAD", v),
None => std::env::remove_var("RUSTAPI_HOT_RELOAD"),
}
Self { previous }
}
}
impl Drop for HotReloadEnvGuard {
fn drop(&mut self) {
match &self.previous {
Some(value) => std::env::set_var("RUSTAPI_HOT_RELOAD", value),
None => std::env::remove_var("RUSTAPI_HOT_RELOAD"),
}
}
}
#[test]
fn print_hot_reload_banner_selects_branch_from_preexisting_env() {
let _guard = HotReloadEnvGuard::set(None);
let app = RustApi::new().hot_reload(true);
assert_eq!(
app.print_hot_reload_banner("127.0.0.1:8080"),
Some(false),
"tip branch when watcher env unset"
);
let _guard = HotReloadEnvGuard::set(Some("1"));
let app = RustApi::new().hot_reload(true);
assert_eq!(
app.print_hot_reload_banner("127.0.0.1:8081"),
Some(true),
"watcher branch when env already active"
);
}