openlatch-client 0.1.5

The open-source security layer for AI agents — client forwarder
//! In-memory credential store for testing.

use std::sync::Mutex;

use secrecy::{ExposeSecret, SecretString};

use crate::error::OlError;

use super::{CredentialStore, ERR_NO_CREDENTIALS};

/// In-memory credential store backed by a `Mutex<Option<String>>`.
/// For testing only — credentials are lost when the store is dropped.
pub struct InMemoryCredentialStore {
    inner: Mutex<Option<String>>,
}

impl InMemoryCredentialStore {
    pub fn new() -> Self {
        Self {
            inner: Mutex::new(None),
        }
    }
}

impl Default for InMemoryCredentialStore {
    fn default() -> Self {
        Self::new()
    }
}

impl CredentialStore for InMemoryCredentialStore {
    fn store(&self, key: SecretString) -> Result<(), OlError> {
        let mut guard = self.inner.lock().unwrap();
        *guard = Some(key.expose_secret().to_string());
        Ok(())
    }

    fn retrieve(&self) -> Result<SecretString, OlError> {
        let guard = self.inner.lock().unwrap();
        match &*guard {
            Some(val) => Ok(SecretString::from(val.clone())),
            None => Err(OlError::new(ERR_NO_CREDENTIALS, "No credential stored")),
        }
    }

    fn delete(&self) -> Result<(), OlError> {
        let mut guard = self.inner.lock().unwrap();
        *guard = None;
        Ok(())
    }
}

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

    #[test]
    fn test_in_memory_store_then_retrieve_returns_same_value() {
        let store = InMemoryCredentialStore::new();
        store
            .store(SecretString::from("test-api-key".to_string()))
            .unwrap();
        let retrieved = store.retrieve().unwrap();
        assert_eq!(retrieved.expose_secret(), "test-api-key");
    }

    #[test]
    fn test_in_memory_delete_then_retrieve_returns_err() {
        let store = InMemoryCredentialStore::new();
        store
            .store(SecretString::from("some-key".to_string()))
            .unwrap();
        store.delete().unwrap();
        let result = store.retrieve();
        assert!(result.is_err());
        assert_eq!(result.unwrap_err().code, ERR_NO_CREDENTIALS);
    }

    #[test]
    fn test_in_memory_retrieve_on_empty_store_returns_err() {
        let store = InMemoryCredentialStore::new();
        let result = store.retrieve();
        assert!(result.is_err());
        assert_eq!(result.unwrap_err().code, ERR_NO_CREDENTIALS);
    }

    #[test]
    fn test_in_memory_store_overwrites_existing() {
        let store = InMemoryCredentialStore::new();
        store
            .store(SecretString::from("first".to_string()))
            .unwrap();
        store
            .store(SecretString::from("second".to_string()))
            .unwrap();
        let retrieved = store.retrieve().unwrap();
        assert_eq!(retrieved.expose_secret(), "second");
    }
}