use actix_tower::prelude::*;
use actix_web::{test, web, App, HttpResponse};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, PartialEq, Debug)]
struct TestData {
name: String,
value: i32,
}
#[actix_web::test]
async fn test_auto_json_extractor() {
async fn handler(AutoJson(data): AutoJson<TestData>) -> impl Responder {
HttpResponse::Ok().json(data)
}
let app = test::init_service(App::new().route("/", web::post().to(handler))).await;
let payload = serde_json::json!({
"name": "test",
"value": 42
});
let req = test::TestRequest::post()
.set_json(&payload)
.uri("/")
.to_request();
let resp: TestData = test::call_and_read_body_json(&app, req).await;
assert_eq!(
resp,
TestData {
name: "test".into(),
value: 42
}
);
}
#[actix_web::test]
async fn test_auto_query_extractor() {
async fn handler(AutoQuery(data): AutoQuery<TestData>) -> impl Responder {
HttpResponse::Ok().json(data)
}
let app = test::init_service(App::new().route("/", web::get().to(handler))).await;
let req = test::TestRequest::get()
.uri("/?name=test&value=42")
.to_request();
let resp: TestData = test::call_and_read_body_json(&app, req).await;
assert_eq!(
resp,
TestData {
name: "test".into(),
value: 42
}
);
}
#[actix_web::test]
async fn test_request_id_middleware() {
let app = test::init_service(App::new().wrap(RequestId::new()).route(
"/",
web::get().to(|| async { HttpResponse::Ok().body("ok") }),
))
.await;
let req = test::TestRequest::get().uri("/").to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.headers().contains_key("x-request-id"));
}
#[actix_web::test]
async fn test_validated_json() {
use actix_tower::extract::validation::Validator;
#[derive(Deserialize)]
struct Input {
name: String,
}
impl Validator for Input {
fn validate(&self) -> Result<(), String> {
if self.name.is_empty() {
Err("name cannot be empty".into())
} else {
Ok(())
}
}
}
async fn handler(ValidatedJson(input): ValidatedJson<Input>) -> impl Responder {
HttpResponse::Ok().json(serde_json::json!({"name": input.name}))
}
let app = test::init_service(App::new().route("/", web::post().to(handler))).await;
let req = test::TestRequest::post()
.set_json(serde_json::json!({"name": "hello"}))
.uri("/")
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let req = test::TestRequest::post()
.set_json(serde_json::json!({"name": ""}))
.uri("/")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), 400);
}
#[actix_web::test]
async fn test_api_error_response() {
async fn handler() -> Result<HttpResponse, ApiError> {
Err(ApiError::not_found("User not found"))
}
let app = test::init_service(App::new().route("/", web::get().to(handler))).await;
let req = test::TestRequest::get().uri("/").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), 404);
}
#[actix_web::test]
async fn test_rate_limit() {
use std::time::Duration;
let app = test::init_service(
App::new()
.wrap(RateLimit::new(2, Duration::from_secs(60)))
.route(
"/",
web::get().to(|| async { HttpResponse::Ok().body("ok") }),
),
)
.await;
for _ in 0..2 {
let req = test::TestRequest::get().uri("/").to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
}
let req = test::TestRequest::get().uri("/").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), 429);
}
#[actix_web::test]
async fn test_typed_response() {
async fn handler() -> impl Responder {
TypedResponse::created(TestData {
name: "created".into(),
value: 100,
})
}
let app = test::init_service(App::new().route("/", web::post().to(handler))).await;
let req = test::TestRequest::post().uri("/").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), 201);
}
#[cfg(feature = "tower")]
#[actix_web::test]
async fn test_tower_poll_ready_call_same_instance() {
use std::cell::Cell;
use std::rc::Rc;
use std::task::{Context, Poll};
use tower::Service;
use tower_layer::Layer;
#[derive(Clone)]
struct ContractVerifier<S> {
__inner: S,
ready: Rc<Cell<bool>>,
}
impl<S> ContractVerifier<S> {
fn new(inner: S) -> Self {
Self {
__inner: inner,
ready: Rc::new(Cell::new(false)),
}
}
}
impl<S, B> Service<http::Request<B>> for ContractVerifier<S>
where
S: Service<http::Request<B>> + 'static,
B: http_body::Body<Data = actix_web::web::Bytes> + 'static,
B::Error: Into<actix_tower::BoxError> + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.ready.set(true);
self.__inner.poll_ready(cx)
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
assert!(
self.ready.get(),
"ISSUE-01: call() was invoked on a service instance that never had \
poll_ready() called on it — the Tower service contract is broken"
);
self.ready.set(false);
self.__inner.call(req)
}
}
#[derive(Clone)]
struct ContractVerifierLayer;
impl<S> Layer<S> for ContractVerifierLayer {
type Service = ContractVerifier<S>;
fn layer(&self, inner: S) -> Self::Service {
ContractVerifier::new(inner)
}
}
let app = test::init_service(
App::new()
.wrap(TowerLayerCompat::new(ContractVerifierLayer))
.route(
"/",
web::get().to(|| async { HttpResponse::Ok().body("ok") }),
),
)
.await;
let mut cx = std::task::Context::from_waker(futures_util::task::noop_waker_ref());
let _ = actix_service::Service::poll_ready(&app, &mut cx);
let req = test::TestRequest::get().uri("/").to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
}
#[cfg(feature = "tower")]
#[actix_web::test]
async fn test_tower_short_circuit_returns_error_not_panic() {
use std::task::{Context, Poll};
use tower::Service;
use tower_layer::Layer;
#[derive(Clone)]
struct AlwaysReject;
impl<B> Service<http::Request<B>> for AlwaysReject
where
B: http_body::Body<Data = actix_web::web::Bytes> + 'static,
B::Error: Into<actix_tower::BoxError> + 'static,
{
type Response = http::Response<http_body_util::Full<actix_web::web::Bytes>>;
type Error = actix_tower::BoxError;
type Future = std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>>>,
>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _req: http::Request<B>) -> Self::Future {
Box::pin(async move {
Ok(http::Response::builder()
.status(401)
.body(http_body_util::Full::new(actix_web::web::Bytes::from(
"Unauthorized",
)))
.unwrap())
})
}
}
#[derive(Clone)]
struct AlwaysRejectLayer;
impl<S> Layer<S> for AlwaysRejectLayer {
type Service = AlwaysReject;
fn layer(&self, _inner: S) -> Self::Service {
AlwaysReject
}
}
let app = test::init_service(
App::new()
.wrap(TowerLayerCompat::new(AlwaysRejectLayer))
.route(
"/",
web::get().to(|| async { HttpResponse::Ok().body("ok") }),
),
)
.await;
let req = test::TestRequest::get().uri("/").to_request();
let res = test::try_call_service(&app, req).await;
assert!(
res.is_ok(),
"short-circuit tower middleware should succeed and return the short-circuited response"
);
let ok_res = res.unwrap();
assert_eq!(ok_res.status(), 401);
}
#[cfg(feature = "tower")]
#[actix_web::test]
async fn test_tower_body_size_limit_returns_413() {
use actix_tower::compat::tower::TowerLayer;
let app = test::init_service(
App::new()
.wrap(
TowerLayer::new(tower_http::trace::TraceLayer::new_for_http())
.with_max_body_bytes(16), )
.route(
"/",
web::post().to(|| async { HttpResponse::Ok().body("ok") }),
),
)
.await;
let req = test::TestRequest::post()
.uri("/")
.set_payload("tiny")
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success(), "small body should pass");
let req = test::TestRequest::post()
.uri("/")
.set_payload("this body is definitely longer than 16 bytes")
.to_request();
let res = test::try_call_service(&app, req).await;
assert!(
res.is_err(),
"body exceeding max_body_bytes should fail with an error"
);
let err = res.err().unwrap();
assert_eq!(err.error_response().status(), 413);
}
#[actix_web::test]
async fn test_two_rate_limiters_independent() {
use std::time::Duration;
let app = test::init_service(
App::new()
.wrap(RateLimit::new(2, Duration::from_secs(60)))
.route(
"/api",
web::get().to(|| async { HttpResponse::Ok().body("api") }),
)
.route(
"/admin",
web::get().to(|| async { HttpResponse::Ok().body("admin") }),
),
)
.await;
for i in 0..2 {
let req = test::TestRequest::get().uri("/api").to_request();
let resp = test::call_service(&app, req).await;
assert!(
resp.status().is_success(),
"request {} to /api should succeed",
i
);
}
let req = test::TestRequest::get().uri("/api").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(
resp.status(),
429,
"/api should be rate-limited after 2 requests"
);
}
#[actix_web::test]
async fn test_cache_respects_vary_header() {
use std::time::Duration;
async fn content_negotiated(req: actix_web::HttpRequest) -> HttpResponse {
let accept = req
.headers()
.get("accept")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if accept.contains("text/html") {
HttpResponse::Ok()
.insert_header(("Content-Type", "text/html"))
.insert_header(("Vary", "accept"))
.body("<h1>hello</h1>")
} else {
HttpResponse::Ok()
.insert_header(("Content-Type", "application/json"))
.insert_header(("Vary", "accept"))
.body(r#"{"hello":"world"}"#)
}
}
let app = test::init_service(
App::new()
.wrap(Cache::new(Duration::from_secs(60)))
.route("/", web::get().to(content_negotiated)),
)
.await;
let req = test::TestRequest::get()
.uri("/")
.insert_header(("Accept", "application/json"))
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let body = test::read_body(resp).await;
assert_eq!(body, r#"{"hello":"world"}"#, "first request should be JSON");
let req = test::TestRequest::get()
.uri("/")
.insert_header(("Accept", "text/html"))
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let body = test::read_body(resp).await;
assert_eq!(
body, "<h1>hello</h1>",
"second request with different Accept should get HTML, not cached JSON"
);
}
#[actix_web::test]
async fn test_cache_fifo_eviction_order() {
use std::time::Duration;
async fn keyed_handler(path: web::Path<u32>) -> HttpResponse {
HttpResponse::Ok().body(format!("value-{}", path.into_inner()))
}
let app = test::init_service(
App::new()
.wrap(Cache::new(Duration::from_secs(60)))
.route("/{id}", web::get().to(keyed_handler)),
)
.await;
for i in 0..5u32 {
let req = test::TestRequest::get().uri(&format!("/{i}")).to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
}
for i in 0..5u32 {
let req = test::TestRequest::get().uri(&format!("/{i}")).to_request();
let resp = test::call_service(&app, req).await;
let body = test::read_body(resp).await;
assert_eq!(body, format!("value-{i}").as_bytes());
}
}
#[cfg(feature = "tower")]
#[actix_web::test]
async fn test_tower_compatibility() {
let app = test::init_service(
App::new()
.wrap(tower_layer!(
tower_http::compression::CompressionLayer::new()
))
.route(
"/",
web::get().to(|| async { HttpResponse::Ok().body("hello compression") }),
),
)
.await;
let req = test::TestRequest::get()
.insert_header(("accept-encoding", "gzip"))
.uri("/")
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
assert_eq!(
resp.headers()
.get("content-encoding")
.unwrap()
.to_str()
.unwrap(),
"gzip"
);
}
#[cfg(feature = "tower")]
#[actix_web::test]
async fn test_tower_compat_alias() {
let app = test::init_service(
App::new()
.wrap(TowerLayerCompat::new(
tower_http::compression::CompressionLayer::new(),
))
.route(
"/",
web::get().to(|| async { HttpResponse::Ok().body("hello compression") }),
),
)
.await;
let req = test::TestRequest::get()
.insert_header(("accept-encoding", "gzip"))
.uri("/")
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
assert_eq!(
resp.headers()
.get("content-encoding")
.unwrap()
.to_str()
.unwrap(),
"gzip"
);
}