kirino 0.7.6

Zero-trust authentication and authorization framework with RBAC and dynamic risk assessment.
Documentation
use anyhow::Result;
use std::time::Duration;

use super::code_store::{generate_numeric_code, VerificationCodeStore};

const DEFAULT_TTL: Duration = Duration::from_secs(300);

pub struct SmsVerifier {
    sender: String,
    store: VerificationCodeStore,
}

impl SmsVerifier {
    #[must_use]
    pub fn new(sender: String) -> Self {
        Self {
            sender,
            store: VerificationCodeStore::new(),
        }
    }

    pub async fn send_code(&self, phone: &str, code: &str) -> Result<()> {
        self.send_code_with_ttl(phone, code, DEFAULT_TTL).await
    }

    pub async fn send_code_with_ttl(&self, phone: &str, code: &str, ttl: Duration) -> Result<()> {
        self.store.store(phone, code, ttl).await;
        tracing::debug!(
            target: "kirino::verify::sms",
            from = %self.sender,
            to = %phone,
            "SMS verification code would be sent"
        );
        Ok(())
    }

    pub async fn verify_code(&self, phone: &str, code: &str) -> Result<bool> {
        self.store.verify(phone, code).await
    }

    #[must_use]
    pub fn generate_code(len: usize) -> String {
        generate_numeric_code(len)
    }
}

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

    #[tokio::test]
    async fn test_send_and_verify() {
        let v = SmsVerifier::new("+1234567890".to_string());
        let code = SmsVerifier::generate_code(6);
        v.send_code("+9876543210", &code).await.unwrap();
        assert!(v.verify_code("+9876543210", &code).await.unwrap());
    }

    #[tokio::test]
    async fn test_wrong_code() {
        let v = SmsVerifier::new("+1234567890".to_string());
        v.send_code("+9876543210", "654321").await.unwrap();
        assert!(!v.verify_code("+9876543210", "000000").await.unwrap());
    }

    #[tokio::test]
    async fn test_unknown_phone() {
        let v = SmsVerifier::new("+1234567890".to_string());
        assert!(!v.verify_code("+0000000000", "123456").await.unwrap());
    }

    #[test]
    fn test_generate_code_length() {
        let code = SmsVerifier::generate_code(6);
        assert_eq!(code.len(), 6);
        assert!(code.chars().all(|c| c.is_ascii_digit()));
    }

    #[tokio::test]
    async fn test_code_single_use() {
        let v = SmsVerifier::new("+1234567890".to_string());
        v.send_code("+1111111111", "123456").await.unwrap();
        assert!(v.verify_code("+1111111111", "123456").await.unwrap());
        assert!(!v.verify_code("+1111111111", "123456").await.unwrap());
    }

    #[tokio::test]
    async fn test_multiple_phones() {
        let v = SmsVerifier::new("+1234567890".to_string());
        v.send_code("+111", "111111").await.unwrap();
        v.send_code("+222", "222222").await.unwrap();
        assert!(v.verify_code("+111", "111111").await.unwrap());
        assert!(v.verify_code("+222", "222222").await.unwrap());
    }

    #[tokio::test]
    async fn test_expired_code_rejected() {
        let v = SmsVerifier::new("+1234567890".to_string());
        v.send_code_with_ttl("+9876543210", "123456", Duration::from_millis(1))
            .await
            .unwrap();
        tokio::time::sleep(Duration::from_millis(10)).await;
        assert!(!v.verify_code("+9876543210", "123456").await.unwrap());
    }

    #[tokio::test]
    async fn test_resend_replaces_old_code() {
        let v = SmsVerifier::new("+1234567890".to_string());
        v.send_code("+9876543210", "111111").await.unwrap();
        v.send_code("+9876543210", "222222").await.unwrap();
        assert!(v.verify_code("+9876543210", "222222").await.unwrap());
    }

    #[test]
    fn test_generate_code_different_each_time() {
        let codes: Vec<String> = (0..10).map(|_| SmsVerifier::generate_code(6)).collect();
        let unique: std::collections::HashSet<_> = codes.iter().collect();
        assert!(
            unique.len() > 1,
            "generated codes should not all be identical"
        );
    }
}