passay-rs 0.1.0

A password validation library inspired by the Java Passay library.
Documentation
use crate::rule::rule_result::RuleResult;
use crate::rule::{PasswordData, Rule};
use fancy_regex::Regex;
use std::collections::HashMap;

const ERROR_CODE: &str = "ALLOWED_MATCH";

/// Rule for determining if a password matches an allowed regular expression. Passwords must match
/// the expression or validation will fail.
pub struct AllowedRegex {
    regex: Regex,
}

impl AllowedRegex {
    /// Creates a new allowed regex rule.
    ///
    /// # Example
    ///
    /// ```
    ///  use fancy_regex::Regex;
    ///  use passay_rs::rule::allowed_regex::AllowedRegex;
    ///  use passay_rs::rule::PasswordData;
    ///  use passay_rs::rule::Rule;
    ///
    ///  let rule = AllowedRegex::from_regex(Regex::new("^[\\p{Alpha}]+\\d\\d\\d\\d$").unwrap());
    ///  let password = PasswordData::with_password("pwUiNh0248".to_string());
    ///  let result = rule.validate(&password);
    ///  assert!(result.valid());
    /// ```
    pub fn from_regex(regex: Regex) -> AllowedRegex {
        AllowedRegex { regex }
    }
    fn create_rule_result_detail_parameters(&self) -> HashMap<String, String> {
        let mut map = HashMap::with_capacity(1);
        map.insert("pattern".to_string(), self.regex.as_str().to_string());
        map
    }
}

impl Rule for AllowedRegex {
    fn validate(&self, password_data: &PasswordData) -> RuleResult {
        let mut result = RuleResult::default();
        let result1 = self.regex.is_match(password_data.password());
        if result1.is_err() {
            result.add_error(ERROR_CODE, None)
        } else if let Ok(b) = result1
            && !b
        {
            result.add_error(
                ERROR_CODE,
                Some(self.create_rule_result_detail_parameters()),
            )
        }
        result
    }
}

#[cfg(test)]
mod tests {
    use crate::rule::PasswordData;
    use crate::rule::allowed_regex::{AllowedRegex, ERROR_CODE};
    use crate::test::{RulePasswordTestItem, check_messages, check_passwords};
    use fancy_regex::{Regex, RegexBuilder};

    #[test]
    fn test_passwords() {
        let test_cases: Vec<RulePasswordTestItem> = vec![
            // test invalid password
            RulePasswordTestItem(
                Box::new(AllowedRegex::from_regex(
                    Regex::new("\\d\\d\\d\\d").unwrap(),
                )),
                PasswordData::with_password("p4zRcv8#n65".to_string()),
                vec![ERROR_CODE],
            ),
            // test entire password
            RulePasswordTestItem(
                Box::new(AllowedRegex::from_regex(
                    Regex::new("^[\\p{Alpha}]+\\d\\d\\d\\d$").unwrap(),
                )),
                PasswordData::with_password("pwUiNh0248".to_string()),
                vec![],
            ),
            // test find password
            RulePasswordTestItem(
                Box::new(AllowedRegex::from_regex(
                    Regex::new("\\d\\d\\d\\d").unwrap(),
                )),
                PasswordData::with_password("pwUiNh0248".to_string()),
                vec![],
            ),
            // test case-insensitive
            RulePasswordTestItem(
                Box::new(AllowedRegex::from_regex(
                    RegexBuilder::new("abcd").case_insensitive(true).build().unwrap(),
                )),
                PasswordData::with_password("pwUAbbCd0248xwK".to_string()),
                vec![ERROR_CODE],
            ),
            // test case-insensitive
            RulePasswordTestItem(
                Box::new(AllowedRegex::from_regex(Regex::new("(?i)abcd").unwrap())),
                PasswordData::with_password("pwUAbbCd0248xwK".to_string()),
                vec![ERROR_CODE],
            ),
            // test case-insensitive
            RulePasswordTestItem(
                Box::new(AllowedRegex::from_regex(
                    RegexBuilder::new("abcd").case_insensitive(true).build().unwrap(),
                )),
                PasswordData::with_password("pwUAbCd0248xwK".to_string()),
                vec![],
            ),
            // test case-insensitive
            RulePasswordTestItem(
                Box::new(AllowedRegex::from_regex(Regex::new("(?i)abcd").unwrap())),
                PasswordData::with_password("pwUAbCd0248xwK".to_string()),
                vec![],
            ),
        ];
        check_passwords(test_cases);
    }

    #[test]
    fn test_messages() {
        let test_cases: Vec<RulePasswordTestItem> = vec![
            RulePasswordTestItem(
                Box::new(AllowedRegex::from_regex(
                    Regex::new("\\d\\d\\d\\d").unwrap(),
                )),
                PasswordData::with_password("p4zRcv8#n65".to_string()),
                vec!["ALLOWED_MATCH,\"\\\\d\\\\d\\\\d\\\\d\""],
            ),
            RulePasswordTestItem(
                Box::new(AllowedRegex::from_regex(
                    RegexBuilder::new("abcd").case_insensitive(true).build().unwrap(),
                )),
                PasswordData::with_password("p4zRabCCdv8#n65".to_string()),
                vec!["ALLOWED_MATCH,\"abcd\""],
            ),
            RulePasswordTestItem(
                Box::new(AllowedRegex::from_regex(Regex::new("(?i)abcd").unwrap())),
                PasswordData::with_password("p4zRabCCdv8#n65".to_string()),
                vec!["ALLOWED_MATCH,\"(?i)abcd\""],
            ),
        ];
        check_messages(test_cases);
    }
}