baseid_wallet_core/
store.rs

1//! In-memory credential store for testing and development.
2
3use std::collections::BTreeMap;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::Mutex;
6
7use baseid_core::credential::CredentialSummary;
8use baseid_core::types::CredentialId;
9
10use crate::error::WalletError;
11use crate::holder::WalletStore;
12use crate::{AnyCredential, CredentialFilter, CredentialRecord};
13
14/// An in-memory credential store backed by a `BTreeMap`.
15///
16/// Suitable for testing and development. For production, use an encrypted
17/// store implementation backed by platform-specific secure storage.
18pub struct InMemoryStore {
19    credentials: Mutex<BTreeMap<String, CredentialRecord>>,
20    next_id: AtomicU64,
21}
22
23impl InMemoryStore {
24    /// Create a new empty in-memory store.
25    pub fn new() -> Self {
26        Self {
27            credentials: Mutex::new(BTreeMap::new()),
28            next_id: AtomicU64::new(1),
29        }
30    }
31
32    /// Returns the number of stored credentials.
33    pub fn len(&self) -> usize {
34        self.credentials.lock().unwrap().len()
35    }
36
37    /// Returns true if the store is empty.
38    pub fn is_empty(&self) -> bool {
39        self.len() == 0
40    }
41}
42
43impl Default for InMemoryStore {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49impl WalletStore for InMemoryStore {
50    async fn store_credential(&self, cred: AnyCredential) -> baseid_core::Result<CredentialId> {
51        let id_num = self.next_id.fetch_add(1, Ordering::SeqCst);
52        let id = CredentialId(format!("cred-{id_num}"));
53
54        let record = CredentialRecord {
55            id: id.clone(),
56            credential: cred,
57            raw: None,
58        };
59
60        self.credentials
61            .lock()
62            .unwrap()
63            .insert(id.0.clone(), record);
64
65        Ok(id)
66    }
67
68    async fn get_credential(&self, id: &CredentialId) -> baseid_core::Result<AnyCredential> {
69        let store = self.credentials.lock().unwrap();
70        let record = store
71            .get(&id.0)
72            .ok_or(WalletError::CredentialNotFound(id.clone()))?;
73        Ok(record.credential.clone())
74    }
75
76    async fn list_credentials(
77        &self,
78        filter: CredentialFilter,
79    ) -> baseid_core::Result<Vec<CredentialSummary>> {
80        let store = self.credentials.lock().unwrap();
81        let summaries = store
82            .values()
83            .filter(|record| {
84                if let Some(ref fmt) = filter.format {
85                    if record.credential.credential_format() != *fmt {
86                        return false;
87                    }
88                }
89                if let Some(ref issuer) = filter.issuer {
90                    if record.credential.issuer_id() != *issuer {
91                        return false;
92                    }
93                }
94                true
95            })
96            .map(|record| record.summary())
97            .collect();
98        Ok(summaries)
99    }
100
101    async fn delete_credential(&self, id: &CredentialId) -> baseid_core::Result<()> {
102        let mut store = self.credentials.lock().unwrap();
103        store
104            .remove(&id.0)
105            .ok_or(WalletError::CredentialNotFound(id.clone()))?;
106        Ok(())
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use baseid_core::types::CredentialFormat;
114    use baseid_vc::credential::Issuer;
115    use baseid_vc::VerifiableCredential;
116
117    fn sample_vc(type_name: &str, issuer: &str) -> AnyCredential {
118        AnyCredential::W3cVc(VerifiableCredential {
119            context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
120            id: None,
121            r#type: vec!["VerifiableCredential".to_string(), type_name.to_string()],
122            issuer: Issuer::Uri(issuer.to_string()),
123            valid_from: Some("2024-01-01T00:00:00Z".to_string()),
124            valid_until: None,
125            credential_subject: serde_json::json!({}),
126            credential_status: None,
127            proof: None,
128        })
129    }
130
131    #[tokio::test]
132    async fn store_and_get_roundtrip() {
133        let store = InMemoryStore::new();
134        let cred = sample_vc("UniversityDegree", "did:key:issuer1");
135        let id = store.store_credential(cred).await.unwrap();
136
137        let retrieved = store.get_credential(&id).await.unwrap();
138        assert_eq!(retrieved.credential_format(), CredentialFormat::W3cVc);
139        assert_eq!(retrieved.issuer_id(), "did:key:issuer1");
140    }
141
142    #[tokio::test]
143    async fn store_generates_unique_ids() {
144        let store = InMemoryStore::new();
145        let id1 = store
146            .store_credential(sample_vc("A", "issuer"))
147            .await
148            .unwrap();
149        let id2 = store
150            .store_credential(sample_vc("B", "issuer"))
151            .await
152            .unwrap();
153        assert_ne!(id1, id2);
154        assert_eq!(store.len(), 2);
155    }
156
157    #[tokio::test]
158    async fn list_all() {
159        let store = InMemoryStore::new();
160        store
161            .store_credential(sample_vc("A", "issuer-a"))
162            .await
163            .unwrap();
164        store
165            .store_credential(sample_vc("B", "issuer-b"))
166            .await
167            .unwrap();
168
169        let all = store
170            .list_credentials(CredentialFilter::default())
171            .await
172            .unwrap();
173        assert_eq!(all.len(), 2);
174    }
175
176    #[tokio::test]
177    async fn list_filter_by_issuer() {
178        let store = InMemoryStore::new();
179        store
180            .store_credential(sample_vc("A", "issuer-a"))
181            .await
182            .unwrap();
183        store
184            .store_credential(sample_vc("B", "issuer-b"))
185            .await
186            .unwrap();
187
188        let filtered = store
189            .list_credentials(CredentialFilter {
190                format: None,
191                issuer: Some("issuer-a".to_string()),
192            })
193            .await
194            .unwrap();
195        assert_eq!(filtered.len(), 1);
196        assert_eq!(filtered[0].metadata.issuer, "issuer-a");
197    }
198
199    #[tokio::test]
200    async fn delete_credential() {
201        let store = InMemoryStore::new();
202        let id = store
203            .store_credential(sample_vc("A", "issuer"))
204            .await
205            .unwrap();
206        assert_eq!(store.len(), 1);
207
208        store.delete_credential(&id).await.unwrap();
209        assert!(store.is_empty());
210    }
211
212    #[tokio::test]
213    async fn get_not_found() {
214        let store = InMemoryStore::new();
215        let result = store
216            .get_credential(&CredentialId("nonexistent".to_string()))
217            .await;
218        assert!(result.is_err());
219    }
220
221    #[tokio::test]
222    async fn delete_not_found() {
223        let store = InMemoryStore::new();
224        let result = store
225            .delete_credential(&CredentialId("nonexistent".to_string()))
226            .await;
227        assert!(result.is_err());
228    }
229}