| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use account_common::{AccountManagerError, FidlLocalAccountId, LocalAccountId, ResultExt}; |
| use anyhow::{format_err, Error}; |
| use fidl::endpoints::{create_endpoints, ClientEnd, ServerEnd}; |
| use fidl_fuchsia_auth::{AppConfig, AuthenticationContextProviderMarker, Status as AuthStatus}; |
| use fidl_fuchsia_auth::{AuthProviderConfig, UserProfileInfo}; |
| use fidl_fuchsia_identity_account::{ |
| AccountAuthState, AccountListenerMarker, AccountListenerOptions, AccountManagerRequest, |
| AccountManagerRequestStream, AccountMarker, Error as ApiError, Lifetime, Scenario, |
| }; |
| use fuchsia_component::fuchsia_single_component_package_url; |
| use fuchsia_inspect::{Inspector, Property}; |
| use futures::lock::Mutex; |
| use futures::prelude::*; |
| use lazy_static::lazy_static; |
| use log::{info, warn}; |
| use std::path::PathBuf; |
| use std::sync::Arc; |
| |
| use crate::account_event_emitter::{AccountEvent, AccountEventEmitter}; |
| use crate::account_handler_connection::AccountHandlerConnection; |
| use crate::account_handler_context::AccountHandlerContext; |
| use crate::account_map::AccountMap; |
| use crate::inspect; |
| |
| const SELF_URL: &str = fuchsia_single_component_package_url!("account_manager"); |
| |
| lazy_static! { |
| |
| /// The Auth scopes used for authorization during service provider-based account provisioning. |
| /// An empty vector means that the auth provider should use its default scopes. |
| static ref APP_SCOPES: Vec<String> = Vec::default(); |
| } |
| |
| /// The core component of the account system for Fuchsia. |
| /// |
| /// The AccountManager maintains the set of Fuchsia accounts that are provisioned on the device, |
| /// launches and configures AuthenticationProvider components to perform authentication via |
| /// service providers, and launches and delegates to AccountHandler component instances to |
| /// determine the detailed state and authentication for each account. |
| /// |
| /// `AHC` An AccountHandlerConnection used to spawn new AccountHandler components. |
| pub struct AccountManager<AHC: AccountHandlerConnection> { |
| /// The account map maintains the state of all accounts as well as connections to their account |
| /// handlers. |
| account_map: Mutex<AccountMap<AHC>>, |
| |
| /// Contains the client ends of all AccountListeners which are subscribed to account events. |
| event_emitter: AccountEventEmitter, |
| |
| /// Helper for outputting auth_provider information via fuchsia_inspect. Must be retained |
| /// to avoid dropping the static properties it contains. |
| _auth_providers_inspect: inspect::AuthProviders, |
| } |
| |
| impl<AHC: AccountHandlerConnection> AccountManager<AHC> { |
| /// Constructs a new AccountManager, loading existing set of accounts from `data_dir`, and an |
| /// auth provider configuration. The directory must exist at construction. |
| pub fn new( |
| data_dir: PathBuf, |
| auth_provider_config: &[AuthProviderConfig], |
| auth_mechanism_ids: &[String], |
| inspector: &Inspector, |
| ) -> Result<AccountManager<AHC>, Error> { |
| let context = |
| Arc::new(AccountHandlerContext::new(auth_provider_config, auth_mechanism_ids)); |
| let account_map = AccountMap::load(data_dir, Arc::clone(&context), inspector.root())?; |
| |
| // Initialize the structs used to output state through the inspect system. |
| let auth_providers_inspect = inspect::AuthProviders::new(inspector.root()); |
| let auth_provider_types: Vec<String> = |
| auth_provider_config.iter().map(|apc| apc.auth_provider_type.clone()).collect(); |
| auth_providers_inspect.types.set(&auth_provider_types.join(",")); |
| let event_emitter = AccountEventEmitter::new(inspector.root()); |
| |
| Ok(Self { |
| account_map: Mutex::new(account_map), |
| event_emitter, |
| _auth_providers_inspect: auth_providers_inspect, |
| }) |
| } |
| |
| /// Asynchronously handles the supplied stream of `AccountManagerRequest` messages. |
| pub async fn handle_requests_from_stream( |
| &self, |
| mut stream: AccountManagerRequestStream, |
| ) -> Result<(), Error> { |
| while let Some(req) = stream.try_next().await? { |
| self.handle_request(req).await?; |
| } |
| Ok(()) |
| } |
| |
| /// Handles a single request to the AccountManager. |
| pub async fn handle_request(&self, req: AccountManagerRequest) -> Result<(), fidl::Error> { |
| match req { |
| AccountManagerRequest::GetAccountIds { responder } => { |
| let response = self.get_account_ids().await; |
| responder.send(&response)?; |
| } |
| AccountManagerRequest::GetAccountAuthStates { scenario, responder } => { |
| let mut response = self.get_account_auth_states(scenario).await; |
| responder.send(&mut response)?; |
| } |
| AccountManagerRequest::GetAccount { id, context_provider, account, responder } => { |
| let mut response = self.get_account(id.into(), context_provider, account).await; |
| responder.send(&mut response)?; |
| } |
| AccountManagerRequest::RegisterAccountListener { listener, options, responder } => { |
| let mut response = self.register_account_listener(listener, options).await; |
| responder.send(&mut response)?; |
| } |
| AccountManagerRequest::RemoveAccount { id, force, responder } => { |
| let mut response = self.remove_account(id.into(), force).await; |
| responder.send(&mut response)?; |
| } |
| AccountManagerRequest::ProvisionFromAuthProvider { |
| auth_context_provider, |
| auth_provider_type, |
| lifetime, |
| auth_mechanism_id, |
| responder, |
| } => { |
| let mut response = self |
| .provision_from_auth_provider( |
| auth_context_provider, |
| auth_provider_type, |
| lifetime, |
| auth_mechanism_id, |
| ) |
| .await; |
| responder.send(&mut response)?; |
| } |
| AccountManagerRequest::ProvisionNewAccount { |
| lifetime, |
| auth_mechanism_id, |
| responder, |
| } => { |
| let mut response = self.provision_new_account(lifetime, auth_mechanism_id).await; |
| responder.send(&mut response)?; |
| } |
| AccountManagerRequest::GetAuthenticationMechanisms { responder } => { |
| responder.send(&mut Err(ApiError::UnsupportedOperation))?; |
| } |
| } |
| Ok(()) |
| } |
| |
| async fn get_account_ids(&self) -> Vec<FidlLocalAccountId> { |
| self.account_map.lock().await.get_account_ids().iter().map(|id| id.clone().into()).collect() |
| } |
| |
| async fn get_account_auth_states( |
| &self, |
| _scenario: Scenario, |
| ) -> Result<Vec<AccountAuthState>, ApiError> { |
| // TODO(jsankey): Collect authentication state from AccountHandler instances rather than |
| // returning a fixed value. This will involve opening account handler connections (in |
| // parallel) for all of the accounts where encryption keys for the account's data partition |
| // are available. |
| return Err(ApiError::UnsupportedOperation); |
| } |
| |
| async fn get_account( |
| &self, |
| id: LocalAccountId, |
| auth_context_provider: ClientEnd<AuthenticationContextProviderMarker>, |
| account: ServerEnd<AccountMarker>, |
| ) -> Result<(), ApiError> { |
| let mut account_map = self.account_map.lock().await; |
| let account_handler = account_map.get_handler(&id).await.map_err(|err| { |
| warn!("Failure getting account handler connection: {:?}", err); |
| err.api_error |
| })?; |
| account_handler.proxy().unlock_account().await.map_err(|_| ApiError::Resource)??; |
| |
| account_handler.proxy().get_account(auth_context_provider, account).await.map_err( |
| |err| { |
| warn!("Failure calling get account: {:?}", err); |
| ApiError::Resource |
| }, |
| )? |
| } |
| |
| async fn register_account_listener( |
| &self, |
| listener: ClientEnd<AccountListenerMarker>, |
| options: AccountListenerOptions, |
| ) -> Result<(), ApiError> { |
| match (&options.scenario, &options.granularity) { |
| (None, Some(_)) => return Err(ApiError::InvalidRequest), |
| (Some(_), _) => return Err(ApiError::UnsupportedOperation), |
| (None, None) => {} |
| }; |
| let account_ids = self.account_map.lock().await.get_account_ids(); |
| let proxy = listener.into_proxy().map_err(|err| { |
| warn!("Could not convert AccountListener client end to proxy {:?}", err); |
| ApiError::InvalidRequest |
| })?; |
| self.event_emitter.add_listener(proxy, options, &account_ids).await.map_err(|err| { |
| warn!("Could not instantiate AccountListener client {:?}", err); |
| ApiError::Unknown |
| })?; |
| info!("AccountListener established"); |
| Ok(()) |
| } |
| |
| async fn remove_account( |
| &self, |
| account_id: LocalAccountId, |
| force: bool, |
| ) -> Result<(), ApiError> { |
| let mut account_map = self.account_map.lock().await; |
| let account_handler = account_map.get_handler(&account_id).await.map_err(|err| { |
| warn!("Could not get account handler for account removal {:?}", err); |
| err.api_error |
| })?; |
| // TODO(fxbug.dev/43491): Make a conscious decision on what should happen when removing |
| // a locked account. |
| account_handler.proxy().unlock_account().await.map_err(|_| ApiError::Resource)??; |
| account_handler.proxy().remove_account(force).await.map_err(|_| ApiError::Resource)??; |
| account_handler.terminate().await; |
| // Emphemeral accounts were never included in the StoredAccountList and so it does not need |
| // to be modified when they are removed. |
| // TODO(fxbug.dev/39455): Handle irrecoverable, corrupt state. |
| account_map.remove_account(&account_id).await.map_err(|err| { |
| warn!("Could not remove account: {:?}", err); |
| // TODO(fxbug.dev/39829): Improve error mapping. |
| if err.api_error == ApiError::NotFound { |
| // We already checked for existence, so NotFound is unexpected |
| ApiError::Internal |
| } else { |
| err.api_error |
| } |
| })?; |
| let event = AccountEvent::AccountRemoved(account_id.clone()); |
| self.event_emitter.publish(&event).await; |
| Ok(()) |
| } |
| |
| /// Creates a new account handler connection, then creates an account within it, |
| /// and finally returns the connection. |
| async fn create_account_internal( |
| &self, |
| lifetime: Lifetime, |
| auth_mechanism_id: Option<String>, |
| ) -> Result<Arc<AHC>, ApiError> { |
| let account_handler = |
| self.account_map.lock().await.new_handler(lifetime).map_err(|err| { |
| warn!("Could not initialize account handler: {:?}", err); |
| err.api_error |
| })?; |
| account_handler |
| .proxy() |
| .create_account(auth_mechanism_id.as_ref().map(|x| &**x)) |
| .await |
| .map_err(|err| { |
| warn!("Could not create account: {:?}", err); |
| ApiError::Resource |
| })??; |
| Ok(account_handler) |
| } |
| |
| async fn provision_new_account( |
| &self, |
| lifetime: Lifetime, |
| auth_mechanism_id: Option<String>, |
| ) -> Result<FidlLocalAccountId, ApiError> { |
| let account_handler = self.create_account_internal(lifetime, auth_mechanism_id).await?; |
| let account_id = account_handler.get_account_id(); |
| |
| // Persist the account both in memory and on disk |
| if let Err(err) = self.add_account(account_handler.clone()).await { |
| warn!("Failure adding account: {:?}", err); |
| account_handler.terminate().await; |
| Err(err.api_error) |
| } else { |
| info!("Adding new local account {:?}", account_id); |
| Ok(account_id.clone().into()) |
| } |
| } |
| |
| async fn provision_from_auth_provider( |
| &self, |
| auth_context_provider: ClientEnd<AuthenticationContextProviderMarker>, |
| auth_provider_type: String, |
| lifetime: Lifetime, |
| auth_mechanism_id: Option<String>, |
| ) -> Result<FidlLocalAccountId, ApiError> { |
| let account_handler = self.create_account_internal(lifetime, auth_mechanism_id).await?; |
| let account_id = account_handler.get_account_id(); |
| |
| // Add a service provider to the account |
| let _user_profile = match Self::add_service_provider_account( |
| auth_context_provider, |
| auth_provider_type, |
| Arc::clone(&account_handler), |
| ) |
| .await |
| { |
| Ok(user_profile) => user_profile, |
| Err(err) => { |
| // TODO(dnordstrom): Remove the newly created account handler as a cleanup. |
| warn!("Failure adding service provider account: {:?}", err); |
| account_handler.terminate().await; |
| return Err(err.api_error); |
| } |
| }; |
| |
| // Persist the account both in memory and on disk |
| if let Err(err) = self.add_account(Arc::clone(&account_handler)).await { |
| warn!("Failure adding service provider account: {:?}", err); |
| account_handler.terminate().await; |
| Err(err.api_error) |
| } else { |
| info!("Adding new account {:?}", &account_id); |
| Ok(account_id.clone().into()) |
| } |
| } |
| |
| // Attach a service provider account to this Fuchsia account |
| async fn add_service_provider_account( |
| auth_context_provider: ClientEnd<AuthenticationContextProviderMarker>, |
| auth_provider_type: String, |
| account_handler: Arc<AHC>, |
| ) -> Result<UserProfileInfo, AccountManagerError> { |
| // Use account handler to get a channel to the account |
| let (account_client_end, account_server_end) = |
| create_endpoints().account_manager_error(ApiError::Resource)?; |
| account_handler.proxy().get_account(auth_context_provider, account_server_end).await??; |
| let account_proxy = |
| account_client_end.into_proxy().account_manager_error(ApiError::Resource)?; |
| |
| // Use the account to get the persona |
| let (persona_client_end, persona_server_end) = |
| create_endpoints().account_manager_error(ApiError::Resource)?; |
| account_proxy.get_default_persona(persona_server_end).await??; |
| let persona_proxy = |
| persona_client_end.into_proxy().account_manager_error(ApiError::Resource)?; |
| |
| // Use the persona to get the token manager |
| let (tm_client_end, tm_server_end) = |
| create_endpoints().account_manager_error(ApiError::Resource)?; |
| persona_proxy.get_token_manager(SELF_URL, tm_server_end).await??; |
| let tm_proxy = tm_client_end.into_proxy().account_manager_error(ApiError::Resource)?; |
| |
| // Use the token manager to authorize |
| let mut app_config = AppConfig { |
| auth_provider_type, |
| client_id: None, |
| client_secret: None, |
| redirect_uri: None, |
| }; |
| let fut = tm_proxy.authorize( |
| &mut app_config, |
| None, /* auth_ui_context */ |
| &mut APP_SCOPES.iter().map(|x| &**x), |
| None, /* user_profile_id */ |
| None, /* auth_code */ |
| ); |
| match fut.await? { |
| (AuthStatus::Ok, None) => Err(AccountManagerError::new(ApiError::Internal) |
| .with_cause(format_err!("Invalid response from token manager"))), |
| (AuthStatus::Ok, Some(user_profile)) => Ok(*user_profile), |
| (auth_status, _) => Err(AccountManagerError::from(auth_status)), |
| } |
| } |
| |
| // Add the account to the AccountManager, including persistent state. |
| async fn add_account(&self, account_handler: Arc<AHC>) -> Result<(), AccountManagerError> { |
| let mut account_map = self.account_map.lock().await; |
| |
| account_map.add_account(Arc::clone(&account_handler)).await.map_err(|err| { |
| warn!("Could not add account: {:?}", err); |
| // TODO(fxbug.dev/39829): Improve error mapping. |
| if err.api_error == ApiError::FailedPrecondition { |
| ApiError::Internal |
| } else { |
| err.api_error |
| } |
| })?; |
| let event = AccountEvent::AccountAdded(account_handler.get_account_id().clone()); |
| self.event_emitter.publish(&event).await; |
| Ok(()) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use crate::account_handler_connection::AccountHandlerConnectionImpl; |
| use crate::stored_account_list::{StoredAccountList, StoredAccountMetadata}; |
| use fidl::endpoints::{create_request_stream, RequestStream}; |
| use fidl_fuchsia_identity_account::{ |
| AccountListenerRequest, AccountManagerProxy, AccountManagerRequestStream, |
| AuthChangeGranularity, InitialAccountState, Scenario, ThreatScenario, |
| }; |
| use fuchsia_async as fasync; |
| use fuchsia_inspect::{assert_inspect_tree, Inspector}; |
| use fuchsia_zircon as zx; |
| use futures::future::join; |
| use lazy_static::lazy_static; |
| use std::path::Path; |
| use tempfile::TempDir; |
| |
| type TestAccountManager = AccountManager<AccountHandlerConnectionImpl>; |
| |
| lazy_static! { |
| /// Configuration for a set of fake auth providers used for testing. |
| /// This can be populated later if needed. |
| static ref AUTH_PROVIDER_CONFIG: Vec<AuthProviderConfig> = vec![]; |
| |
| static ref AUTH_MECHANISM_IDS: Vec<String> = vec![]; |
| |
| static ref TEST_SCENARIO: Scenario = |
| Scenario { include_test: false, threat_scenario: ThreatScenario::BasicAttacker }; |
| |
| static ref TEST_GRANULARITY: AuthChangeGranularity = AuthChangeGranularity { |
| engagement_changes: true, |
| presence_changes: false, |
| summary_changes: true, |
| }; |
| } |
| |
| const FORCE_REMOVE_ON: bool = true; |
| |
| fn request_stream_test<TestFn, Fut>(account_manager: TestAccountManager, test_fn: TestFn) |
| where |
| TestFn: FnOnce(AccountManagerProxy, Arc<TestAccountManager>) -> Fut, |
| Fut: Future<Output = Result<(), Error>>, |
| { |
| let mut executor = fasync::Executor::new().expect("Failed to create executor"); |
| let (server_chan, client_chan) = zx::Channel::create().expect("Failed to create channel"); |
| let proxy = AccountManagerProxy::new(fasync::Channel::from_channel(client_chan).unwrap()); |
| let request_stream = AccountManagerRequestStream::from_channel( |
| fasync::Channel::from_channel(server_chan).unwrap(), |
| ); |
| |
| let account_manager_arc = Arc::new(account_manager); |
| let account_manager_clone = Arc::clone(&account_manager_arc); |
| // TODO(fxbug.dev/39745): Migrate off of fuchsia_async::spawn. |
| fasync::Task::spawn(async move { |
| account_manager_clone |
| .handle_requests_from_stream(request_stream) |
| .await |
| .unwrap_or_else(|err| panic!("Fatal error handling test request: {:?}", err)) |
| }) |
| .detach(); |
| |
| executor |
| .run_singlethreaded(test_fn(proxy, account_manager_arc)) |
| .expect("Executor run failed.") |
| } |
| |
| // Construct an account manager initialized with the supplied set of accounts. |
| fn create_accounts( |
| existing_ids: Vec<u64>, |
| data_dir: &Path, |
| inspector: &Inspector, |
| ) -> TestAccountManager { |
| let stored_account_list = existing_ids |
| .iter() |
| .map(|&id| StoredAccountMetadata::new(LocalAccountId::new(id))) |
| .collect(); |
| StoredAccountList::new(stored_account_list) |
| .save(data_dir) |
| .expect("Couldn't write account list"); |
| |
| read_accounts(data_dir, inspector) |
| } |
| |
| // Contructs an account manager that reads its accounts from the supplied directory. |
| fn read_accounts( |
| data_dir: &Path, |
| inspector: &Inspector, |
| ) -> AccountManager<AccountHandlerConnectionImpl> { |
| AccountManager::new( |
| data_dir.to_path_buf(), |
| &AUTH_PROVIDER_CONFIG, |
| &AUTH_MECHANISM_IDS, |
| inspector, |
| ) |
| .unwrap() |
| } |
| |
| /// Note: Many AccountManager methods launch instances of an AccountHandler. Since its |
| /// currently not convenient to mock out this component launching in Rust, we rely on the |
| /// hermetic component test to provide coverage for these areas and only cover the in-process |
| /// behavior with this unit-test. |
| |
| #[test] |
| fn test_new() { |
| let inspector = Inspector::new(); |
| let data_dir = TempDir::new().unwrap(); |
| request_stream_test( |
| AccountManager::new( |
| data_dir.path().into(), |
| &AUTH_PROVIDER_CONFIG, |
| &AUTH_MECHANISM_IDS, |
| &inspector, |
| ) |
| .unwrap(), |
| |proxy, _| async move { |
| assert_eq!(proxy.get_account_ids().await?.len(), 0); |
| Ok(()) |
| }, |
| ); |
| } |
| |
| #[test] |
| fn test_unsupported_scenario() { |
| let data_dir = TempDir::new().unwrap(); |
| request_stream_test( |
| create_accounts(vec![], data_dir.path(), &Inspector::new()), |
| |proxy, _test_object| async move { |
| assert_eq!(proxy.get_account_ids().await?.len(), 0); |
| assert_eq!( |
| proxy.get_account_auth_states(&mut TEST_SCENARIO.clone()).await?, |
| Err(ApiError::UnsupportedOperation) |
| ); |
| Ok(()) |
| }, |
| ); |
| } |
| |
| #[test] |
| fn test_initially_empty() { |
| let data_dir = TempDir::new().unwrap(); |
| let inspector = Inspector::new(); |
| request_stream_test( |
| create_accounts(vec![], data_dir.path(), &inspector), |
| |proxy, _test_object| async move { |
| assert_eq!(proxy.get_account_ids().await?.len(), 0); |
| assert_inspect_tree!(inspector, root: contains { |
| accounts: { |
| active: 0 as u64, |
| total: 0 as u64, |
| }, |
| listeners: { |
| active: 0 as u64, |
| events: 0 as u64, |
| total_opened: 0 as u64, |
| }, |
| }); |
| Ok(()) |
| }, |
| ); |
| } |
| |
| #[test] |
| fn test_remove_missing_account() { |
| // Manually create an account manager with one account. |
| let data_dir = TempDir::new().unwrap(); |
| let stored_account_list = |
| StoredAccountList::new(vec![StoredAccountMetadata::new(LocalAccountId::new(1))]); |
| stored_account_list.save(data_dir.path()).unwrap(); |
| let inspector = Inspector::new(); |
| request_stream_test(read_accounts(data_dir.path(), &inspector), |proxy, _test_object| { |
| async move { |
| // Try to delete a very different account from the one we added. |
| assert_eq!( |
| proxy.remove_account(LocalAccountId::new(42).into(), FORCE_REMOVE_ON).await?, |
| Err(ApiError::NotFound) |
| ); |
| assert_inspect_tree!(inspector, root: contains { |
| accounts: { |
| total: 1 as u64, |
| active: 0 as u64, |
| }, |
| }); |
| Ok(()) |
| } |
| }); |
| } |
| |
| /// Sets up an AccountListener which an init event. |
| #[test] |
| fn test_account_listener() { |
| let mut options = AccountListenerOptions { |
| initial_state: true, |
| add_account: true, |
| remove_account: true, |
| scenario: None, |
| granularity: None, |
| }; |
| |
| let data_dir = TempDir::new().unwrap(); |
| let inspector = Inspector::new(); |
| request_stream_test( |
| create_accounts(vec![1, 2], data_dir.path(), &inspector), |
| |proxy, _| { |
| async move { |
| let (client_end, mut stream) = |
| create_request_stream::<AccountListenerMarker>().unwrap(); |
| let serve_fut = async move { |
| let request = stream.try_next().await.expect("stream error"); |
| if let Some(AccountListenerRequest::OnInitialize { |
| account_states, |
| responder, |
| }) = request |
| { |
| assert_eq!( |
| account_states, |
| vec![ |
| InitialAccountState { account_id: 1, auth_state: None }, |
| InitialAccountState { account_id: 2, auth_state: None }, |
| ] |
| ); |
| responder.send().unwrap(); |
| } else { |
| panic!("Unexpected message received"); |
| }; |
| if let Some(_) = stream.try_next().await.expect("stream error") { |
| panic!("Unexpected message, channel should be closed"); |
| } |
| }; |
| let request_fut = async move { |
| // The registering itself triggers the init event. |
| assert_eq!( |
| proxy |
| .register_account_listener(client_end, &mut options) |
| .await |
| .unwrap(), |
| Ok(()) |
| ); |
| }; |
| join(request_fut, serve_fut).await; |
| Ok(()) |
| } |
| }, |
| ); |
| } |
| |
| /// Registers an account listener with invalid request arguments. |
| #[test] |
| fn test_account_listener_invalid_requests() { |
| let mut options = AccountListenerOptions { |
| initial_state: true, |
| add_account: true, |
| remove_account: true, |
| scenario: None, |
| granularity: Some(Box::new(TEST_GRANULARITY.clone())), |
| }; |
| |
| let data_dir = TempDir::new().unwrap(); |
| let inspector = Inspector::new(); |
| request_stream_test( |
| create_accounts(vec![1, 2], data_dir.path(), &inspector), |
| |proxy, _| async move { |
| let (client_end, _) = create_request_stream::<AccountListenerMarker>().unwrap(); |
| assert_eq!( |
| proxy.register_account_listener(client_end, &mut options).await?, |
| Err(ApiError::InvalidRequest) |
| ); |
| Ok(()) |
| }, |
| ); |
| } |
| |
| /// Registers an account listener with a scenario, which is currently unsupported. |
| #[test] |
| fn test_account_listener_scenario() { |
| let mut options = AccountListenerOptions { |
| initial_state: true, |
| add_account: true, |
| remove_account: true, |
| scenario: Some(Box::new(TEST_SCENARIO.clone())), |
| granularity: None, |
| }; |
| |
| let data_dir = TempDir::new().unwrap(); |
| let inspector = Inspector::new(); |
| request_stream_test( |
| create_accounts(vec![1, 2], data_dir.path(), &inspector), |
| |proxy, _| async move { |
| let (client_end, _) = create_request_stream::<AccountListenerMarker>().unwrap(); |
| assert_eq!( |
| proxy.register_account_listener(client_end, &mut options).await?, |
| Err(ApiError::UnsupportedOperation) |
| ); |
| Ok(()) |
| }, |
| ); |
| } |
| |
| /// Registers an account listener with a scenario and a granularity, which is currently |
| /// unsupported. |
| #[test] |
| fn test_account_listener_scenario_granularity() { |
| let mut options = AccountListenerOptions { |
| initial_state: true, |
| add_account: true, |
| remove_account: true, |
| scenario: Some(Box::new(TEST_SCENARIO.clone())), |
| granularity: Some(Box::new(TEST_GRANULARITY.clone())), |
| }; |
| |
| let data_dir = TempDir::new().unwrap(); |
| let inspector = Inspector::new(); |
| request_stream_test( |
| create_accounts(vec![1, 2], data_dir.path(), &inspector), |
| |proxy, _| async move { |
| let (client_end, _) = create_request_stream::<AccountListenerMarker>().unwrap(); |
| assert_eq!( |
| proxy.register_account_listener(client_end, &mut options).await?, |
| Err(ApiError::UnsupportedOperation) |
| ); |
| Ok(()) |
| }, |
| ); |
| } |
| } |