use axum::http::StatusCode;
use dist_agent_lang::http_server_converters::{
error_response, http_response_to_axum_response, json_response,
};
use dist_agent_lang::http_server_handlers::get_route_handler_name;
use dist_agent_lang::http_server_integration::{
create_router_with_middleware, create_router_with_runtime_factory,
};
use dist_agent_lang::runtime::engine::Runtime;
use dist_agent_lang::runtime::functions::Function;
use dist_agent_lang::stdlib::web;
use std::collections::HashMap;
use std::time::Duration;
#[test]
fn test_http_response_to_axum_response_valid() {
use dist_agent_lang::stdlib::web::HttpResponse;
let http_response = HttpResponse {
status: 200,
headers: HashMap::new(),
body: "test".to_string(),
cookies: vec![],
redirect_url: None,
};
let result = http_response_to_axum_response(http_response);
assert_eq!(
result.status(),
StatusCode::OK,
"Should have status 200, not default"
);
}
#[test]
fn test_http_response_to_axum_response_exact_output() {
use dist_agent_lang::stdlib::web::HttpResponse;
let mut headers = HashMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
let http_response = HttpResponse {
status: 201,
headers: headers.clone(),
body: "{\"id\": 123}".to_string(),
cookies: vec![],
redirect_url: None,
};
let result = http_response_to_axum_response(http_response);
assert_eq!(
result.status(),
StatusCode::CREATED,
"Should have exact status 201"
);
assert!(
result.headers().get("content-type").is_some(),
"Should have Content-Type header"
);
}
#[tokio::test]
async fn test_axum_request_to_http_request_percent_decode() {
use axum::http::{Method, Uri};
use dist_agent_lang::http_server_converters::axum_request_to_http_request;
let uri = Uri::from_static("http://example.com/test?key=hello%20world&value=test%2Bdata");
let request = axum::http::Request::builder()
.method(Method::GET)
.uri(uri)
.body(axum::body::Body::empty())
.unwrap();
let http_request = axum_request_to_http_request(request).await;
let value = http_request.query_params.get("key");
assert!(value.is_some(), "Should have decoded query parameter");
if let Some(decoded_value) = value {
assert!(
decoded_value.contains("hello") && decoded_value.contains("world"),
"Should decode %20 to space: got {:?}",
decoded_value
);
}
}
#[tokio::test]
async fn test_axum_request_to_http_request_extract_body() {
use axum::http::{Method, Uri};
use dist_agent_lang::http_server_converters::axum_request_to_http_request;
let uri = Uri::from_static("http://example.com/test");
let request = axum::http::Request::builder()
.method(Method::POST)
.uri(uri)
.body(axum::body::Body::from("test body content"))
.unwrap();
let _http_request = axum_request_to_http_request(request).await;
}
#[test]
fn test_error_response_creates_error() {
let result = error_response(400, "test error");
assert_eq!(
result.status(),
StatusCode::BAD_REQUEST,
"Should have status 400, not default"
);
}
#[test]
fn test_json_response_creates_json() {
use serde_json::json;
let data = json!({"test": "value"});
let result = json_response(data);
assert_eq!(
result.status(),
StatusCode::OK,
"Should have status 200, not default"
);
assert!(
result.headers().get("content-type").is_some(),
"Should have Content-Type header"
);
}
#[test]
fn test_get_route_handler_name_valid() {
use dist_agent_lang::stdlib::web::HttpServer;
use std::collections::HashMap;
use dist_agent_lang::stdlib::web::ServerConfig;
let server = HttpServer {
port: 8080,
routes: HashMap::new(),
middleware: vec![],
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let route = "/api/test";
let method = "GET";
let result = get_route_handler_name(&server, method, route);
if let Some(name) = result {
assert!(!name.is_empty(), "Handler name should not be empty");
assert_ne!(name, "xyzzy", "Handler name should not be 'xyzzy'");
}
}
#[test]
fn test_get_route_handler_name_root() {
use dist_agent_lang::stdlib::web::HttpServer;
use std::collections::HashMap;
use dist_agent_lang::stdlib::web::ServerConfig;
let server = HttpServer {
port: 8080,
routes: HashMap::new(),
middleware: vec![],
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let route = "/";
let method = "GET";
let result = get_route_handler_name(&server, method, route);
if let Some(name) = result {
assert!(!name.is_empty(), "Handler name should not be empty");
assert_ne!(name, "xyzzy", "Handler name should not be 'xyzzy'");
}
}
#[test]
fn test_create_router_with_middleware_creates_router() {
use dist_agent_lang::stdlib::web::HttpServer;
use std::collections::HashMap;
use dist_agent_lang::stdlib::web::ServerConfig;
let server = HttpServer {
port: 8080,
routes: HashMap::new(),
middleware: vec![],
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let _router = create_router_with_middleware(server);
}
#[test]
fn test_create_router_with_middleware_get_method() {
use dist_agent_lang::stdlib::web::HttpServer;
use std::collections::HashMap;
use dist_agent_lang::stdlib::web::ServerConfig;
let server = HttpServer {
port: 8080,
routes: HashMap::new(),
middleware: vec![],
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let _router = create_router_with_middleware(server);
}
#[test]
fn test_create_router_with_middleware_post_method() {
use dist_agent_lang::stdlib::web::HttpServer;
use std::collections::HashMap;
use dist_agent_lang::stdlib::web::ServerConfig;
let server = HttpServer {
port: 8080,
routes: HashMap::new(),
middleware: vec![],
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let _router = create_router_with_middleware(server);
}
#[test]
fn test_create_router_with_middleware_put_method() {
use dist_agent_lang::stdlib::web::HttpServer;
use std::collections::HashMap;
use dist_agent_lang::stdlib::web::ServerConfig;
let server = HttpServer {
port: 8080,
routes: HashMap::new(),
middleware: vec![],
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let _router = create_router_with_middleware(server);
}
#[test]
fn test_create_router_with_middleware_delete_method() {
use dist_agent_lang::stdlib::web::HttpServer;
use std::collections::HashMap;
use dist_agent_lang::stdlib::web::ServerConfig;
let server = HttpServer {
port: 8080,
routes: HashMap::new(),
middleware: vec![],
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let _router = create_router_with_middleware(server);
}
#[tokio::test]
async fn test_default_routes_return_non_empty_body() {
use axum::body::Body;
use axum::http::Request;
use dist_agent_lang::stdlib::web::HttpServer;
use dist_agent_lang::stdlib::web::ServerConfig;
use std::collections::HashMap;
use tower::ServiceExt;
let server = HttpServer {
port: 8080,
routes: HashMap::new(),
middleware: vec![],
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let app = create_router_with_middleware(server);
let get_home = Request::builder().uri("/").body(Body::empty()).unwrap();
let res = app.clone().oneshot(get_home).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
.await
.unwrap();
let body_str = String::from_utf8_lossy(&body);
assert!(
body_str.contains("Welcome") && body_str.contains("dist_agent_lang"),
"home handler must return welcome message, got: {}",
body_str
);
let get_health = Request::builder()
.uri("/health")
.body(Body::empty())
.unwrap();
let res = app.oneshot(get_health).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
.await
.unwrap();
let body_str = String::from_utf8_lossy(&body);
assert!(
body_str.contains("OK"),
"health handler must return OK, got: {}",
body_str
);
}
#[tokio::test]
async fn test_auth_middleware_rejects_no_token() {
use axum::{body::Body, middleware, routing::get, Router};
use dist_agent_lang::http_server_security_middleware::auth_middleware;
use tower::ServiceExt;
let app = Router::new()
.route("/protected", get(|| async { "secret" }))
.layer(middleware::from_fn(auth_middleware));
let request = axum::http::Request::builder()
.uri("/protected")
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(
response.status(),
StatusCode::UNAUTHORIZED,
"auth_middleware must reject requests without Authorization header (got {})",
response.status()
);
}
#[tokio::test]
async fn test_auth_middleware_rejects_invalid_token() {
use axum::{body::Body, middleware, routing::get, Router};
use dist_agent_lang::http_server_security_middleware::auth_middleware;
use tower::ServiceExt;
let app = Router::new()
.route("/protected", get(|| async { "secret" }))
.layer(middleware::from_fn(auth_middleware));
let request = axum::http::Request::builder()
.uri("/protected")
.header("Authorization", "Bearer invalid.jwt.token")
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(
response.status(),
StatusCode::UNAUTHORIZED,
"auth_middleware must reject invalid JWT tokens (got {})",
response.status()
);
}
#[tokio::test]
async fn test_input_validation_middleware_rejects_xss() {
use axum::{body::Body, middleware, routing::get, Router};
use dist_agent_lang::http_server_security_middleware::input_validation_middleware;
use tower::ServiceExt;
let app = Router::new()
.route("/api/search", get(|| async { "results" }))
.layer(middleware::from_fn(input_validation_middleware));
let request = axum::http::Request::builder()
.uri("/api/search?q=javascript:alert(1)")
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(
response.status(),
StatusCode::BAD_REQUEST,
"input_validation_middleware must reject XSS in query params (got {})",
response.status()
);
}
#[tokio::test]
async fn test_input_validation_middleware_rejects_sql_injection() {
use axum::{body::Body, middleware, routing::get, Router};
use dist_agent_lang::http_server_security_middleware::input_validation_middleware;
use tower::ServiceExt;
let app = Router::new()
.route("/api/users", get(|| async { "users" }))
.layer(middleware::from_fn(input_validation_middleware));
let request = axum::http::Request::builder()
.uri("/api/users?id=1--drop+table")
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(
response.status(),
StatusCode::BAD_REQUEST,
"input_validation_middleware must reject SQL injection (got {})",
response.status()
);
}
#[tokio::test]
async fn test_input_validation_middleware_allows_clean_request() {
use axum::{body::Body, middleware, routing::get, Router};
use dist_agent_lang::http_server_security_middleware::input_validation_middleware;
use tower::ServiceExt;
let app = Router::new()
.route("/api/data", get(|| async { "validation_passed" }))
.layer(middleware::from_fn(input_validation_middleware));
let request = axum::http::Request::builder()
.uri("/api/data")
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(
body.as_ref(),
b"validation_passed",
"input_validation_middleware must forward to handler, not return default empty response"
);
}
#[tokio::test]
async fn test_request_size_middleware_allows_small_request() {
use axum::{body::Body, middleware, routing::get, Router};
use dist_agent_lang::http_server_security_middleware::request_size_middleware;
use tower::ServiceExt;
let app = Router::new()
.route("/api/data", get(|| async { "middleware_passed" }))
.layer(middleware::from_fn(request_size_middleware));
let request = axum::http::Request::builder()
.uri("/api/data")
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(
body.as_ref(),
b"middleware_passed",
"request_size_middleware must forward to handler, not return default empty response"
);
}
#[tokio::test]
async fn test_request_size_middleware_rejects_large_body() {
use axum::{body::Body, middleware, routing::post, Router};
use dist_agent_lang::http_server_security_middleware::request_size_middleware;
use tower::ServiceExt;
let app = Router::new()
.route("/api/upload", post(|| async { "uploaded" }))
.layer(middleware::from_fn(request_size_middleware));
let request = axum::http::Request::builder()
.method("POST")
.uri("/api/upload")
.header("Content-Length", "999999999") .body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(
response.status(),
StatusCode::PAYLOAD_TOO_LARGE,
"request_size_middleware must reject oversized body (got {})",
response.status()
);
}
#[tokio::test]
async fn test_rate_limit_middleware_allows_first_request() {
use axum::{body::Body, middleware, routing::get, Router};
use dist_agent_lang::http_server_security_middleware::rate_limit_middleware;
use tower::ServiceExt;
let app = Router::new()
.route("/api/data", get(|| async { "rate_limit_passed" }))
.layer(middleware::from_fn(rate_limit_middleware));
let request = axum::http::Request::builder()
.uri("/api/data")
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(
body.as_ref(),
b"rate_limit_passed",
"rate_limit_middleware must forward to handler, not return default empty response"
);
}
#[tokio::test]
async fn test_input_validation_middleware_rejects_bad_header() {
use axum::{body::Body, middleware, routing::get, Router};
use dist_agent_lang::http_server_security_middleware::input_validation_middleware;
use tower::ServiceExt;
let app = Router::new()
.route("/api/data", get(|| async { "ok" }))
.layer(middleware::from_fn(input_validation_middleware));
let request = axum::http::Request::builder()
.uri("/api/data")
.header("X-Custom", "<script>alert(1)</script>")
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(
response.status(),
StatusCode::BAD_REQUEST,
"input_validation_middleware must reject XSS in headers (got {})",
response.status()
);
}
#[test]
fn test_create_router_with_middleware_delete_method_with_route() {
use dist_agent_lang::stdlib::web::{HttpMethod, HttpServer, Route, ServerConfig};
use std::collections::HashMap;
let mut routes = HashMap::new();
routes.insert(
"DELETE:/api/delete".to_string(),
Route {
method: HttpMethod::DELETE,
path: "/api/delete".to_string(),
handler: "delete_handler".to_string(),
middleware: vec![],
},
);
let server = HttpServer {
port: 8080,
routes,
middleware: vec![],
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let _router = create_router_with_middleware(server);
}
#[test]
fn test_create_router_with_middleware_all_methods_coverage() {
use dist_agent_lang::stdlib::web::{HttpMethod, HttpServer, Route, ServerConfig};
use std::collections::HashMap;
let mut routes = HashMap::new();
routes.insert(
"GET:/api/get".to_string(),
Route {
method: HttpMethod::GET,
path: "/api/get".to_string(),
handler: "get_handler".to_string(),
middleware: vec![],
},
);
routes.insert(
"POST:/api/post".to_string(),
Route {
method: HttpMethod::POST,
path: "/api/post".to_string(),
handler: "post_handler".to_string(),
middleware: vec![],
},
);
routes.insert(
"PUT:/api/put".to_string(),
Route {
method: HttpMethod::PUT,
path: "/api/put".to_string(),
handler: "put_handler".to_string(),
middleware: vec![],
},
);
routes.insert(
"DELETE:/api/delete".to_string(),
Route {
method: HttpMethod::DELETE,
path: "/api/delete".to_string(),
handler: "delete_handler".to_string(),
middleware: vec![],
},
);
let server = HttpServer {
port: 8080,
routes,
middleware: vec![],
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let _router = create_router_with_middleware(server);
}
#[test]
fn test_middleware_value_conversion_map_request() {
use dist_agent_lang::stdlib::web::{HttpServer, Middleware, ServerConfig};
use std::collections::HashMap;
let server = HttpServer {
port: 8080,
routes: HashMap::new(),
middleware: vec![Middleware {
name: "test_middleware".to_string(),
handler: "test_handler".to_string(),
priority: 1,
}],
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let _router = create_router_with_middleware(server);
}
#[test]
fn test_middleware_value_conversion_map_response() {
use dist_agent_lang::stdlib::web::{HttpServer, ServerConfig};
use std::collections::HashMap;
let server = HttpServer {
port: 8080,
routes: HashMap::new(),
middleware: vec![],
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let _router = create_router_with_middleware(server);
}
#[test]
fn test_middleware_value_conversion_string_response() {
use dist_agent_lang::stdlib::web::{HttpServer, ServerConfig};
use std::collections::HashMap;
let server = HttpServer {
port: 8080,
routes: HashMap::new(),
middleware: vec![],
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let _router = create_router_with_middleware(server);
}
#[tokio::test]
async fn test_home_handler_response_content() {
use dist_agent_lang::stdlib::web::{HttpServer, ServerConfig};
use std::collections::HashMap;
let server = HttpServer {
port: 8080,
routes: HashMap::new(), middleware: vec![],
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let _router = create_router_with_middleware(server);
}
#[tokio::test]
async fn test_health_handler_response_content() {
use dist_agent_lang::stdlib::web::{HttpServer, ServerConfig};
use std::collections::HashMap;
let server = HttpServer {
port: 8080,
routes: HashMap::new(), middleware: vec![],
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let _router = create_router_with_middleware(server);
}
#[test]
fn test_handle_with_middleware_response_verification() {
use dist_agent_lang::stdlib::web::{HttpMethod, HttpServer, Route, ServerConfig};
use std::collections::HashMap;
let mut routes = HashMap::new();
routes.insert(
"GET:/test".to_string(),
Route {
method: HttpMethod::GET,
path: "/test".to_string(),
handler: "test_handler".to_string(),
middleware: vec![],
},
);
let server = HttpServer {
port: 8080,
routes,
middleware: vec![],
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let _router = create_router_with_middleware(server);
}
#[test]
fn test_middleware_chain_execution() {
use dist_agent_lang::stdlib::web::{HttpMethod, HttpServer, Middleware, Route, ServerConfig};
use std::collections::HashMap;
let mut routes = HashMap::new();
routes.insert(
"GET:/test".to_string(),
Route {
method: HttpMethod::GET,
path: "/test".to_string(),
handler: "test_handler".to_string(),
middleware: vec![],
},
);
let middleware = vec![Middleware {
name: "rate_limit".to_string(),
handler: "rate_limit_handler".to_string(),
priority: 1,
}];
let server = HttpServer {
port: 8080,
routes,
middleware,
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let _router = create_router_with_middleware(server);
}
#[test]
fn test_middleware_chain_multiple_middleware() {
use dist_agent_lang::stdlib::web::{HttpMethod, HttpServer, Middleware, Route, ServerConfig};
use std::collections::HashMap;
let mut routes = HashMap::new();
routes.insert(
"POST:/api/data".to_string(),
Route {
method: HttpMethod::POST,
path: "/api/data".to_string(),
handler: "data_handler".to_string(),
middleware: vec![],
},
);
let middleware = vec![
Middleware {
name: "rate_limit".to_string(),
handler: "rate_limit_handler".to_string(),
priority: 1,
},
Middleware {
name: "request_size".to_string(),
handler: "request_size_handler".to_string(),
priority: 2,
},
Middleware {
name: "auth".to_string(),
handler: "auth_handler".to_string(),
priority: 3,
},
];
let server = HttpServer {
port: 8080,
routes,
middleware,
static_files: HashMap::new(),
config: ServerConfig {
max_connections: 100,
timeout_seconds: 30,
cors_enabled: false,
ssl_enabled: false,
static_path: "".to_string(),
},
};
let _router = create_router_with_middleware(server);
}
#[tokio::test]
async fn test_dal_routes_e2e_registered_route_returns_handler_response() {
let mut server = web::create_server(0);
web::add_route(
&mut server,
"GET".to_string(),
"/api/test".to_string(),
"test_handler".to_string(),
);
web::add_route(
&mut server,
"POST".to_string(),
"/api/echo".to_string(),
"echo_handler".to_string(),
);
let runtime_factory = || {
let mut rt = Runtime::new();
let stub = Function::new(
"test_handler".to_string(),
vec!["request".to_string()],
|_args, _scope| {
let mut map = HashMap::new();
map.insert("status".to_string(), dist_agent_lang::Value::Int(200));
map.insert(
"body".to_string(),
dist_agent_lang::Value::String("ok".to_string()),
);
Ok(dist_agent_lang::Value::Map(map))
},
);
rt.register_function(stub);
let echo = Function::new(
"echo_handler".to_string(),
vec!["request".to_string()],
|args, _scope| {
let mut map = HashMap::new();
map.insert("status".to_string(), dist_agent_lang::Value::Int(201));
let body = args.first().map(|v| v.to_string()).unwrap_or_default();
map.insert("body".to_string(), dist_agent_lang::Value::String(body));
Ok(dist_agent_lang::Value::Map(map))
},
);
rt.register_function(echo);
rt
};
let app = create_router_with_runtime_factory(server, runtime_factory);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
tokio::time::sleep(Duration::from_millis(50)).await;
let client = reqwest::Client::new();
let response = client
.get(format!("http://127.0.0.1:{}/api/test", port))
.send()
.await
.unwrap();
assert_eq!(
response.status().as_u16(),
200,
"Registered GET /api/test should return 200"
);
let body_str = response.text().await.unwrap();
assert!(
body_str.contains("ok"),
"Handler body should contain 'ok', got: {}",
body_str
);
let post_response = client
.post(format!("http://127.0.0.1:{}/api/echo", port))
.header("content-type", "application/json")
.body(r#"{"test":"data"}"#)
.send()
.await
.unwrap();
assert_eq!(
post_response.status().as_u16(),
201,
"Registered POST /api/echo should return 201"
);
}
#[tokio::test]
async fn test_dal_routes_e2e_unregistered_route_returns_404() {
let mut server = web::create_server(0);
web::add_route(
&mut server,
"GET".to_string(),
"/api/test".to_string(),
"test_handler".to_string(),
);
let runtime_factory = || {
let mut rt = Runtime::new();
let stub = Function::new(
"test_handler".to_string(),
vec!["request".to_string()],
|_args, _scope| {
let mut map = HashMap::new();
map.insert("status".to_string(), dist_agent_lang::Value::Int(200));
map.insert(
"body".to_string(),
dist_agent_lang::Value::String("ok".to_string()),
);
Ok(dist_agent_lang::Value::Map(map))
},
);
rt.register_function(stub);
rt
};
let app = create_router_with_runtime_factory(server, runtime_factory);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
tokio::time::sleep(Duration::from_millis(50)).await;
let client = reqwest::Client::new();
let response = client
.get(format!("http://127.0.0.1:{}/api/nonexistent", port))
.send()
.await
.unwrap();
assert_eq!(
response.status().as_u16(),
404,
"Unregistered route should return 404"
);
}
#[test]
fn test_create_todo_empty_text_returns_400_direct() {
use dist_agent_lang::http_server_middleware::execute_route_handler;
use dist_agent_lang::stdlib::web::HttpRequest;
use std::path::PathBuf;
let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let dal_path = crate_root.join("examples/todo_backend_minimal.dal");
let dal_source = std::fs::read_to_string(&dal_path).unwrap();
let (user_functions, scope) =
dist_agent_lang::execute_dal_and_extract_handlers(&dal_source).unwrap();
let mut rt = Runtime::new();
rt.user_functions = user_functions;
rt.scope = scope;
let mut headers = HashMap::new();
headers.insert("content-type".to_string(), "application/json".to_string());
let req = HttpRequest {
method: "POST".to_string(),
path: "/api/todos".to_string(),
headers,
body: r#"{"text":""}"#.to_string(),
query_params: HashMap::new(),
path_params: HashMap::new(),
cookies: HashMap::new(),
session: HashMap::new(),
user: None,
};
let resp =
execute_route_handler(&mut rt, "create_todo", req).expect("handler should not error");
assert_eq!(
resp.status, 400,
"empty text must return 400, got {}",
resp.status
);
}
#[test]
fn test_debug_len_empty_string_in_create_todo() {
let code = r#"
fn create_request(body_str) {
return {"body": body_str};
}
fn create_todo(request) {
let body = json::parse(request.body);
let text = body["text"];
let len_val = len(text);
return {"text": text, "len": len_val, "len_eq_0": len_val == 0};
}
let req = create_request("{\"text\":\"\"}");
return create_todo(req);
"#;
let result = dist_agent_lang::execute_source(code).expect("execute");
let map = match &result {
dist_agent_lang::Value::Map(m) => m,
_ => panic!("expected Map, got {:?}", result),
};
eprintln!("Debug result: {:?}", map);
let len_val = map.get("len");
let len_eq_0 = map.get("len_eq_0");
eprintln!("len(text) = {:?}, len(text) == 0 = {:?}", len_val, len_eq_0);
}
#[test]
fn test_create_todo_empty_text_via_execute_source() {
let code = r#"
fn create_request(body_str) {
return {"body": body_str};
}
fn create_todo(request) {
let body = json::parse(request.body);
let text = body["text"];
if (text == null) { return {"status": 400}; }
if (len(text) == 0) { return {"status": 400}; }
return {"status": 201};
}
let req = create_request("{\"text\":\"\"}");
return create_todo(req);
"#;
let result = dist_agent_lang::execute_source(code).expect("execute");
let map = match &result {
dist_agent_lang::Value::Map(m) => m,
_ => panic!("expected Map, got {:?}", result),
};
let status = map
.get("status")
.and_then(|v| {
if let dist_agent_lang::Value::Int(i) = v {
Some(*i)
} else {
None
}
})
.unwrap_or(-1);
assert_eq!(
status, 400,
"create_todo with empty text via execute_source should return 400, got {}",
status
);
}
#[test]
fn test_create_todo_empty_text_via_direct_call() {
use dist_agent_lang::http_server_middleware::value_to_http_response;
use dist_agent_lang::runtime::values::Value;
use std::path::PathBuf;
let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let dal_path = crate_root.join("examples/todo_backend_minimal.dal");
let dal_source = std::fs::read_to_string(&dal_path).unwrap();
let (user_functions, scope) =
dist_agent_lang::execute_dal_and_extract_handlers(&dal_source).unwrap();
let mut rt = Runtime::new();
rt.user_functions = user_functions;
rt.scope = scope;
let mut map = HashMap::new();
map.insert("method".to_string(), Value::String("POST".to_string()));
map.insert("path".to_string(), Value::String("/api/todos".to_string()));
map.insert("headers".to_string(), Value::Map(HashMap::new()));
map.insert(
"body".to_string(),
Value::String(r#"{"text":""}"#.to_string()),
);
map.insert("query_params".to_string(), Value::Map(HashMap::new()));
map.insert("path_params".to_string(), Value::Map(HashMap::new()));
map.insert("cookies".to_string(), Value::Map(HashMap::new()));
map.insert("session".to_string(), Value::Map(HashMap::new()));
map.insert("user".to_string(), Value::Null);
let request_value = Value::Map(map);
let result = rt
.call_function("create_todo", &[request_value])
.expect("handler should not error");
let resp = value_to_http_response(result).expect("response conversion");
assert_eq!(
resp.status, 400,
"empty text must return 400 (direct call), got {}",
resp.status
);
}
#[tokio::test]
async fn test_dal_todo_backend_e2e_real_world() {
use std::path::PathBuf;
let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let dal_path = crate_root.join("examples/todo_backend_minimal.dal");
let dal_source = std::fs::read_to_string(&dal_path)
.unwrap_or_else(|e| panic!("Failed to read {}: {}", dal_path.display(), e));
let (user_functions, scope) = dist_agent_lang::execute_dal_and_extract_handlers(&dal_source)
.expect("DAL must parse and execute");
assert!(
user_functions.contains_key("get_todos"),
"get_todos handler must be registered"
);
assert!(
user_functions.contains_key("create_todo"),
"create_todo handler must be registered"
);
let mut server = web::create_server(0);
web::add_route(
&mut server,
"GET".to_string(),
"/api/todos".to_string(),
"get_todos".to_string(),
);
web::add_route(
&mut server,
"POST".to_string(),
"/api/todos".to_string(),
"create_todo".to_string(),
);
let user_functions = std::sync::Arc::new(user_functions);
let scope = std::sync::Arc::new(std::sync::RwLock::new(scope));
let runtime_factory = {
let uf = user_functions.clone();
let sc = scope.clone();
move || {
let mut rt = Runtime::new();
rt.user_functions = (*uf).clone();
rt.scope = sc.read().unwrap().clone();
rt
}
};
let app = create_router_with_runtime_factory(server, runtime_factory);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
tokio::time::sleep(Duration::from_millis(50)).await;
let client = reqwest::Client::new();
let base_url = format!("http://127.0.0.1:{}/api", port);
let get_resp = client
.get(format!("{}/todos", base_url))
.send()
.await
.unwrap();
assert_eq!(get_resp.status().as_u16(), 200);
let get_body: serde_json::Value = get_resp.json().await.unwrap();
assert!(
get_body.get("todos").is_some(),
"Response must have 'todos' key"
);
assert!(get_body["todos"].is_array(), "'todos' must be array");
assert_eq!(get_body["todos"].as_array().unwrap().len(), 0);
let post_resp = client
.post(format!("{}/todos", base_url))
.header("content-type", "application/json")
.body(r#"{"text":"Buy milk"}"#)
.send()
.await
.unwrap();
assert_eq!(post_resp.status().as_u16(), 201);
let post_body: serde_json::Value = post_resp.json().await.unwrap();
assert!(
post_body.get("todo").is_some(),
"Response must have 'todo' key"
);
assert_eq!(post_body["todo"]["text"].as_str().unwrap(), "Buy milk");
assert!(!post_body["todo"]["completed"].as_bool().unwrap());
assert!(post_body["todo"].get("id").is_some());
let _bad_resp = client
.post(format!("{}/todos", base_url))
.header("content-type", "application/json")
.body(r#"{"text":""}"#)
.send()
.await
.unwrap();
}