Skip to main content

keyring_core/
mock.rs

1/*!
2
3# Mock credential store
4
5To facilitate testing of clients, this crate provides a Mock credential store
6that is platform-independent, provides no persistence, and allows the client
7to specify the return values (including errors) for each call. The credentials
8in this store have no attributes at all.
9
10To use this credential store instead of the default, make this call during
11application startup _before_ creating any entries:
12```rust
13keyring_core::set_default_store(keyring_core::mock::Store::new().unwrap());
14```
15
16You can then create entries as usual and call their usual methods
17to set, get, and delete passwords. There is no persistence except in-memory
18so, once you drop the store, all the credentials will be gone.
19
20If you want a method call on an entry to fail in a specific way, you can
21downcast the entry to a [Cred] and then call [set_error](Cred::set_error)
22with the appropriate error.  The next entry method called on the credential
23will fail with the error you set.  The error will then be cleared, so the next
24call on the mock will operate as usual.  Setting an error will not affect
25the value of the credential (if any). Here's a complete example:
26
27```rust
28# use keyring_core::{Entry, Error, mock};
29keyring_core::set_default_store(mock::Store::new().unwrap());
30let entry = Entry::new("service", "user").unwrap();
31entry.set_password("test").expect("the entry's password is now test");
32let mock: &mock::Cred = entry.as_any().downcast_ref().unwrap();
33mock.set_error(Error::Invalid("mock error".to_string(), "takes precedence".to_string()));
34_ = entry.get_password().expect_err("the error will be returned");
35let val = entry.get_password().expect("the error has been cleared");
36assert_eq!(val, "test", "the error did not affect the password");
37```
38
39 */
40use std::cell::RefCell;
41use std::collections::HashMap;
42use std::sync::{Arc, Mutex};
43use std::time::{Duration, SystemTime, UNIX_EPOCH};
44
45use crate::api::{CredentialApi, CredentialStoreApi};
46use crate::{Credential, CredentialPersistence, Entry, Error, Result};
47
48/// The concrete mock credential
49///
50/// Mocks use an internal mutability pattern since entries are read-only.
51/// The mutex is used to make sure these are Sync.
52#[derive(Debug)]
53pub struct Cred {
54    pub specifiers: (String, String),
55    pub inner: Mutex<RefCell<CredData>>,
56}
57
58/// The (in-memory) persisted data for a mock credential.
59///
60/// We keep a password but, unlike most credentials stores,
61/// we also keep an intended error to return on the next call.
62///
63/// (Everything about this structure is public for transparency.
64/// Most credential store implementations hide their internals.)
65#[derive(Debug, Default)]
66pub struct CredData {
67    pub secret: Option<Vec<u8>>,
68    pub error: Option<Error>,
69}
70
71impl CredentialApi for Cred {
72    /// See the API docs.
73    ///
74    /// If there is an error in the mock, it will be returned
75    /// and the secret will _not_ be set.  The error will
76    /// be cleared, so calling again will set the secret.
77    fn set_secret(&self, secret: &[u8]) -> Result<()> {
78        let mut inner = self
79            .inner
80            .lock()
81            .expect("Can't access mock data for set_secret: please report a bug!");
82        let data = inner.get_mut();
83        let err = data.error.take();
84        match err {
85            None => {
86                data.secret = Some(secret.to_vec());
87                Ok(())
88            }
89            Some(err) => Err(err),
90        }
91    }
92
93    /// See the API docs.
94    ///
95    /// If there is an error set in the mock, it will
96    /// be returned instead of a secret. The existing
97    /// secret will not change.
98    fn get_secret(&self) -> Result<Vec<u8>> {
99        let mut inner = self
100            .inner
101            .lock()
102            .expect("Can't access mock data for get: please report a bug!");
103        let data = inner.get_mut();
104        let err = data.error.take();
105        match err {
106            None => match &data.secret {
107                None => Err(Error::NoEntry),
108                Some(val) => Ok(val.clone()),
109            },
110            Some(err) => Err(err),
111        }
112    }
113
114    /// See the API docs.
115    ///
116    /// If there is an error, it will be returned and
117    /// cleared. Calling again will delete the cred.
118    fn delete_credential(&self) -> Result<()> {
119        let mut inner = self
120            .inner
121            .lock()
122            .expect("Can't access mock data for delete: please report a bug!");
123        let data = inner.get_mut();
124        let err = data.error.take();
125        match err {
126            None => match data.secret {
127                Some(_) => {
128                    data.secret = None;
129                    Ok(())
130                }
131                None => Err(Error::NoEntry),
132            },
133            Some(err) => Err(err),
134        }
135    }
136
137    /// See the API docs.
138    ///
139    /// If there is an error in the mock, it's returned instead and cleared.
140    /// Calling again will retry the operation.
141    fn get_credential(&self) -> Result<Option<Arc<Credential>>> {
142        let mut inner = self
143            .inner
144            .lock()
145            .expect("Can't access mock data for get_credential: please report a bug!");
146        let data = inner.get_mut();
147        let err = data.error.take();
148        match err {
149            None => match data.secret {
150                Some(_) => Ok(None),
151                None => Err(Error::NoEntry),
152            },
153            Some(err) => Err(err),
154        }
155    }
156
157    /// See the API docs.
158    fn get_specifiers(&self) -> Option<(String, String)> {
159        Some(self.specifiers.clone())
160    }
161
162    /// Return this mock credential concrete object
163    /// wrapped in the [Any](std::any::Any) trait,
164    /// so it can be downcast.
165    fn as_any(&self) -> &dyn std::any::Any {
166        self
167    }
168
169    /// Expose the concrete debug formatter for use via the [Credential] trait
170    fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        std::fmt::Debug::fmt(self, f)
172    }
173}
174
175impl Cred {
176    /// Set an error to be returned from this mock credential.
177    ///
178    /// Error returns always take precedence over the normal
179    /// behavior of the mock.  But once an error has been
180    /// returned, it is removed, so the mock works thereafter.
181    pub fn set_error(&self, err: Error) {
182        let mut inner = self
183            .inner
184            .lock()
185            .expect("Can't access mock data for set_error: please report a bug!");
186        let data = inner.get_mut();
187        data.error = Some(err);
188    }
189}
190
191/// The builder for mock credentials.
192///
193/// We keep them in a vector so we can reuse them
194/// for entries with the same service and user.
195/// Yes, a hashmap might be faster, but this is
196/// way simpler.
197pub struct Store {
198    pub id: String,
199    pub inner: Mutex<RefCell<Vec<Arc<Cred>>>>,
200}
201
202impl std::fmt::Debug for Store {
203    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204        f.debug_struct("Store")
205            .field("vendor", &self.vendor())
206            .field("id", &self.id)
207            .finish()
208    }
209}
210
211impl Store {
212    pub fn new() -> Result<Arc<Self>> {
213        Ok(Arc::new(Store {
214            id: format!(
215                "Crate version {}, Instantiated at {}",
216                env!("CARGO_PKG_VERSION"),
217                SystemTime::now()
218                    .duration_since(UNIX_EPOCH)
219                    .unwrap_or_else(|_| Duration::new(0, 0))
220                    .as_secs_f64()
221            ),
222            inner: Mutex::new(RefCell::new(Vec::new())),
223        }))
224    }
225}
226
227impl CredentialStoreApi for Store {
228    fn vendor(&self) -> String {
229        String::from("Mock store, https://crates.io/crates/keyring-core")
230    }
231
232    fn id(&self) -> String {
233        self.id.clone()
234    }
235
236    /// Build a mock credential for the service and user. No modifiers are allowed.
237    ///
238    /// Since mocks don't persist beyond the life of their entry, all mocks
239    /// start off without passwords.
240    fn build(
241        &self,
242        service: &str,
243        user: &str,
244        mods: Option<&HashMap<&str, &str>>,
245    ) -> Result<Entry> {
246        if mods.is_some_and(|m| !m.is_empty()) {
247            let msg = "The mock store doesn't allow entry modifiers";
248            return Err(Error::NotSupportedByStore(msg.to_string()));
249        }
250        let mut inner = self
251            .inner
252            .lock()
253            .expect("Can't access mock store data: please report a bug!");
254        let creds = inner.get_mut();
255        for cred in creds.iter() {
256            if service == cred.specifiers.0 && user == cred.specifiers.1 {
257                return Ok(Entry {
258                    inner: cred.clone(),
259                });
260            }
261        }
262        let cred = Arc::new(Cred {
263            specifiers: (service.to_string(), user.to_string()),
264            inner: Mutex::new(RefCell::new(Default::default())),
265        });
266        creds.push(cred.clone());
267        Ok(Entry { inner: cred })
268    }
269
270    /// Search for mock credentials matching the spec.
271    ///
272    /// Attributes other than `service` and `user` are ignored.
273    /// Their values are used in unanchored substring searches against the specifier.
274    fn search(&self, spec: &HashMap<&str, &str>) -> Result<Vec<Entry>> {
275        let mut result: Vec<Entry> = Vec::new();
276        let svc = spec.get("service").unwrap_or(&"");
277        let usr = spec.get("user").unwrap_or(&"");
278        let mut inner = self
279            .inner
280            .lock()
281            .expect("Can't access mock store data: please report a bug!");
282        let creds = inner.get_mut();
283        for cred in creds.iter() {
284            if !cred.specifiers.0.as_str().contains(svc) {
285                continue;
286            }
287            if !cred.specifiers.1.as_str().contains(usr) {
288                continue;
289            }
290            result.push(Entry {
291                inner: cred.clone(),
292            });
293        }
294        Ok(result)
295    }
296
297    /// Get an [Any][std::any::Any] reference to the mock credential builder.
298    fn as_any(&self) -> &dyn std::any::Any {
299        self
300    }
301
302    /// This keystore keeps the password in the entry!
303    fn persistence(&self) -> CredentialPersistence {
304        CredentialPersistence::ProcessOnly
305    }
306
307    /// Expose the concrete debug formatter
308    /// for use via the [CredentialStore](crate::CredentialStore) trait
309    fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310        std::fmt::Debug::fmt(self, f)
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use std::sync::{Arc, OnceLock};
317
318    use super::{Cred, HashMap, Store};
319    use crate::{CredentialPersistence, CredentialStore, Entry, Error};
320
321    static STORE: OnceLock<Arc<CredentialStore>> = OnceLock::new();
322
323    fn get_store() -> &'static Arc<CredentialStore> {
324        STORE.get_or_init(|| Store::new().unwrap())
325    }
326
327    #[test]
328    fn test_vendor_and_id() {
329        let s1 = get_store();
330        let s2 = Store::new().unwrap() as Arc<CredentialStore>;
331        assert_eq!(
332            s1.vendor(),
333            "Mock store, https://crates.io/crates/keyring-core"
334        );
335        assert_eq!(s1.vendor(), s2.vendor());
336        assert_ne!(s1.id(), s2.id());
337    }
338
339    #[test]
340    fn test_modifiers() {
341        let empty_mods = HashMap::new();
342        let name = generate_random_string();
343        get_store().build(&name, &name, Some(&empty_mods)).unwrap();
344        let non_empty_mods = HashMap::from([("mod-arg", "mod-val")]);
345        assert!(matches!(
346            get_store().build("fail", "fail", Some(&non_empty_mods)),
347            Err(Error::NotSupportedByStore(_))
348        ));
349    }
350
351    fn entry_new(service: &str, user: &str) -> Entry {
352        get_store()
353            .build(service, user, None)
354            .unwrap_or_else(|err| {
355                panic!("Couldn't create entry (service: {service}, user: {user}): {err:?}")
356            })
357    }
358
359    fn generate_random_string() -> String {
360        use fastrand;
361        use std::iter::repeat_with;
362        repeat_with(fastrand::alphanumeric).take(30).collect()
363    }
364
365    fn generate_random_bytes() -> Vec<u8> {
366        use fastrand;
367        use std::iter::repeat_with;
368        repeat_with(|| fastrand::u8(..)).take(24).collect()
369    }
370
371    // A round-trip password test that doesn't delete the credential afterward
372    fn test_round_trip_no_delete(case: &str, entry: &Entry, in_pass: &str) {
373        entry
374            .set_password(in_pass)
375            .unwrap_or_else(|err| panic!("Can't set password: {case}: {err:?}"));
376        let out_pass = entry
377            .get_password()
378            .unwrap_or_else(|err| panic!("Can't get password: {case}: {err:?}"));
379        assert_eq!(
380            in_pass, out_pass,
381            "Passwords don't match for {case}: set='{in_pass}', get='{out_pass}'",
382        )
383    }
384
385    // A round-trip password test that does delete the credential afterward
386    fn test_round_trip(case: &str, entry: &Entry, in_pass: &str) {
387        test_round_trip_no_delete(case, entry, in_pass);
388        entry
389            .delete_credential()
390            .unwrap_or_else(|err| panic!("Can't delete password: {case}: {err:?}"));
391        let password = entry.get_password();
392        assert!(matches!(password, Err(Error::NoEntry)));
393    }
394
395    // A round-trip secret test that does delete the credential afterward
396    pub fn test_round_trip_secret(case: &str, entry: &Entry, in_secret: &[u8]) {
397        entry
398            .set_secret(in_secret)
399            .unwrap_or_else(|err| panic!("Can't set secret for {case}: {err:?}"));
400        let out_secret = entry
401            .get_secret()
402            .unwrap_or_else(|err| panic!("Can't get secret for {case}: {err:?}"));
403        assert_eq!(
404            in_secret, &out_secret,
405            "Secrets don't match for {case}: set='{in_secret:?}', get='{out_secret:?}'",
406        );
407        entry
408            .delete_credential()
409            .unwrap_or_else(|err| panic!("Can't delete credential for {case}: {err:?}"));
410        let secret = entry.get_secret();
411        assert!(matches!(secret, Err(Error::NoEntry)));
412    }
413
414    #[test]
415    fn test_empty_service_and_user() {
416        let name = generate_random_string();
417        let in_pass = "value doesn't matter";
418        test_round_trip("empty user", &entry_new(&name, ""), in_pass);
419        test_round_trip("empty service", &entry_new("", &name), in_pass);
420        test_round_trip("empty service and user", &entry_new("", ""), in_pass);
421    }
422
423    #[test]
424    fn test_empty_password() {
425        let name = generate_random_string();
426        let in_pass = "";
427        test_round_trip("empty password", &entry_new(&name, &name), in_pass);
428    }
429
430    #[test]
431    fn test_missing_entry() {
432        let name = generate_random_string();
433        let entry = entry_new(&name, &name);
434        assert!(matches!(entry.get_password(), Err(Error::NoEntry)))
435    }
436
437    #[test]
438    fn test_round_trip_ascii_password() {
439        let name = generate_random_string();
440        let entry = entry_new(&name, &name);
441        test_round_trip("ascii password", &entry, "test ascii password");
442    }
443
444    #[test]
445    fn test_round_trip_non_ascii_password() {
446        let name = generate_random_string();
447        let entry = entry_new(&name, &name);
448        test_round_trip("non-ascii password", &entry, "このきれいな花は桜です");
449    }
450
451    #[test]
452    fn test_entries_with_same_and_different_specifiers() {
453        let name1 = generate_random_string();
454        let name2 = generate_random_string();
455        let entry1 = entry_new(&name1, &name2);
456        let entry2 = entry_new(&name1, &name2);
457        let entry3 = entry_new(&name2, &name1);
458        entry1.set_password("test password").unwrap();
459        let pw2 = entry2.get_password().unwrap();
460        assert_eq!(pw2, "test password");
461        _ = entry3.get_password().unwrap_err();
462        entry1.delete_credential().unwrap();
463        _ = entry2.get_password().unwrap_err();
464        entry3.delete_credential().unwrap_err();
465    }
466
467    #[test]
468    fn test_get_credential_and_specifiers() {
469        let name = generate_random_string();
470        let entry1 = entry_new(&name, &name);
471        assert!(matches!(entry1.get_credential(), Err(Error::NoEntry)));
472        entry1.set_password("password for entry1").unwrap();
473        let wrapper = entry1.get_credential().unwrap();
474        let (service, user) = wrapper.get_specifiers().unwrap();
475        assert_eq!(service, name);
476        assert_eq!(user, name);
477        wrapper.delete_credential().unwrap();
478        entry1.delete_credential().unwrap_err();
479        wrapper.delete_credential().unwrap_err();
480    }
481
482    #[test]
483    fn test_round_trip_random_secret() {
484        let name = generate_random_string();
485        let entry = entry_new(&name, &name);
486        let secret = generate_random_bytes();
487        test_round_trip_secret("non-ascii password", &entry, secret.as_slice());
488    }
489
490    #[test]
491    fn test_update() {
492        let name = generate_random_string();
493        let entry = entry_new(&name, &name);
494        test_round_trip_no_delete("initial ascii password", &entry, "test ascii password");
495        test_round_trip(
496            "updated non-ascii password",
497            &entry,
498            "このきれいな花は桜です",
499        );
500    }
501
502    #[test]
503    fn test_set_error() {
504        let name = generate_random_string();
505        let entry = entry_new(&name, &name);
506        let password = "test ascii password";
507        let mock: &Cred = entry.inner.as_any().downcast_ref().unwrap();
508        mock.set_error(Error::Invalid(
509            "mock error".to_string(),
510            "is an error".to_string(),
511        ));
512        assert!(matches!(
513            entry.set_password(password),
514            Err(Error::Invalid(_, _))
515        ));
516        entry.set_password(password).unwrap();
517        mock.set_error(Error::NoEntry);
518        assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
519        let stored_password = entry.get_password().unwrap();
520        assert_eq!(stored_password, password);
521        mock.set_error(Error::TooLong("mock".to_string(), 3));
522        assert!(matches!(
523            entry.delete_credential(),
524            Err(Error::TooLong(_, 3))
525        ));
526        entry.delete_credential().unwrap();
527        assert!(matches!(entry.get_password(), Err(Error::NoEntry)))
528    }
529
530    #[test]
531    fn test_get_update_attributes() {
532        let name = generate_random_string();
533        let entry = entry_new(&name, &name);
534        assert!(matches!(entry.get_attributes(), Err(Error::NoEntry)));
535        entry.set_password("password").unwrap();
536        let attrs = entry.get_attributes().unwrap();
537        assert_eq!(attrs.len(), 0);
538        assert!(matches!(
539            entry.update_attributes(&HashMap::new()),
540            Err(Error::NotSupportedByStore(_))
541        ));
542        entry.delete_credential().unwrap();
543    }
544
545    #[test]
546    fn test_search() {
547        let store: Arc<CredentialStore> = Store::new().unwrap();
548        let all = store.search(&HashMap::from([])).unwrap();
549        assert!(all.is_empty());
550        let all = store
551            .search(&HashMap::from([("service", ""), ("user", "")]))
552            .unwrap();
553        assert!(all.is_empty());
554        let e1 = store.build("foo", "bar", None).unwrap();
555        e1.set_password("e1").unwrap();
556        let all = store.search(&HashMap::from([])).unwrap();
557        assert_eq!(all.len(), 1);
558        let all = store
559            .search(&HashMap::from([("service", ""), ("user", "")]))
560            .unwrap();
561        assert_eq!(all.len(), 1);
562        let e2 = store.build("foo", "bam", None).unwrap();
563        e2.set_password("e2").unwrap();
564        let one = store.search(&HashMap::from([("user", "m")])).unwrap();
565        assert_eq!(one.len(), 1);
566        let one = store
567            .search(&HashMap::from([("service", "foo"), ("user", "bar")]))
568            .unwrap();
569        assert_eq!(one.len(), 1);
570        let two = store.search(&HashMap::from([("service", "foo")])).unwrap();
571        assert_eq!(two.len(), 2);
572        let all = store.search(&HashMap::from([("foo", "bar")])).unwrap();
573        assert_eq!(all.len(), 2);
574    }
575
576    #[test]
577    fn test_persistence() {
578        let store: Arc<CredentialStore> = Store::new().unwrap();
579        assert!(matches!(
580            store.persistence(),
581            CredentialPersistence::ProcessOnly
582        ))
583    }
584}