use std::net::IpAddr;
use axum::body::Body;
use axum::http::Request;
use base64::Engine;
use folk_plugin_http::config::HttpConfig;
use folk_plugin_http::payload::{decode_response, encode_request};
use folk_plugin_http::server::resolve_client_ip;
#[tokio::test]
async fn response_payload_roundtrip() {
let value = serde_json::json!({
"status": 200,
"headers": {"content-type": "text/plain"},
"body": "hello",
});
let response = decode_response(value).unwrap();
assert_eq!(response.status(), 200);
assert_eq!(
response.headers().get("content-type").unwrap(),
"text/plain"
);
}
#[tokio::test]
async fn request_payload_roundtrip() {
let req = Request::builder()
.method("POST")
.uri("/test?foo=bar")
.header("content-type", "application/json")
.body(Body::from(r#"{"key":"value"}"#))
.unwrap();
let value = encode_request(req, 1024 * 1024).await.unwrap();
assert_eq!(value["method"], "POST");
assert_eq!(value["uri"], "/test?foo=bar");
assert_eq!(value["headers"]["content-type"], "application/json");
assert_eq!(value["body"], r#"{"key":"value"}"#);
assert!(value.get("body_encoding").is_none());
}
#[tokio::test]
async fn request_binary_body_base64_encoded() {
let binary: Vec<u8> = vec![0x00, 0x01, 0xFF, 0xFE, 0x89, 0x50, 0x4E, 0x47];
let req = Request::builder()
.method("POST")
.uri("/upload")
.header("content-type", "application/octet-stream")
.body(Body::from(binary.clone()))
.unwrap();
let value = encode_request(req, 1024 * 1024).await.unwrap();
assert_eq!(value["body_encoding"], "base64");
let decoded = base64::engine::general_purpose::STANDARD
.decode(value["body"].as_str().unwrap())
.unwrap();
assert_eq!(decoded, binary);
}
#[tokio::test]
async fn response_base64_body_decoded() {
let binary: Vec<u8> = vec![0x00, 0x01, 0xFF, 0xFE];
let encoded = base64::engine::general_purpose::STANDARD.encode(&binary);
let value = serde_json::json!({
"status": 200,
"headers": {"content-type": "application/octet-stream"},
"body": encoded,
"body_encoding": "base64",
});
let response = decode_response(value).unwrap();
let body_bytes = axum::body::to_bytes(response.into_body(), 1024 * 1024)
.await
.unwrap();
assert_eq!(body_bytes.as_ref(), &binary);
}
#[tokio::test]
async fn request_body_limit_enforced() {
let big_body = "x".repeat(1024);
let req = Request::builder()
.method("POST")
.uri("/upload")
.body(Body::from(big_body))
.unwrap();
let result = encode_request(req, 512).await;
assert!(result.is_err());
}
#[test]
fn config_defaults() {
let config = HttpConfig::default();
assert_eq!(config.listen.to_string(), "0.0.0.0:8080");
assert_eq!(config.read_timeout.as_secs(), 10);
assert_eq!(config.write_timeout.as_secs(), 30);
assert_eq!(config.max_request_size, 10 * 1024 * 1024);
assert!(!config.access_log);
assert!(config.trusted_proxies.is_empty());
}
#[test]
fn config_deserialize_with_new_fields() {
let toml = r#"
listen = "127.0.0.1:9090"
read_timeout = "5s"
write_timeout = "15s"
max_request_size = "5mb"
access_log = true
trusted_proxies = ["10.0.0.0/8", "172.16.0.0/12"]
"#;
let config: HttpConfig = toml::from_str(toml).unwrap();
assert_eq!(config.listen.to_string(), "127.0.0.1:9090");
assert_eq!(config.read_timeout.as_secs(), 5);
assert_eq!(config.max_request_size, 5 * 1024 * 1024);
assert!(config.access_log);
assert_eq!(config.trusted_proxies.len(), 2);
assert_eq!(config.trusted_proxies[0].to_string(), "10.0.0.0/8");
}
#[test]
fn config_deserialize_minimal() {
let toml = r#"listen = "0.0.0.0:3000""#;
let config: HttpConfig = toml::from_str(toml).unwrap();
assert_eq!(config.listen.to_string(), "0.0.0.0:3000");
assert_eq!(config.max_request_size, 10 * 1024 * 1024);
assert!(!config.access_log);
}
#[test]
fn config_tls_parsing() {
let toml = r#"
listen = "0.0.0.0:443"
[tls]
cert = "/etc/ssl/cert.pem"
key = "/etc/ssl/key.pem"
"#;
let config: HttpConfig = toml::from_str(toml).unwrap();
let tls = config.tls.unwrap();
assert_eq!(tls.cert.to_str().unwrap(), "/etc/ssl/cert.pem");
assert_eq!(tls.key.to_str().unwrap(), "/etc/ssl/key.pem");
}
#[test]
fn config_no_tls_by_default() {
let config = HttpConfig::default();
assert!(config.tls.is_none());
assert!(!config.h2c);
}
#[test]
fn config_h2c_parsing() {
let toml = r#"h2c = true"#;
let config: HttpConfig = toml::from_str(toml).unwrap();
assert!(config.h2c);
}
#[test]
fn config_compression_defaults() {
let config = HttpConfig::default();
assert!(!config.compression.enabled);
assert_eq!(config.compression.algorithms.len(), 3);
assert_eq!(config.compression.min_size, 256);
}
#[test]
fn config_compression_parsing() {
let toml = r#"
[compression]
enabled = true
algorithms = ["br", "zstd"]
min_size = 1024
"#;
let config: HttpConfig = toml::from_str(toml).unwrap();
assert!(config.compression.enabled);
assert_eq!(config.compression.algorithms.len(), 2);
assert_eq!(
config.compression.algorithms[0],
folk_plugin_http::config::CompressionAlgorithm::Br
);
assert_eq!(config.compression.min_size, 1024);
}
#[test]
fn config_compression_min_size_human_readable() {
let toml = r#"
[compression]
enabled = true
min_size = "4kb"
"#;
let config: HttpConfig = toml::from_str(toml).unwrap();
assert_eq!(config.compression.min_size, 4096);
}
#[test]
fn parse_byte_size_strings() {
use folk_plugin_http::config::parse_byte_size;
assert_eq!(parse_byte_size("10mb").unwrap(), 10 * 1024 * 1024);
assert_eq!(parse_byte_size("512kb").unwrap(), 512 * 1024);
assert_eq!(parse_byte_size("1gb").unwrap(), 1024 * 1024 * 1024);
assert_eq!(parse_byte_size("256b").unwrap(), 256);
assert_eq!(parse_byte_size("10MiB").unwrap(), 10 * 1024 * 1024);
assert_eq!(parse_byte_size("1024").unwrap(), 1024);
assert!(parse_byte_size("abc").is_err());
assert!(parse_byte_size("mb").is_err());
}
#[test]
fn config_deserialize_max_request_size_as_integer() {
let toml = r#"max_request_size = 2097152"#;
let config: HttpConfig = toml::from_str(toml).unwrap();
assert_eq!(config.max_request_size, 2_097_152);
}
fn parse_nets(cidrs: &[&str]) -> Vec<ipnet::IpNet> {
cidrs.iter().map(|s| s.parse().unwrap()).collect()
}
#[test]
fn trusted_proxies_empty_returns_peer() {
let peer: IpAddr = "1.2.3.4".parse().unwrap();
let result = resolve_client_ip(peer, Some("5.6.7.8"), &[]);
assert_eq!(result, peer);
}
#[test]
fn trusted_proxies_peer_not_trusted_returns_peer() {
let peer: IpAddr = "1.2.3.4".parse().unwrap();
let trusted = parse_nets(&["10.0.0.0/8"]);
let result = resolve_client_ip(peer, Some("5.6.7.8"), &trusted);
assert_eq!(result, peer);
}
#[test]
fn trusted_proxies_extracts_rightmost_non_trusted() {
let peer: IpAddr = "10.0.0.1".parse().unwrap(); let trusted = parse_nets(&["10.0.0.0/8"]);
let result = resolve_client_ip(peer, Some("1.2.3.4, 10.0.0.2"), &trusted);
assert_eq!(result, "1.2.3.4".parse::<IpAddr>().unwrap());
}
#[test]
fn trusted_proxies_single_xff() {
let peer: IpAddr = "10.0.0.1".parse().unwrap();
let trusted = parse_nets(&["10.0.0.0/8"]);
let result = resolve_client_ip(peer, Some("203.0.113.50"), &trusted);
assert_eq!(result, "203.0.113.50".parse::<IpAddr>().unwrap());
}
#[test]
fn trusted_proxies_all_trusted_returns_peer() {
let peer: IpAddr = "10.0.0.1".parse().unwrap();
let trusted = parse_nets(&["10.0.0.0/8", "172.16.0.0/12"]);
let result = resolve_client_ip(peer, Some("10.0.0.2, 172.16.0.1"), &trusted);
assert_eq!(result, peer);
}
#[test]
fn trusted_proxies_no_xff_header() {
let peer: IpAddr = "10.0.0.1".parse().unwrap();
let trusted = parse_nets(&["10.0.0.0/8"]);
let result = resolve_client_ip(peer, None, &trusted);
assert_eq!(result, peer);
}
#[tokio::test]
#[ignore = "requires php + msgpack"]
async fn http_plugin_serves_real_request() {
todo!("requires full Folk server with PHP workers")
}