wekan-cli 0.11.0

Agent-first, human-friendly command-line client for Wekan
Documentation
use std::io::BufRead;

use secrecy::SecretString;

use crate::error::AppError;

pub(super) fn read_raw_secret(mut reader: impl std::io::Read) -> Result<SecretString, AppError> {
    let mut value = String::new();
    reader
        .read_to_string(&mut value)
        .map_err(|_| AppError::invalid_input("could not read UTF-8 secret input from stdin"))?;
    Ok(SecretString::from(value))
}

pub(super) fn read_password_line(mut reader: impl BufRead) -> Result<SecretString, AppError> {
    let password = read_line(&mut reader, "password")?;
    super::validate_password(password, None)
}

pub(super) fn read_code_line(mut reader: impl BufRead) -> Result<SecretString, AppError> {
    let code = read_line(&mut reader, "two-factor code")?;
    super::validate_code(code)
}

fn read_line(reader: &mut impl BufRead, field: &str) -> Result<String, AppError> {
    let mut value = String::new();
    let bytes_read = reader.read_line(&mut value).map_err(|error| {
        AppError::invalid_input(format!("could not read the {field} from stdin: {error}"))
    })?;
    if bytes_read == 0 {
        return Err(AppError::invalid_input(format!(
            "standard input ended before a {field} was read"
        )));
    }
    if value.ends_with('\n') {
        value.pop();
        if value.ends_with('\r') {
            value.pop();
        }
    }
    Ok(value)
}

#[cfg(test)]
mod tests {
    use secrecy::ExposeSecret;

    use super::{read_code_line, read_password_line};

    #[test]
    fn raw_input_preserves_every_byte_and_accepts_empty_passwords() {
        for input in ["", "first\nsecond\r\n", " leading and trailing \n"] {
            let secret = super::read_raw_secret(input.as_bytes()).unwrap();
            assert_eq!(secret.expose_secret(), input);
        }
        assert!(super::read_raw_secret(&b"\xff"[..]).is_err());
    }

    #[test]
    fn removes_only_the_line_ending() {
        let password = read_password_line(&b"  keep spaces  \r\n"[..]).unwrap();
        assert_eq!(password.expose_secret(), "  keep spaces  ");
    }

    #[test]
    fn rejects_empty_input() {
        assert!(read_password_line(&b""[..]).is_err());
        assert!(read_password_line(&b"\n"[..]).is_err());
    }

    #[test]
    fn reads_password_and_code_lines_in_order() {
        let mut input = &b"  password  \r\n 123456 \n"[..];
        let password = read_password_line(&mut input).unwrap();
        let code = read_code_line(&mut input).unwrap();

        assert_eq!(password.expose_secret(), "  password  ");
        assert_eq!(code.expose_secret(), " 123456 ");
    }

    #[test]
    fn rejects_empty_or_missing_codes() {
        assert!(read_code_line(&b""[..]).is_err());
        assert!(read_code_line(&b"\r\n"[..]).is_err());
    }
}