refactor: Upload extractor, 429 refusal test, stricter contract test
This commit is contained in:
@@ -64,6 +64,17 @@ fn slow_down(reason: RejectionReason) -> axum::response::Response {
|
||||
}
|
||||
}
|
||||
|
||||
/// Every `/api/v1` limiter is built here so its refusals use the one error shape.
|
||||
fn api_governor(quota: Quota) -> axum_governor::GovernorConfig<std::net::IpAddr> {
|
||||
GovernorConfigBuilder::default()
|
||||
.with_extractor(PeerIp::default())
|
||||
.expect_connect_info()
|
||||
.quota_default(quota)
|
||||
.error_handler(slow_down)
|
||||
.finish()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn per_minute(n: u64) -> Quota {
|
||||
let n = NonZeroU32::new(n.clamp(1, u32::MAX as u64) as u32).unwrap();
|
||||
Quota::requests_per_minute(n)
|
||||
@@ -130,23 +141,11 @@ fn cors_layer(origins: &str) -> CorsLayer {
|
||||
}
|
||||
|
||||
fn api_routes(rate_limit: u64, cors_origins: &str) -> Router<AppState> {
|
||||
let cfg = GovernorConfigBuilder::default()
|
||||
.with_extractor(PeerIp::default())
|
||||
.expect_connect_info()
|
||||
.quota_default(per_minute(rate_limit))
|
||||
.error_handler(slow_down)
|
||||
.finish()
|
||||
.unwrap();
|
||||
let cfg = api_governor(per_minute(rate_limit));
|
||||
|
||||
let base = api::router().merge(federation::api_routes());
|
||||
|
||||
let webhook_cfg = GovernorConfigBuilder::default()
|
||||
.with_extractor(PeerIp::default())
|
||||
.expect_connect_info()
|
||||
.quota_default(per_minute(rate_limit / 4))
|
||||
.error_handler(slow_down)
|
||||
.finish()
|
||||
.unwrap();
|
||||
let webhook_cfg = api_governor(per_minute(rate_limit / 4));
|
||||
let webhook_routes = api::webhook_router().layer(GovernorLayer::new(webhook_cfg));
|
||||
|
||||
Router::new()
|
||||
@@ -154,3 +153,46 @@ fn api_routes(rate_limit: u64, cors_origins: &str) -> Router<AppState> {
|
||||
.nest("/api/v1", webhook_routes)
|
||||
.layer(cors_layer(cors_origins))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::{body::Body, http::Request};
|
||||
use tower::ServiceExt;
|
||||
|
||||
async fn body_json(resp: axum::response::Response) -> serde_json::Value {
|
||||
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
serde_json::from_slice(&bytes).unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_rate_limit_refusal_is_the_one_error_shape() {
|
||||
let app = Router::new()
|
||||
.route("/x", routing::get(|| async { "ok" }))
|
||||
.layer(GovernorLayer::new(api_governor(per_minute(1))));
|
||||
let addr: std::net::SocketAddr = "127.0.0.1:1".parse().unwrap();
|
||||
let mut last = None;
|
||||
for _ in 0..3 {
|
||||
let mut req = Request::builder().uri("/x").body(Body::empty()).unwrap();
|
||||
req.extensions_mut()
|
||||
.insert(axum::extract::ConnectInfo::<std::net::SocketAddr>(addr));
|
||||
last = Some(app.clone().oneshot(req).await.unwrap());
|
||||
}
|
||||
let resp = last.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
|
||||
let body = body_json(resp).await;
|
||||
assert_eq!(body["code"], "RATE_LIMITED");
|
||||
assert!(body["error"].is_string());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unidentifiable_caller_is_refused_in_the_one_error_shape() {
|
||||
let resp = slow_down(RejectionReason::KeyExtractionFailed(
|
||||
axum_governor::ExtractionError::MissingConnectInfo,
|
||||
));
|
||||
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(body_json(resp).await["code"], "INTERNAL");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user