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