//! Stateful fake implementation of [`ObjectClient`] for end-to-end tests //! of cloud preferences sync. //! //! Unlike the auto-generated [`MockObjectClient`] (which requires each test //! to script per-method `expect_*()` calls up front), this fake maintains //! an in-memory store of preferences that can be read back after the //! syncer runs. Tests use [`FakeObjectClient::seed_preference`] to add //! cloud-side state and [`FakeObjectClient::cloud_value`] to assert on //! the result after a sync roundtrip. //! //! Only the subset of [`ObjectClient`] methods actually called by the //! cloud preferences syncer has a real implementation — every other //! method panics with `unimplemented!()`. This is deliberate: callers //! that hit one of these panics have likely wired a non-preferences //! code path through the fake by mistake. use std::collections::HashMap; use std::sync::{Arc, Mutex}; use anyhow::{Result, anyhow}; use async_channel::Sender; use async_trait::async_trait; use chrono::{DateTime, Utc}; use cloud_object_client::ObjectPermissionUpdateResult; use warp_graphql::object_permissions::AccessLevel; use crate::cloud_object::model::actions::{ObjectActionHistory, ObjectActionType}; use crate::cloud_object::model::generic_string_model::GenericStringObjectId; use crate::cloud_object::{ BulkCreateCloudObjectResult, BulkCreateGenericStringObjectsRequest, CreateCloudObjectResult, CreateObjectRequest, CreatedCloudObject, GenericStringObjectFormat, GenericStringObjectUniqueKey, JsonObjectType, ObjectDeleteResult, ObjectIdType, ObjectMetadataUpdateResult, ObjectPermissionsUpdateData, ObjectType, ObjectsToUpdate, Owner, Revision, RevisionAndLastEditor, ServerFolder, ServerMetadata, ServerNotebook, ServerObject, ServerPermissions, ServerPreference, ServerWorkflow, UpdateCloudObjectResult, }; use crate::drive::folders::FolderId; use crate::drive::sharing::SharingAccessLevel; use crate::notebooks::NotebookId; use crate::server::cloud_objects::listener::ObjectUpdateMessage; use crate::server::cloud_objects::update_manager::{GetCloudObjectResponse, InitialLoadResponse}; use crate::server::ids::{ServerId, ServerIdAndType, SyncId}; use crate::server::server_api::object::{GuestIdentifier, ObjectClient}; use crate::server::sync_queue::SerializedModel; use crate::settings::cloud_preferences::{CloudPreferenceModel, Platform, Preference}; use crate::workflows::WorkflowId; /// A stateful fake cloud preferences backend. #[derive(Clone, Default)] pub struct FakeObjectClient { state: Arc>, } #[derive(Default)] struct FakeCloudState { /// Stored preferences keyed by `GenericStringObjectId`. The /// preferences syncer addresses updates and deletes by id, so this /// is the authoritative lookup. preferences: HashMap, /// Monotonically increasing counter used to allocate fresh /// [`ServerId`]s for newly created preferences. next_server_id: i64, } struct StoredPreference { model: CloudPreferenceModel, revision: Revision, metadata_ts: chrono::DateTime, } impl FakeObjectClient { /// Seeds a preference into the cloud store as if another client had /// already written it. Returns the allocated [`ServerId`] so tests /// can reference it later if needed. /// /// The `value_json` argument is the JSON-serialized setting value /// (e.g. `"14.0"` for a float, `"true"` for a bool, `"\"Hack\""` /// for a string). This matches the format used by /// [`Preference::new`]. pub fn seed_preference( &self, storage_key: &str, value_json: &str, platform: Platform, ) -> ServerId { let preference = build_preference(storage_key, value_json, platform); let model = CloudPreferenceModel::new(preference); let mut state = self.state.lock().unwrap(); let server_id = state.alloc_server_id(); state.preferences.insert( GenericStringObjectId::from(server_id), StoredPreference { model, revision: Revision::now(), metadata_ts: Utc::now(), }, ); server_id } /// Returns the current cloud value for the preference with the /// given storage key and platform, or `None` if the fake has no /// such preference. /// /// The returned string is the JSON-serialized setting value (same /// shape as [`Self::seed_preference`]). pub fn cloud_value(&self, storage_key: &str, platform: Platform) -> Option { let state = self.state.lock().unwrap(); state .preferences .values() .find(|stored| { stored.model.string_model.storage_key == storage_key && stored.model.string_model.platform == platform }) .map(|stored| stored.model.string_model.value.to_string()) } /// Builds an [`InitialLoadResponse`] that reflects the current /// state of the fake. Tests pass the return value to /// [`UpdateManager::mock_initial_load`] to simulate the syncer's /// initial load seeing the seeded cloud state. pub fn snapshot_as_initial_load_response(&self) -> InitialLoadResponse { let state = self.state.lock().unwrap(); let server_objects: Vec> = state .preferences .iter() .map(|(id, stored)| { let metadata = ServerMetadata { uid: ServerId::from(*id), revision: stored.revision, metadata_last_updated_ts: stored.metadata_ts.into(), trashed_ts: None, folder_id: None, is_welcome_object: false, creator_uid: None, last_editor_uid: None, current_editor_uid: None, }; let permissions = ServerPermissions { space: Owner::mock_current_user(), guests: Vec::new(), anyone_link_sharing: None, permissions_last_updated_ts: stored.metadata_ts.into(), }; let server_pref = ServerPreference::new( SyncId::ServerId(ServerId::from(*id)), stored.model.clone(), metadata, permissions, ); Box::new(server_pref) as Box }) .collect(); let mut response = InitialLoadResponse::default(); if !server_objects.is_empty() { response.updated_generic_string_objects.insert( GenericStringObjectFormat::Json(JsonObjectType::Preference), server_objects, ); } response } } impl FakeCloudState { fn alloc_server_id(&mut self) -> ServerId { self.next_server_id += 1; ServerId::from(self.next_server_id) } } /// Builds a [`Preference`] struct from a storage key, JSON value, and /// explicit platform. This bypasses [`Preference::new`]'s syncing-mode /// inference so tests can seed preferences with an arbitrary platform. fn build_preference(storage_key: &str, value_json: &str, platform: Platform) -> Preference { let value: serde_json::Value = serde_json::from_str(value_json) .unwrap_or_else(|err| panic!("invalid JSON value {value_json:?}: {err}")); Preference { storage_key: storage_key.to_owned(), value, platform, } } #[cfg_attr(not(target_family = "wasm"), async_trait)] #[cfg_attr(target_family = "wasm", async_trait(?Send))] impl ObjectClient for FakeObjectClient { async fn bulk_create_generic_string_objects( &self, _owner: Owner, objects: &[BulkCreateGenericStringObjectsRequest], ) -> Result { let mut state = self.state.lock().unwrap(); let mut created = Vec::with_capacity(objects.len()); for request in objects { let serialized = request.serialized_model.model_as_str(); let model = CloudPreferenceModel::deserialize_owned(serialized) .map_err(|err| anyhow!("fake cloud failed to deserialize preference: {err}"))?; let server_id = state.alloc_server_id(); let now = Utc::now(); state.preferences.insert( GenericStringObjectId::from(server_id), StoredPreference { model, revision: Revision::now(), metadata_ts: now, }, ); created.push(CreatedCloudObject { client_id: request.id, revision_and_editor: RevisionAndLastEditor { revision: Revision::now(), last_editor_uid: None, }, metadata_ts: now.into(), server_id_and_type: ServerIdAndType { id: server_id, id_type: ObjectIdType::GenericStringObject, }, creator_uid: None, permissions: ServerPermissions::mock_personal(), }); } Ok(BulkCreateCloudObjectResult::Success { created_cloud_objects: created, }) } async fn update_generic_string_object( &self, object_id: GenericStringObjectId, model: SerializedModel, _revision: Option, ) -> Result>> { let mut state = self.state.lock().unwrap(); let stored = state .preferences .get_mut(&object_id) .ok_or_else(|| anyhow!("fake cloud: no preference with id {object_id:?}"))?; stored.model = CloudPreferenceModel::deserialize_owned(model.model_as_str()) .map_err(|err| anyhow!("fake cloud failed to deserialize preference: {err}"))?; stored.revision = Revision::now(); stored.metadata_ts = Utc::now(); Ok(UpdateCloudObjectResult::Success { revision_and_editor: RevisionAndLastEditor { revision: stored.revision, last_editor_uid: None, }, }) } async fn delete_object(&self, id: ServerId) -> Result { let mut state = self.state.lock().unwrap(); state.preferences.remove(&GenericStringObjectId::from(id)); Ok(ObjectDeleteResult::Success { deleted_ids: vec![SyncId::ServerId(id)], }) } async fn fetch_changed_objects( &self, _objects_to_update: ObjectsToUpdate, _force_refresh: bool, ) -> Result { Ok(self.snapshot_as_initial_load_response()) } async fn fetch_environment_last_task_run_timestamps( &self, ) -> Result>> { Ok(HashMap::new()) } // ─────────────────────────────────────────────────────────────── // The methods below are not exercised by CloudPreferencesSyncer, // so they intentionally panic. If a future change to the syncer // starts calling one of these, the test that triggered the call // will fail loudly with a clear message rather than silently // misbehave. // ─────────────────────────────────────────────────────────────── async fn create_workflow( &self, _request: CreateObjectRequest, ) -> Result { unimplemented!("FakeObjectClient::create_workflow") } async fn update_workflow( &self, _workflow_id: WorkflowId, _data: SerializedModel, _revision: Option, ) -> Result> { unimplemented!("FakeObjectClient::update_workflow") } async fn create_generic_string_object( &self, _format: GenericStringObjectFormat, _uniqueness_key: Option, _request: CreateObjectRequest, ) -> Result { unimplemented!("FakeObjectClient::create_generic_string_object") } async fn create_notebook( &self, _request: CreateObjectRequest, ) -> Result { unimplemented!("FakeObjectClient::create_notebook") } async fn update_notebook( &self, _notebook_id: NotebookId, _title: Option, _data: Option, _revision: Option, ) -> Result> { unimplemented!("FakeObjectClient::update_notebook") } async fn create_folder( &self, _request: CreateObjectRequest, ) -> Result { unimplemented!("FakeObjectClient::create_folder") } async fn update_folder( &self, _folder_id: FolderId, _name: SerializedModel, ) -> Result> { unimplemented!("FakeObjectClient::update_folder") } async fn grab_notebook_edit_access(&self, _notebook_id: NotebookId) -> Result { unimplemented!("FakeObjectClient::grab_notebook_edit_access") } async fn give_up_notebook_edit_access( &self, _notebook_id: NotebookId, ) -> Result { unimplemented!("FakeObjectClient::give_up_notebook_edit_access") } async fn get_warp_drive_updates( &self, _message_sender: Sender, _stream_ready_sender: Sender<()>, ) -> Result<()> { unimplemented!("FakeObjectClient::get_warp_drive_updates") } async fn fetch_single_cloud_object(&self, _id: ServerId) -> Result { unimplemented!("FakeObjectClient::fetch_single_cloud_object") } async fn transfer_notebook_owner( &self, _notebook_id: NotebookId, _owner: Owner, ) -> Result { unimplemented!("FakeObjectClient::transfer_notebook_owner") } async fn transfer_workflow_owner( &self, _workflow_id: WorkflowId, _owner: Owner, ) -> Result { unimplemented!("FakeObjectClient::transfer_workflow_owner") } async fn transfer_generic_string_object_owner( &self, _id: GenericStringObjectId, _owner: Owner, ) -> Result { unimplemented!("FakeObjectClient::transfer_generic_string_object_owner") } async fn trash_object(&self, _id: ServerId) -> Result { unimplemented!("FakeObjectClient::trash_object") } async fn untrash_object(&self, _id: ServerId) -> Result { unimplemented!("FakeObjectClient::untrash_object") } async fn empty_trash(&self, _owner: Owner) -> Result { unimplemented!("FakeObjectClient::empty_trash") } async fn move_object( &self, _id: ServerId, _folder_id: Option, _owner: Owner, _object_type: ObjectType, ) -> Result { unimplemented!("FakeObjectClient::move_object") } async fn record_object_action( &self, _id: ServerId, _action_type: ObjectActionType, _timestamp: DateTime, _data: Option, ) -> Result { unimplemented!("FakeObjectClient::record_object_action") } async fn leave_object(&self, _id: ServerId) -> Result { unimplemented!("FakeObjectClient::leave_object") } async fn set_object_link_permissions( &self, _object_id: ServerId, _access_level: SharingAccessLevel, ) -> Result { unimplemented!("FakeObjectClient::set_object_link_permissions") } async fn remove_object_link_permissions( &self, _object_id: ServerId, ) -> Result { unimplemented!("FakeObjectClient::remove_object_link_permissions") } async fn add_object_guests( &self, _object_id: ServerId, _guest_emails: Vec, _access_level: AccessLevel, ) -> Result { unimplemented!("FakeObjectClient::add_object_guests") } async fn update_object_guests( &self, _object_id: ServerId, _guest_emails: Vec, _access_level: AccessLevel, ) -> Result { unimplemented!("FakeObjectClient::update_object_guests") } async fn remove_object_guest( &self, _object_id: ServerId, _guest: GuestIdentifier, ) -> Result { unimplemented!("FakeObjectClient::remove_object_guest") } }