proptest-http-message 0.1.0

Proptest strategies for generating HTTP request and response messages or individual components components
Documentation
//! URL authority's user info strategies.

use std::{ops::RangeInclusive, sync::LazyLock};

use array_concat::{concat_arrays, concat_arrays_size};
use proptest::prelude::Strategy;

use crate::request_line::target::components::{
  UNRESERVED, char_diff_intervals, safe_and_percent_encoded_char, url_chars_to_string,
};

static USER_INFO_UNSAFE_CHARS: LazyLock<Vec<RangeInclusive<char>>> =
  LazyLock::new(|| char_diff_intervals(&USER_INFO_SAFE_CHARS));

// even if RFC RFC 3986 states that:
// userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
// RFC 3986 explicitly notes that the user:password format in userinfo is deprecated due to security risks.
// Many parsers and systems are strict about special characters in credentials.
// which means we should not include sub_delims in USER_INFO_SAFE_CHARS
const USER_INFO_SAFE_CHARS: [char; concat_arrays_size!(UNRESERVED)] = concat_arrays!(UNRESERVED);

fn user_info_subcomponent() -> impl Strategy<Value = String> {
  proptest::collection::vec(
    safe_and_percent_encoded_char(&USER_INFO_SAFE_CHARS, &USER_INFO_UNSAFE_CHARS),
    0..=50,
  )
  .prop_map(url_chars_to_string)
}

#[derive(Debug)]
pub struct UserInfo {
  pub username: String,
  pub password: Option<String>,
}

/// URI authority's user information.
///
/// user info does not have a standard format, buf for HTTP, it usually takes the form:
/// > `<username>[:[<password>]]`.
///
/// where the password is optional, and can be an empty string.
/// # Returns
/// `UserInfo` along with it's representation.
pub fn user_info() -> impl Strategy<Value = (UserInfo, String)> {
  (user_info_subcomponent(), user_info_subcomponent()).prop_map(|(username, password)| {
    let repr = format!("{username}:{password}");
    // if password is empty replace it with None
    (UserInfo { username, password: if password.is_empty() { None } else { Some(password) } }, repr)
  })
}

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

  use super::*;
  proptest! {
    #[test]
    fn userinfo_works((_, repr) in user_info()) {
      println!("{repr}");
      assert!(repr.chars().all(|c| c == '%' || c == ':' || USER_INFO_SAFE_CHARS.contains(&c)));
    }
  }
}