use std::sync::Arc;use std::time::Duration; use chrono::Utc;use cloud_object_client::MockObjectClient;use lazy_static::lazy_static;use mockall::Sequence;use rand::Rng;use settings::{RespectUserSyncSetting, SyncToCloud};use warpui::{App, ModelHandle}; use super::*;use crate::auth::auth_manager::AuthManager;use crate::auth::user::TEST_USER_UID;use crate::auth::{AuthStateProvider, UserUid};use crate::cloud_object::model::actions::ObjectActions;use crate::cloud_object::model::generic_string_model::GenericStringModel;use crate::cloud_object::model::view::{ CloudViewModel, EDITOR_TIMEOUT_DURATION_MINUTES, EditorState, UpdateTimestamp,};use crate::cloud_object::{ CloudObjectMetadata, CloudObjectPermissions, CloudObjectStatuses, CloudObjectSyncStatus, NumInFlightRequests, ObjectIdType, Owner, ServerMetadata, ServerPermissions,};use crate::drive::DriveIndexVariant;use crate::drive::folders::{CloudFolderModel, FolderId};use crate::features::FeatureFlag;use crate::notebooks::{CloudNotebookModel, NotebookId};use crate::server::cloud_objects::listener::ObjectUpdateMessage;use crate::server::cloud_objects::update_manager::InitialLoadResponse;use crate::server::ids::{ServerId, ServerIdAndType};use crate::server::server_api::ServerApiProvider;use crate::server::server_api::object::ObjectClient;use crate::server::server_api::team::MockTeamClient;use crate::server::server_api::workspace::MockWorkspaceClient;use crate::server::sync_queue::SyncQueue;use crate::server::telemetry::context_provider::AppTelemetryContextProvider;use crate::settings::{Preference, init_and_register_user_preferences};use crate::system::SystemStats;use crate::workflows::CloudWorkflowModel;use crate::workspaces::team::Team;use crate::workspaces::team_tester::TeamTesterStatus;use crate::workspaces::user_profiles::UserProfiles;use crate::workspaces::user_workspaces::UserWorkspaces;use crate::workspaces::workspace::{Workspace, WorkspaceUid};use crate::{NetworkStatus, UpdateManager}; fn create_cloud_model( app: &mut App, objects: Vec<Box<dyn CloudObject>>,) -> ModelHandle<CloudModel> { // Make sure to register the CloudModel singleton - some CloudObject methods // find it and other dependencies via the AppContext. app.add_singleton_model(|_ctx| CloudModel::new(None, objects, None))} lazy_static! { /// Mock the user being on _a_ team in tests, so that the team drive is available. /// Otherwise, any team objects will appear shared. static ref TEST_TEAM: Team = Team::from_local_cache( ServerId::from(1), "Test Team".to_string(), None, None, None, None, ); static ref TEST_WORKSPACE: Workspace = Workspace::from_local_cache( WorkspaceUid::from(ServerId::from(1)), "Test Workspace".to_string(), Some(vec![TEST_TEAM.clone()]), None, );} fn initialize_app( app: &mut App, cached_objects: Vec<Box<dyn CloudObject>>, cloud_object_server_api_mock: Arc<impl ObjectClient>,) { let team_client_mock = Arc::new(MockTeamClient::new()); let workspace_client_mock = Arc::new(MockWorkspaceClient::new()); // Add the necessary singleton models to the App app.add_singleton_model(|_| NetworkStatus::new()); app.add_singleton_model(|_| SystemStats::new()); app.add_singleton_model(|_| ServerApiProvider::new_for_test()); app.add_singleton_model(|_| AuthStateProvider::new_for_test()); app.add_singleton_model(AppTelemetryContextProvider::new_context_provider); app.add_singleton_model(AuthManager::new_for_test); app.add_singleton_model(|ctx| { UserWorkspaces::mock( team_client_mock.clone(), workspace_client_mock.clone(), vec![TEST_WORKSPACE.clone()], ctx, ) }); app.add_singleton_model(TeamTesterStatus::new); app.add_singleton_model(SyncQueue::mock); app.add_singleton_model(|_ctx| CloudModel::new(None, cached_objects, None)); app.add_singleton_model(|ctx| UpdateManager::new(None, cloud_object_server_api_mock, ctx)); app.add_singleton_model(|_| UserProfiles::new(Vec::new())); app.add_singleton_model(CloudViewModel::new); app.add_singleton_model(|_| ObjectActions::new(Vec::new())); // The start of polling is normally triggered by authentication completion, but // we need to do it manually for tests. TeamTesterStatus::handle(app).update(app, |team_tester, ctx| { team_tester.initiate_data_pollers(false, ctx); });} fn mock_random_workflows(start_id: i64, owner: Owner) -> Vec<ServerWorkflow> { let mut rng = rand::thread_rng(); // pick how many workflows to generate at random let number_of_workflows = rng.gen_range(1..10); mock_server_workflows(start_id, owner, number_of_workflows)} fn mock_server_metadata() -> ServerMetadata { ServerMetadata { uid: ServerId::default(), revision: Revision::now(), metadata_last_updated_ts: Utc::now().into(), trashed_ts: None, folder_id: None, is_welcome_object: false, creator_uid: None, last_editor_uid: None, current_editor_uid: None, }} fn mock_server_permissions(owner: Owner) -> ServerPermissions { ServerPermissions { space: owner, guests: Vec::new(), permissions_last_updated_ts: Utc::now().into(), anyone_link_sharing: None, }} fn mock_permissions() -> CloudObjectPermissions { CloudObjectPermissions { owner: Owner::mock_current_user(), guests: Vec::new(), permissions_last_updated_ts: None, anyone_with_link: None, }} fn mock_server_workflows( start_id: i64, owner: Owner, number_of_workflows: i64,) -> Vec<ServerWorkflow> { (0..number_of_workflows) .map(|idx| { ServerWorkflow::new( SyncId::ServerId((start_id + idx).into()), CloudWorkflowModel::new(Workflow::new( format!("w{}", start_id + idx), format!("c{}", start_id + idx), )), mock_server_metadata(), mock_server_permissions(owner), ) }) .collect()} fn mock_random_folders(start_id: i64, owner: Owner) -> Vec<ServerFolder> { let mut rng = rand::thread_rng(); // pick how many folders to generate at random let number_of_workflows = rng.gen_range(1..10); mock_server_folders(start_id, owner, number_of_workflows)} fn mock_server_folders(start_id: i64, owner: Owner, number_of_folders: i64) -> Vec<ServerFolder> { (0..number_of_folders) .map(|idx| { ServerFolder::new( SyncId::ServerId((start_id + idx).into()), CloudFolderModel::new(&format!("f{}", start_id + idx), false), mock_server_metadata(), mock_server_permissions(owner), ) }) .collect()} fn mock_server_notebooks() -> Vec<ServerNotebook> { let owner = Owner::mock_current_user(); vec![ ServerNotebook::new( SyncId::ServerId(1.into()), CloudNotebookModel { title: "t1".to_string(), data: "d1".to_string(), ai_document_id: None, conversation_id: None, }, mock_server_metadata(), mock_server_permissions(owner), ), ServerNotebook::new( SyncId::ServerId(2.into()), CloudNotebookModel { title: "t2".to_string(), data: "d2".to_string(), ai_document_id: None, conversation_id: None, }, mock_server_metadata(), mock_server_permissions(owner), ), ServerNotebook::new( SyncId::ServerId(3.into()), CloudNotebookModel { title: "t3".to_string(), data: "d3".to_string(), ai_document_id: None, conversation_id: None, }, mock_server_metadata(), mock_server_permissions(owner), ), ServerNotebook::new( SyncId::ServerId(4.into()), CloudNotebookModel { title: "t4".to_string(), data: "d4".to_string(), ai_document_id: None, conversation_id: None, }, mock_server_metadata(), mock_server_permissions(owner), ), ]} fn mock_cloud_folder(id: SyncId, name: String, folder_id: Option<SyncId>) -> CloudFolder { CloudFolder::new( id, CloudFolderModel { name, is_open: true, is_warp_pack: false, }, CloudObjectMetadata { pending_changes_statuses: CloudObjectStatuses { content_sync_status: CloudObjectSyncStatus::NoLocalChanges, has_pending_metadata_change: false, has_pending_permissions_change: false, pending_untrash: false, pending_delete: false, }, folder_id, revision: Default::default(), metadata_last_updated_ts: Default::default(), current_editor_uid: Default::default(), trashed_ts: Default::default(), is_welcome_object: false, creator_uid: None, last_editor_uid: None, last_task_run_ts: None, }, mock_permissions(), )} fn mock_cloud_notebook(id: SyncId, title: String, folder_id: Option<SyncId>) -> CloudNotebook { CloudNotebook::new( id, CloudNotebookModel { title, data: "test".into(), ai_document_id: None, conversation_id: None, }, CloudObjectMetadata { pending_changes_statuses: CloudObjectStatuses { content_sync_status: CloudObjectSyncStatus::NoLocalChanges, has_pending_metadata_change: false, has_pending_permissions_change: false, pending_untrash: false, pending_delete: false, }, folder_id, revision: Default::default(), metadata_last_updated_ts: Default::default(), current_editor_uid: Default::default(), trashed_ts: Default::default(), is_welcome_object: false, creator_uid: None, last_editor_uid: None, last_task_run_ts: None, }, mock_permissions(), )} fn mock_trashed_cloud_folder(id: SyncId, name: String, folder_id: Option<SyncId>) -> CloudFolder { let mut folder = mock_cloud_folder(id, name, folder_id); folder.metadata.trashed_ts = Some(ServerTimestamp::from_unix_timestamp_micros(10).unwrap()); folder} fn folder_from_cloud_model(model: &CloudModel, id: SyncId) -> &CloudFolder { model.get_folder_by_uid(&id.uid()).expect("is a folder")} /// Mock receiving an RTC update. These tests update objects by mocking RTC messages so that they/// don't need to mock the server API for updates. The unit tests for [`UpdateManager`] ensure that/// updates from both RTC and client actions emit the same events.fn receive_rtc_update(message: ObjectUpdateMessage, app: &mut App) { UpdateManager::handle(app).update(app, |update_manager, ctx| { update_manager.received_message_from_server(message, ctx) });} fn move_object(id: ServerId, folder_id: Option<FolderId>, app: &mut App) { let message = CloudModel::handle(app).read(app, |cloud_model, _| { let object = cloud_model .get_by_uid(&id.uid()) .expect("Expected object to exist in cloud model"); let metadata = ServerMetadata { uid: id, revision: object.metadata().revision.expect("Revision is required"), current_editor_uid: object.metadata().current_editor_uid.clone(), metadata_last_updated_ts: (object .metadata() .metadata_last_updated_ts .expect("Metadata TS is required") .utc() + chrono::Duration::seconds(1)) .into(), trashed_ts: object.metadata().trashed_ts, folder_id, is_welcome_object: object.metadata().is_welcome_object, creator_uid: object.metadata().creator_uid.clone(), last_editor_uid: object.metadata().last_editor_uid.clone(), }; ObjectUpdateMessage::ObjectMetadataChanged { metadata } }); receive_rtc_update(message, app);} #[test]fn test_update_with_deleted_objects() { let workflows = mock_server_workflows( 5, Owner::Team { team_uid: ServerId::from(1), }, 3, ); let notebooks = mock_server_notebooks(); App::test((), |mut app| async move { let cloud_model = create_cloud_model( &mut app, workflows .iter() .map(|workflow| CloudWorkflow::new_from_server(workflow.clone())) .map(|o| Box::new(o) as Box<dyn CloudObject>) .collect(), ); cloud_model.update(&mut app, |model, ctx| { for notebook in notebooks.clone() { model.upsert_from_server_notebook(notebook, ctx); } }); // Validate there's some notebooks and workflows in memory cloud_model.read(&app, |cloud_model, _| { assert_eq!( 3, cloud_model.get_all_active_and_inactive_workflows().count() ); assert_eq!( 4, cloud_model.get_all_active_and_inactive_notebooks().count() ); assert_eq!(7, cloud_model.as_cloud_objects().count()); }); // Apply the "update from server" cloud_model.update(&mut app, |cloud_model, ctx| { // Set 3rd notebook to have pending changes. This should keep it in memory, // even though it's not returned from the server. let notebook_id: SyncId = SyncId::ServerId(3.into()); if let Some(object) = cloud_model.get_notebook_mut(¬ebook_id) { object.set_pending_content_changes_status(CloudObjectSyncStatus::InFlight( NumInFlightRequests(1), )); } cloud_model.update_objects(notebooks.into_iter().take(2), ctx); cloud_model.update_objects(workflows.into_iter().take(2), ctx); }); cloud_model.read(&app, |cloud_model, _| { // expected: 3rd workflow was removed on the server, and so we don't want it in // memory assert_eq!( 2, cloud_model.get_all_active_and_inactive_workflows().count() ); // expected: 3rd notebook has local changes, so we want to keep it, but 4th // doesn't and also wasn't returned from the server, so we want to remove it. assert_eq!( 3, cloud_model.get_all_active_and_inactive_notebooks().count() ); assert_eq!(5, cloud_model.as_cloud_objects().count()); }); })} #[test]fn test_update_object_server_id_for_notebook() { let client_id = ClientId::new(); let server_id: NotebookId = 1.into(); let notebooks: Vec<Box<dyn CloudObject>> = vec![Box::new(CloudNotebook::new( SyncId::ClientId(client_id), CloudNotebookModel { title: "t1".to_string(), data: "d1".to_string(), ai_document_id: None, conversation_id: None, }, CloudObjectMetadata { pending_changes_statuses: CloudObjectStatuses { content_sync_status: CloudObjectSyncStatus::NoLocalChanges, has_pending_metadata_change: false, has_pending_permissions_change: false, pending_untrash: false, pending_delete: false, }, folder_id: Default::default(), revision: Default::default(), metadata_last_updated_ts: Default::default(), current_editor_uid: Default::default(), trashed_ts: Default::default(), is_welcome_object: false, creator_uid: None, last_editor_uid: None, last_task_run_ts: None, }, mock_permissions(), ))]; App::test((), |mut app| async move { let cloud_model = create_cloud_model(&mut app, notebooks); cloud_model.update(&mut app, |model, ctx| { model.update_object_after_server_creation( client_id, ServerCreationInfo { creator_uid: None, permissions: ServerPermissions::mock_personal(), server_id_and_type: ServerIdAndType { id: server_id.to_server_id(), id_type: ObjectIdType::Notebook, }, }, ctx, ) }); cloud_model.read(&app, |model, _| { let notebook = model .get_notebook(&SyncId::ServerId(server_id.into())) .unwrap(); assert_eq!(notebook.id, SyncId::ServerId(server_id.into())); }); })} #[test]fn test_create_json_object() { let client_id = ClientId::default(); let id = SyncId::ClientId(client_id); let json_object: Box<dyn CloudObject> = Box::new(CloudPreference::new( id, GenericStringModel::new( Preference::new( "test_storage_key".to_owned(), "{\"test_key\": \"test_value\"}", SyncToCloud::Globally(RespectUserSyncSetting::Yes), ) .expect("error creating preference"), ), CloudObjectMetadata { pending_changes_statuses: CloudObjectStatuses { content_sync_status: CloudObjectSyncStatus::NoLocalChanges,Lines 1–500 of 1984. Load more
use std::sync::Arc; use std::time::Duration; use chrono::Utc; use cloud_object_client::MockObjectClient; use lazy_static::lazy_static; use mockall::Sequence; use rand::Rng; use settings::{RespectUserSyncSetting, SyncToCloud}; use warpui::{App, ModelHandle}; use super::*; use crate::auth::auth_manager::AuthManager; use crate::auth::user::TEST_USER_UID; use crate::auth::{AuthStateProvider, UserUid}; use crate::cloud_object::model::actions::ObjectActions; use crate::cloud_object::model::generic_string_model::GenericStringModel; use crate::cloud_object::model::view::{ CloudViewModel, EDITOR_TIMEOUT_DURATION_MINUTES, EditorState, UpdateTimestamp, }; use crate::cloud_object::{ CloudObjectMetadata, CloudObjectPermissions, CloudObjectStatuses, CloudObjectSyncStatus, NumInFlightRequests, ObjectIdType, Owner, ServerMetadata, ServerPermissions, }; use crate::drive::DriveIndexVariant; use crate::drive::folders::{CloudFolderModel, FolderId}; use crate::features::FeatureFlag; use crate::notebooks::{CloudNotebookModel, NotebookId}; use crate::server::cloud_objects::listener::ObjectUpdateMessage; use crate::server::cloud_objects::update_manager::InitialLoadResponse; use crate::server::ids::{ServerId, ServerIdAndType}; use crate::server::server_api::ServerApiProvider; use crate::server::server_api::object::ObjectClient; use crate::server::server_api::team::MockTeamClient; use crate::server::server_api::workspace::MockWorkspaceClient; use crate::server::sync_queue::SyncQueue; use crate::server::telemetry::context_provider::AppTelemetryContextProvider; use crate::settings::{Preference, init_and_register_user_preferences}; use crate::system::SystemStats; use crate::workflows::CloudWorkflowModel; use crate::workspaces::team::Team; use crate::workspaces::team_tester::TeamTesterStatus; use crate::workspaces::user_profiles::UserProfiles; use crate::workspaces::user_workspaces::UserWorkspaces; use crate::workspaces::workspace::{Workspace, WorkspaceUid}; use crate::{NetworkStatus, UpdateManager}; fn create_cloud_model( app: &mut App, objects: Vec<Box<dyn CloudObject>>, ) -> ModelHandle<CloudModel> { // Make sure to register the CloudModel singleton - some CloudObject methods // find it and other dependencies via the AppContext. app.add_singleton_model(|_ctx| CloudModel::new(None, objects, None)) } lazy_static! { /// Mock the user being on _a_ team in tests, so that the team drive is available. /// Otherwise, any team objects will appear shared. static ref TEST_TEAM: Team = Team::from_local_cache( ServerId::from(1), "Test Team".to_string(), None, None, None, None, ); static ref TEST_WORKSPACE: Workspace = Workspace::from_local_cache( WorkspaceUid::from(ServerId::from(1)), "Test Workspace".to_string(), Some(vec![TEST_TEAM.clone()]), None, ); } fn initialize_app( app: &mut App, cached_objects: Vec<Box<dyn CloudObject>>, cloud_object_server_api_mock: Arc<impl ObjectClient>, ) { let team_client_mock = Arc::new(MockTeamClient::new()); let workspace_client_mock = Arc::new(MockWorkspaceClient::new()); // Add the necessary singleton models to the App app.add_singleton_model(|_| NetworkStatus::new()); app.add_singleton_model(|_| SystemStats::new()); app.add_singleton_model(|_| ServerApiProvider::new_for_test()); app.add_singleton_model(|_| AuthStateProvider::new_for_test()); app.add_singleton_model(AppTelemetryContextProvider::new_context_provider); app.add_singleton_model(AuthManager::new_for_test); app.add_singleton_model(|ctx| { UserWorkspaces::mock( team_client_mock.clone(), workspace_client_mock.clone(), vec![TEST_WORKSPACE.clone()], ctx, ) }); app.add_singleton_model(TeamTesterStatus::new); app.add_singleton_model(SyncQueue::mock); app.add_singleton_model(|_ctx| CloudModel::new(None, cached_objects, None)); app.add_singleton_model(|ctx| UpdateManager::new(None, cloud_object_server_api_mock, ctx)); app.add_singleton_model(|_| UserProfiles::new(Vec::new())); app.add_singleton_model(CloudViewModel::new); app.add_singleton_model(|_| ObjectActions::new(Vec::new())); // The start of polling is normally triggered by authentication completion, but // we need to do it manually for tests. TeamTesterStatus::handle(app).update(app, |team_tester, ctx| { team_tester.initiate_data_pollers(false, ctx); }); } fn mock_random_workflows(start_id: i64, owner: Owner) -> Vec<ServerWorkflow> { let mut rng = rand::thread_rng(); // pick how many workflows to generate at random let number_of_workflows = rng.gen_range(1..10); mock_server_workflows(start_id, owner, number_of_workflows) } fn mock_server_metadata() -> ServerMetadata { ServerMetadata { uid: ServerId::default(), revision: Revision::now(), metadata_last_updated_ts: Utc::now().into(), trashed_ts: None, folder_id: None, is_welcome_object: false, creator_uid: None, last_editor_uid: None, current_editor_uid: None, } } fn mock_server_permissions(owner: Owner) -> ServerPermissions { ServerPermissions { space: owner, guests: Vec::new(), permissions_last_updated_ts: Utc::now().into(), anyone_link_sharing: None, } } fn mock_permissions() -> CloudObjectPermissions { CloudObjectPermissions { owner: Owner::mock_current_user(), guests: Vec::new(), permissions_last_updated_ts: None, anyone_with_link: None, } } fn mock_server_workflows( start_id: i64, owner: Owner, number_of_workflows: i64, ) -> Vec<ServerWorkflow> { (0..number_of_workflows) .map(|idx| { ServerWorkflow::new( SyncId::ServerId((start_id + idx).into()), CloudWorkflowModel::new(Workflow::new( format!("w{}", start_id + idx), format!("c{}", start_id + idx), )), mock_server_metadata(), mock_server_permissions(owner), ) }) .collect() } fn mock_random_folders(start_id: i64, owner: Owner) -> Vec<ServerFolder> { let mut rng = rand::thread_rng(); // pick how many folders to generate at random let number_of_workflows = rng.gen_range(1..10); mock_server_folders(start_id, owner, number_of_workflows) } fn mock_server_folders(start_id: i64, owner: Owner, number_of_folders: i64) -> Vec<ServerFolder> { (0..number_of_folders) .map(|idx| { ServerFolder::new( SyncId::ServerId((start_id + idx).into()), CloudFolderModel::new(&format!("f{}", start_id + idx), false), mock_server_metadata(), mock_server_permissions(owner), ) }) .collect() } fn mock_server_notebooks() -> Vec<ServerNotebook> { let owner = Owner::mock_current_user(); vec![ ServerNotebook::new( SyncId::ServerId(1.into()), CloudNotebookModel { title: "t1".to_string(), data: "d1".to_string(), ai_document_id: None, conversation_id: None, }, mock_server_metadata(), mock_server_permissions(owner), ), ServerNotebook::new( SyncId::ServerId(2.into()), CloudNotebookModel { title: "t2".to_string(), data: "d2".to_string(), ai_document_id: None, conversation_id: None, }, mock_server_metadata(), mock_server_permissions(owner), ), ServerNotebook::new( SyncId::ServerId(3.into()), CloudNotebookModel { title: "t3".to_string(), data: "d3".to_string(), ai_document_id: None, conversation_id: None, }, mock_server_metadata(), mock_server_permissions(owner), ), ServerNotebook::new( SyncId::ServerId(4.into()), CloudNotebookModel { title: "t4".to_string(), data: "d4".to_string(), ai_document_id: None, conversation_id: None, }, mock_server_metadata(), mock_server_permissions(owner), ), ] } fn mock_cloud_folder(id: SyncId, name: String, folder_id: Option<SyncId>) -> CloudFolder { CloudFolder::new( id, CloudFolderModel { name, is_open: true, is_warp_pack: false, }, CloudObjectMetadata { pending_changes_statuses: CloudObjectStatuses { content_sync_status: CloudObjectSyncStatus::NoLocalChanges, has_pending_metadata_change: false, has_pending_permissions_change: false, pending_untrash: false, pending_delete: false, }, folder_id, revision: Default::default(), metadata_last_updated_ts: Default::default(), current_editor_uid: Default::default(), trashed_ts: Default::default(), is_welcome_object: false, creator_uid: None, last_editor_uid: None, last_task_run_ts: None, }, mock_permissions(), ) } fn mock_cloud_notebook(id: SyncId, title: String, folder_id: Option<SyncId>) -> CloudNotebook { CloudNotebook::new( id, CloudNotebookModel { title, data: "test".into(), ai_document_id: None, conversation_id: None, }, CloudObjectMetadata { pending_changes_statuses: CloudObjectStatuses { content_sync_status: CloudObjectSyncStatus::NoLocalChanges, has_pending_metadata_change: false, has_pending_permissions_change: false, pending_untrash: false, pending_delete: false, }, folder_id, revision: Default::default(), metadata_last_updated_ts: Default::default(), current_editor_uid: Default::default(), trashed_ts: Default::default(), is_welcome_object: false, creator_uid: None, last_editor_uid: None, last_task_run_ts: None, }, mock_permissions(), ) } fn mock_trashed_cloud_folder(id: SyncId, name: String, folder_id: Option<SyncId>) -> CloudFolder { let mut folder = mock_cloud_folder(id, name, folder_id); folder.metadata.trashed_ts = Some(ServerTimestamp::from_unix_timestamp_micros(10).unwrap()); folder } fn folder_from_cloud_model(model: &CloudModel, id: SyncId) -> &CloudFolder { model.get_folder_by_uid(&id.uid()).expect("is a folder") } /// Mock receiving an RTC update. These tests update objects by mocking RTC messages so that they /// don't need to mock the server API for updates. The unit tests for [`UpdateManager`] ensure that /// updates from both RTC and client actions emit the same events. fn receive_rtc_update(message: ObjectUpdateMessage, app: &mut App) { UpdateManager::handle(app).update(app, |update_manager, ctx| { update_manager.received_message_from_server(message, ctx) }); } fn move_object(id: ServerId, folder_id: Option<FolderId>, app: &mut App) { let message = CloudModel::handle(app).read(app, |cloud_model, _| { let object = cloud_model .get_by_uid(&id.uid()) .expect("Expected object to exist in cloud model"); let metadata = ServerMetadata { uid: id, revision: object.metadata().revision.expect("Revision is required"), current_editor_uid: object.metadata().current_editor_uid.clone(), metadata_last_updated_ts: (object .metadata() .metadata_last_updated_ts .expect("Metadata TS is required") .utc() + chrono::Duration::seconds(1)) .into(), trashed_ts: object.metadata().trashed_ts, folder_id, is_welcome_object: object.metadata().is_welcome_object, creator_uid: object.metadata().creator_uid.clone(), last_editor_uid: object.metadata().last_editor_uid.clone(), }; ObjectUpdateMessage::ObjectMetadataChanged { metadata } }); receive_rtc_update(message, app); } #[test] fn test_update_with_deleted_objects() { let workflows = mock_server_workflows( 5, Owner::Team { team_uid: ServerId::from(1), }, 3, ); let notebooks = mock_server_notebooks(); App::test((), |mut app| async move { let cloud_model = create_cloud_model( &mut app, workflows .iter() .map(|workflow| CloudWorkflow::new_from_server(workflow.clone())) .map(|o| Box::new(o) as Box<dyn CloudObject>) .collect(), ); cloud_model.update(&mut app, |model, ctx| { for notebook in notebooks.clone() { model.upsert_from_server_notebook(notebook, ctx); } }); // Validate there's some notebooks and workflows in memory cloud_model.read(&app, |cloud_model, _| { assert_eq!( 3, cloud_model.get_all_active_and_inactive_workflows().count() ); assert_eq!( 4, cloud_model.get_all_active_and_inactive_notebooks().count() ); assert_eq!(7, cloud_model.as_cloud_objects().count()); }); // Apply the "update from server" cloud_model.update(&mut app, |cloud_model, ctx| { // Set 3rd notebook to have pending changes. This should keep it in memory, // even though it's not returned from the server. let notebook_id: SyncId = SyncId::ServerId(3.into()); if let Some(object) = cloud_model.get_notebook_mut(¬ebook_id) { object.set_pending_content_changes_status(CloudObjectSyncStatus::InFlight( NumInFlightRequests(1), )); } cloud_model.update_objects(notebooks.into_iter().take(2), ctx); cloud_model.update_objects(workflows.into_iter().take(2), ctx); }); cloud_model.read(&app, |cloud_model, _| { // expected: 3rd workflow was removed on the server, and so we don't want it in // memory assert_eq!( 2, cloud_model.get_all_active_and_inactive_workflows().count() ); // expected: 3rd notebook has local changes, so we want to keep it, but 4th // doesn't and also wasn't returned from the server, so we want to remove it. assert_eq!( 3, cloud_model.get_all_active_and_inactive_notebooks().count() ); assert_eq!(5, cloud_model.as_cloud_objects().count()); }); }) } #[test] fn test_update_object_server_id_for_notebook() { let client_id = ClientId::new(); let server_id: NotebookId = 1.into(); let notebooks: Vec<Box<dyn CloudObject>> = vec![Box::new(CloudNotebook::new( SyncId::ClientId(client_id), CloudNotebookModel { title: "t1".to_string(), data: "d1".to_string(), ai_document_id: None, conversation_id: None, }, CloudObjectMetadata { pending_changes_statuses: CloudObjectStatuses { content_sync_status: CloudObjectSyncStatus::NoLocalChanges, has_pending_metadata_change: false, has_pending_permissions_change: false, pending_untrash: false, pending_delete: false, }, folder_id: Default::default(), revision: Default::default(), metadata_last_updated_ts: Default::default(), current_editor_uid: Default::default(), trashed_ts: Default::default(), is_welcome_object: false, creator_uid: None, last_editor_uid: None, last_task_run_ts: None, }, mock_permissions(), ))]; App::test((), |mut app| async move { let cloud_model = create_cloud_model(&mut app, notebooks); cloud_model.update(&mut app, |model, ctx| { model.update_object_after_server_creation( client_id, ServerCreationInfo { creator_uid: None, permissions: ServerPermissions::mock_personal(), server_id_and_type: ServerIdAndType { id: server_id.to_server_id(), id_type: ObjectIdType::Notebook, }, }, ctx, ) }); cloud_model.read(&app, |model, _| { let notebook = model .get_notebook(&SyncId::ServerId(server_id.into())) .unwrap(); assert_eq!(notebook.id, SyncId::ServerId(server_id.into())); }); }) } #[test] fn test_create_json_object() { let client_id = ClientId::default(); let id = SyncId::ClientId(client_id); let json_object: Box<dyn CloudObject> = Box::new(CloudPreference::new( id, GenericStringModel::new( Preference::new( "test_storage_key".to_owned(), "{\"test_key\": \"test_value\"}", SyncToCloud::Globally(RespectUserSyncSetting::Yes), ) .expect("error creating preference"), ), CloudObjectMetadata { pending_changes_statuses: CloudObjectStatuses { content_sync_status: CloudObjectSyncStatus::NoLocalChanges,