bitwarden_vault/folder/
folder_sync_handler.rs1use 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#[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 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 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 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}