Skip to main content

bitwarden_vault/folder/
folder_sync_handler.rs

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