use std::time::Duration;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use axum::response::IntoResponse;
use axum::routing::{get, post};
use tower::ServiceExt;
use af_web::{
base_router, health, join_tasks, metrics, operations_router, with_edge_middleware,
with_standard_middleware, ApiError, EdgeConfig, Readiness,
};
#[test]
fn api_error_constructors_and_status() {
assert_eq!(ApiError::bad_request("x").status, StatusCode::BAD_REQUEST);
assert_eq!(ApiError::not_found("x").status, StatusCode::NOT_FOUND);
assert_eq!(
ApiError::internal("x").status,
StatusCode::INTERNAL_SERVER_ERROR
);
let e = ApiError::new(StatusCode::CONFLICT, "dup").with_request_id("req_1".parse().unwrap());
assert_eq!(e.status, StatusCode::CONFLICT);
assert_eq!(e.request_id.as_deref(), Some("req_1"));
let resp = e.into_response();
assert_eq!(resp.status(), StatusCode::CONFLICT);
}
#[tokio::test]
async fn health_payload() {
let axum::Json(v) = health().await;
assert_eq!(v["status"], "ok");
}
#[tokio::test]
async fn metrics_payload() {
let response = metrics().await;
assert_eq!(response.1, "agent_factory_up 1\n");
}
#[test]
fn router_helpers_compose() {
let _r: axum::Router = with_standard_middleware(base_router::<()>());
let readiness = Readiness::default();
readiness.set(true);
assert!(readiness.is_ready());
let _r = with_edge_middleware(operations_router(readiness), EdgeConfig::default());
}
#[tokio::test]
async fn edge_stack_sets_request_id_denies_cors_and_limits_rate() {
let config = EdgeConfig {
requests_per_window: 1,
rate_window: Duration::from_secs(60),
..EdgeConfig::default()
};
let app = with_edge_middleware(
operations_router(Readiness::default()).route("/work", get(|| async { "ok" })),
config,
);
let first = app
.clone()
.oneshot(
Request::builder()
.uri("/work")
.header("origin", "https://not-allowed.invalid")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(first.status(), StatusCode::OK);
assert!(first.headers().contains_key("x-request-id"));
assert!(!first.headers().contains_key("access-control-allow-origin"));
let second = app
.clone()
.oneshot(Request::builder().uri("/work").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(second.status(), StatusCode::TOO_MANY_REQUESTS);
let health = app
.oneshot(Request::builder().uri("/live").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(health.status(), StatusCode::OK);
}
#[tokio::test]
async fn edge_stack_times_out_slow_requests() {
let app = with_edge_middleware(
axum::Router::new().route(
"/slow",
get(|| async {
tokio::time::sleep(Duration::from_millis(50)).await;
"late"
}),
),
EdgeConfig {
request_timeout: Duration::from_millis(1),
..EdgeConfig::default()
},
);
let response = app
.oneshot(Request::builder().uri("/slow").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::REQUEST_TIMEOUT);
}
#[tokio::test]
async fn edge_stack_rejects_oversized_bodies() {
let app = with_edge_middleware(
axum::Router::new().route("/body", post(|_body: String| async { "ok" })),
EdgeConfig {
max_body_bytes: 8,
..EdgeConfig::default()
},
);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/body")
.body(Body::from("more than eight bytes"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
}
#[tokio::test]
async fn cancellation_drains_tasks_within_grace_period() {
let cancel = tokio_util::sync::CancellationToken::new();
let child = cancel.clone();
let task = tokio::spawn(async move { child.cancelled().await });
cancel.cancel();
assert!(join_tasks(vec![task], Duration::from_secs(1)).await);
}
#[tokio::test]
async fn task_panics_fail_shutdown_join() {
let task = tokio::spawn(async { panic!("task failed") });
assert!(!join_tasks(vec![task], Duration::from_secs(1)).await);
}
#[tokio::test]
async fn supervisor_failure_revokes_readiness_and_cancels_siblings() {
for panics in [false, true] {
let readiness = Readiness::default();
readiness.set(true);
let cancel = tokio_util::sync::CancellationToken::new();
let child = cancel.clone();
let sibling = tokio::spawn(async move { child.cancelled().await });
let failure = tokio::spawn(async move { assert!(!panics, "worker failed") });
assert!(
!af_web::supervise_tasks(
vec![sibling, failure],
readiness.clone(),
cancel.clone(),
Duration::from_secs(1)
)
.await
);
assert!(!readiness.is_ready());
assert!(cancel.is_cancelled());
}
}
#[tokio::test]
async fn requested_shutdown_drains_and_timeout_aborts_workers() {
let cancel = tokio_util::sync::CancellationToken::new();
cancel.cancel();
assert!(
af_web::supervise_tasks(vec![], Readiness::default(), cancel, Duration::from_secs(1)).await
);
let task = tokio::spawn(std::future::pending::<()>());
let abort = task.abort_handle();
assert!(!join_tasks(vec![task], Duration::ZERO).await);
tokio::task::yield_now().await;
assert!(abort.is_finished());
}