use std::collections::HashMap;
use std::io;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use elektromail::{Server, ServerConfig};
use serde_json::Value;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
async fn http_request(addr: SocketAddr, method: &str, path: &str) -> io::Result<String> {
http_request_with_headers(addr, method, path, &[]).await
}
async fn http_request_with_headers(
addr: SocketAddr,
method: &str,
path: &str,
headers: &[(&str, &str)],
) -> io::Result<String> {
let mut stream = TcpStream::connect(addr).await?;
let request = build_request(method, path, headers);
stream.write_all(request.as_bytes()).await?;
let mut response = String::new();
stream.read_to_string(&mut response).await?;
Ok(response)
}
fn build_request(method: &str, path: &str, headers: &[(&str, &str)]) -> String {
let mut request = format!(
"{} {} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n",
method, path
);
for (key, value) in headers {
let _ = std::fmt::Write::write_fmt(&mut request, format_args!("{key}: {value}\r\n"));
}
request.push_str("\r\n");
request
}
fn http_addr() -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
}
fn parse_response(response: &str) -> (u16, HashMap<String, String>, String) {
let (head, body) = response.split_once("\r\n\r\n").unwrap_or((response, ""));
let mut lines = head.split("\r\n");
let status_line = lines.next().unwrap_or("");
let status_code = status_line
.split_whitespace()
.nth(1)
.and_then(|code| code.parse::<u16>().ok())
.unwrap_or(0);
let mut headers = HashMap::new();
for line in lines {
if let Some((key, value)) = line.split_once(':') {
headers.insert(key.trim().to_ascii_lowercase(), value.trim().to_string());
}
}
(status_code, headers, body.to_string())
}
#[tokio::test]
async fn http_openapi_json_served() -> io::Result<()> {
let config = ServerConfig {
http_addr: Some(http_addr()),
..Default::default()
};
let server = Server::start(config).await?;
let http_addr = server.http_addr().expect("HTTP should be enabled");
let response = http_request(http_addr, "GET", "/openapi.json").await?;
let (status, headers, body) = parse_response(&response);
assert_eq!(status, 200, "Should return 200: {}", response);
let content_type = headers.get("content-type").cloned().unwrap_or_default();
assert!(
content_type
.to_ascii_lowercase()
.contains("application/json"),
"Should return JSON content type: {}",
response
);
let json: Value = serde_json::from_str(&body).unwrap_or(Value::Null);
assert!(json.get("openapi").is_some(), "Should contain OpenAPI");
server.stop().await?;
Ok(())
}
#[tokio::test]
async fn http_docs_html_served() -> io::Result<()> {
let config = ServerConfig {
http_addr: Some(http_addr()),
..Default::default()
};
let server = Server::start(config).await?;
let http_addr = server.http_addr().expect("HTTP should be enabled");
let response = http_request(http_addr, "GET", "/docs").await?;
let (status, headers, body) = parse_response(&response);
assert_eq!(status, 200, "Should return 200: {}", response);
let content_type = headers.get("content-type").cloned().unwrap_or_default();
assert!(
content_type.to_ascii_lowercase().contains("text/html"),
"Should return HTML content type: {}",
response
);
assert!(
body.contains("SwaggerUIBundle"),
"Should include Swagger UI bundle: {}",
response
);
server.stop().await?;
Ok(())
}
#[tokio::test]
async fn http_docs_require_token_when_configured() -> io::Result<()> {
let config = ServerConfig {
http_addr: Some(http_addr()),
http_token: Some("secret-token".to_string()),
..Default::default()
};
let server = Server::start(config).await?;
let http_addr = server.http_addr().expect("HTTP should be enabled");
let response = http_request(http_addr, "GET", "/docs").await?;
let (status, _headers, _body) = parse_response(&response);
assert_eq!(status, 401, "Docs should require token: {}", response);
let response = http_request(http_addr, "GET", "/openapi.json").await?;
let (status, _headers, _body) = parse_response(&response);
assert_eq!(status, 401, "OpenAPI should require token: {}", response);
let response = http_request(http_addr, "GET", "/docs?token=secret-token").await?;
let (status, _headers, body) = parse_response(&response);
assert_eq!(status, 200, "Docs should allow token query: {}", response);
assert!(
body.contains("requestInterceptor"),
"Docs should inject auth interceptor: {}",
response
);
let response = http_request(http_addr, "GET", "/openapi.json?token=secret-token").await?;
let (status, _headers, body) = parse_response(&response);
assert_eq!(
status, 200,
"OpenAPI should allow token query: {}",
response
);
let json: Value = serde_json::from_str(&body).unwrap_or(Value::Null);
assert!(json.get("openapi").is_some(), "Should contain OpenAPI");
let response = http_request_with_headers(
http_addr,
"GET",
"/openapi.json",
&[("Authorization", "Bearer secret-token")],
)
.await?;
let (status, _headers, _body) = parse_response(&response);
assert_eq!(
status, 200,
"OpenAPI should allow auth header: {}",
response
);
server.stop().await?;
Ok(())
}