use std::time::{Duration, SystemTime, UNIX_EPOCH};
use axum::{Router, routing::get};
use jsonwebtoken::{EncodingKey, Header, encode};
use rs_zero::rest::{ApiResponse, AuthConfig, RestConfig, RestError, RestServer};
use serde::Serialize;
use tower::ServiceExt;
#[derive(Debug, Serialize)]
struct Claims {
exp: usize,
}
fn test_config() -> RestConfig {
RestConfig {
timeout: Duration::from_secs(1),
auth: Some(AuthConfig {
secret: "test-secret".to_string(),
public_paths: vec!["/ready".to_string(), "/public".to_string()],
}),
..RestConfig::default()
}
}
fn valid_token() -> String {
let exp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time")
.as_secs() as usize
+ 3600;
encode(
&Header::default(),
&Claims { exp },
&EncodingKey::from_secret(b"test-secret"),
)
.expect("token")
}
async fn body_text(response: axum::response::Response) -> String {
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
String::from_utf8(bytes.to_vec()).expect("utf8")
}
#[tokio::test]
async fn ready_path_is_public() {
let router = Router::new().route("/ready", get(|| async { ApiResponse::success("ok") }));
let app = RestServer::new(test_config(), router).into_router();
let response = app
.oneshot(
axum::http::Request::builder()
.uri("/ready")
.body(axum::body::Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), axum::http::StatusCode::OK);
assert!(body_text(response).await.contains("\"success\":true"));
}
#[tokio::test]
async fn protected_route_requires_token() {
let router = Router::new().route("/private", get(|| async { ApiResponse::success("secret") }));
let app = RestServer::new(test_config(), router).into_router();
let response = app
.oneshot(
axum::http::Request::builder()
.uri("/private")
.body(axum::body::Body::empty())
.expect("request"),
)
.await
.expect("response");
let body = body_text(response).await;
assert!(body.contains("\"success\":false"));
assert!(body.contains("UNAUTHORIZED"));
}
#[tokio::test]
async fn protected_route_accepts_valid_token() {
let router = Router::new().route("/private", get(|| async { ApiResponse::success("secret") }));
let app = RestServer::new(test_config(), router).into_router();
let response = app
.oneshot(
axum::http::Request::builder()
.uri("/private")
.header("authorization", format!("Bearer {}", valid_token()))
.body(axum::body::Body::empty())
.expect("request"),
)
.await
.expect("response");
assert!(body_text(response).await.contains("\"success\":true"));
}
#[tokio::test]
async fn handler_error_uses_uniform_response() {
let router = Router::new().route(
"/public",
get(|| async { RestError::BadRequest("invalid input".to_string()) }),
);
let app = RestServer::new(test_config(), router).into_router();
let response = app
.oneshot(
axum::http::Request::builder()
.uri("/public")
.body(axum::body::Body::empty())
.expect("request"),
)
.await
.expect("response");
let body = body_text(response).await;
assert!(body.contains("\"success\":false"));
assert!(body.contains("BAD_REQUEST"));
}
#[tokio::test]
async fn request_id_is_propagated() {
let router = Router::new().route("/ready", get(|| async { ApiResponse::success("ok") }));
let app = RestServer::new(test_config(), router).into_router();
let response = app
.oneshot(
axum::http::Request::builder()
.uri("/ready")
.header("x-request-id", "req-1")
.body(axum::body::Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(
response.headers().get("x-request-id").expect("request id"),
"req-1"
);
}
#[tokio::test]
async fn explicit_rest_layer_stack_matches_rest_server_defaults() {
let config = RestConfig::default();
let app = rs_zero::rest::RestLayerStack::new(config)
.layer(Router::new().route("/ready", get(|| async { ApiResponse::success("ok") })));
let response = app
.oneshot(
axum::http::Request::builder()
.uri("/ready")
.body(axum::body::Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), axum::http::StatusCode::OK);
assert!(response.headers().contains_key("x-request-id"));
}