#![allow(dead_code)]
#![allow(unused_variables)]
#![allow(unused_imports)]
use serde::{Deserialize, Serialize};
use server_less::{http, response, route, server};
#[allow(unused_imports)]
use server_less::IntoErrorCode as _;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct Item {
id: String,
name: String,
}
#[derive(Debug)]
#[allow(dead_code)]
enum ItemError {
NotFound,
Invalid,
}
impl std::fmt::Display for ItemError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ItemError::NotFound => write!(f, "Item not found"),
ItemError::Invalid => write!(f, "Invalid item"),
}
}
}
impl std::error::Error for ItemError {}
#[derive(Clone)]
struct ItemService {
items: std::sync::Arc<std::sync::Mutex<Vec<Item>>>,
}
impl ItemService {
fn new() -> Self {
Self {
items: std::sync::Arc::new(std::sync::Mutex::new(vec![Item {
id: "1".to_string(),
name: "Test".to_string(),
}])),
}
}
}
#[http(prefix = "/api/v1")]
impl ItemService {
pub fn list_items(&self) -> Vec<Item> {
self.items.lock().unwrap().clone()
}
pub fn get_item(&self, item_id: String) -> Option<Item> {
self.items
.lock()
.unwrap()
.iter()
.find(|i| i.id == item_id)
.cloned()
}
pub fn create_item(&self, name: String) -> Result<Item, ItemError> {
if name.is_empty() {
return Err(ItemError::Invalid);
}
let mut items = self.items.lock().unwrap();
let item = Item {
id: (items.len() + 1).to_string(),
name,
};
items.push(item.clone());
Ok(item)
}
}
#[test]
fn test_http_router_created() {
let service = ItemService::new();
let _router = service.http_router();
}
#[test]
fn test_openapi_spec_generated() {
let spec = ItemService::http_openapi_spec();
assert_eq!(spec.get("openapi").unwrap(), "3.0.0");
let info = spec.get("info").unwrap();
assert_eq!(info.get("title").unwrap(), "ItemService");
let paths = spec.get("paths").unwrap().as_object().unwrap();
assert!(paths.contains_key("/api/v1/items"));
}
#[test]
fn test_openapi_contains_operations() {
let spec = ItemService::http_openapi_spec();
let paths = spec.get("paths").unwrap();
let items_path = paths.get("/api/v1/items").unwrap();
assert!(items_path.get("get").is_some());
assert!(items_path.get("post").is_some());
let get_op = items_path.get("get").unwrap();
assert_eq!(get_op.get("operationId").unwrap(), "list_items");
assert_eq!(get_op.get("summary").unwrap(), "List all items");
}
#[test]
fn test_http_openapi_paths_generated() {
let paths = ItemService::http_openapi_paths();
assert_eq!(paths.len(), 3);
let list_path = paths
.iter()
.find(|p| p.operation.operation_id == Some("list_items".to_string()));
assert!(list_path.is_some());
let list_path = list_path.unwrap();
assert_eq!(list_path.path, "/api/v1/items");
assert_eq!(list_path.method, "get");
assert_eq!(
list_path.operation.summary,
Some("List all items".to_string())
);
let create_path = paths
.iter()
.find(|p| p.operation.operation_id == Some("create_item".to_string()));
assert!(create_path.is_some());
let create_path = create_path.unwrap();
assert_eq!(create_path.path, "/api/v1/items");
assert_eq!(create_path.method, "post");
assert!(create_path.operation.request_body.is_some());
}
#[derive(Clone)]
struct OverrideService;
#[http(prefix = "/api")]
impl OverrideService {
#[route(path = "/custom-endpoint")]
pub fn my_method(&self) -> String {
"custom".to_string()
}
#[route(method = "POST")]
pub fn get_data(&self, payload: String) -> String {
payload
}
#[route(method = "PUT", path = "/special/{id}")]
pub fn do_something(&self, id: String) -> String {
id
}
#[route(skip)]
pub fn internal_helper(&self) -> String {
"internal".to_string()
}
#[route(hidden)]
pub fn secret_endpoint(&self) -> String {
"secret".to_string()
}
pub fn list_things(&self) -> Vec<String> {
vec![]
}
}
#[test]
fn test_custom_path_in_openapi() {
let spec = OverrideService::http_openapi_spec();
let paths = spec.get("paths").unwrap().as_object().unwrap();
assert!(
paths.contains_key("/api/custom-endpoint"),
"Expected /api/custom-endpoint, got: {:?}",
paths.keys().collect::<Vec<_>>()
);
}
#[test]
fn test_method_override_in_openapi() {
let spec = OverrideService::http_openapi_spec();
let paths = spec.get("paths").unwrap();
let data_path = paths.get("/api/datas").unwrap();
assert!(
data_path.get("post").is_some(),
"Expected POST for get_data, got: {:?}",
data_path
);
}
#[test]
fn test_combined_override_in_openapi() {
let spec = OverrideService::http_openapi_spec();
let paths = spec.get("paths").unwrap();
let special_path = paths.get("/api/special/{id}").unwrap();
assert!(
special_path.get("put").is_some(),
"Expected PUT for do_something, got: {:?}",
special_path
);
}
#[test]
fn test_skipped_method_not_in_openapi() {
let spec = OverrideService::http_openapi_spec();
let paths = spec.get("paths").unwrap().as_object().unwrap();
for (path, _) in paths {
assert!(
!path.contains("internal") && !path.contains("helper"),
"Skipped method should not appear in OpenAPI: {}",
path
);
}
}
#[test]
fn test_hidden_method_not_in_openapi() {
let spec = OverrideService::http_openapi_spec();
let paths = spec.get("paths").unwrap().as_object().unwrap();
for (path, _) in paths {
assert!(
!path.contains("secret"),
"Hidden method should not appear in OpenAPI: {}",
path
);
}
}
#[test]
fn test_normal_method_still_works() {
let spec = OverrideService::http_openapi_spec();
let paths = spec.get("paths").unwrap();
assert!(
paths.get("/api/things").is_some(),
"list_things should generate /api/things"
);
}
#[test]
fn test_override_service_router_created() {
let service = OverrideService;
let _router = service.http_router();
}
#[derive(Clone)]
struct SchemaService;
#[http(prefix = "/api")]
impl SchemaService {
pub fn list_items(&self, page: Option<u32>, limit: Option<u32>) -> Vec<String> {
vec![]
}
pub fn get_item(&self, item_id: String) -> Option<String> {
None
}
pub fn create_item(&self, name: String, description: Option<String>) -> String {
name
}
pub fn update_item(&self, item_id: String, name: String) -> Result<String, String> {
Ok(name)
}
}
#[test]
fn test_openapi_query_parameters() {
let spec = SchemaService::http_openapi_spec();
let paths = spec.get("paths").unwrap();
let list_path = paths.get("/api/items").unwrap();
let get_op = list_path.get("get").unwrap();
let params = get_op.get("parameters").unwrap().as_array().unwrap();
let param_names: Vec<_> = params
.iter()
.map(|p| p.get("name").unwrap().as_str().unwrap())
.collect();
assert!(param_names.contains(&"page"), "Expected 'page' parameter");
assert!(param_names.contains(&"limit"), "Expected 'limit' parameter");
for param in params {
assert_eq!(param.get("in").unwrap(), "query");
}
}
#[test]
fn test_openapi_path_parameters() {
let spec = SchemaService::http_openapi_spec();
let paths = spec.get("paths").unwrap();
let get_path = paths.get("/api/items/{item_id}").unwrap();
let get_op = get_path.get("get").unwrap();
let params = get_op.get("parameters").unwrap().as_array().unwrap();
let path_params: Vec<_> = params
.iter()
.filter(|p| p.get("in").unwrap() == "path")
.collect();
assert!(!path_params.is_empty(), "Expected path parameters");
}
#[test]
fn test_openapi_request_body() {
let spec = SchemaService::http_openapi_spec();
let paths = spec.get("paths").unwrap();
let create_path = paths.get("/api/items").unwrap();
let post_op = create_path.get("post").unwrap();
assert!(
post_op.get("requestBody").is_some(),
"Expected requestBody for POST"
);
let body = post_op.get("requestBody").unwrap();
let content = body.get("content").unwrap();
let json_schema = content.get("application/json").unwrap();
let schema = json_schema.get("schema").unwrap();
let props = schema.get("properties").unwrap().as_object().unwrap();
assert!(props.contains_key("name"), "Expected 'name' property");
}
#[test]
fn test_openapi_error_responses() {
let spec = SchemaService::http_openapi_spec();
let paths = spec.get("paths").unwrap();
let update_path = paths.get("/api/items/{item_id}").unwrap();
let put_op = update_path.get("put").unwrap();
let responses = put_op.get("responses").unwrap().as_object().unwrap();
assert!(responses.contains_key("200"), "Expected 200 response");
assert!(
responses.contains_key("400"),
"Expected 400 response for Result"
);
assert!(
responses.contains_key("500"),
"Expected 500 response for Result"
);
}
#[derive(Clone)]
struct ResponseService;
#[http(prefix = "/api")]
impl ResponseService {
#[response(status = 201)]
pub fn create_resource(&self, name: String) -> Item {
Item {
id: "1".to_string(),
name,
}
}
#[response(status = 204)]
pub fn delete_resource(&self, id: String) {
}
#[response(content_type = "application/octet-stream")]
pub fn download_file(&self, id: String) -> Vec<u8> {
vec![1, 2, 3]
}
#[response(header = "X-Custom-Header", value = "custom-value")]
pub fn get_with_header(&self, id: String) -> String {
"data".to_string()
}
#[response(status = 201)]
#[response(content_type = "application/vnd.api+json")]
#[response(header = "X-Resource-Id", value = "123")]
#[response(header = "X-Version", value = "1.0")]
pub fn create_with_all(&self, data: String) -> String {
data
}
pub fn get_normal(&self, id: String) -> String {
"normal".to_string()
}
}
#[test]
fn test_response_service_router_created() {
let service = ResponseService;
let _router = service.http_router();
}
#[test]
fn test_response_status_override_in_openapi() {
let spec = ResponseService::http_openapi_spec();
let paths = spec.get("paths").unwrap();
let create_path = paths.get("/api/resources").unwrap();
let post_op = create_path.get("post").unwrap();
let responses = post_op.get("responses").unwrap().as_object().unwrap();
assert!(
responses.contains_key("201"),
"Expected 201 Created response, got: {:?}",
responses.keys().collect::<Vec<_>>()
);
}
#[test]
fn test_response_no_content_in_openapi() {
let spec = ResponseService::http_openapi_spec();
let paths = spec.get("paths").unwrap();
let delete_path = paths.get("/api/resources/{id}").unwrap();
let delete_op = delete_path.get("delete").unwrap();
let responses = delete_op.get("responses").unwrap().as_object().unwrap();
assert!(
responses.contains_key("204"),
"Expected 204 No Content response"
);
}
#[test]
fn test_response_content_type_in_openapi() {
let spec = ResponseService::http_openapi_spec();
let paths = spec.get("paths").unwrap();
let download_path = paths.get("/api/download-files").unwrap();
let post_op = download_path.get("post").unwrap();
let responses = post_op.get("responses").unwrap();
let ok_response = responses.get("200").unwrap();
if let Some(content) = ok_response.get("content") {
let content_obj = content.as_object().unwrap();
assert!(
content_obj.contains_key("application/octet-stream"),
"Expected application/octet-stream content type, got: {:?}",
content_obj.keys().collect::<Vec<_>>()
);
}
}
#[test]
fn test_response_custom_headers_in_openapi() {
let spec = ResponseService::http_openapi_spec();
let paths = spec.get("paths").unwrap();
let get_path = paths.get("/api/with-headers/{id}").unwrap();
let get_op = get_path.get("get").unwrap();
let responses = get_op.get("responses").unwrap();
let ok_response = responses.get("200").unwrap();
if let Some(headers) = ok_response.get("headers") {
let headers_obj = headers.as_object().unwrap();
assert!(
headers_obj.contains_key("X-Custom-Header"),
"Expected X-Custom-Header in response headers"
);
}
}
#[test]
fn test_response_combined_overrides_in_openapi() {
let spec = ResponseService::http_openapi_spec();
let paths = spec.get("paths").unwrap();
let create_path = paths.get("/api/with-alls").unwrap();
let post_op = create_path.get("post").unwrap();
let responses = post_op.get("responses").unwrap().as_object().unwrap();
assert!(
responses.contains_key("201"),
"Expected 201 status for combined override"
);
let created_response = responses.get("201").unwrap();
if let Some(content) = created_response.get("content") {
let content_obj = content.as_object().unwrap();
assert!(
content_obj.contains_key("application/vnd.api+json"),
"Expected custom content type"
);
}
if let Some(headers) = created_response.get("headers") {
let headers_obj = headers.as_object().unwrap();
assert!(
headers_obj.contains_key("X-Resource-Id"),
"Expected X-Resource-Id header"
);
assert!(
headers_obj.contains_key("X-Version"),
"Expected X-Version header"
);
}
}
#[test]
fn test_response_normal_method_unchanged() {
let spec = ResponseService::http_openapi_spec();
let paths = spec.get("paths").unwrap();
let normal_path = paths.get("/api/normals/{id}").unwrap();
let get_op = normal_path.get("get").unwrap();
let responses = get_op.get("responses").unwrap().as_object().unwrap();
assert!(
responses.contains_key("200"),
"Expected default 200 response for normal method"
);
}
#[derive(Clone)]
struct ServerSkipService;
#[http(prefix = "/api")]
impl ServerSkipService {
pub fn get_public(&self) -> String {
"public".to_string()
}
#[server(skip)]
pub fn get_internal(&self) -> String {
"internal".to_string()
}
}
#[test]
fn test_server_skip_not_in_openapi() {
let spec = ServerSkipService::http_openapi_spec();
let paths = spec.get("paths").unwrap().as_object().unwrap();
assert!(
paths.contains_key("/api/publics"),
"get_public should generate /api/publics, got: {:?}",
paths.keys().collect::<Vec<_>>()
);
for path_key in paths.keys() {
assert!(
!path_key.contains("internal"),
"#[server(skip)] method must not appear in OpenAPI paths: {}",
path_key
);
}
}
#[test]
fn test_server_skip_router_still_created() {
let service = ServerSkipService;
let _router = service.http_router();
}
#[derive(Clone)]
struct UserApi;
#[http]
impl UserApi {
fn list_users(&self) -> Vec<String> {
vec!["alice".to_string(), "bob".to_string()]
}
fn get_user(&self, id: String) -> String {
format!("User: {}", id)
}
fn create_user(&self, name: String) -> String {
format!("Created: {}", name)
}
}
#[derive(Clone)]
struct PostApi;
#[http]
impl PostApi {
fn list_posts(&self) -> Vec<String> {
vec!["post1".to_string()]
}
}
#[derive(Clone)]
struct HttpApp {
user_api: UserApi,
post_api: PostApi,
}
#[http]
impl HttpApp {
fn get_health(&self) -> String {
"ok".to_string()
}
fn users(&self) -> &UserApi {
&self.user_api
}
fn posts(&self) -> &PostApi {
&self.post_api
}
}
#[test]
fn test_http_static_mount_router_created() {
let app = HttpApp {
user_api: UserApi,
post_api: PostApi,
};
let _router = app.http_router();
}
#[test]
fn test_http_static_mount_openapi_paths() {
let spec = HttpApp::http_openapi_spec();
let paths = spec.get("paths").unwrap().as_object().unwrap();
let path_keys: Vec<_> = paths.keys().collect();
assert!(
paths.contains_key("/healths"),
"Expected /healths path, got: {:?}",
path_keys
);
assert!(
path_keys.iter().any(|p| p.starts_with("/users/")),
"Expected child paths under /users/, got: {:?}",
path_keys
);
assert!(
path_keys.iter().any(|p| p.starts_with("/posts/")),
"Expected child paths under /posts/, got: {:?}",
path_keys
);
}
#[test]
fn test_http_mount_openapi_paths_composed() {
let paths = HttpApp::http_openapi_paths();
let path_strs: Vec<&str> = paths.iter().map(|p| p.path.as_str()).collect();
assert!(
path_strs.iter().any(|p| p.starts_with("/users/")),
"Mounted UserApi paths should appear under /users/: {:?}",
path_strs
);
assert!(
path_strs.iter().any(|p| p.starts_with("/posts/")),
"Mounted PostApi paths should appear under /posts/: {:?}",
path_strs
);
assert!(
path_strs.contains(&"/healths"),
"Parent leaf path /healths should be included: {:?}",
path_strs
);
}
#[test]
fn test_http_mount_trait_implemented() {
use server_less::HttpMount;
let router = <UserApi as HttpMount>::http_mount_router(std::sync::Arc::new(UserApi));
let _ = router; }
#[derive(Clone)]
struct HiddenHttpService;
#[http]
impl HiddenHttpService {
pub fn get_public(&self) -> String {
"public".to_string()
}
#[server(hidden)]
pub fn get_hidden(&self) -> String {
"hidden".to_string()
}
}
#[test]
fn test_http_server_hidden_not_in_openapi_paths() {
let paths = HiddenHttpService::http_openapi_paths();
let path_keys: Vec<_> = paths.iter().map(|p| p.path.as_str()).collect();
assert!(
path_keys.iter().any(|p| p.contains("public")),
"public endpoint must appear in OpenAPI paths"
);
assert!(
!path_keys.iter().any(|p| p.contains("hidden")),
"hidden endpoint must not appear in OpenAPI paths"
);
}
#[test]
fn test_http_server_hidden_not_in_openapi_spec() {
let spec = HiddenHttpService::http_openapi_spec();
let paths = spec.get("paths").unwrap().as_object().unwrap();
let paths_str = serde_json::to_string(paths).unwrap();
assert!(!paths_str.contains("hidden"), "hidden endpoint must not appear in openapi_spec paths");
}
#[test]
fn test_http_server_hidden_router_is_created() {
let svc = HiddenHttpService;
let _router = svc.http_router();
}
#[derive(Debug, server_less::ServerlessError)]
enum ValidationError {
#[error(code = 422, message = "Input failed validation")]
InputInvalid,
#[error(code = 409, message = "Resource already exists")]
AlreadyExists,
}
#[derive(Clone)]
struct ValidationService;
#[http(prefix = "/api")]
impl ValidationService {
pub fn create_validated(&self, fail: bool) -> Result<String, ValidationError> {
if fail {
Err(ValidationError::InputInvalid)
} else {
Ok("ok".to_string())
}
}
pub fn create_unique(&self, exists: bool) -> Result<String, ValidationError> {
if exists {
Err(ValidationError::AlreadyExists)
} else {
Ok("created".to_string())
}
}
}
#[tokio::test]
async fn test_serverless_error_code_422_maps_to_http_422() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let router = ValidationService.http_router();
let response = router
.oneshot(
Request::builder()
.method("POST")
.uri("/api/validateds")
.header("content-type", "application/json")
.body(Body::from(r#"{"fail":true}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::UNPROCESSABLE_ENTITY,
"Expected HTTP 422 from #[error(code = 422)], got: {}",
response.status()
);
}
#[tokio::test]
async fn test_serverless_error_code_409_maps_to_http_409() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let router = ValidationService.http_router();
let response = router
.oneshot(
Request::builder()
.method("POST")
.uri("/api/uniques")
.header("content-type", "application/json")
.body(Body::from(r#"{"exists":true}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::CONFLICT,
"Expected HTTP 409 from #[error(code = 409)], got: {}",
response.status()
);
}
#[tokio::test]
async fn test_serverless_error_ok_returns_200() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let router = ValidationService.http_router();
let response = router
.oneshot(
Request::builder()
.method("POST")
.uri("/api/validateds")
.header("content-type", "application/json")
.body(Body::from(r#"{"fail":false}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::OK,
"Expected HTTP 200 for Ok case, got: {}",
response.status()
);
}
#[derive(Clone)]
struct ParamService;
#[http(prefix = "/api")]
impl ParamService {
pub fn search_items(
&self,
#[param(name = "q")] query: String,
) -> Vec<String> {
vec![query]
}
#[route(method = "GET", path = "/param-query")]
pub fn get_param_query(
&self,
#[param(query)] filter: String,
) -> String {
filter
}
#[route(path = "/by-slug/{slug}")]
pub fn get_by_slug(
&self,
#[param(path)] slug: String,
) -> Option<String> {
Some(slug)
}
#[route(method = "POST", path = "/param-body")]
pub fn post_param_body(
&self,
#[param(body)] payload: String,
) -> String {
payload
}
pub fn list_with_defaults(
&self,
#[param(default = 1)] page: u32,
#[param(default = 20)] size: u32,
) -> Vec<String> {
vec![format!("page={page},size={size}")]
}
pub fn get_secured(
&self,
#[param(header, name = "X-Api-Key")] api_key: Option<String>,
) -> String {
api_key.unwrap_or_default()
}
pub fn find_users(
&self,
#[param(help = "Substring to match against user names")] name: Option<String>,
) -> Vec<String> {
vec![name.unwrap_or_default()]
}
}
#[test]
fn test_param_service_router_created() {
let service = ParamService;
let _router = service.http_router();
}
#[test]
fn test_param_custom_wire_name_in_openapi() {
let paths = ParamService::http_openapi_paths();
let search = paths
.iter()
.find(|p| p.operation.operation_id == Some("search_items".to_string()))
.expect("search_items path missing");
let param_names: Vec<&str> = search
.operation
.parameters
.iter()
.map(|p| p.name.as_str())
.collect();
assert!(
param_names.contains(&"q"),
"#[param(name = \"q\")] should rename the wire parameter to 'q'; got: {:?}",
param_names
);
assert!(
!param_names.contains(&"query"),
"original Rust name 'query' should not appear as the wire name; got: {:?}",
param_names
);
}
#[test]
fn test_param_explicit_query_in_openapi() {
let paths = ParamService::http_openapi_paths();
let route = paths
.iter()
.find(|p| p.operation.operation_id == Some("get_param_query".to_string()))
.expect("get_param_query path missing");
let filter_param = route
.operation
.parameters
.iter()
.find(|p| p.name == "filter")
.expect("'filter' parameter missing from get_param_query OpenAPI spec");
assert_eq!(
filter_param.location, "query",
"#[param(query)] should place the parameter in query location; got: {:?}",
filter_param.location
);
}
#[test]
fn test_param_explicit_path_in_openapi() {
let paths = ParamService::http_openapi_paths();
let route = paths
.iter()
.find(|p| p.operation.operation_id == Some("get_by_slug".to_string()))
.expect("get_by_slug path missing");
let slug_param = route
.operation
.parameters
.iter()
.find(|p| p.name == "slug")
.expect("'slug' parameter missing from get_by_slug OpenAPI spec");
assert_eq!(
slug_param.location, "path",
"#[param(path)] should place the parameter in path location; got: {:?}",
slug_param.location
);
assert!(
slug_param.required,
"path parameters must be required in OpenAPI"
);
}
#[test]
fn test_param_explicit_body_in_openapi() {
let paths = ParamService::http_openapi_paths();
let route = paths
.iter()
.find(|p| p.operation.operation_id == Some("post_param_body".to_string()))
.expect("post_param_body path missing");
assert!(
route.operation.request_body.is_some(),
"#[param(body)] should produce a requestBody; got: {:?}",
route.operation
);
let param_names: Vec<&str> = route
.operation
.parameters
.iter()
.map(|p| p.name.as_str())
.collect();
assert!(
!param_names.contains(&"payload"),
"#[param(body)] param should not appear in parameters array; got: {:?}",
param_names
);
let body = route.operation.request_body.as_ref().unwrap();
let schema = body
.get("content")
.and_then(|c| c.get("application/json"))
.and_then(|j| j.get("schema"))
.and_then(|s| s.get("properties"))
.expect("requestBody should have content.application/json.schema.properties");
assert!(
schema.get("payload").is_some(),
"'payload' should be a property in the request body schema; got: {:?}",
schema
);
}
#[test]
fn test_param_default_value_not_required_in_openapi() {
let paths = ParamService::http_openapi_paths();
let route = paths
.iter()
.find(|p| p.operation.operation_id == Some("list_with_defaults".to_string()))
.expect("list_with_defaults path missing");
for param in &route.operation.parameters {
if param.name == "page" || param.name == "size" {
assert!(
!param.required,
"#[param(default = ...)] parameter '{}' should be optional in OpenAPI; \
got required=true",
param.name
);
}
}
let param_names: Vec<&str> = route
.operation
.parameters
.iter()
.map(|p| p.name.as_str())
.collect();
assert!(
param_names.contains(&"page"),
"expected 'page' parameter; got: {:?}",
param_names
);
assert!(
param_names.contains(&"size"),
"expected 'size' parameter; got: {:?}",
param_names
);
}
#[test]
fn test_param_header_in_openapi() {
let paths = ParamService::http_openapi_paths();
let route = paths
.iter()
.find(|p| p.operation.operation_id == Some("get_secured".to_string()))
.expect("get_secured path missing");
let header_param = route
.operation
.parameters
.iter()
.find(|p| p.name == "X-Api-Key")
.expect("'X-Api-Key' header parameter missing from get_secured OpenAPI spec");
assert_eq!(
header_param.location, "header",
"#[param(header)] should place the parameter in header location; got: {:?}",
header_param.location
);
}
#[test]
fn test_param_help_route_in_openapi() {
let paths = ParamService::http_openapi_paths();
let route = paths
.iter()
.find(|p| p.operation.operation_id == Some("find_users".to_string()))
.expect("find_users path missing");
let name_param = route
.operation
.parameters
.iter()
.find(|p| p.name == "name");
assert!(
name_param.is_some(),
"'name' parameter should appear in find_users OpenAPI spec; got: {:?}",
route.operation.parameters
);
assert_eq!(
name_param.unwrap().description.as_deref(),
Some("Substring to match against user names"),
"#[param(help = \"...\")] should populate the OpenAPI parameter description"
);
}
#[derive(Clone)]
struct DebugService;
#[http(debug = true)]
impl DebugService {
pub fn list_debug_items(&self) -> Vec<String> {
vec!["a".to_string(), "b".to_string()]
}
pub fn get_debug_item(&self, item_id: String) -> Option<String> {
if item_id == "1" {
Some("found".to_string())
} else {
None
}
}
}
#[test]
fn test_debug_impl_block_compiles_and_router_created() {
let service = DebugService;
let _router = service.http_router();
}
#[test]
fn test_debug_impl_block_openapi_still_works() {
let spec = DebugService::http_openapi_spec();
assert_eq!(spec.get("openapi").unwrap(), "3.0.0");
let paths = spec.get("paths").unwrap().as_object().unwrap();
assert!(
!paths.is_empty(),
"Expected OpenAPI paths from DebugService; got empty"
);
}
#[derive(Clone)]
struct PerMethodDebugService;
#[http]
impl PerMethodDebugService {
#[http(debug = true)]
pub fn list_verbose_items(&self) -> Vec<String> {
vec!["x".to_string()]
}
pub fn list_quiet_items(&self) -> Vec<String> {
vec!["y".to_string()]
}
}
#[test]
fn test_per_method_debug_compiles_and_router_created() {
let service = PerMethodDebugService;
let _router = service.http_router();
}
#[test]
fn test_per_method_debug_openapi_still_works() {
let paths = PerMethodDebugService::http_openapi_paths();
assert_eq!(
paths.len(),
2,
"Both methods should appear in OpenAPI; got: {:?}",
paths
.iter()
.map(|p| p.operation.operation_id.as_deref().unwrap_or("?"))
.collect::<Vec<_>>()
);
}
#[derive(Clone)]
struct TraceService;
#[http(prefix = "/trace", trace = true)]
impl TraceService {
pub fn list_trace_items(&self) -> Vec<String> {
vec!["a".to_string(), "b".to_string()]
}
pub fn get_trace_item(&self, item_id: String) -> Option<String> {
if item_id == "1" {
Some("found".to_string())
} else {
None
}
}
#[route(path = "/filtered-trace-items")]
pub fn find_trace_items(&self, query: String, limit: Option<u32>) -> Vec<String> {
vec![format!("{}:{}", query, limit.unwrap_or(10))]
}
}
#[test]
fn test_trace_impl_block_compiles_and_router_created() {
let service = TraceService;
let _router = service.http_router();
}
#[test]
fn test_trace_impl_block_openapi_still_works() {
let spec = TraceService::http_openapi_spec();
assert_eq!(spec.get("openapi").unwrap(), "3.0.0");
let paths = spec.get("paths").unwrap().as_object().unwrap();
assert!(
!paths.is_empty(),
"Expected OpenAPI paths from TraceService; got empty"
);
}
#[derive(Clone)]
struct PerMethodTraceService;
#[http]
impl PerMethodTraceService {
#[http(trace = true)]
pub fn list_verbose_trace(&self, kind: String) -> Vec<String> {
vec![kind]
}
pub fn list_quiet_trace(&self) -> Vec<String> {
vec!["y".to_string()]
}
}
#[test]
fn test_per_method_trace_compiles_and_router_created() {
let service = PerMethodTraceService;
let _router = service.http_router();
}
#[test]
fn test_per_method_trace_openapi_still_works() {
let paths = PerMethodTraceService::http_openapi_paths();
assert_eq!(
paths.len(),
2,
"Both methods should appear in OpenAPI; got: {:?}",
paths
.iter()
.map(|p| p.operation.operation_id.as_deref().unwrap_or("?"))
.collect::<Vec<_>>()
);
}
#[derive(Clone)]
struct RequiredParamService;
#[http(prefix = "/test")]
impl RequiredParamService {
pub fn list_things(&self, page: u32, limit: u32) -> String {
format!("page {} limit {}", page, limit)
}
}
#[tokio::test]
async fn test_missing_required_query_param_returns_400() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let response = RequiredParamService
.http_router()
.oneshot(
Request::builder()
.method("GET")
.uri("/test/things")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::BAD_REQUEST,
"Missing required query param should return 400; got: {}",
response.status()
);
}
#[tokio::test]
async fn test_invalid_required_query_param_type_returns_400() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let response = RequiredParamService
.http_router()
.oneshot(
Request::builder()
.method("GET")
.uri("/test/things?page=not-a-number&limit=10")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::BAD_REQUEST,
"Unparseable required query param should return 400; got: {}",
response.status()
);
}
#[tokio::test]
async fn test_valid_required_query_params_returns_200() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let response = RequiredParamService
.http_router()
.oneshot(
Request::builder()
.method("GET")
.uri("/test/things?page=1&limit=10")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::OK,
"Valid required query params should return 200; got: {}",
response.status()
);
}
#[derive(Clone)]
struct RequiredBodyService;
#[http(prefix = "/test-body")]
impl RequiredBodyService {
pub fn create_widget(&self, name: String, count: u32) -> String {
format!("{} x{}", name, count)
}
}
#[tokio::test]
async fn test_missing_required_body_field_returns_400() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let response = RequiredBodyService
.http_router()
.oneshot(
Request::builder()
.method("POST")
.uri("/test-body/widgets")
.header("content-type", "application/json")
.body(Body::from(r#"{"name":"widget"}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::BAD_REQUEST,
"Missing required body field should return 400; got: {}",
response.status()
);
}
#[tokio::test]
async fn test_valid_required_body_fields_returns_200() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let response = RequiredBodyService
.http_router()
.oneshot(
Request::builder()
.method("POST")
.uri("/test-body/widgets")
.header("content-type", "application/json")
.body(Body::from(r#"{"name":"widget","count":5}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::OK,
"Valid required body fields should return 200; got: {}",
response.status()
);
}
#[tokio::test]
async fn test_trace_handler_returns_correct_response() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let router = TraceService.http_router();
let response = router
.oneshot(
Request::builder()
.method("GET")
.uri("/trace/trace-items/1")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::OK,
"Traced handler should return 200 OK; got: {}",
response.status()
);
}
#[derive(Clone)]
struct OptionalParamService;
#[http(prefix = "/opt")]
impl OptionalParamService {
pub fn list_opt(&self, page: Option<u32>) -> Vec<String> {
vec![format!("page={}", page.unwrap_or(1))]
}
}
#[derive(Clone)]
struct OptionalBodyService;
#[http(prefix = "/opt-body")]
impl OptionalBodyService {
pub fn create_opt(&self, name: String, count: Option<u32>) -> String {
format!("{} x{}", name, count.unwrap_or(1))
}
}
#[tokio::test]
async fn test_optional_query_param_invalid_returns_400() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let response = OptionalParamService
.http_router()
.oneshot(
Request::builder()
.method("GET")
.uri("/opt/opts?page=not-a-number")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::BAD_REQUEST,
"Optional query param present-but-invalid should return 400; got: {}",
response.status()
);
}
#[tokio::test]
async fn test_optional_query_param_absent_returns_200() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let response = OptionalParamService
.http_router()
.oneshot(
Request::builder()
.method("GET")
.uri("/opt/opts")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::OK,
"Optional query param absent should return 200; got: {}",
response.status()
);
}
#[tokio::test]
async fn test_optional_query_param_valid_returns_200() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let response = OptionalParamService
.http_router()
.oneshot(
Request::builder()
.method("GET")
.uri("/opt/opts?page=3")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::OK,
"Optional query param valid should return 200; got: {}",
response.status()
);
}
#[tokio::test]
async fn test_optional_body_field_invalid_returns_400() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let response = OptionalBodyService
.http_router()
.oneshot(
Request::builder()
.method("POST")
.uri("/opt-body/opts")
.header("content-type", "application/json")
.body(Body::from(r#"{"name":"widget","count":"not-a-number"}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::BAD_REQUEST,
"Optional body field present-but-invalid should return 400; got: {}",
response.status()
);
}
#[tokio::test]
async fn test_optional_body_field_absent_returns_200() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let response = OptionalBodyService
.http_router()
.oneshot(
Request::builder()
.method("POST")
.uri("/opt-body/opts")
.header("content-type", "application/json")
.body(Body::from(r#"{"name":"widget"}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::OK,
"Optional body field absent should return 200; got: {}",
response.status()
);
}
#[tokio::test]
async fn test_optional_body_field_valid_returns_200() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let response = OptionalBodyService
.http_router()
.oneshot(
Request::builder()
.method("POST")
.uri("/opt-body/opts")
.header("content-type", "application/json")
.body(Body::from(r#"{"name":"widget","count":5}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::OK,
"Optional body field valid should return 200; got: {}",
response.status()
);
}
#[derive(Clone)]
struct WarnService;
#[http(prefix = "/warn")]
impl WarnService {
pub fn list_warn(&self, known: String) -> Vec<String> {
vec![known]
}
pub fn create_warn(&self, known: String) -> String {
known
}
}
#[tokio::test]
async fn test_unknown_query_param_logs_warning() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let response = WarnService
.http_router()
.oneshot(
Request::builder()
.method("GET")
.uri("/warn/warns?known=hello&unknown_extra=foo")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::OK,
"Unknown query param should warn (not reject); got: {}",
response.status()
);
}
#[tokio::test]
async fn test_unknown_body_field_logs_warning() {
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
let response = WarnService
.http_router()
.oneshot(
Request::builder()
.method("POST")
.uri("/warn/warns")
.header("content-type", "application/json")
.body(Body::from(r#"{"known":"hello","unexpected_field":"bar"}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::OK,
"Unknown body field should warn (not reject); got: {}",
response.status()
);
}