1 //! POP3 Integration Test Example
2 //!
3 //! This example demonstrates how to write integration tests for POP3 servers
4 //! and clients, showing best practices for testing mail systems.
5 //!
6 //! Usage:
7 //! cargo run --example pop3_integration_test
8 //!
9 //! This example shows:
10 //! - Setting up test fixtures
11 //! - Testing complete POP3 workflows
12 //! - Mocking server responses
13 //! - Testing error conditions
14 //! - Integration between SMTP and POP3
15
16 use std::io::{BufRead, BufReader, Write};
17 use std::net::{TcpListener, TcpStream};
18 use std::sync::{Arc, Mutex};
19 use std::thread;
20 use std::time::Duration;
21
22 /// Test result
23 type TestResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;
24
25 /// Simple POP3 test client
26 struct TestPop3Client {
27 stream: TcpStream,
28 reader: BufReader<TcpStream>,
29 }
30
31 impl TestPop3Client {
32 fn connect(addr: &str) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
33 let stream = TcpStream::connect(addr)?;
34 stream.set_read_timeout(Some(Duration::from_secs(5)))?;
35 let reader = BufReader::new(stream.try_clone()?);
36
37 let mut client = TestPop3Client { stream, reader };
38 let greeting = client.read_response()?;
39
40 if !greeting.starts_with("+OK") {
41 return Err(format!("Invalid greeting: {}", greeting).into());
42 }
43
44 Ok(client)
45 }
46
47 fn send_command(&mut self, cmd: &str) -> TestResult {
48 writeln!(self.stream, "{}", cmd)?;
49 self.stream.flush()?;
50 Ok(())
51 }
52
53 fn read_response(&mut self) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
54 let mut response = String::new();
55 self.reader.read_line(&mut response)?;
56 Ok(response.trim_end().to_string())
57 }
58
59 fn read_multiline(&mut self) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
60 let mut lines = Vec::new();
61 loop {
62 let mut line = String::new();
63 self.reader.read_line(&mut line)?;
64 let line = line.trim_end().to_string();
65 if line == "." {
66 break;
67 }
68 lines.push(line);
69 }
70 Ok(lines)
71 }
72 }
73
74 /// Mock POP3 server for testing
75 struct MockPop3Server {
76 listener: TcpListener,
77 running: Arc<Mutex<bool>>,
78 }
79
80 impl MockPop3Server {
81 fn new(port: u16) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
82 let listener = TcpListener::bind(format!("127.0.0.1:{}", port))?;
83 listener.set_nonblocking(true)?;
84
85 Ok(MockPop3Server {
86 listener,
87 running: Arc::new(Mutex::new(true)),
88 })
89 }
90
91 fn start(&self) -> TestResult {
92 let listener = self.listener.try_clone()?;
93 let running = Arc::clone(&self.running);
94
95 thread::spawn(move || {
96 while *running.lock().unwrap() {
97 match listener.accept() {
98 Ok((mut stream, _)) => {
99 let _ = Self::handle_client(&mut stream);
100 }
101 Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
102 thread::sleep(Duration::from_millis(10));
103 }
104 Err(_) => break,
105 }
106 }
107 });
108
109 thread::sleep(Duration::from_millis(100));
110 Ok(())
111 }
112
113 fn handle_client(stream: &mut TcpStream) -> TestResult {
114 writeln!(stream, "+OK Mock POP3 server ready")?;
115 stream.flush()?;
116
117 let mut reader = BufReader::new(stream.try_clone()?);
118
119 loop {
120 let mut line = String::new();
121 if reader.read_line(&mut line).is_err() {
122 break;
123 }
124
125 let cmd = line.trim().to_uppercase();
126
127 let response = match cmd.as_str() {
128 s if s.starts_with("USER") => "+OK User accepted",
129 s if s.starts_with("PASS") => "+OK Password accepted",
130 "STAT" => "+OK 2 640",
131 "LIST" => "+OK 2 messages (640 octets)\r\n1 320\r\n2 320\r\n.",
132 s if s.starts_with("RETR 1") => {
133 "+OK 320 octets\r\nFrom: test@example.com\r\nSubject: Test\r\n\r\nTest body\r\n."
134 }
135 s if s.starts_with("DELE") => "+OK Message deleted",
136 "RSET" => "+OK Reset",
137 "NOOP" => "+OK",
138 "QUIT" => {
139 writeln!(stream, "+OK Bye")?;
140 stream.flush()?;
141 break;
142 }
143 _ => "-ERR Unknown command",
144 };
145
146 writeln!(stream, "{}", response)?;
147 stream.flush()?;
148 }
149
150 Ok(())
151 }
152
153 fn stop(&self) {
154 *self.running.lock().unwrap() = false;
155 }
156 }
157
158 /// Test 1: Basic connection and authentication
159 fn test_basic_authentication() -> TestResult {
160 println!("\n=== Test 1: Basic Authentication ===");
161
162 let server = MockPop3Server::new(11001)?;
163 server.start()?;
164
165 thread::sleep(Duration::from_millis(100));
166
167 let mut client = TestPop3Client::connect("127.0.0.1:11001")?;
168
169 client.send_command("USER testuser")?;
170 let response = client.read_response()?;
171 assert!(response.starts_with("+OK"), "USER failed: {}", response);
172
173 client.send_command("PASS testpass")?;
174 let response = client.read_response()?;
175 assert!(response.starts_with("+OK"), "PASS failed: {}", response);
176
177 client.send_command("QUIT")?;
178 let _ = client.read_response();
179
180 server.stop();
181
182 println!("✓ Authentication test passed");
183 Ok(())
184 }
185
186 /// Test 2: Message listing and retrieval
187 fn test_message_operations() -> TestResult {
188 println!("\n=== Test 2: Message Operations ===");
189
190 let server = MockPop3Server::new(11002)?;
191 server.start()?;
192
193 thread::sleep(Duration::from_millis(100));
194
195 let mut client = TestPop3Client::connect("127.0.0.1:11002")?;
196
197 // Authenticate
198 client.send_command("USER testuser")?;
199 let _ = client.read_response()?;
200 client.send_command("PASS testpass")?;
201 let _ = client.read_response()?;
202
203 // Get statistics
204 client.send_command("STAT")?;
205 let response = client.read_response()?;
206 assert!(response.contains("2 640"), "STAT failed: {}", response);
207
208 // List messages
209 client.send_command("LIST")?;
210 let _ = client.read_response()?;
211 let messages = client.read_multiline()?;
212 assert!(messages.len() >= 2, "LIST failed: expected 2 messages");
213
214 // Retrieve message
215 client.send_command("RETR 1")?;
216 let _ = client.read_response()?;
217 let content = client.read_multiline()?;
218 assert!(!content.is_empty(), "RETR failed: empty message");
219
220 client.send_command("QUIT")?;
221 let _ = client.read_response();
222
223 server.stop();
224
225 println!("✓ Message operations test passed");
226 Ok(())
227 }
228
229 /// Test 3: Message deletion workflow
230 fn test_deletion_workflow() -> TestResult {
231 println!("\n=== Test 3: Deletion Workflow ===");
232
233 let server = MockPop3Server::new(11003)?;
234 server.start()?;
235
236 thread::sleep(Duration::from_millis(100));
237
238 let mut client = TestPop3Client::connect("127.0.0.1:11003")?;
239
240 // Authenticate
241 client.send_command("USER testuser")?;
242 let _ = client.read_response()?;
243 client.send_command("PASS testpass")?;
244 let _ = client.read_response()?;
245
246 // Delete message
247 client.send_command("DELE 1")?;
248 let response = client.read_response()?;
249 assert!(response.starts_with("+OK"), "DELE failed: {}", response);
250
251 // Reset
252 client.send_command("RSET")?;
253 let response = client.read_response()?;
254 assert!(response.starts_with("+OK"), "RSET failed: {}", response);
255
256 // Delete again
257 client.send_command("DELE 1")?;
258 let response = client.read_response()?;
259 assert!(response.starts_with("+OK"), "DELE failed: {}", response);
260
261 client.send_command("QUIT")?;
262 let _ = client.read_response();
263
264 server.stop();
265
266 println!("✓ Deletion workflow test passed");
267 Ok(())
268 }
269
270 /// Test 4: Error handling
271 fn test_error_handling() -> TestResult {
272 println!("\n=== Test 4: Error Handling ===");
273
274 // Test connection to non-existent server
275 println!(" Testing connection to non-existent server...");
276 match TcpStream::connect_timeout(
277 &"127.0.0.1:9999".parse().unwrap(),
278 Duration::from_secs(1)
279 ) {
280 Ok(_) => return Err("Should have failed to connect".into()),
281 Err(_) => println!(" ✓ Connection refused as expected"),
282 }
283
284 println!("✓ Error handling test passed");
285 Ok(())
286 }
287
288 /// Test 5: Complete session workflow
289 fn test_complete_session() -> TestResult {
290 println!("\n=== Test 5: Complete Session Workflow ===");
291
292 let server = MockPop3Server::new(11005)?;
293 server.start()?;
294
295 thread::sleep(Duration::from_millis(100));
296
297 let mut client = TestPop3Client::connect("127.0.0.1:11005")?;
298
299 // Full workflow
300 let commands = vec![
301 ("USER testuser", "+OK"),
302 ("PASS testpass", "+OK"),
303 ("STAT", "+OK"),
304 ("LIST", "+OK"),
305 ("NOOP", "+OK"),
306 ("QUIT", "+OK"),
307 ];
308
309 for (cmd, expected) in commands {
310 client.send_command(cmd)?;
311 let response = client.read_response()?;
312
313 // Handle multiline responses
314 if cmd == "LIST" {
315 let _ = client.read_multiline()?;
316 }
317
318 assert!(
319 response.starts_with(expected),
320 "{} failed: expected '{}', got '{}'",
321 cmd, expected, response
322 );
323
324 if cmd == "QUIT" {
325 break;
326 }
327 }
328
329 server.stop();
330
331 println!("✓ Complete session test passed");
332 Ok(())
333 }
334
335 /// Test 6: Concurrent connections
336 fn test_concurrent_connections() -> TestResult {
337 println!("\n=== Test 6: Concurrent Connections ===");
338
339 let server = MockPop3Server::new(11006)?;
340 server.start()?;
341
342 thread::sleep(Duration::from_millis(100));
343
344 let mut handles = vec![];
345
346 for i in 0..5 {
347 let handle = thread::spawn(move || -> TestResult {
348 let mut client = TestPop3Client::connect("127.0.0.1:11006")?;
349
350 client.send_command("USER testuser")?;
351 let _ = client.read_response()?;
352 client.send_command("PASS testpass")?;
353 let _ = client.read_response()?;
354 client.send_command("STAT")?;
355 let _ = client.read_response()?;
356 client.send_command("QUIT")?;
357 let _ = client.read_response();
358
359 println!(" ✓ Client {} completed", i + 1);
360 Ok(())
361 });
362 handles.push(handle);
363 }
364
365 for handle in handles {
366 handle.join().unwrap()?;
367 }
368
369 server.stop();
370
371 println!("✓ Concurrent connections test passed");
372 Ok(())
373 }
374
375 /// Test 7: Command sequencing
376 fn test_command_sequencing() -> TestResult {
377 println!("\n=== Test 7: Command Sequencing ===");
378
379 let server = MockPop3Server::new(11007)?;
380 server.start()?;
381
382 thread::sleep(Duration::from_millis(100));
383
384 let mut client = TestPop3Client::connect("127.0.0.1:11007")?;
385
386 // Test proper sequence
387 client.send_command("USER testuser")?;
388 let _ = client.read_response()?;
389 client.send_command("PASS testpass")?;
390 let _ = client.read_response()?;
391
392 // Commands that require authentication
393 let commands = vec!["STAT", "LIST", "NOOP"];
394 for cmd in commands {
395 client.send_command(cmd)?;
396 let response = client.read_response()?;
397 if cmd == "LIST" {
398 let _ = client.read_multiline()?;
399 }
400 assert!(response.starts_with("+OK"), "{} failed: {}", cmd, response);
401 }
402
403 client.send_command("QUIT")?;
404 let _ = client.read_response();
405
406 server.stop();
407
408 println!("✓ Command sequencing test passed");
409 Ok(())
410 }
411
412 fn main() {
413 println!("========================================");
414 println!("POP3 Integration Test Examples");
415 println!("========================================");
416 println!("\nRunning comprehensive integration tests...\n");
417
418 let tests: Vec<(&str, fn() -> TestResult)> = vec![
419 ("Basic Authentication", test_basic_authentication),
420 ("Message Operations", test_message_operations),
421 ("Deletion Workflow", test_deletion_workflow),
422 ("Error Handling", test_error_handling),
423 ("Complete Session", test_complete_session),
424 ("Concurrent Connections", test_concurrent_connections),
425 ("Command Sequencing", test_command_sequencing),
426 ];
427
428 let mut passed = 0;
429 let mut failed = 0;
430
431 for (name, test_fn) in tests {
432 match test_fn() {
433 Ok(_) => {
434 passed += 1;
435 }
436 Err(e) => {
437 println!("✗ Test '{}' failed: {}", name, e);
438 failed += 1;
439 }
440 }
441 }
442
443 println!("\n========================================");
444 println!("Test Results");
445 println!("========================================");
446 println!("Passed: {}", passed);
447 println!("Failed: {}", failed);
448 println!("Total: {}", passed + failed);
449
450 if failed == 0 {
451 println!("\n🎉 All tests passed!");
452 } else {
453 println!("\n⚠️ Some tests failed!");
454 }
455 }