1use std::{
6 cell::Cell,
7 collections::{HashMap, HashSet},
8};
9
10pub use super::http_client::{GetDeviceResponse as Device, PushSubscription};
11use super::{
12 commands::{self, IncomingDeviceCommand, PrivateCommandKeys, PublicCommandKeys},
13 http_client::{
14 DeviceUpdateRequest, DeviceUpdateRequestBuilder, PendingCommand, UpdateDeviceResponse,
15 },
16 scopes, telemetry, util, CachedResponse, FirefoxAccount,
17};
18use crate::{info, warn, DeviceCapability, Error, LocalDevice, Result};
19use sync15::DeviceType;
20
21const DEVICES_FRESHNESS_THRESHOLD: u64 = 60_000; thread_local! {
25 pub static COMMAND_MAX_PAYLOAD_SIZE: Cell<usize> = const { Cell::new(16 * 1024) }
30}
31
32#[derive(Clone, Copy)]
34pub enum CommandFetchReason {
35 Poll,
37 Push(u64),
39}
40
41impl FirefoxAccount {
42 pub fn get_devices(&mut self, ignore_cache: bool) -> Result<Vec<Device>> {
48 if let Some(d) = &self.devices_cache {
49 if !ignore_cache && util::now() < d.cached_at + DEVICES_FRESHNESS_THRESHOLD {
50 return Ok(d.response.clone());
51 }
52 }
53
54 let refresh_token = self.get_refresh_token()?;
55 let response = self
56 .client
57 .get_devices(self.state.config(), refresh_token)?;
58
59 self.devices_cache = Some(CachedResponse {
60 response: response.clone(),
61 cached_at: util::now(),
62 etag: "".into(),
63 });
64
65 Ok(response)
66 }
67
68 pub fn get_current_device(&mut self) -> Result<Option<Device>> {
69 Ok(self
70 .get_devices(false)?
71 .into_iter()
72 .find(|d| d.is_current_device))
73 }
74
75 fn register_capabilities(
79 &mut self,
80 capabilities: &[DeviceCapability],
81 ) -> Result<HashMap<String, String>> {
82 let mut commands = HashMap::new();
83 for capability in capabilities.iter().collect::<HashSet<_>>() {
84 match capability {
85 DeviceCapability::SendTab => {
86 let send_tab_command_data =
87 self.generate_command_data(DeviceCapability::SendTab)?;
88 commands.insert(
89 commands::send_tab::COMMAND_NAME.to_owned(),
90 send_tab_command_data,
91 );
92 }
93 DeviceCapability::CloseTabs => {
94 let close_tabs_command_data =
95 self.generate_command_data(DeviceCapability::CloseTabs)?;
96 commands.insert(
97 commands::close_tabs::COMMAND_NAME.to_owned(),
98 close_tabs_command_data,
99 );
100 }
101 }
102 }
103 Ok(commands)
104 }
105
106 pub fn initialize_device(
111 &mut self,
112 name: &str,
113 device_type: DeviceType,
114 capabilities: &[DeviceCapability],
115 ) -> Result<LocalDevice> {
116 self.state
117 .set_device_capabilities(capabilities.iter().cloned());
118 let commands = self.register_capabilities(capabilities)?;
119 let update = DeviceUpdateRequestBuilder::new()
120 .display_name(name)
121 .device_type(&device_type)
122 .available_commands(&commands)
123 .build();
124 self.update_device(update)
125 }
126
127 pub fn ensure_capabilities(
135 &mut self,
136 capabilities: &[DeviceCapability],
137 ) -> Result<LocalDevice> {
138 self.state
139 .set_device_capabilities(capabilities.iter().cloned());
140 if let Some(local_device) = self.state.server_local_device_info() {
142 if capabilities == local_device.capabilities {
143 return Ok(local_device.clone());
144 }
145 }
146 let commands = self.register_capabilities(capabilities)?;
147 let update = DeviceUpdateRequestBuilder::new()
148 .available_commands(&commands)
149 .build();
150 self.update_device(update)
151 }
152
153 pub(crate) fn reregister_current_capabilities(&mut self) -> Result<()> {
155 let capabilities: Vec<_> = self.state.device_capabilities().iter().cloned().collect();
156 let commands = self.register_capabilities(&capabilities)?;
157 let update = DeviceUpdateRequestBuilder::new()
158 .available_commands(&commands)
159 .build();
160 self.update_device(update)?;
161 Ok(())
162 }
163
164 pub(crate) fn invoke_command(
165 &self,
166 command: &str,
167 target: &Device,
168 payload: &serde_json::Value,
169 ttl: Option<u64>,
170 ) -> Result<()> {
171 let refresh_token = self.get_refresh_token()?;
172 self.client.invoke_command(
173 self.state.config(),
174 refresh_token,
175 command,
176 &target.id,
177 payload,
178 ttl,
179 )
180 }
181
182 pub fn poll_device_commands(
190 &mut self,
191 reason: CommandFetchReason,
192 ) -> Result<Vec<IncomingDeviceCommand>> {
193 let last_command_index = self.state.last_handled_command_index().unwrap_or(0);
194 self.fetch_and_parse_commands(last_command_index + 1, None, reason)
196 }
197
198 pub fn get_command_for_index(&mut self, index: u64) -> Result<IncomingDeviceCommand> {
199 let refresh_token = self.get_refresh_token()?;
200 let pending_commands =
201 self.client
202 .get_pending_commands(self.state.config(), refresh_token, index, Some(1))?;
203 self.parse_commands_messages(pending_commands.messages, CommandFetchReason::Push(index))?
204 .into_iter()
205 .next()
206 .ok_or_else(|| Error::CommandNotFound)
207 }
208
209 fn fetch_and_parse_commands(
210 &mut self,
211 index: u64,
212 limit: Option<u64>,
213 reason: CommandFetchReason,
214 ) -> Result<Vec<IncomingDeviceCommand>> {
215 let refresh_token = self.get_refresh_token()?;
216 let pending_commands =
217 self.client
218 .get_pending_commands(self.state.config(), refresh_token, index, limit)?;
219 if pending_commands.messages.is_empty() {
220 return Ok(Vec::new());
221 }
222 info!("Handling {} messages", pending_commands.messages.len());
223 let device_commands = self.parse_commands_messages(pending_commands.messages, reason)?;
224 self.state
225 .set_last_handled_command_index(pending_commands.index);
226 Ok(device_commands)
227 }
228
229 fn parse_commands_messages(
230 &mut self,
231 messages: Vec<PendingCommand>,
232 reason: CommandFetchReason,
233 ) -> Result<Vec<IncomingDeviceCommand>> {
234 let devices = self.get_devices(false)?;
235 let parsed_commands = messages
236 .into_iter()
237 .filter_map(|msg| match self.parse_command(msg, &devices, reason) {
238 Ok(device_command) => Some(device_command),
239 Err(e) => {
240 error_support::report_error!(
241 "fxaclient-command",
242 "Error while processing command: {}",
243 e
244 );
245 None
246 }
247 })
248 .collect();
249 Ok(parsed_commands)
250 }
251
252 fn parse_command(
253 &mut self,
254 command: PendingCommand,
255 devices: &[Device],
256 reason: CommandFetchReason,
257 ) -> Result<IncomingDeviceCommand> {
258 let telem_reason = match reason {
259 CommandFetchReason::Poll => telemetry::ReceivedReason::Poll,
260 CommandFetchReason::Push(index) if command.index < index => {
261 telemetry::ReceivedReason::PushMissed
262 }
263 _ => telemetry::ReceivedReason::Push,
264 };
265 let command_data = command.data;
266 let sender = command_data
267 .sender
268 .and_then(|s| devices.iter().find(|i| i.id == s).cloned());
269 match command_data.command.as_str() {
270 commands::send_tab::COMMAND_NAME => {
271 self.handle_send_tab_command(sender, command_data.payload, telem_reason)
272 }
273 commands::close_tabs::COMMAND_NAME => {
274 self.handle_close_tabs_command(sender, command_data.payload, telem_reason)
275 }
276 _ => Err(Error::UnknownCommand(command_data.command)),
277 }
278 }
279
280 pub fn set_device_name(&mut self, name: &str) -> Result<LocalDevice> {
281 let update = DeviceUpdateRequestBuilder::new().display_name(name).build();
282 self.update_device(update)
283 }
284
285 pub fn clear_device_name(&mut self) -> Result<()> {
286 let update = DeviceUpdateRequestBuilder::new()
287 .clear_display_name()
288 .build();
289 self.update_device(update)?;
290 Ok(())
291 }
292
293 pub fn set_push_subscription(
294 &mut self,
295 push_subscription: PushSubscription,
296 ) -> Result<LocalDevice> {
297 let update = DeviceUpdateRequestBuilder::new()
298 .push_subscription(&push_subscription)
299 .build();
300 self.update_device(update)
301 }
302
303 pub(crate) fn replace_device(
304 &mut self,
305 display_name: &str,
306 device_type: &DeviceType,
307 push_subscription: &Option<PushSubscription>,
308 commands: &HashMap<String, String>,
309 ) -> Result<()> {
310 self.state.clear_server_local_device_info();
311 let mut builder = DeviceUpdateRequestBuilder::new()
312 .display_name(display_name)
313 .device_type(device_type)
314 .available_commands(commands);
315 if let Some(push_subscription) = push_subscription {
316 builder = builder.push_subscription(push_subscription)
317 }
318 self.update_device(builder.build())?;
319 Ok(())
320 }
321
322 fn update_device(&mut self, update: DeviceUpdateRequest<'_>) -> Result<LocalDevice> {
323 let refresh_token = self.get_refresh_token()?;
324 let res = self
325 .client
326 .update_device_record(self.state.config(), refresh_token, update);
327 match res {
328 Ok(resp) => {
329 self.state.set_current_device_id(resp.id.clone());
330 let local_device = LocalDevice::from(resp);
331 self.state
332 .update_server_local_device_info(local_device.clone());
333 Ok(local_device)
334 }
335 Err(err) => {
336 self.state.clear_server_local_device_info();
339 Err(err)
340 }
341 }
342 }
343
344 pub fn get_current_device_id(&mut self) -> Result<String> {
346 match self.state.current_device_id() {
347 Some(ref device_id) => Ok(device_id.to_string()),
348 None => Err(Error::NoCurrentDeviceId),
349 }
350 }
351
352 pub(crate) fn generate_command_data(&mut self, capability: DeviceCapability) -> Result<String> {
357 let own_keys = self.load_or_generate_command_keys(capability)?;
358 let public_keys: PublicCommandKeys = own_keys.into();
359 let oldsync_key = self.get_scoped_key(scopes::OLD_SYNC)?;
360 public_keys.as_command_data(oldsync_key)
361 }
362
363 fn load_or_generate_command_keys(
364 &mut self,
365 capability: DeviceCapability,
366 ) -> Result<PrivateCommandKeys> {
367 match capability {
368 DeviceCapability::SendTab => self.load_or_generate_send_tab_keys(),
369 DeviceCapability::CloseTabs => self.load_or_generate_close_tabs_keys(),
370 }
371 }
372}
373
374impl TryFrom<String> for DeviceCapability {
375 type Error = Error;
376
377 fn try_from(command: String) -> Result<Self> {
378 match command.as_str() {
379 commands::send_tab::COMMAND_NAME => Ok(DeviceCapability::SendTab),
380 commands::close_tabs::COMMAND_NAME => Ok(DeviceCapability::CloseTabs),
381 _ => Err(Error::UnknownCommand(command)),
382 }
383 }
384}
385
386impl From<UpdateDeviceResponse> for LocalDevice {
387 fn from(resp: UpdateDeviceResponse) -> Self {
388 Self {
389 id: resp.id,
390 display_name: resp.display_name,
391 device_type: resp.device_type,
392 capabilities: resp
393 .available_commands
394 .into_keys()
395 .filter_map(|command| match command.try_into() {
396 Ok(capability) => Some(capability),
397 Err(e) => {
398 warn!("While parsing UpdateDeviceResponse: {e}");
399 None
400 }
401 })
402 .collect(),
403 push_subscription: resp.push_subscription.map(Into::into),
404 push_endpoint_expired: resp.push_endpoint_expired,
405 }
406 }
407}
408
409impl TryFrom<Device> for crate::Device {
410 type Error = Error;
411 fn try_from(d: Device) -> Result<Self> {
412 let capabilities: Vec<_> = d
413 .available_commands
414 .keys()
415 .filter_map(|k| match k.as_str() {
416 commands::send_tab::COMMAND_NAME => Some(DeviceCapability::SendTab),
417 commands::close_tabs::COMMAND_NAME => Some(DeviceCapability::CloseTabs),
418 _ => None,
419 })
420 .collect();
421 Ok(crate::Device {
422 id: d.common.id,
423 display_name: d.common.display_name,
424 device_type: d.common.device_type,
425 capabilities,
426 push_subscription: d.common.push_subscription.map(Into::into),
427 push_endpoint_expired: d.common.push_endpoint_expired,
428 is_current_device: d.is_current_device,
429 last_access_time: d.last_access_time.map(TryFrom::try_from).transpose()?,
430 })
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use crate::internal::http_client::*;
438 use crate::internal::oauth::RefreshToken;
439 use crate::internal::Config;
440 use crate::ScopedKey;
441 use mockall::predicate::always;
442 use mockall::predicate::eq;
443 use nss::ensure_initialized;
444 use std::collections::HashSet;
445 use std::sync::Arc;
446
447 fn setup() -> FirefoxAccount {
448 ensure_initialized();
449
450 let config = Config::stable_dev("12345678", "https://foo.bar");
453 let mut fxa = FirefoxAccount::with_config(config);
454 fxa.state.force_refresh_token(RefreshToken {
455 token: "refreshtok".to_string(),
456 scopes: HashSet::default(),
457 });
458 fxa.state.insert_scoped_key("https://identity.mozilla.com/apps/oldsync", ScopedKey {
459 kty: "oct".to_string(),
460 scope: "https://identity.mozilla.com/apps/oldsync".to_string(),
461 k: "kMtwpVC0ZaYFJymPza8rXK_0CgCp3KMwRStwGfBRBDtL6hXRDVJgQFaoOQ2dimw0Bko5WVv2gNTy7RX5zFYZHg".to_string(),
462 kid: "1542236016429-Ox1FbJfFfwTe5t-xq4v2hQ".to_string(),
463 });
464 fxa
465 }
466
467 #[test]
468 fn test_ensure_capabilities_does_not_hit_the_server_if_nothing_has_changed() {
469 let mut fxa = setup();
470
471 let mut client = MockFxAClient::new();
473 client
474 .expect_update_device_record()
475 .with(always(), eq("refreshtok"), always())
476 .times(1)
477 .returning(|_, _, _| {
478 Ok(UpdateDeviceResponse {
479 id: "device1".to_string(),
480 display_name: "".to_string(),
481 device_type: DeviceType::Desktop,
482 push_subscription: None,
483 available_commands: HashMap::from([(
484 commands::send_tab::COMMAND_NAME.to_owned(),
485 "fake-command-data".to_owned(),
486 )]),
487 push_endpoint_expired: false,
488 })
489 });
490 fxa.set_client(Arc::new(client));
491 fxa.ensure_capabilities(&[DeviceCapability::SendTab])
492 .unwrap();
493 let saved = fxa.to_json().unwrap();
494
495 fxa.ensure_capabilities(&[DeviceCapability::SendTab])
498 .unwrap();
499
500 let mut restored = FirefoxAccount::from_json(&saved).unwrap();
503 restored.set_client(Arc::new(MockFxAClient::new()));
504 restored
505 .ensure_capabilities(&[DeviceCapability::SendTab])
506 .unwrap();
507 }
508
509 #[test]
510 fn test_ensure_capabilities_updates_the_server_if_capabilities_increase() {
511 let mut fxa = setup();
512
513 let mut client = MockFxAClient::new();
515 client
516 .expect_update_device_record()
517 .with(always(), eq("refreshtok"), always())
518 .times(1)
519 .returning(|_, _, _| {
520 Ok(UpdateDeviceResponse {
521 id: "device1".to_string(),
522 display_name: "".to_string(),
523 device_type: DeviceType::Desktop,
524 push_subscription: None,
525 available_commands: HashMap::default(),
526 push_endpoint_expired: false,
527 })
528 });
529 fxa.set_client(Arc::new(client));
530
531 fxa.ensure_capabilities(&[]).unwrap();
532 let saved = fxa.to_json().unwrap();
533
534 let mut client = MockFxAClient::new();
536 client
537 .expect_update_device_record()
538 .with(always(), eq("refreshtok"), always())
539 .times(1)
540 .returning(|_, _, _| {
541 Ok(UpdateDeviceResponse {
542 id: "device1".to_string(),
543 display_name: "".to_string(),
544 device_type: DeviceType::Desktop,
545 push_subscription: None,
546 available_commands: HashMap::from([(
547 commands::send_tab::COMMAND_NAME.to_owned(),
548 "fake-command-data".to_owned(),
549 )]),
550 push_endpoint_expired: false,
551 })
552 });
553 fxa.set_client(Arc::new(client));
554
555 fxa.ensure_capabilities(&[DeviceCapability::SendTab])
556 .unwrap();
557
558 let mut restored = FirefoxAccount::from_json(&saved).unwrap();
561 let mut client = MockFxAClient::new();
562 client
563 .expect_update_device_record()
564 .with(always(), eq("refreshtok"), always())
565 .returning(|_, _, _| {
566 Ok(UpdateDeviceResponse {
567 id: "device1".to_string(),
568 display_name: "".to_string(),
569 device_type: DeviceType::Desktop,
570 push_subscription: None,
571 available_commands: HashMap::from([(
572 commands::send_tab::COMMAND_NAME.to_owned(),
573 "fake-command-data".to_owned(),
574 )]),
575 push_endpoint_expired: false,
576 })
577 });
578 restored.set_client(Arc::new(client));
579
580 restored
581 .ensure_capabilities(&[DeviceCapability::SendTab])
582 .unwrap();
583 }
584
585 #[test]
586 fn test_ensure_capabilities_updates_the_server_if_capabilities_reduce() {
587 let mut fxa = setup();
588
589 let mut client = MockFxAClient::new();
591 client
592 .expect_update_device_record()
593 .with(always(), eq("refreshtok"), always())
594 .times(1)
595 .returning(|_, _, _| {
596 Ok(UpdateDeviceResponse {
597 id: "device1".to_string(),
598 display_name: "".to_string(),
599 device_type: DeviceType::Desktop,
600 push_subscription: None,
601 available_commands: HashMap::from([(
602 commands::send_tab::COMMAND_NAME.to_owned(),
603 "fake-command-data".to_owned(),
604 )]),
605 push_endpoint_expired: false,
606 })
607 });
608 fxa.set_client(Arc::new(client));
609
610 fxa.ensure_capabilities(&[DeviceCapability::SendTab])
611 .unwrap();
612 let saved = fxa.to_json().unwrap();
613
614 let mut client = MockFxAClient::new();
616 client
617 .expect_update_device_record()
618 .with(always(), eq("refreshtok"), always())
619 .times(1)
620 .returning(|_, _, _| {
621 Ok(UpdateDeviceResponse {
622 id: "device1".to_string(),
623 display_name: "".to_string(),
624 device_type: DeviceType::Desktop,
625 push_subscription: None,
626 available_commands: HashMap::default(),
627 push_endpoint_expired: false,
628 })
629 });
630 fxa.set_client(Arc::new(client));
631
632 fxa.ensure_capabilities(&[]).unwrap();
633
634 let mut restored = FirefoxAccount::from_json(&saved).unwrap();
637 let mut client = MockFxAClient::new();
638 client
639 .expect_update_device_record()
640 .with(always(), eq("refreshtok"), always())
641 .times(1)
642 .returning(|_, _, _| {
643 Ok(UpdateDeviceResponse {
644 id: "device1".to_string(),
645 display_name: "".to_string(),
646 device_type: DeviceType::Desktop,
647 push_subscription: None,
648 available_commands: HashMap::default(),
649 push_endpoint_expired: false,
650 })
651 });
652 restored.set_client(Arc::new(client));
653
654 restored.ensure_capabilities(&[]).unwrap();
655 }
656
657 #[test]
658 fn test_ensure_capabilities_will_reregister_after_new_login_flow() {
659 let mut fxa = setup();
660
661 let mut client = MockFxAClient::new();
663 client
664 .expect_update_device_record()
665 .with(always(), eq("refreshtok"), always())
666 .times(1)
667 .returning(|_, _, _| {
668 Ok(UpdateDeviceResponse {
669 id: "device1".to_string(),
670 display_name: "".to_string(),
671 device_type: DeviceType::Desktop,
672 push_subscription: None,
673 available_commands: HashMap::from([(
674 commands::send_tab::COMMAND_NAME.to_owned(),
675 "fake-command-data".to_owned(),
676 )]),
677 push_endpoint_expired: false,
678 })
679 });
680 fxa.set_client(Arc::new(client));
681 fxa.ensure_capabilities(&[DeviceCapability::SendTab])
682 .unwrap();
683
684 let mut client = MockFxAClient::new();
687 client
688 .expect_destroy_access_token()
689 .with(always(), always())
690 .times(1)
691 .returning(|_, _| {
692 Err(Error::RemoteError {
693 code: 500,
694 errno: 999,
695 error: "server error".to_string(),
696 message: "this will be ignored anyway".to_string(),
697 info: "".to_string(),
698 })
699 });
700 client
701 .expect_get_devices()
702 .with(always(), always())
703 .times(1)
704 .returning(|_, _| {
705 Err(Error::RemoteError {
706 code: 500,
707 errno: 999,
708 error: "server error".to_string(),
709 message: "this will be ignored anyway".to_string(),
710 info: "".to_string(),
711 })
712 });
713 client
714 .expect_destroy_refresh_token()
715 .with(always(), always())
716 .times(1)
717 .returning(|_, _| {
718 Err(Error::RemoteError {
719 code: 500,
720 errno: 999,
721 error: "server error".to_string(),
722 message: "this will be ignored anyway".to_string(),
723 info: "".to_string(),
724 })
725 });
726 fxa.set_client(Arc::new(client));
727
728 fxa.handle_oauth_response(
729 OAuthTokenResponse {
730 keys_jwe: None,
731 refresh_token: Some("newRefreshTok".to_string()),
732 session_token: None,
733 expires_in: 12345,
734 scope: "profile".to_string(),
735 access_token: "accesstok".to_string(),
736 },
737 None,
738 )
739 .unwrap();
740
741 assert!(fxa.state.server_local_device_info().is_none());
742
743 let mut client = MockFxAClient::new();
746 client
747 .expect_update_device_record()
748 .with(always(), eq("newRefreshTok"), always())
749 .times(1)
750 .returning(|_, _, _| {
751 Ok(UpdateDeviceResponse {
752 id: "device1".to_string(),
753 display_name: "".to_string(),
754 device_type: DeviceType::Desktop,
755 push_subscription: None,
756 available_commands: HashMap::from([(
757 commands::send_tab::COMMAND_NAME.to_owned(),
758 "fake-command-data".to_owned(),
759 )]),
760 push_endpoint_expired: false,
761 })
762 });
763 fxa.set_client(Arc::new(client));
764 fxa.ensure_capabilities(&[DeviceCapability::SendTab])
765 .unwrap();
766 }
767
768 #[test]
769 fn test_ensure_capabilities_updates_the_server_if_previous_attempt_failed() {
770 let mut fxa = setup();
771
772 let mut client = MockFxAClient::new();
774 client
775 .expect_update_device_record()
776 .with(always(), eq("refreshtok"), always())
777 .times(1)
778 .returning(|_, _, _| {
779 Err(Error::RemoteError {
780 code: 500,
781 errno: 999,
782 error: "server error".to_string(),
783 message: "this will be ignored anyway".to_string(),
784 info: "".to_string(),
785 })
786 });
787 fxa.set_client(Arc::new(client));
788
789 fxa.ensure_capabilities(&[DeviceCapability::SendTab])
790 .unwrap_err();
791
792 let mut client = MockFxAClient::new();
794 client
795 .expect_update_device_record()
796 .with(always(), eq("refreshtok"), always())
797 .times(1)
798 .returning(|_, _, _| {
799 Ok(UpdateDeviceResponse {
800 id: "device1".to_string(),
801 display_name: "".to_string(),
802 device_type: DeviceType::Desktop,
803 push_subscription: None,
804 available_commands: HashMap::from([(
805 commands::send_tab::COMMAND_NAME.to_owned(),
806 "fake-command-data".to_owned(),
807 )]),
808 push_endpoint_expired: false,
809 })
810 });
811 fxa.set_client(Arc::new(client));
812
813 fxa.ensure_capabilities(&[DeviceCapability::SendTab])
814 .unwrap();
815 }
816
817 #[test]
818 fn test_get_devices() {
819 let mut fxa = setup();
820 let mut client = MockFxAClient::new();
821 client
822 .expect_get_devices()
823 .with(always(), always())
824 .times(1)
825 .returning(|_, _| {
826 Ok(vec![Device {
827 common: DeviceResponseCommon {
828 id: "device1".into(),
829 display_name: "".to_string(),
830 device_type: DeviceType::Desktop,
831 push_subscription: None,
832 available_commands: HashMap::new(),
833 push_endpoint_expired: true,
834 },
835 is_current_device: true,
836 location: DeviceLocation {
837 city: None,
838 country: None,
839 state: None,
840 state_code: None,
841 },
842 last_access_time: None,
843 }])
844 });
845
846 fxa.set_client(Arc::new(client));
847 assert!(fxa.devices_cache.is_none());
848
849 assert!(fxa.get_devices(false).is_ok());
850 assert!(fxa.devices_cache.is_some());
851
852 let cache = fxa.devices_cache.clone().unwrap();
853 assert!(!cache.response.is_empty());
854 assert!(cache.cached_at > 0);
855
856 let cached_devices = cache.response;
857 assert_eq!(cached_devices[0].id, "device1".to_string());
858
859 assert!(fxa.get_devices(false).is_ok());
861 assert!(fxa.devices_cache.is_some());
862
863 let cache2 = fxa.devices_cache.unwrap();
864 let cached_devices2 = cache2.response;
865
866 assert_eq!(cache.cached_at, cache2.cached_at);
867 assert_eq!(cached_devices.len(), cached_devices2.len());
868 assert_eq!(cached_devices[0].id, cached_devices2[0].id);
869 }
870
871 #[test]
872 fn test_get_devices_network_errors() {
873 let mut fxa = setup();
874 let mut client = MockFxAClient::new();
875 client
876 .expect_get_devices()
877 .with(always(), always())
878 .times(1)
879 .returning(|_, _| {
880 Err(Error::RemoteError {
881 code: 500,
882 errno: 101,
883 error: "Did not work!".to_owned(),
884 message: "Did not work!".to_owned(),
885 info: "Did not work!".to_owned(),
886 })
887 });
888
889 fxa.set_client(Arc::new(client));
890 assert!(fxa.devices_cache.is_none());
891
892 let res = fxa.get_devices(false);
893
894 assert!(res.is_err());
895 assert!(fxa.devices_cache.is_none());
896 }
897}