firebase_admin/auth/users/
operations.rs1use crate::auth::error::{parse_identity_toolkit_response, AuthError};
4use crate::auth::identity_toolkit::requests::{
5 AccountsResponse, DeleteRequest, LookupRequest, SignUpRequest, UpdateRequest,
6};
7use crate::auth::identity_toolkit::IdentityToolkitEndpoints;
8use crate::auth::users::model::{CreateUserRequest, UpdateUserRequest, UserRecord};
9use crate::auth::users::query::UserPage;
10use crate::core::{CoreError, HttpClient};
11
12pub struct UserOperations<'a> {
18 http: &'a HttpClient,
19 endpoints: &'a IdentityToolkitEndpoints,
20 bearer_token: Option<&'a str>,
21}
22
23impl<'a> UserOperations<'a> {
24 pub fn new(
27 http: &'a HttpClient,
28 endpoints: &'a IdentityToolkitEndpoints,
29 bearer_token: Option<&'a str>,
30 ) -> Self {
31 Self {
32 http,
33 endpoints,
34 bearer_token,
35 }
36 }
37
38 fn request(&self, url: &str) -> reqwest::RequestBuilder {
39 let builder = self.http.inner().post(url);
40 match self.bearer_token {
41 Some(token) => builder.bearer_auth(token),
42 None => builder,
43 }
44 }
45
46 pub async fn get_user(&self, uid: &str) -> Result<UserRecord, AuthError> {
48 let request = LookupRequest {
49 local_id: vec![uid.to_string()],
50 email: vec![],
51 };
52 let response = self
53 .request(&self.endpoints.lookup())
54 .json(&request)
55 .send()
56 .await?;
57 let parsed: AccountsResponse = parse_identity_toolkit_response(response).await?;
58 parsed
59 .users
60 .into_iter()
61 .next()
62 .map(UserRecord::from)
63 .ok_or(AuthError::UserNotFound)
64 }
65
66 pub async fn get_user_by_email(&self, email: &str) -> Result<UserRecord, AuthError> {
68 let request = LookupRequest {
69 local_id: vec![],
70 email: vec![email.to_string()],
71 };
72 let response = self
73 .request(&self.endpoints.lookup())
74 .json(&request)
75 .send()
76 .await?;
77 let parsed: AccountsResponse = parse_identity_toolkit_response(response).await?;
78 parsed
79 .users
80 .into_iter()
81 .next()
82 .map(UserRecord::from)
83 .ok_or(AuthError::UserNotFound)
84 }
85
86 pub async fn create_user(&self, request: CreateUserRequest) -> Result<UserRecord, AuthError> {
88 let body = SignUpRequest {
89 local_id: request.uid,
90 email: request.email,
91 password: request.password,
92 display_name: request.display_name,
93 disabled: request.disabled,
94 };
95 let response = self
96 .request(&self.endpoints.sign_up())
97 .json(&body)
98 .send()
99 .await?;
100 let local_id: serde_json::Value = parse_identity_toolkit_response(response).await?;
101 let uid = local_id
102 .get("localId")
103 .and_then(|v| v.as_str())
104 .ok_or_else(|| AuthError::Api {
105 status: 200,
106 message: "signUp response missing localId".to_string(),
107 error_code: None,
108 })?;
109 self.get_user(uid).await
110 }
111
112 pub async fn update_user(
114 &self,
115 uid: &str,
116 request: UpdateUserRequest,
117 ) -> Result<UserRecord, AuthError> {
118 let custom_attributes = request
119 .custom_claims
120 .map(|claims| serde_json::to_string(&claims))
121 .transpose()
122 .map_err(CoreError::Deserialize)?;
123
124 let body = UpdateRequest {
125 local_id: uid.to_string(),
126 email: request.email,
127 display_name: request.display_name,
128 disable_user: request.disabled,
129 custom_attributes,
130 };
131 let response = self
132 .request(&self.endpoints.update())
133 .json(&body)
134 .send()
135 .await?;
136 let _: serde_json::Value = parse_identity_toolkit_response(response).await?;
137 self.get_user(uid).await
138 }
139
140 pub async fn set_custom_user_claims(
142 &self,
143 uid: &str,
144 claims: serde_json::Map<String, serde_json::Value>,
145 ) -> Result<(), AuthError> {
146 self.update_user(
147 uid,
148 UpdateUserRequest {
149 custom_claims: Some(claims),
150 ..Default::default()
151 },
152 )
153 .await?;
154 Ok(())
155 }
156
157 pub async fn delete_user(&self, uid: &str) -> Result<(), AuthError> {
159 let body = DeleteRequest {
160 local_id: uid.to_string(),
161 };
162 let response = self
163 .request(&self.endpoints.delete())
164 .json(&body)
165 .send()
166 .await?;
167 let _: serde_json::Value = parse_identity_toolkit_response(response).await?;
168 Ok(())
169 }
170
171 pub async fn list_users(
173 &self,
174 max_results: u32,
175 page_token: Option<&str>,
176 ) -> Result<UserPage, AuthError> {
177 #[derive(serde::Serialize)]
178 struct BatchGetQuery<'a> {
179 #[serde(rename = "maxResults")]
180 max_results: u32,
181 #[serde(rename = "nextPageToken", skip_serializing_if = "Option::is_none")]
182 next_page_token: Option<&'a str>,
183 }
184
185 let response = self
186 .request(&self.endpoints.batch_get())
187 .json(&BatchGetQuery {
188 max_results,
189 next_page_token: page_token,
190 })
191 .send()
192 .await?;
193 let parsed: AccountsResponse = parse_identity_toolkit_response(response).await?;
194
195 Ok(UserPage {
196 users: parsed.users.into_iter().map(UserRecord::from).collect(),
197 next_page_token: parsed.next_page_token,
198 })
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use serde_json::json;
206 use wiremock::matchers::{method, path};
207 use wiremock::{Mock, MockServer, ResponseTemplate};
208
209 async fn operations_against(server: &MockServer) -> (HttpClient, IdentityToolkitEndpoints) {
210 (
211 HttpClient::default(),
212 IdentityToolkitEndpoints::custom(server.uri()),
213 )
214 }
215
216 #[tokio::test]
217 async fn get_user_returns_the_matching_record() {
218 let server = MockServer::start().await;
219 Mock::given(method("POST"))
220 .and(path("/accounts:lookup"))
221 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
222 "users": [{
223 "localId": "uid-1",
224 "email": "user@example.com",
225 "emailVerified": true,
226 }]
227 })))
228 .mount(&server)
229 .await;
230
231 let (http, endpoints) = operations_against(&server).await;
232 let ops = UserOperations::new(&http, &endpoints, Some("token"));
233
234 let user = ops.get_user("uid-1").await.unwrap();
235 assert_eq!(user.uid, "uid-1");
236 assert_eq!(user.email.as_deref(), Some("user@example.com"));
237 assert!(user.email_verified);
238 }
239
240 #[tokio::test]
241 async fn get_user_with_no_matches_is_user_not_found() {
242 let server = MockServer::start().await;
243 Mock::given(method("POST"))
244 .and(path("/accounts:lookup"))
245 .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "users": [] })))
246 .mount(&server)
247 .await;
248
249 let (http, endpoints) = operations_against(&server).await;
250 let ops = UserOperations::new(&http, &endpoints, Some("token"));
251
252 let err = ops.get_user("missing-uid").await.unwrap_err();
253 assert!(matches!(err, AuthError::UserNotFound));
254 }
255
256 #[tokio::test]
257 async fn create_user_looks_up_the_new_user_after_sign_up() {
258 let server = MockServer::start().await;
259 Mock::given(method("POST"))
260 .and(path("/accounts:signUp"))
261 .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "localId": "new-uid" })))
262 .mount(&server)
263 .await;
264 Mock::given(method("POST"))
265 .and(path("/accounts:lookup"))
266 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
267 "users": [{ "localId": "new-uid", "email": "new@example.com" }]
268 })))
269 .mount(&server)
270 .await;
271
272 let (http, endpoints) = operations_against(&server).await;
273 let ops = UserOperations::new(&http, &endpoints, Some("token"));
274
275 let user = ops
276 .create_user(CreateUserRequest {
277 email: Some("new@example.com".to_string()),
278 password: Some("correct horse battery staple".to_string()),
279 ..Default::default()
280 })
281 .await
282 .unwrap();
283 assert_eq!(user.uid, "new-uid");
284 }
285
286 #[tokio::test]
287 async fn delete_user_succeeds_on_a_200_response() {
288 let server = MockServer::start().await;
289 Mock::given(method("POST"))
290 .and(path("/accounts:delete"))
291 .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
292 .mount(&server)
293 .await;
294
295 let (http, endpoints) = operations_against(&server).await;
296 let ops = UserOperations::new(&http, &endpoints, Some("token"));
297
298 ops.delete_user("uid-1").await.unwrap();
299 }
300
301 #[tokio::test]
302 async fn list_users_returns_records_and_next_page_token() {
303 let server = MockServer::start().await;
304 Mock::given(method("POST"))
305 .and(path("/accounts:batchGet"))
306 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
307 "users": [
308 { "localId": "uid-1" },
309 { "localId": "uid-2" },
310 ],
311 "nextPageToken": "page-2",
312 })))
313 .mount(&server)
314 .await;
315
316 let (http, endpoints) = operations_against(&server).await;
317 let ops = UserOperations::new(&http, &endpoints, Some("token"));
318
319 let page = ops.list_users(10, None).await.unwrap();
320 assert_eq!(page.users.len(), 2);
321 assert_eq!(page.next_page_token.as_deref(), Some("page-2"));
322 }
323
324 #[tokio::test]
325 async fn a_structured_api_error_populates_error_code() {
326 let server = MockServer::start().await;
327 Mock::given(method("POST"))
328 .and(path("/accounts:signUp"))
329 .respond_with(ResponseTemplate::new(400).set_body_json(json!({
330 "error": {
331 "code": 400,
332 "message": "EMAIL_EXISTS",
333 "errors": [{ "message": "EMAIL_EXISTS", "reason": "EMAIL_EXISTS" }]
334 }
335 })))
336 .mount(&server)
337 .await;
338
339 let (http, endpoints) = operations_against(&server).await;
340 let ops = UserOperations::new(&http, &endpoints, Some("token"));
341
342 let err = ops
343 .create_user(CreateUserRequest {
344 email: Some("taken@example.com".to_string()),
345 password: Some("correct horse battery staple".to_string()),
346 ..Default::default()
347 })
348 .await
349 .unwrap_err();
350
351 match err {
352 AuthError::Api {
353 status, error_code, ..
354 } => {
355 assert_eq!(status, 400);
356 assert_eq!(error_code.as_deref(), Some("EMAIL_EXISTS"));
357 }
358 other => panic!("expected AuthError::Api, got {other:?}"),
359 }
360 }
361}