copilot/add-pop3-protocol-examples
Testing Guide for POP3 Implementation
This document provides comprehensive testing examples and strategies for POP3 protocol implementations.
Table of Contents
Unit Testing
Testing POP3 Command Parser
#[cfg(test)]
mod command_parser_tests {
use super::*;
#[test]
fn test_parse_user_command() {
let cmd = "USER testuser";
let parsed = parse_command(cmd);
assert_eq!(parsed.command, "USER");
assert_eq!(parsed.args, vec!["testuser"]);
}
#[test]
fn test_parse_pass_command() {
let cmd = "PASS secret123";
let parsed = parse_command(cmd);
assert_eq!(parsed.command, "PASS");
assert_eq!(parsed.args, vec!["secret123"]);
}
#[test]
fn test_parse_retr_command() {
let cmd = "RETR 5";
let parsed = parse_command(cmd);
assert_eq!(parsed.command, "RETR");
assert_eq!(parsed.args, vec!["5"]);
}
#[test]
fn test_parse_command_case_insensitive() {
let cmd = "user TestUser";
let parsed = parse_command(cmd);
assert_eq!(parsed.command, "USER");
assert_eq!(parsed.args, vec!["TestUser"]);
}
#[test]
fn test_parse_invalid_command() {
let cmd = "INVALID";
let result = parse_command(cmd);
assert!(result.is_err());
}
}
Testing Authentication Logic
#[cfg(test)]
mod auth_tests {
use super::*;
#[test]
fn test_valid_authentication() {
let authenticator = create_test_authenticator();
let result = authenticator.authenticate("testuser", "testpass");
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_invalid_password() {
let authenticator = create_test_authenticator();
let result = authenticator.authenticate("testuser", "wrongpass");
assert!(result.is_ok());
assert_eq!(result.unwrap(), false);
}
#[test]
fn test_nonexistent_user() {
let authenticator = create_test_authenticator();
let result = authenticator.authenticate("nonexistent", "password");
assert!(result.is_ok());
assert_eq!(result.unwrap(), false);
}
#[test]
fn test_empty_credentials() {
let authenticator = create_test_authenticator();
let result = authenticator.authenticate("", "");
assert!(result.is_ok());
assert_eq!(result.unwrap(), false);
}
#[test]
fn test_special_characters_in_password() {
let authenticator = create_test_authenticator();
// Add user with special characters
authenticator.add_user("special", "p@ss!w0rd#$%");
let result = authenticator.authenticate("special", "p@ss!w0rd#$%");
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
}
Testing Mailbox Operations
#[cfg(test)]
mod mailbox_tests {
use super::*;
#[test]
fn test_stat_empty_mailbox() {
let mailbox = Mailbox::new();
let (count, size) = mailbox.stat();
assert_eq!(count, 0);
assert_eq!(size, 0);
}
#[test]
fn test_stat_with_messages() {
let mut mailbox = Mailbox::new();
mailbox.add_message("Message 1", 100);
mailbox.add_message("Message 2", 200);
let (count, size) = mailbox.stat();
assert_eq!(count, 2);
assert_eq!(size, 300);
}
#[test]
fn test_list_messages() {
let mut mailbox = Mailbox::new();
mailbox.add_message("Message 1", 100);
mailbox.add_message("Message 2", 200);
let list = mailbox.list();
assert_eq!(list.len(), 2);
assert_eq!(list[0], (1, 100));
assert_eq!(list[1], (2, 200));
}
#[test]
fn test_retrieve_message() {
let mut mailbox = Mailbox::new();
mailbox.add_message("Test Message Content", 100);
let msg = mailbox.retrieve(1);
assert!(msg.is_some());
assert_eq!(msg.unwrap(), "Test Message Content");
}
#[test]
fn test_retrieve_nonexistent_message() {
let mailbox = Mailbox::new();
let msg = mailbox.retrieve(999);
assert!(msg.is_none());
}
#[test]
fn test_delete_message() {
let mut mailbox = Mailbox::new();
mailbox.add_message("Message", 100);
let result = mailbox.delete(1);
assert!(result.is_ok());
// Message should be marked deleted but still exist
assert_eq!(mailbox.messages.len(), 1);
assert_eq!(mailbox.messages[0].deleted, true);
}
#[test]
fn test_reset_deletions() {
let mut mailbox = Mailbox::new();
mailbox.add_message("Message", 100);
mailbox.delete(1).unwrap();
mailbox.reset();
// Deletion mark should be removed
assert_eq!(mailbox.messages[0].deleted, false);
}
#[test]
fn test_commit_deletions() {
let mut mailbox = Mailbox::new();
mailbox.add_message("Message 1", 100);
mailbox.add_message("Message 2", 200);
mailbox.delete(1).unwrap();
mailbox.commit();
// Message 1 should be permanently removed
assert_eq!(mailbox.messages.len(), 1);
assert_eq!(mailbox.retrieve(2).is_some(), true);
}
#[test]
fn test_stat_after_delete() {
let mut mailbox = Mailbox::new();
mailbox.add_message("Message 1", 100);
mailbox.add_message("Message 2", 200);
mailbox.delete(1).unwrap();
// STAT should not count deleted messages
let (count, size) = mailbox.stat();
assert_eq!(count, 1);
assert_eq!(size, 200);
}
}
Testing Response Generation
#[cfg(test)]
mod response_tests {
use super::*;
#[test]
fn test_ok_response() {
let response = Response::ok("User accepted");
assert_eq!(response.to_string(), "+OK User accepted\r\n");
}
#[test]
fn test_err_response() {
let response = Response::err("Invalid command");
assert_eq!(response.to_string(), "-ERR Invalid command\r\n");
}
#[test]
fn test_multiline_response() {
let lines = vec!["1 100".to_string(), "2 200".to_string()];
let response = Response::multiline("+OK 2 messages", lines);
let expected = "+OK 2 messages\r\n1 100\r\n2 200\r\n.\r\n";
assert_eq!(response.to_string(), expected);
}
#[test]
fn test_stat_response() {
let response = Response::stat(3, 600);
assert_eq!(response.to_string(), "+OK 3 600\r\n");
}
}
Integration Testing
End-to-End Session Test
#[tokio::test]
async fn test_complete_pop3_session() {
// Start test server
let server = start_test_pop3_server().await;
// Connect client
let mut client = Pop3Client::connect("127.0.0.1", 11000).await.unwrap();
// Authenticate
client.send("USER testuser").await.unwrap();
let resp = client.read_response().await.unwrap();
assert!(resp.starts_with("+OK"));
client.send("PASS testpass").await.unwrap();
let resp = client.read_response().await.unwrap();
assert!(resp.starts_with("+OK"));
// Get statistics
client.send("STAT").await.unwrap();
let resp = client.read_response().await.unwrap();
assert!(resp.contains("+OK"));
// List messages
client.send("LIST").await.unwrap();
let resp = client.read_response().await.unwrap();
assert!(resp.starts_with("+OK"));
// Retrieve message
client.send("RETR 1").await.unwrap();
let resp = client.read_response().await.unwrap();
assert!(resp.starts_with("+OK"));
// Delete message
client.send("DELE 1").await.unwrap();
let resp = client.read_response().await.unwrap();
assert!(resp.starts_with("+OK"));
// Quit
client.send("QUIT").await.unwrap();
let resp = client.read_response().await.unwrap();
assert!(resp.starts_with("+OK"));
server.stop().await;
}
SMTP to POP3 Integration Test
#[tokio::test]
async fn test_smtp_to_pop3_delivery() {
// Start both servers
let smtp_server = start_test_smtp_server().await;
let pop3_server = start_test_pop3_server().await;
// Send email via SMTP
let mut smtp_client = SmtpClient::connect("127.0.0.1", 2525).await.unwrap();
smtp_client.send_mail(
"sender@test.com",
vec!["testuser@test.com"],
"Subject: Test\r\n\r\nTest body"
).await.unwrap();
// Wait for delivery
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// Check with POP3
let mut pop3_client = Pop3Client::connect("127.0.0.1", 110).await.unwrap();
pop3_client.authenticate("testuser", "testpass").await.unwrap();
let (count, _) = pop3_client.stat().await.unwrap();
assert!(count > 0, "No messages received");
let msg = pop3_client.retrieve(count).await.unwrap();
assert!(msg.contains("Test body"));
smtp_server.stop().await;
pop3_server.stop().await;
}
Concurrent Connections Test
#[tokio::test]
async fn test_concurrent_connections() {
let server = start_test_pop3_server().await;
let handles: Vec<_> = (0..10)
.map(|i| {
tokio::spawn(async move {
let mut client = Pop3Client::connect("127.0.0.1", 11000).await.unwrap();
client.authenticate("testuser", "testpass").await.unwrap();
let (count, size) = client.stat().await.unwrap();
client.quit().await.unwrap();
(i, count, size)
})
})
.collect();
for handle in handles {
let (i, count, size) = handle.await.unwrap();
println!("Client {} completed: {} messages, {} bytes", i, count, size);
}
server.stop().await;
}
Mock Testing
Using Mockall for Storage
use mockall::predicate::*;
use mockall::*;
#[automock]
pub trait MessageStorage {
fn get_message(&self, user: &str, msg_id: usize) -> Result<Vec<u8>, Error>;
fn list_messages(&self, user: &str) -> Result<Vec<(usize, usize)>, Error>;
fn delete_message(&self, user: &str, msg_id: usize) -> Result<(), Error>;
}
#[cfg(test)]
mod mock_storage_tests {
use super::*;
#[test]
fn test_retrieve_with_mock() {
let mut mock_storage = MockMessageStorage::new();
mock_storage
.expect_get_message()
.with(eq("testuser"), eq(1))
.times(1)
.returning(|_, _| Ok(b"Test message".to_vec()));
let msg = mock_storage.get_message("testuser", 1).unwrap();
assert_eq!(msg, b"Test message");
}
#[test]
fn test_list_with_mock() {
let mut mock_storage = MockMessageStorage::new();
mock_storage
.expect_list_messages()
.with(eq("testuser"))
.times(1)
.returning(|_| Ok(vec![(1, 100), (2, 200)]));
let messages = mock_storage.list_messages("testuser").unwrap();
assert_eq!(messages.len(), 2);
assert_eq!(messages[0], (1, 100));
}
#[test]
fn test_delete_with_mock() {
let mut mock_storage = MockMessageStorage::new();
mock_storage
.expect_delete_message()
.with(eq("testuser"), eq(1))
.times(1)
.returning(|_, _| Ok(()));
let result = mock_storage.delete_message("testuser", 1);
assert!(result.is_ok());
}
}
Mock Authentication Backend
#[automock]
pub trait Authenticator {
fn authenticate(&self, username: &str, password: &str) -> Result<bool, Error>;
}
#[cfg(test)]
mod mock_auth_tests {
use super::*;
#[test]
fn test_successful_auth() {
let mut mock_auth = MockAuthenticator::new();
mock_auth
.expect_authenticate()
.with(eq("testuser"), eq("testpass"))
.times(1)
.returning(|_, _| Ok(true));
let result = mock_auth.authenticate("testuser", "testpass").unwrap();
assert_eq!(result, true);
}
#[test]
fn test_failed_auth() {
let mut mock_auth = MockAuthenticator::new();
mock_auth
.expect_authenticate()
.with(eq("testuser"), eq("wrongpass"))
.times(1)
.returning(|_, _| Ok(false));
let result = mock_auth.authenticate("testuser", "wrongpass").unwrap();
assert_eq!(result, false);
}
}
Load Testing
Basic Load Test
#[tokio::test]
#[ignore] // Run separately with --ignored flag
async fn test_connection_load() {
let server = start_test_pop3_server().await;
let concurrent_clients = 100;
let start = std::time::Instant::now();
let handles: Vec<_> = (0..concurrent_clients)
.map(|_| {
tokio::spawn(async {
let mut client = Pop3Client::connect("127.0.0.1", 11000).await?;
client.authenticate("testuser", "testpass").await?;
client.stat().await?;
client.quit().await?;
Ok::<_, Box<dyn std::error::Error>>(())
})
})
.collect();
let mut success_count = 0;
for handle in handles {
if handle.await.unwrap().is_ok() {
success_count += 1;
}
}
let duration = start.elapsed();
println!("Completed {} connections in {:?}", success_count, duration);
println!("Throughput: {:.2} connections/sec",
success_count as f64 / duration.as_secs_f64());
assert_eq!(success_count, concurrent_clients);
server.stop().await;
}
Message Retrieval Load Test
#[tokio::test]
#[ignore]
async fn test_message_retrieval_load() {
let server = start_test_pop3_server().await;
let num_retrievals = 1000;
let start = std::time::Instant::now();
let mut client = Pop3Client::connect("127.0.0.1", 11000).await.unwrap();
client.authenticate("testuser", "testpass").await.unwrap();
for i in 1..=num_retrievals {
client.retrieve(1).await.unwrap();
if i % 100 == 0 {
println!("Retrieved {} messages", i);
}
}
client.quit().await.unwrap();
let duration = start.elapsed();
println!("Retrieved {} messages in {:?}", num_retrievals, duration);
println!("Throughput: {:.2} retrievals/sec",
num_retrievals as f64 / duration.as_secs_f64());
server.stop().await;
}
Security Testing
Authentication Brute Force Protection Test
#[tokio::test]
async fn test_rate_limiting() {
let server = start_test_pop3_server().await;
let mut client = Pop3Client::connect("127.0.0.1", 11000).await.unwrap();
// Try multiple failed authentications
for i in 0..10 {
let _ = client.send(format!("USER testuser{}", i)).await;
let _ = client.read_response().await;
let _ = client.send("PASS wrongpass").await;
let resp = client.read_response().await.unwrap();
if i >= 3 {
// After 3 attempts, should be rate limited
assert!(resp.contains("too many") || resp.contains("rate limit"));
}
}
server.stop().await;
}
SQL Injection Test (if using database)
#[tokio::test]
async fn test_sql_injection_protection() {
let server = start_test_pop3_server().await;
let mut client = Pop3Client::connect("127.0.0.1", 11000).await.unwrap();
// Try SQL injection in username
client.send("USER admin' OR '1'='1").await.unwrap();
let _ = client.read_response().await;
client.send("PASS password").await.unwrap();
let resp = client.read_response().await.unwrap();
// Should fail authentication
assert!(resp.starts_with("-ERR"));
server.stop().await;
}
Command Injection Test
#[tokio::test]
async fn test_command_injection() {
let server = start_test_pop3_server().await;
let mut client = Pop3Client::connect("127.0.0.1", 11000).await.unwrap();
// Try command injection
let malicious_commands = vec![
"USER test\r\nSTAT\r\n",
"PASS test; rm -rf /",
"RETR 1; cat /etc/passwd",
];
for cmd in malicious_commands {
client.send(cmd).await.unwrap();
let resp = client.read_response().await.unwrap();
// Should either fail or only execute intended command
assert!(resp.starts_with("+OK") || resp.starts_with("-ERR"));
}
server.stop().await;
}
Testing Best Practices
Test Organization
// Organize tests in modules
#[cfg(test)]
mod tests {
mod unit {
mod authentication { /* auth tests */ }
mod mailbox { /* mailbox tests */ }
mod commands { /* command tests */ }
}
mod integration {
mod sessions { /* session tests */ }
mod protocols { /* protocol tests */ }
}
mod security {
mod injection { /* injection tests */ }
mod rate_limit { /* rate limit tests */ }
}
}
Test Fixtures
// Create reusable test fixtures
fn create_test_mailbox() -> Mailbox {
let mut mailbox = Mailbox::new();
mailbox.add_message("Message 1", 100);
mailbox.add_message("Message 2", 200);
mailbox
}
fn create_test_authenticator() -> FileAuthenticator {
let mut auth = FileAuthenticator::new();
auth.add_user("testuser", "testpass");
auth.add_user("alice", "password123");
auth
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_with_fixture() {
let mailbox = create_test_mailbox();
assert_eq!(mailbox.messages.len(), 2);
}
}
Property-Based Testing
use proptest::prelude::*;
proptest! {
#[test]
fn test_message_id_always_positive(msg_id in 1usize..10000) {
let mut mailbox = Mailbox::new();
mailbox.add_message("Test", 100);
let result = mailbox.retrieve(msg_id);
// Should not panic
prop_assert!(result.is_some() || result.is_none());
}
#[test]
fn test_stat_consistency(
msg_count in 0usize..100,
msg_size in 1usize..10000
) {
let mut mailbox = Mailbox::new();
for _ in 0..msg_count {
mailbox.add_message("Test", msg_size);
}
let (count, size) = mailbox.stat();
prop_assert_eq!(count, msg_count);
prop_assert_eq!(size, msg_count * msg_size);
}
}
Test Coverage
# Install tarpaulin for coverage
cargo install cargo-tarpaulin
# Run tests with coverage
cargo tarpaulin --out Html --output-dir coverage/
# View coverage report
open coverage/index.html
Continuous Integration
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Run tests
run: cargo test --all-features
- name: Run integration tests
run: cargo test --test '*' -- --ignored
- name: Check code coverage
run: |
cargo install cargo-tarpaulin
cargo tarpaulin --out Xml
- name: Upload coverage
uses: codecov/codecov-action@v1
Debugging Tests
Enable Logging in Tests
#[cfg(test)]
mod tests {
use super::*;
fn init_logger() {
let _ = tracing_subscriber::fmt()
.with_test_writer()
.with_max_level(tracing::Level::DEBUG)
.try_init();
}
#[test]
fn test_with_logging() {
init_logger();
// Test code here
tracing::debug!("Debug message in test");
}
}
Using Test Output
# Show test output
cargo test -- --nocapture
# Show specific test output
cargo test test_name -- --nocapture --exact
# Run tests in sequence (not parallel)
cargo test -- --test-threads=1
Conclusion
This testing guide provides a comprehensive framework for testing POP3 implementations. Combine unit tests, integration tests, mock tests, and security tests to ensure robust and reliable code.
Key takeaways: - Test at multiple levels (unit, integration, system) - Use mocks for external dependencies - Include security tests - Measure and improve coverage - Automate testing in CI/CD - Document test scenarios and expectations