digit-cli 0.3.0

A finger protocol client (RFC 1288 / RFC 742)
Documentation
use std::io::{Read, Write};
use std::net::TcpListener;
use std::thread;
use std::time::Duration;

use digit::protocol::{finger, finger_raw};
use digit::query::Query;

#[test]
fn read_timeout_reports_timed_out() {
    // Server accepts connection and reads the query, but never responds.
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind to ephemeral port");
    let port = listener.local_addr().unwrap().port();

    let handle = thread::spawn(move || {
        let (mut stream, _) = listener.accept().expect("accept connection");
        stream.set_read_timeout(Some(Duration::from_secs(5))).ok();

        // Read the query so the client's write succeeds.
        let mut buf = [0u8; 1024];
        let _ = stream.read(&mut buf);

        // Sleep longer than the client's timeout, then drop.
        thread::sleep(Duration::from_secs(5));
    });

    let q = Query::parse(Some(&format!("user@127.0.0.1")), false, port).expect("valid query");
    let result = finger(&q, Duration::from_secs(1), 1_048_576);

    assert!(result.is_err());
    let err = result.unwrap_err();
    let msg = format!("{}", err);
    assert!(
        msg.contains("timed out"),
        "expected 'timed out' but got: {}",
        msg
    );

    handle.join().expect("server thread");
}

/// Start a mock finger server that accepts one connection, reads the query,
/// and responds with `response`. Returns the port it's listening on and
/// a join handle. The `on_query` callback receives the raw query bytes.
fn mock_finger_server(
    response: &str,
    on_query: impl FnOnce(String) + Send + 'static,
) -> (u16, thread::JoinHandle<()>) {
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind to ephemeral port");
    let port = listener.local_addr().unwrap().port();
    let response = response.to_string();

    let handle = thread::spawn(move || {
        let (mut stream, _) = listener.accept().expect("accept connection");
        stream.set_read_timeout(Some(Duration::from_secs(2))).ok();

        let mut buf = Vec::new();
        let mut tmp = [0u8; 1024];
        // Read until we see \r\n (end of finger query).
        loop {
            match stream.read(&mut tmp) {
                Ok(0) => break,
                Ok(n) => {
                    buf.extend_from_slice(&tmp[..n]);
                    if buf.windows(2).any(|w| w == b"\r\n") {
                        break;
                    }
                }
                Err(_) => break,
            }
        }

        let query_str = String::from_utf8_lossy(&buf).into_owned();
        on_query(query_str);

        stream
            .write_all(response.as_bytes())
            .expect("write response");
        // Server closes connection to signal end of response.
    });

    (port, handle)
}

#[test]
fn end_to_end_user_query() {
    let (port, handle) = mock_finger_server("Login: user\r\nName: Test User\r\n", |query| {
        assert_eq!(query, "user\r\n");
    });

    let q = Query::parse(Some(&format!("user@127.0.0.1")), false, port).expect("valid query");
    let result = finger(&q, Duration::from_secs(5), 1_048_576).expect("finger should succeed");

    assert!(result.contains("Login: user"));
    assert!(result.contains("Test User"));

    handle.join().expect("server thread");
}

#[test]
fn end_to_end_list_users() {
    let (port, handle) = mock_finger_server("user1\r\nuser2\r\n", |query| {
        assert_eq!(query, "\r\n");
    });

    let q = Query::parse(Some(&format!("@127.0.0.1")), false, port).expect("valid query");
    let result = finger(&q, Duration::from_secs(5), 1_048_576).expect("finger should succeed");

    assert!(result.contains("user1"));
    assert!(result.contains("user2"));

    handle.join().expect("server thread");
}

#[test]
fn end_to_end_verbose_query() {
    let (port, handle) = mock_finger_server("Verbose info\r\n", |query| {
        assert_eq!(query, "/W user\r\n");
    });

    let q = Query::parse(Some(&format!("user@127.0.0.1")), true, port).expect("valid query");
    let result = finger(&q, Duration::from_secs(5), 1_048_576).expect("finger should succeed");

    assert!(result.contains("Verbose info"));

    handle.join().expect("server thread");
}

#[test]
fn connection_refused_returns_error() {
    // Connect to a port that nothing is listening on.
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
    let port = listener.local_addr().unwrap().port();
    drop(listener); // Close it so the port is free but nothing is listening.

    let q = Query::parse(Some(&format!("user@127.0.0.1")), false, port).expect("valid query");
    let result = finger(&q, Duration::from_secs(2), 1_048_576);

    assert!(result.is_err());
    let err = result.unwrap_err();
    let msg = format!("{}", err);
    assert!(
        msg.contains("could not connect"),
        "unexpected error: {}",
        msg
    );
}

#[test]
fn utf8_lossy_handles_invalid_bytes() {
    // Server sends bytes that aren't valid UTF-8.
    let response_bytes: Vec<u8> = vec![72, 101, 108, 108, 111, 0xFF, 0xFE, 10];
    let response_str = unsafe { String::from_utf8_unchecked(response_bytes) };

    let (port, handle) = mock_finger_server(&response_str, |_| {});

    let q = Query::parse(Some(&format!("user@127.0.0.1")), false, port).expect("valid query");
    let result = finger(&q, Duration::from_secs(5), 1_048_576).expect("finger should succeed");

    // The valid "Hello" portion should be present.
    assert!(result.contains("Hello"));
    // Invalid bytes should be replaced with the replacement character.
    assert!(result.contains('\u{FFFD}'));

    handle.join().expect("server thread");
}

#[test]
fn response_capped_at_max_size() {
    let big_response = "X".repeat(1000);
    let (port, handle) = mock_finger_server(&big_response, |_| {});

    let q = Query::parse(Some(&format!("user@127.0.0.1")), false, port).expect("valid query");
    let result = finger(&q, Duration::from_secs(5), 100).expect("finger should succeed");

    assert_eq!(result.len(), 100);
    assert!(result.chars().all(|c| c == 'X'));

    handle.join().expect("server thread");
}

#[test]
fn finger_raw_returns_bytes() {
    let (port, handle) = mock_finger_server("Hello\r\n", |_| {});

    let q = Query::parse(Some(&format!("user@127.0.0.1")), false, port).expect("valid query");
    let result =
        finger_raw(&q, Duration::from_secs(5), 1_048_576).expect("finger_raw should succeed");

    assert_eq!(result, b"Hello\r\n");

    handle.join().expect("server thread");
}

#[test]
fn finger_raw_preserves_invalid_utf8() {
    let response_bytes: Vec<u8> = vec![72, 101, 108, 108, 111, 0xFF, 0xFE, 10];
    let response_str = unsafe { String::from_utf8_unchecked(response_bytes.clone()) };

    let (port, handle) = mock_finger_server(&response_str, |_| {});

    let q = Query::parse(Some(&format!("user@127.0.0.1")), false, port).expect("valid query");
    let result =
        finger_raw(&q, Duration::from_secs(5), 1_048_576).expect("finger_raw should succeed");

    assert_eq!(result, response_bytes);

    handle.join().expect("server thread");
}