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,
body: &str,
) -> io::Result<String> {
let mut stream = TcpStream::connect(addr).await?;
let request = if body.is_empty() {
format!(
"{} {} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
method, path
)
} else {
format!(
"{} {} HTTP/1.1\r\nHost: localhost\r\nContent-Length: {}\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{}",
method,
path,
body.len(),
body
)
};
stream.write_all(request.as_bytes()).await?;
let mut response = String::new();
stream.read_to_string(&mut response).await?;
Ok(response)
}
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())
}
fn json_body(body: &str) -> Value {
serde_json::from_str(body).unwrap_or(Value::Null)
}
#[tokio::test]
async fn http_reset_clears_state() -> 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 password = ["test", "pass"].concat();
let create_body = serde_json::json!({"user": "testuser", "password": password}).to_string();
let response = http_request(http_addr, "POST", "/users", &create_body).await?;
let (status, _headers, _body) = parse_response(&response);
assert_eq!(status, 201, "User create should succeed: {}", response);
let inject_body =
r#"{"user": "testuser", "mailbox": "INBOX", "raw": "Subject: Test\r\n\r\nBody"}"#;
let response = http_request(http_addr, "POST", "/inject", inject_body).await?;
let (status, _headers, body) = parse_response(&response);
assert_eq!(status, 201, "Inject should succeed: {}", response);
let json = json_body(&body);
assert!(json.get("uid").is_some(), "Should return UID: {}", response);
let response = http_request(http_addr, "GET", "/users", "").await?;
let (status, _headers, body) = parse_response(&response);
assert_eq!(status, 200, "Users should list: {}", response);
let json = json_body(&body);
let users = json
.get("users")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
assert!(
users.iter().any(|u| u == "testuser"),
"User should exist: {}",
response
);
let response = http_request(http_addr, "POST", "/reset", "").await?;
let (status, _headers, body) = parse_response(&response);
assert_eq!(status, 200, "Reset should succeed: {}", response);
let json = json_body(&body);
assert_eq!(json.get("status").and_then(Value::as_str), Some("ok"));
let response = http_request(http_addr, "GET", "/users", "").await?;
let (status, _headers, body) = parse_response(&response);
assert_eq!(status, 200, "Users should list: {}", response);
let json = json_body(&body);
let users = json
.get("users")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
assert!(
!users.iter().any(|u| u == "testuser"),
"Users should be cleared: {}",
response
);
server.stop().await?;
Ok(())
}
#[tokio::test]
async fn http_inject_and_list_messages() -> 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 inject_body = r#"{"user": "alice", "mailbox": "INBOX", "raw": "Subject: Hello World\r\nFrom: sender@test.com\r\n\r\nMessage body"}"#;
let response = http_request(http_addr, "POST", "/inject", inject_body).await?;
let (status, _headers, _body) = parse_response(&response);
assert_eq!(status, 201, "Inject should succeed: {}", response);
let response = http_request(http_addr, "GET", "/messages?user=alice&mailbox=INBOX", "").await?;
let (status, _headers, body) = parse_response(&response);
assert_eq!(status, 200, "List should succeed: {}", response);
let json = json_body(&body);
let messages = json
.get("messages")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
assert!(
messages
.iter()
.any(|msg| msg.get("subject") == Some(&Value::from("Hello World"))),
"Should contain subject: {}",
response
);
assert!(
messages
.iter()
.any(|msg| msg.get("from") == Some(&Value::from("sender@test.com"))),
"Should contain from: {}",
response
);
server.stop().await?;
Ok(())
}
#[tokio::test]
async fn http_delete_message() -> 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 inject_body =
r#"{"user": "bob", "mailbox": "INBOX", "raw": "Subject: Delete me\r\n\r\nBody"}"#;
let response = http_request(http_addr, "POST", "/inject", inject_body).await?;
let (status, _headers, body) = parse_response(&response);
assert_eq!(status, 201, "Inject should succeed: {}", response);
let json = json_body(&body);
assert_eq!(json.get("uid").and_then(Value::as_u64), Some(1));
let response = http_request(http_addr, "GET", "/messages?user=bob&mailbox=INBOX", "").await?;
let (status, _headers, body) = parse_response(&response);
assert_eq!(status, 200, "List should succeed: {}", response);
let json = json_body(&body);
let messages = json
.get("messages")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
assert!(
messages
.iter()
.any(|msg| msg.get("subject") == Some(&Value::from("Delete me"))),
"Message should exist: {}",
response
);
let response = http_request(
http_addr,
"DELETE",
"/messages?user=bob&mailbox=INBOX&uid=1",
"",
)
.await?;
let (status, _headers, _body) = parse_response(&response);
assert_eq!(status, 200, "Delete should succeed: {}", response);
let response = http_request(http_addr, "GET", "/messages?user=bob&mailbox=INBOX", "").await?;
let (status, _headers, body) = parse_response(&response);
assert_eq!(status, 200, "List should succeed: {}", response);
let json = json_body(&body);
let messages = json
.get("messages")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
assert!(
!messages
.iter()
.any(|msg| msg.get("subject") == Some(&Value::from("Delete me"))),
"Message should be deleted: {}",
response
);
server.stop().await?;
Ok(())
}
#[tokio::test]
async fn http_inject_invalid_json() -> 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, "POST", "/inject", "{").await?;
let (status, _headers, body) = parse_response(&response);
assert_eq!(status, 400, "Invalid JSON should 400: {}", response);
let json = json_body(&body);
assert_eq!(
json.get("error").and_then(Value::as_str),
Some("Invalid JSON")
);
server.stop().await?;
Ok(())
}
#[tokio::test]
async fn http_404_for_unknown_path() -> 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", "/unknown", "").await?;
let (status, _headers, _body) = parse_response(&response);
assert_eq!(status, 404, "Unknown path should 404: {}", response);
server.stop().await?;
Ok(())
}
#[tokio::test]
async fn http_405_for_invalid_method() -> 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", "/reset", "").await?;
let (status, _headers, _body) = parse_response(&response);
assert_eq!(status, 405, "GET /reset should be 405: {}", response);
let response = http_request(http_addr, "POST", "/config", "").await?;
let (status, _headers, _body) = parse_response(&response);
assert_eq!(status, 405, "POST /config should be 405: {}", response);
server.stop().await?;
Ok(())
}