Skip to main content

pidge_client/auth/
store.rs

1//! OS keychain access for OAuth tokens.
2//!
3//! Service name: "pidge". Account name: the user's email.
4//! The value is a JSON-serialized `TokenSet`.
5
6use crate::auth::tokens::TokenSet;
7use crate::error::ClientError;
8
9const SERVICE_NAME: &str = "pidge";
10
11pub struct KeychainStore;
12
13impl KeychainStore {
14    fn entry(email: &str) -> Result<keyring::Entry, ClientError> {
15        keyring::Entry::new(SERVICE_NAME, email).map_err(ClientError::Keychain)
16    }
17
18    /// Load tokens for an email. Returns `None` if there's no entry for that account.
19    pub fn load(email: &str) -> Result<Option<TokenSet>, ClientError> {
20        let entry = Self::entry(email)?;
21        match entry.get_password() {
22            Ok(blob) => {
23                let tokens: TokenSet = serde_json::from_str(&blob)?;
24                Ok(Some(tokens))
25            }
26            Err(keyring::Error::NoEntry) => Ok(None),
27            Err(e) => Err(ClientError::Keychain(e)),
28        }
29    }
30
31    /// Save tokens for an email, overwriting any existing entry.
32    pub fn save(email: &str, tokens: &TokenSet) -> Result<(), ClientError> {
33        let blob = serde_json::to_string(tokens)?;
34        let entry = Self::entry(email)?;
35        entry.set_password(&blob).map_err(ClientError::Keychain)
36    }
37
38    /// Remove tokens for an email. No-op if no entry exists.
39    pub fn delete(email: &str) -> Result<(), ClientError> {
40        let entry = Self::entry(email)?;
41        match entry.delete_credential() {
42            Ok(()) => Ok(()),
43            Err(keyring::Error::NoEntry) => Ok(()),
44            Err(e) => Err(ClientError::Keychain(e)),
45        }
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    // Keychain tests are platform-dependent and require credential-store backends.
52    // We trust the `keyring` crate's own integration tests for backend correctness
53    // and limit ourselves to a serialization-only test that doesn't touch the OS.
54
55    use super::*;
56    use chrono::{Duration, Utc};
57
58    #[test]
59    fn token_blob_serializes_and_deserializes() {
60        let t = TokenSet {
61            access_token: "abc".into(),
62            refresh_token: "xyz".into(),
63            expires_at: Utc::now() + Duration::seconds(3600),
64        };
65        let blob = serde_json::to_string(&t).unwrap();
66        let t2: TokenSet = serde_json::from_str(&blob).unwrap();
67        assert_eq!(t, t2);
68    }
69}