etrade 0.6.0

Wraps the etrade API and implements the oauth flows
Documentation
use std::collections::HashMap;

use async_trait::async_trait;
use secstr::SecUtf8;

use crate::Store;
use anyhow::{anyhow, Result};
use secret_service::{EncryptionType, SecretService};

#[derive(Debug)]
pub struct KeychainStore;

impl KeychainStore {
  pub async fn new() -> Result<Self> {
    Ok(Self)
  }
}

#[async_trait]
impl Store for KeychainStore {
  async fn put(
    &self,
    namespace: impl Into<String> + Send,
    key: impl Into<String> + Send,
    value: impl Into<SecUtf8> + Send,
  ) -> Result<()> {
    let ns = namespace.into();
    let k = key.into();
    let label = format!("secret for etradectl {}@{}", &k, &ns);
    let svc = SecretService::connect(EncryptionType::Dh)
      .await
      .map_err(|e| anyhow!("failed to acquire secret service: {}", e))?;
    let coll = svc
      .get_default_collection()
      .await
      .map_err(|e| anyhow!("failed to acquire secret service collection: {}", e))?;
    coll
      .create_item(
        &label,
        vec![(&ns as &str, &k as &str)].into_iter().collect(),
        value.into().unsecure().as_bytes(),
        true,
        "text/plain",
      )
      .await
      .map(|_| ())
      .map_err(|e| anyhow!("failed to create secret: {}", e))
  }

  async fn del(&self, namespace: impl AsRef<str> + Send, key: impl AsRef<str> + Send) -> Result<()> {
    let svc = SecretService::connect(EncryptionType::Dh)
      .await
      .map_err(|e| anyhow!("failed to acquire secret service: {}", e))?;

    let coll = svc
      .get_default_collection()
      .await
      .map_err(|e| anyhow!("failed to acquire secret service collection: {}", e))?;

    let ns_key = (namespace.as_ref(), key.as_ref());

    let results = coll
      .search_items(HashMap::from([ns_key]))
      .await
      .map_err(|e| anyhow!("failed to find secret ({}:{}): {}", namespace.as_ref(), key.as_ref(), e))?;

    match results.get(0) {
      Some(item) => item
        .delete()
        .await
        .map_err(|e| anyhow!("failed to delete secret {}", e)),
      _ => Ok(()),
    }
  }

  async fn get(&self, namespace: impl AsRef<str> + Send, key: impl AsRef<str> + Send) -> Result<Option<SecUtf8>> {
    let svc = SecretService::connect(EncryptionType::Dh)
      .await
      .map_err(|e| anyhow!("failed to acquire secret service: {}", e))?;
    let coll = svc
      .get_default_collection()
      .await
      .map_err(|e| anyhow!("failed to acquire secret service collection: {}", e))?;

    let ns_key = (namespace.as_ref(), key.as_ref());

    let results = coll
      .search_items(HashMap::from([ns_key]))
      .await
      .map_err(|e| anyhow!("failed to find secret ({}:{}): {}", namespace.as_ref(), key.as_ref(), e))?;

    match results.get(0) {
      Some(item) => {
        let secret = item
          .get_secret()
          .await
          .map_err(|e| anyhow!("failed to get secret: {}", e))?;

        if secret.is_empty() {
          return Ok(None);
        }
        Ok(Some(String::from_utf8(secret)?.into()))
      }
      None => Ok(None),
    }
  }
}

#[cfg(test)]
mod tests {
  use super::KeychainStore;
  use anyhow::Result;
  use secstr::SecUtf8;

  #[tokio::test]
  async fn test_secret_service_store() {
    verify_token_store(KeychainStore::new().await.unwrap()).await
  }

  async fn verify_token_store(token_store: impl crate::Store) {
    let expected: Result<SecUtf8> = Ok("hello".into());
    token_store.put("my_svc", "api_key", "hello").await.unwrap();
    assert_eq!(token_store.get("my_svc", "api_key").await.ok(), Some(expected.ok()));
    assert!(token_store.del("my_svc", "api_key").await.is_ok());
    assert!(token_store.get("my_svc", "api_key").await.unwrap().is_none());
  }
}