1pub use super::http_client::ProfileResponse as Profile;
6use super::{scopes, util, CachedResponse, FirefoxAccount};
7use crate::{Error, Result};
8
9const PROFILE_FRESHNESS_THRESHOLD: u64 = 120_000; impl FirefoxAccount {
13 pub fn get_profile(&mut self, ignore_cache: bool) -> Result<Profile> {
23 match self.get_profile_helper(ignore_cache) {
24 Ok(res) => Ok(res),
25 Err(e) => match e {
26 Error::RemoteError { code: 401, .. } => {
27 crate::warn!(
28 "Access token rejected, clearing the tokens cache and trying again."
29 );
30 self.clear_access_token_cache();
31 self.clear_devices_and_attached_clients_cache();
32 self.get_profile_helper(ignore_cache)
33 }
34 _ => Err(e),
35 },
36 }
37 }
38
39 fn get_profile_helper(&mut self, ignore_cache: bool) -> Result<Profile> {
40 let mut etag = None;
41 if let Some(cached_profile) = self.state.last_seen_profile() {
42 if !ignore_cache && util::now() < cached_profile.cached_at + PROFILE_FRESHNESS_THRESHOLD
43 {
44 return Ok(cached_profile.response.clone());
45 }
46 etag = Some(cached_profile.etag.clone());
47 }
48 let profile_access_token = self.get_access_token(scopes::PROFILE, None)?.token;
49 match self
50 .client
51 .get_profile(self.state.config(), &profile_access_token, etag)?
52 {
53 Some(response_and_etag) => {
54 if let Some(etag) = response_and_etag.etag {
55 self.state.set_last_seen_profile(CachedResponse {
56 response: response_and_etag.response.clone(),
57 cached_at: util::now(),
58 etag,
59 });
60 }
61 Ok(response_and_etag.response)
62 }
63 None => {
64 match self.state.last_seen_profile() {
65 Some(cached_profile) => {
66 let response = cached_profile.response.clone();
67 let new_cached_profile = CachedResponse {
69 response: cached_profile.response.clone(),
70 cached_at: util::now(),
71 etag: cached_profile.etag.clone(),
72 };
73 self.state.set_last_seen_profile(new_cached_profile);
74 Ok(response)
75 }
76 None => Err(Error::ApiClientError(
77 "Got a 304 without having sent an eTag.",
78 )),
79 }
80 }
81 }
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::internal::{
89 http_client::*,
90 oauth::{AccessTokenInfo, RefreshToken},
91 Config,
92 };
93 use mockall::predicate::always;
94 use mockall::predicate::eq;
95 use std::sync::Arc;
96
97 impl FirefoxAccount {
98 pub fn add_cached_profile(&mut self, uid: &str, email: &str) {
99 self.state.set_last_seen_profile(CachedResponse {
100 response: Profile {
101 uid: uid.into(),
102 email: email.into(),
103 display_name: None,
104 avatar: "".into(),
105 avatar_default: true,
106 },
107 cached_at: util::now(),
108 etag: "fake etag".into(),
109 });
110 }
111 }
112
113 #[test]
114 fn test_fetch_profile() {
115 let config = Config::stable_dev("12345678", "https://foo.bar");
116 let mut fxa = FirefoxAccount::with_config(config);
117
118 fxa.add_cached_token(
119 "profile",
120 AccessTokenInfo {
121 scope: "profile".to_string(),
122 token: "profiletok".to_string(),
123 key: None,
124 expires_at: u64::MAX,
125 },
126 );
127
128 let mut client = MockFxAClient::new();
129 client
130 .expect_get_profile()
131 .with(always(), eq("profiletok"), always())
132 .times(1)
133 .returning(|_, _, _| {
134 Ok(Some(ResponseAndETag {
135 response: ProfileResponse {
136 uid: "12345ab".to_string(),
137 email: "foo@bar.com".to_string(),
138 display_name: None,
139 avatar: "https://foo.avatar".to_string(),
140 avatar_default: true,
141 },
142 etag: None,
143 }))
144 });
145 fxa.set_client(Arc::new(client));
146
147 let p = fxa.get_profile(false).unwrap();
148 assert_eq!(p.email, "foo@bar.com");
149 }
150
151 #[test]
152 fn test_expired_access_token_refetch() {
153 let config = Config::stable_dev("12345678", "https://foo.bar");
154 let mut fxa = FirefoxAccount::with_config(config);
155
156 fxa.add_cached_token(
157 "profile",
158 AccessTokenInfo {
159 scope: "profile".to_string(),
160 token: "bad_access_token".to_string(),
161 key: None,
162 expires_at: u64::MAX,
163 },
164 );
165 let mut refresh_token_scopes = std::collections::HashSet::new();
166 refresh_token_scopes.insert("profile".to_owned());
167 fxa.state.force_refresh_token(RefreshToken {
168 token: "refreshtok".to_owned(),
169 scopes: refresh_token_scopes,
170 });
171
172 let mut client = MockFxAClient::new();
173 client
175 .expect_get_profile()
176 .with(always(), eq("bad_access_token"), always())
177 .times(1)
178 .returning(|_, _, _| Err(Error::RemoteError{
179 code: 401,
180 errno: 110,
181 error: "Unauthorized".to_owned(),
182 message: "Invalid authentication token in request signature".to_owned(),
183 info: "https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-format".to_owned(),
184 }));
185 client
187 .expect_create_access_token_using_refresh_token()
188 .with(always(), eq("refreshtok"), always(), always())
189 .times(1)
190 .returning(|_, _, _, _| {
191 Ok(OAuthTokenResponse {
192 keys_jwe: None,
193 refresh_token: None,
194 expires_in: 6_000_000,
195 scope: "profile".to_owned(),
196 access_token: "good_profile_token".to_owned(),
197 session_token: None,
198 })
199 });
200 client
202 .expect_get_profile()
203 .with(always(), eq("good_profile_token"), always())
204 .times(1)
205 .returning(|_, _, _| {
206 Ok(Some(ResponseAndETag {
207 response: ProfileResponse {
208 uid: "12345ab".to_string(),
209 email: "foo@bar.com".to_string(),
210 display_name: None,
211 avatar: "https://foo.avatar".to_string(),
212 avatar_default: true,
213 },
214 etag: None,
215 }))
216 });
217 fxa.set_client(Arc::new(client));
218
219 let p = fxa.get_profile(false).unwrap();
220 assert_eq!(p.email, "foo@bar.com");
221 }
222}