use crate::proxy::service::ProxyService;
use crate::proxy::types::*;
use crate::proxy::AuthConfig;
use axum::body::Body;
use axum::extract::Request as ExtractRequest;
use axum::http::{Request, StatusCode};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpListener;
use tower::ServiceExt;
async fn run_mock_backend(port: u16) -> Result<(), Box<dyn std::error::Error>> {
let app = axum::Router::new()
.route("/", axum::routing::get(|| async { "Hello from backend" }))
.route(
"/echo",
axum::routing::post(|body: String| async move { body }),
)
.route(
"/status/{code}",
axum::routing::get(
|axum::extract::Path(code): axum::extract::Path<u16>| async move {
let status = StatusCode::from_u16(code).unwrap_or(StatusCode::OK);
(status, "Status response")
},
),
)
.route(
"/slow",
axum::routing::get(|| async {
tokio::time::sleep(Duration::from_millis(TIMEOUT_SHORT_MS)).await;
"Slow response"
}),
)
.route(
"/large",
axum::routing::get(|| async {
"x".repeat(BYTES_1MB) }),
)
.fallback(|request: ExtractRequest| async move {
(
StatusCode::NOT_FOUND,
format!("Not found: {} {}", request.method(), request.uri()),
)
});
let addr = SocketAddr::from(([127, 0, 0, 1], port));
let listener = TcpListener::bind(addr).await?;
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
tokio::time::sleep(Duration::from_millis(TIMEOUT_SHORT_MS)).await;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_basic_proxy_flow() {
run_mock_backend(TEST_PORT_BASE + 1)
.await
.expect("Failed to start mock backend");
let config = ProxyConfig {
max_request_size: RequestSizeLimit::try_new(BYTES_10MB).unwrap(),
max_response_size: ResponseSizeLimit::try_new(BYTES_10MB).unwrap(),
request_timeout: Duration::from_secs(5),
ring_buffer: RingBufferConfig::default(),
bedrock_region: None,
};
let mut auth_config = AuthConfig::default();
auth_config
.api_keys
.insert(ApiKey::try_new("test-key".to_string()).unwrap());
let service = ProxyService::new(config);
let ring_buffer = service.ring_buffer();
let app = service.into_router(auth_config);
let request = Request::builder()
.method("GET")
.uri("http://localhost:8080/")
.header("Authorization", "Bearer test-key")
.header(
crate::proxy::headers::X_TARGET_URL,
format!("http://localhost:{}/", TEST_PORT_BASE + 1),
)
.body(Body::empty())
.unwrap();
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(&body_bytes[..], b"Hello from backend");
tokio::time::sleep(Duration::from_millis(10)).await; let stats = ring_buffer.stats();
assert!(stats.total_writes > 0, "Should have recorded audit events");
let request_body = "Test request body";
let request = Request::builder()
.method("POST")
.uri("http://localhost:8080/echo")
.header("Authorization", "Bearer test-key")
.header(
crate::proxy::headers::X_TARGET_URL,
"http://localhost:8081/echo",
)
.header("Content-Type", "text/plain")
.body(Body::from(request_body))
.unwrap();
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(&body_bytes[..], request_body.as_bytes());
let request = Request::builder()
.method("GET")
.uri("http://localhost:8080/")
.header(
crate::proxy::headers::X_TARGET_URL,
"http://localhost:8081/",
)
.body(Body::empty())
.unwrap();
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
let request = Request::builder()
.method("GET")
.uri("http://localhost:8080/health")
.body(Body::empty())
.unwrap();
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_error_handling() {
run_mock_backend(8082)
.await
.expect("Failed to start mock backend");
let config = ProxyConfig::default();
let mut auth_config = AuthConfig::default();
auth_config
.api_keys
.insert(ApiKey::try_new("test-key".to_string()).unwrap());
let service = ProxyService::new(config);
let app = service.into_router(auth_config);
let request = Request::builder()
.method("GET")
.uri("http://localhost:8080/")
.header("Authorization", "Bearer test-key")
.header(crate::proxy::headers::X_TARGET_URL, "not-a-url")
.body(Body::empty())
.unwrap();
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let request = Request::builder()
.method("GET")
.uri("http://localhost:8080/status/500")
.header("Authorization", "Bearer test-key")
.header(
crate::proxy::headers::X_TARGET_URL,
"http://localhost:8082/status/500",
)
.body(Body::empty())
.unwrap();
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
let request = Request::builder()
.method("GET")
.uri("http://localhost:8080/")
.header("Authorization", "Bearer test-key")
.header(
crate::proxy::headers::X_TARGET_URL,
"http://localhost:9999/",
) .body(Body::empty())
.unwrap();
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
}
#[tokio::test]
async fn test_request_size_limits() {
run_mock_backend(8083)
.await
.expect("Failed to start mock backend");
let config = ProxyConfig {
max_request_size: RequestSizeLimit::try_new(1024).unwrap(), max_response_size: ResponseSizeLimit::try_new(10 * 1024 * 1024).unwrap(),
request_timeout: Duration::from_secs(5),
ring_buffer: RingBufferConfig::default(),
bedrock_region: None,
};
let mut auth_config = AuthConfig::default();
auth_config
.api_keys
.insert(ApiKey::try_new("test-key".to_string()).unwrap());
let service = ProxyService::new(config);
let app = service.into_router(auth_config);
let large_body = "x".repeat(2048); let request = Request::builder()
.method("POST")
.uri("http://localhost:8080/echo")
.header("Authorization", "Bearer test-key")
.header(
crate::proxy::headers::X_TARGET_URL,
"http://localhost:8083/echo",
)
.header("Content-Type", "text/plain")
.body(Body::from(large_body))
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
}
#[tokio::test]
async fn test_response_size_limits() {
run_mock_backend(8084)
.await
.expect("Failed to start mock backend");
let config = ProxyConfig {
max_request_size: RequestSizeLimit::try_new(10 * 1024 * 1024).unwrap(),
max_response_size: ResponseSizeLimit::try_new(1024).unwrap(), request_timeout: Duration::from_secs(5),
ring_buffer: RingBufferConfig::default(),
bedrock_region: None,
};
let mut auth_config = AuthConfig::default();
auth_config
.api_keys
.insert(ApiKey::try_new("test-key".to_string()).unwrap());
let service = ProxyService::new(config);
let app = service.into_router(auth_config);
let request = Request::builder()
.method("GET")
.uri("http://localhost:8080/large")
.header("Authorization", "Bearer test-key")
.header(
crate::proxy::headers::X_TARGET_URL,
"http://localhost:8084/large",
)
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_timeout_handling() {
run_mock_backend(8085)
.await
.expect("Failed to start mock backend");
let config = ProxyConfig {
max_request_size: RequestSizeLimit::try_new(10 * 1024 * 1024).unwrap(),
max_response_size: ResponseSizeLimit::try_new(10 * 1024 * 1024).unwrap(),
request_timeout: Duration::from_millis(50), ring_buffer: RingBufferConfig::default(),
bedrock_region: None,
};
let mut auth_config = AuthConfig::default();
auth_config
.api_keys
.insert(ApiKey::try_new("test-key".to_string()).unwrap());
let service = ProxyService::new(config);
let app = service.into_router(auth_config);
let request = Request::builder()
.method("GET")
.uri("http://localhost:8080/slow")
.header("Authorization", "Bearer test-key")
.header(
crate::proxy::headers::X_TARGET_URL,
"http://localhost:8085/slow",
)
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::REQUEST_TIMEOUT);
}
#[tokio::test]
async fn test_concurrent_requests() {
run_mock_backend(8086)
.await
.expect("Failed to start mock backend");
let config = ProxyConfig::default();
let mut auth_config = AuthConfig::default();
auth_config
.api_keys
.insert(ApiKey::try_new("test-key".to_string()).unwrap());
let service = ProxyService::new(config);
let app = Arc::new(service.into_router(auth_config));
let mut handles = vec![];
for i in 0..10 {
let app = app.clone();
let handle = tokio::spawn(async move {
let request = Request::builder()
.method("POST")
.uri("http://localhost:8080/echo")
.header("Authorization", "Bearer test-key")
.header(
crate::proxy::headers::X_TARGET_URL,
"http://localhost:8086/echo",
)
.header("Content-Type", "text/plain")
.body(Body::from(format!("Request {i}")))
.unwrap();
let response = (*app).clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
String::from_utf8(body_bytes.to_vec()).unwrap()
});
handles.push(handle);
}
let results: Vec<String> = futures_util::future::join_all(handles)
.await
.into_iter()
.map(|r| r.unwrap())
.collect();
assert_eq!(results.len(), 10);
for result in &results {
assert!(result.contains("Request"));
}
}
#[tokio::test]
async fn test_audit_event_recording() {
run_mock_backend(8087)
.await
.expect("Failed to start mock backend");
let config = ProxyConfig::default();
let mut auth_config = AuthConfig::default();
auth_config
.api_keys
.insert(ApiKey::try_new("test-key".to_string()).unwrap());
let service = ProxyService::new(config);
let ring_buffer = service.ring_buffer();
let app = service.into_router(auth_config);
let request = Request::builder()
.method("POST")
.uri("http://localhost:8080/echo")
.header("Authorization", "Bearer test-key")
.header(
crate::proxy::headers::X_TARGET_URL,
"http://localhost:8087/echo",
)
.header("Content-Type", "text/plain")
.body(Body::from("Test audit"))
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
tokio::time::sleep(Duration::from_millis(10)).await;
let stats = ring_buffer.stats();
assert!(
stats.total_writes >= 2,
"Should have written at least 2 audit events (request + response), found: {}",
stats.total_writes
);
}
#[tokio::test]
async fn test_invalid_http_methods() {
run_mock_backend(8088)
.await
.expect("Failed to start mock backend");
let config = ProxyConfig::default();
let mut auth_config = AuthConfig::default();
auth_config
.api_keys
.insert(ApiKey::try_new("test-key".to_string()).unwrap());
let service = ProxyService::new(config);
let ring_buffer = service.ring_buffer();
let app = service.into_router(auth_config);
let methods = [
"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "CONNECT", "TRACE",
];
for method in &methods {
let request = Request::builder()
.method(*method)
.uri("http://localhost:8080/")
.header("Authorization", "Bearer test-key")
.header(
crate::proxy::headers::X_TARGET_URL,
"http://localhost:8088/",
)
.body(Body::empty())
.unwrap();
let response = app.clone().oneshot(request).await.unwrap();
assert!(
response.status().is_success()
|| response.status().is_client_error()
|| response.status().is_server_error()
);
}
tokio::time::sleep(Duration::from_millis(50)).await;
let mut error_events = 0;
while let Some((_, data)) = ring_buffer.read() {
if let Ok(event) = serde_json::from_slice::<AuditEvent>(&data) {
if matches!(
event.event_type,
AuditEventType::Error {
phase: ErrorPhase::RequestParsing,
..
}
) {
error_events += 1;
}
}
}
assert_eq!(
error_events, 0,
"Should not have any parsing errors for standard HTTP methods"
);
}
}