Skip to main content

bitwarden_vault/folder/
folder_sync_handler.rs

1use std::sync::Arc;
2
3use bitwarden_core::{FromClient, require};
4use bitwarden_state::{registry::StateRegistryError, repository::Repository};
5use bitwarden_sync::{SyncHandler, SyncHandlerError};
6
7use crate::{Folder, FolderId};
8
9/// Sync handler for folders
10///
11/// This handler persists folders to SDK-managed storage.
12#[derive(FromClient)]
13pub struct FolderSyncHandler {
14    repository: Option<Arc<dyn Repository<Folder>>>,
15}
16
17#[async_trait::async_trait]
18impl SyncHandler for FolderSyncHandler {
19    async fn on_sync(
20        &self,
21        response: &bitwarden_api_api::models::SyncResponseModel,
22    ) -> Result<(), SyncHandlerError> {
23        let repository = self
24            .repository
25            .as_ref()
26            .ok_or(StateRegistryError::DatabaseNotInitialized)?;
27        let api_folders = require!(response.folders.as_ref());
28
29        let folders: Vec<(FolderId, Folder)> = api_folders
30            .iter()
31            .filter_map(|f| {
32                Folder::try_from(f.clone())
33                    .inspect_err(
34                        |e| tracing::error!(id = ?f.id, error = ?e, "Failed to deserialize folder"),
35                    )
36                    .ok()
37                    .and_then(|folder| {
38                        let id = folder.id.or_else(|| {
39                            tracing::error!("Skipping folder with missing id");
40                            None
41                        })?;
42                        Some((id, folder))
43                    })
44            })
45            .collect();
46
47        repository.replace_all(folders).await?;
48
49        Ok(())
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use std::sync::Arc;
56
57    use bitwarden_api_api::models::{FolderResponseModel, SyncResponseModel};
58    use bitwarden_test::MemoryRepository;
59
60    use super::*;
61
62    /// Valid EncString in type 2 format (Aes256CbcHmac): `2.<iv>|<data>|<mac>`
63    const ENCRYPTED_NAME: &str = "2.AAAAAAAAAAAAAAAAAAAAAA==|AAAAAAAAAAAAAAAAAAAAAA==|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
64
65    fn make_folder_response(id: uuid::Uuid) -> FolderResponseModel {
66        FolderResponseModel {
67            object: Some("folder".to_string()),
68            id: Some(id),
69            name: Some(ENCRYPTED_NAME.to_string()),
70            revision_date: Some("2025-01-01T00:00:00Z".to_string()),
71        }
72    }
73
74    #[tokio::test]
75    async fn test_on_sync_replaces_existing_folders() {
76        let repository = Arc::new(MemoryRepository::<Folder>::default());
77        let handler = FolderSyncHandler {
78            repository: Some(repository.clone()),
79        };
80
81        // First sync with two folders
82        let id1 = uuid::Uuid::new_v4();
83        let id2 = uuid::Uuid::new_v4();
84        let response = SyncResponseModel {
85            folders: Some(vec![make_folder_response(id1), make_folder_response(id2)]),
86            ..Default::default()
87        };
88        handler.on_sync(&response).await.unwrap();
89        assert_eq!(repository.list().await.unwrap().len(), 2);
90
91        // Second sync with only one folder — old ones should be gone
92        let id3 = uuid::Uuid::new_v4();
93        let response = SyncResponseModel {
94            folders: Some(vec![make_folder_response(id3)]),
95            ..Default::default()
96        };
97        handler.on_sync(&response).await.unwrap();
98
99        let stored = repository.list().await.unwrap();
100        assert_eq!(stored.len(), 1);
101        assert!(repository.get(FolderId::new(id1)).await.unwrap().is_none());
102        assert!(repository.get(FolderId::new(id2)).await.unwrap().is_none());
103        assert!(repository.get(FolderId::new(id3)).await.unwrap().is_some());
104    }
105
106    #[tokio::test]
107    async fn test_on_sync_no_folders_returns_error() {
108        let repository = Arc::new(MemoryRepository::<Folder>::default());
109        let handler = FolderSyncHandler {
110            repository: Some(repository.clone()),
111        };
112
113        let response = SyncResponseModel::default();
114        let result = handler.on_sync(&response).await;
115        assert!(result.is_err());
116    }
117}