1use bitwarden_core::key_management::MasterPasswordAuthenticationData;
2#[cfg(feature = "wasm")]
3use wasm_bindgen::prelude::*;
4
5use crate::login::{
6 LoginClient,
7 api::{request::LoginApiRequest, send_login_request},
8 login_via_password::{PasswordLoginApiRequest, PasswordLoginError, PasswordLoginRequest},
9 models::LoginResponse,
10};
11
12#[cfg_attr(feature = "wasm", wasm_bindgen)]
13impl LoginClient {
14 pub async fn login_via_password(
19 &self,
20 request: PasswordLoginRequest,
21 ) -> Result<LoginResponse, PasswordLoginError> {
22 let master_password_authentication = MasterPasswordAuthenticationData::derive(
23 &request.password,
24 &request.prelogin_response.kdf,
25 &request.prelogin_response.salt,
26 )?;
27
28 let api_request: LoginApiRequest<PasswordLoginApiRequest> =
29 (request, master_password_authentication).into();
30
31 let api_configs = self.client.internal.get_api_configurations();
32
33 let response = send_login_request(&api_configs.identity_config, &api_request).await;
34
35 response.map_err(Into::into)
36 }
37}
38
39#[cfg(test)]
40mod tests {
41 use bitwarden_core::{ClientSettings, DeviceType};
42 use bitwarden_crypto::Kdf;
43 use bitwarden_test::start_api_mock;
44 use wiremock::{Mock, ResponseTemplate, matchers};
45
46 use super::*;
47 use crate::login::{
48 login_via_password::{PasswordLoginRequest, PasswordPreloginResponse},
49 models::{LoginDeviceRequest, LoginRequest, LoginResponse},
50 };
51
52 const TEST_EMAIL: &str = "[email protected]";
53 const TEST_PASSWORD: &str = "test-password-123";
54 const TEST_SALT: &str = "test-salt-value";
55 const TEST_CLIENT_ID: &str = "connector";
56 const TEST_DEVICE_IDENTIFIER: &str = "test-device-id";
57 const TEST_DEVICE_NAME: &str = "Test Device";
58
59 #[derive(Debug, Clone, Copy)]
60 enum TestKdfType {
61 Pbkdf2,
62 Argon2id,
63 }
64
65 const TEST_ACCESS_TOKEN: &str = "test_access_token";
67 const TEST_TOKEN_TYPE: &str = "Bearer";
68 const TEST_EXPIRES_IN: u64 = 3600;
69 const TEST_SCOPE: &str = "api offline_access";
70 const TEST_REFRESH_TOKEN: &str = "test_refresh_token";
71 const TEST_PRIVATE_KEY: &str = "2.SVgjObXyZZKLDVxM3y197w==|tUHZ+bo2o7Y9NyAPPqWOhhuaDiiYT26R2vPI0ILqg8W1vtjq+kzsGHPRZhA1nOXAcJ/ACe77YGFicueH+tryWZHgF1whGZxXza8JPYVtd4k8vO2NE7j8MUZ0FHHq7O+mUiVql0+mC1Af9gM5xp8W022aWgobyu4IZQi6l5hmJZ76NvzUbxDRFadzd8/sxFh+g3I4lEl5kQfzIi3IT0PmX3h75I/8jyGzgWxuUpLiko8hNkIwcjLXesCE641hH8oCtTtwzowZfuRUTO6O/WSR5fHMR2nR2IKf+YvK3SvlywvFTbOAzi7GLNd6NPOZ5ohJrJWtThUZ+65N3CFIczhjj/KvtR5NYVlXlCKWGRLjMsG5Aj8MPCAtAGH8AT6qRoDyh7jXF8SjMo/7BpFay9Xp+kd8M79LEFyUVMybShJ/1Es1qDNCZlnYP8iy1uQe1osLIzSk4IcH2uAD91jvWAOaJGw+HuAOjhqBlP2I7hI8jST5pJAeAzZeY1mnfryYB92wdDVPWKHp+nFcDl34w9lwQRAxken+yxCaepJCRyTXYzpzDNW7Si47PKndchSof9j27MBXTjoOgcsCN2s/V6mNomNybwfN/8J5ts8BNatTnCfiDhV/zrHP9N7wjRXjYoVTLTHXBJqehnLXCNFjnWWmbUTz0fMIRC5q4iNRnSmGMuuCGZfCvlhaIaSVbw35K7ksjTvakJQ8npZU+ULq0Z49jw10GULUbXrP0h/VG+ScKGsRG3E1AOYtd2ff2oe8ht03IpopQWKKk8vqofhDKG++E+SYd/VgMo2O9tuOKilrKCoOBW17/FIftCpWqdGmbG3OBnKiXNOeelqd51i0n9G2ddYhgt+a++8J3UfmrNTX5483+g2usJeJBkKfIbB87FaCxBRSBdvy+bPIPqm6dEWLhk5m3GGkPCndpZywef+tpV7NkC6J8cUDQS0ah1w7r9DG5kNdoSWHbvwhuPR8Ytk8uPdAHI2vOcO/4E6CCPGlsGbXq6egZ39XypO7QJ4+NWTzGDiNGSVOB4Mrxe23++GYRqaMS3bGX0cLKXvCuR1sjYYiM8kechXcmIBGKavs3JrZcT7qEJ8bEpnFQcV+F0iW1bvRTCclVM8XSTbeX6SktHs6fO3vrV+bfkVJsWUAbqR/2di0B9Ye97kJign/03oKUUpg8ksapMfr+IE4CVdHeEC4Xq/y5I+R5TRP/EXiIu2mDIgx7nITj0oTysl070t0OC8QLFrpUkZxjx7ELq76NjMc0IIgumWsivRyBeqz6r3lIA25b6H/3+9xrpjZFb/K/M/NMXFdenjflhYaQLzzsO9Cz7EAorYTf6bV0+g43GyUOC6w0D8R7rerfsVSnwIENlEwpd4s5TC+rWjNPG1r1w91E+It1UbuvBDBTMIZw4BRrCd5/2G0nQyNnNWxn5WLkg3xRCmPYqcVFygagJLh6baYGLb1SVmRu8NF2QMggRsYDkckql6gseq5gGGCfcaFLtAHgfdlfV4jnSZ0tuYpjsLRYhUD/oFGlM56sxnMe/EX6DdDnoGFlAxkRNeHuiY6tdlNhbOAyRjJwQL1Vnweip5vvrHpbEsR6z71E05dwEDnK+2Gz7gVq2x4BIzkLm3MwlOmZFsbLewHr6vB5mm+rgM=|YfKU1iB2Yn/pqeBDbE2IXnpVIlGUR0Sjv9twpnNklHU=";
72 const TEST_PUBLIC_KEY: &str = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqRwZGmKLN34tUq+lLT50JoXJaEJh2E13g8IMFYd5xaywJxA63rnQ5rDa6HFrjjyhg0kbhY60Igv7tpeR7Hq6VTU2CnsRmT47+3ZKm2Y8w/h8Dk0X/a8QcxMbvJZP+2wQ0/6lIbfxRYm7cCi8KZz03mz79lUBJxioy8N+46rMwlj9HQCb8tle5gyEYtF+XtWeAP3JpVvRs3unNvlgThCETnusAIruIJzNX8e+0z7HkzNyFQ3/jY+MyZZUTz3X+r3werc8r94W/4EgoLdjg4651KBQbJuMiknlRzpN+gipClDyjgILxiswtGjuCr80Dyk+jhpDmYhytRcpinnjqkLlzwIDAQAB";
73 const TEST_ENCRYPTED_USER_KEY: &str = "2.EvwbalCwa3ba6j/eEtGOLA==|Nd+7WgEZpd3fsGmpDHOknPhS9e8SVeXpmeJQDTLI3Ki9S7BB/L+k0TxzRnUtcMx646d4Nfco5mz7Q1mMrGO/PGtf4FNleyCR9LMIzHneiRI=|B9bEzJ4LLh0Vz2zexhBwZBQSmXWsPdRKL+haJG/KB6c=";
74 const TEST_KDF_TYPE: i32 = 0;
75 const TEST_KDF_ITERATIONS: i32 = 600000;
76 const TEST_PUSH_TOKEN: &str = "test_push_token";
77
78 fn make_login_client(mock_server: &wiremock::MockServer) -> LoginClient {
79 let settings = ClientSettings {
80 identity_url: format!("http://{}/identity", mock_server.address()),
81 api_url: format!("http://{}/api", mock_server.address()),
82 user_agent: "Bitwarden Rust-SDK [TEST]".into(),
83 device_type: DeviceType::SDK,
84 device_identifier: None,
85 bitwarden_client_version: None,
86 bitwarden_package_type: None,
87 };
88 LoginClient::new(settings)
89 }
90
91 fn make_password_login_request(kdf_type: TestKdfType) -> PasswordLoginRequest {
92 let kdf = match kdf_type {
93 TestKdfType::Pbkdf2 => Kdf::default_pbkdf2(),
94 TestKdfType::Argon2id => Kdf::default_argon2(),
95 };
96
97 PasswordLoginRequest {
98 login_request: LoginRequest {
99 client_id: TEST_CLIENT_ID.to_string(),
100 device: LoginDeviceRequest {
101 device_type: DeviceType::SDK,
102 device_identifier: TEST_DEVICE_IDENTIFIER.to_string(),
103 device_name: TEST_DEVICE_NAME.to_string(),
104 device_push_token: Some(TEST_PUSH_TOKEN.to_string()),
105 },
106 },
107 email: TEST_EMAIL.to_string(),
108 password: TEST_PASSWORD.to_string(),
109 prelogin_response: PasswordPreloginResponse {
110 kdf,
111 salt: TEST_SALT.to_string(),
112 },
113 }
114 }
115
116 fn add_standard_login_headers(mock_builder: wiremock::MockBuilder) -> wiremock::MockBuilder {
117 mock_builder
118 .and(matchers::header(
119 reqwest::header::CONTENT_TYPE.as_str(),
120 "application/x-www-form-urlencoded",
121 ))
122 .and(matchers::header(
123 reqwest::header::ACCEPT.as_str(),
124 "application/json",
125 ))
126 .and(matchers::header(
127 reqwest::header::CACHE_CONTROL.as_str(),
128 "no-store",
129 ))
130 .and(matchers::header(
131 reqwest::header::PRAGMA.as_str(),
132 "no-cache",
133 ))
134 }
135
136 fn make_mock_success_response() -> serde_json::Value {
137 serde_json::json!({
138 "access_token": TEST_ACCESS_TOKEN,
139 "expires_in": TEST_EXPIRES_IN,
140 "token_type": TEST_TOKEN_TYPE,
141 "refresh_token": TEST_REFRESH_TOKEN,
142 "scope": TEST_SCOPE,
143 "PrivateKey": TEST_PRIVATE_KEY,
144 "AccountKeys": {
145 "publicKeyEncryptionKeyPair": {
146 "wrappedPrivateKey": TEST_PRIVATE_KEY,
147 "publicKey": TEST_PUBLIC_KEY,
148 "Object": "publicKeyEncryptionKeyPair"
149 },
150 "Object": "privateKeys"
151 },
152 "Key": TEST_ENCRYPTED_USER_KEY,
153 "MasterPasswordPolicy": {
154 "Object": "masterPasswordPolicy"
155 },
156 "ForcePasswordReset": false,
157 "Kdf": TEST_KDF_TYPE,
158 "KdfIterations": TEST_KDF_ITERATIONS,
159 "KdfMemory": null,
160 "KdfParallelism": null,
161 "UserDecryptionOptions": {
162 "HasMasterPassword": true,
163 "MasterPasswordUnlock": {
164 "Kdf": {
165 "KdfType": TEST_KDF_TYPE,
166 "Iterations": TEST_KDF_ITERATIONS
167 },
168 "MasterKeyEncryptedUserKey": TEST_ENCRYPTED_USER_KEY,
169 "Salt": TEST_EMAIL
170 },
171 "Object": "userDecryptionOptions"
172 }
173 })
174 }
175
176 fn assert_login_success_response(login_response: &LoginResponse) {
177 match login_response {
178 LoginResponse::Authenticated(success_response) => {
179 assert_eq!(success_response.access_token, TEST_ACCESS_TOKEN);
180 assert_eq!(success_response.token_type, TEST_TOKEN_TYPE);
181 assert_eq!(success_response.expires_in, TEST_EXPIRES_IN);
182 assert_eq!(success_response.scope, TEST_SCOPE);
183 assert_eq!(
184 success_response.refresh_token,
185 Some(TEST_REFRESH_TOKEN.to_string())
186 );
187 assert_eq!(
188 success_response.user_key_wrapped_user_private_key,
189 Some(TEST_PRIVATE_KEY.to_string())
190 );
191 assert_eq!(success_response.two_factor_token, None);
192 assert_eq!(success_response.force_password_reset, Some(false));
193 assert_eq!(success_response.api_use_key_connector, None);
194
195 let decryption_options = &success_response.user_decryption_options;
197 assert!(decryption_options.master_password_unlock.is_some());
198 let mp_unlock = decryption_options.master_password_unlock.as_ref().unwrap();
199 assert_eq!(
200 mp_unlock.master_key_wrapped_user_key.to_string(),
201 TEST_ENCRYPTED_USER_KEY
202 );
203 assert_eq!(mp_unlock.salt, TEST_EMAIL);
204
205 assert!(success_response.master_password_policy.is_some());
207 }
208 }
209 }
210
211 #[tokio::test]
212 async fn test_login_via_password_success() {
213 let kdf_types = [TestKdfType::Pbkdf2, TestKdfType::Argon2id];
214
215 for kdf_type in kdf_types {
216 let raw_success = make_mock_success_response();
217
218 let mock = add_standard_login_headers(
219 Mock::given(matchers::method("POST")).and(matchers::path("identity/connect/token")),
220 )
221 .respond_with(ResponseTemplate::new(200).set_body_json(raw_success));
222
223 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
224 let login_client = make_login_client(&mock_server);
225
226 let request = make_password_login_request(kdf_type);
227 let result = login_client.login_via_password(request).await;
228
229 assert!(result.is_ok(), "Failed for KDF type: {kdf_type:?}");
230 let login_response = result.unwrap();
231 assert_login_success_response(&login_response);
232 }
233 }
234
235 #[tokio::test]
236 async fn test_login_via_password_invalid_credentials() {
237 let error_response = serde_json::json!({
238 "error": "invalid_grant",
239 "error_description": "invalid_username_or_password"
240 });
241
242 let mock = Mock::given(matchers::method("POST"))
243 .and(matchers::path("identity/connect/token"))
244 .respond_with(ResponseTemplate::new(400).set_body_json(error_response));
245
246 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
247 let login_client = make_login_client(&mock_server);
248
249 let request = make_password_login_request(TestKdfType::Pbkdf2);
250 let result = login_client.login_via_password(request).await;
251
252 assert!(result.is_err());
253 let error = result.unwrap_err();
254
255 assert!(matches!(
256 error,
257 PasswordLoginError::InvalidUsernameOrPassword
258 ));
259 }
260
261 #[tokio::test]
262 async fn test_login_via_password_invalid_request() {
263 let error_response = serde_json::json!({
264 "error": "invalid_request",
265 "error_description": "Missing required parameter"
266 });
267
268 let mock = Mock::given(matchers::method("POST"))
269 .and(matchers::path("identity/connect/token"))
270 .respond_with(ResponseTemplate::new(400).set_body_json(error_response));
271
272 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
273 let login_client = make_login_client(&mock_server);
274
275 let request = make_password_login_request(TestKdfType::Pbkdf2);
276 let result = login_client.login_via_password(request).await;
277
278 assert!(result.is_err());
279 let error = result.unwrap_err();
280
281 match error {
282 PasswordLoginError::Unknown(msg) => {
283 assert!(msg.contains("Invalid request"));
284 assert!(msg.contains("Missing required parameter"));
285 }
286 _ => panic!("Expected Unknown error variant"),
287 }
288 }
289
290 #[tokio::test]
291 async fn test_login_via_password_invalid_client() {
292 let error_response = serde_json::json!({
293 "error": "invalid_client",
294 "error_description": "Client authentication failed"
295 });
296
297 let mock = Mock::given(matchers::method("POST"))
298 .and(matchers::path("identity/connect/token"))
299 .respond_with(ResponseTemplate::new(401).set_body_json(error_response));
300
301 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
302 let login_client = make_login_client(&mock_server);
303
304 let request = make_password_login_request(TestKdfType::Pbkdf2);
305 let result = login_client.login_via_password(request).await;
306
307 assert!(result.is_err());
308 let error = result.unwrap_err();
309
310 match error {
311 PasswordLoginError::Unknown(msg) => {
312 assert!(msg.contains("Invalid client"));
313 assert!(msg.contains("Client authentication failed"));
314 }
315 _ => panic!("Expected Unknown error variant"),
316 }
317 }
318
319 #[tokio::test]
320 async fn test_login_via_password_unexpected_error() {
321 let error_response = serde_json::json!({
322 "unexpected_field": "unexpected_value"
323 });
324
325 let mock = Mock::given(matchers::method("POST"))
326 .and(matchers::path("identity/connect/token"))
327 .respond_with(ResponseTemplate::new(500).set_body_json(error_response));
328
329 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
330 let login_client = make_login_client(&mock_server);
331
332 let request = make_password_login_request(TestKdfType::Pbkdf2);
333 let result = login_client.login_via_password(request).await;
334
335 assert!(result.is_err());
336 let error = result.unwrap_err();
337
338 match error {
339 PasswordLoginError::Unknown(msg) => {
340 assert!(msg.contains("Unexpected error"));
341 }
342 _ => panic!("Expected Unknown error variant"),
343 }
344 }
345
346 #[tokio::test]
347 async fn test_login_via_password_invalid_kdf_configuration() {
348 let (mock_server, _api_config) = start_api_mock(vec![]).await;
350 let login_client = make_login_client(&mock_server);
351
352 let request = PasswordLoginRequest {
355 login_request: LoginRequest {
356 client_id: TEST_CLIENT_ID.to_string(),
357 device: LoginDeviceRequest {
358 device_type: DeviceType::SDK,
359 device_identifier: TEST_DEVICE_IDENTIFIER.to_string(),
360 device_name: TEST_DEVICE_NAME.to_string(),
361 device_push_token: Some(TEST_PUSH_TOKEN.to_string()),
362 },
363 },
364 email: TEST_EMAIL.to_string(),
365 password: TEST_PASSWORD.to_string(),
366 prelogin_response: PasswordPreloginResponse {
367 kdf: Kdf::PBKDF2 {
368 iterations: std::num::NonZeroU32::new(100).unwrap(), },
370 salt: TEST_SALT.to_string(),
371 },
372 };
373
374 let result = login_client.login_via_password(request).await;
375
376 assert!(result.is_err());
377 let error = result.unwrap_err();
378
379 assert!(
381 matches!(
382 error,
383 PasswordLoginError::PasswordAuthenticationDataDerivation(_)
384 ),
385 "Expected PasswordAuthenticationDataDerivation error, got: {:?}",
386 error
387 );
388 }
389
390 #[tokio::test]
393 async fn test_login_via_password_connection_refused() {
394 let settings = ClientSettings {
396 identity_url: "http://127.0.0.1:1".to_string(), api_url: "http://127.0.0.1:1".to_string(),
398 user_agent: "Bitwarden Rust-SDK [TEST]".into(),
399 device_type: DeviceType::SDK,
400 device_identifier: None,
401 bitwarden_client_version: None,
402 bitwarden_package_type: None,
403 };
404 let login_client = LoginClient::new(settings);
405
406 let request = make_password_login_request(TestKdfType::Pbkdf2);
407 let result = login_client.login_via_password(request).await;
408
409 assert!(result.is_err());
411 let error = result.unwrap_err();
412 assert!(
413 matches!(error, PasswordLoginError::Unknown(_)),
414 "Expected Unknown error for connection refused, got: {:?}",
415 error
416 );
417 }
418
419 #[tokio::test]
420 async fn test_login_via_password_dns_failure() {
421 let settings = ClientSettings {
423 identity_url: "http://this-domain-definitely-does-not-exist-12345.invalid".to_string(),
424 api_url: "http://this-domain-definitely-does-not-exist-12345.invalid".to_string(),
425 user_agent: "Bitwarden Rust-SDK [TEST]".into(),
426 device_type: DeviceType::SDK,
427 device_identifier: None,
428 bitwarden_client_version: None,
429 bitwarden_package_type: None,
430 };
431 let login_client = LoginClient::new(settings);
432
433 let request = make_password_login_request(TestKdfType::Pbkdf2);
434 let result = login_client.login_via_password(request).await;
435
436 assert!(result.is_err());
438 let error = result.unwrap_err();
439 assert!(
440 matches!(error, PasswordLoginError::Unknown(_)),
441 "Expected Unknown error for DNS failure, got: {:?}",
442 error
443 );
444 }
445
446 #[tokio::test]
449 async fn test_login_via_password_empty_response_body() {
450 let mock = Mock::given(matchers::method("POST"))
452 .and(matchers::path("identity/connect/token"))
453 .respond_with(ResponseTemplate::new(200).set_body_string(""));
454
455 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
456 let login_client = make_login_client(&mock_server);
457
458 let request = make_password_login_request(TestKdfType::Pbkdf2);
459 let result = login_client.login_via_password(request).await;
460
461 assert!(result.is_err());
463 let error = result.unwrap_err();
464 assert!(
465 matches!(error, PasswordLoginError::Unknown(_)),
466 "Expected Unknown error for empty response, got: {:?}",
467 error
468 );
469 }
470
471 #[tokio::test]
472 async fn test_login_via_password_malformed_json() {
473 let mock = Mock::given(matchers::method("POST"))
475 .and(matchers::path("identity/connect/token"))
476 .respond_with(ResponseTemplate::new(200).set_body_string("{invalid json"));
477
478 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
479 let login_client = make_login_client(&mock_server);
480
481 let request = make_password_login_request(TestKdfType::Pbkdf2);
482 let result = login_client.login_via_password(request).await;
483
484 assert!(result.is_err());
486 let error = result.unwrap_err();
487 assert!(
488 matches!(error, PasswordLoginError::Unknown(_)),
489 "Expected Unknown error for malformed JSON, got: {:?}",
490 error
491 );
492 }
493
494 #[tokio::test]
495 async fn test_login_via_password_incomplete_success_response() {
496 let incomplete_response = serde_json::json!({
498 "access_token": TEST_ACCESS_TOKEN,
499 });
501
502 let mock = Mock::given(matchers::method("POST"))
503 .and(matchers::path("identity/connect/token"))
504 .respond_with(ResponseTemplate::new(200).set_body_json(incomplete_response));
505
506 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
507 let login_client = make_login_client(&mock_server);
508
509 let request = make_password_login_request(TestKdfType::Pbkdf2);
510 let result = login_client.login_via_password(request).await;
511
512 assert!(result.is_err());
514 let error = result.unwrap_err();
515 assert!(
516 matches!(error, PasswordLoginError::Unknown(_)),
517 "Expected Unknown error for incomplete response, got: {:?}",
518 error
519 );
520 }
521
522 #[tokio::test]
523 async fn test_login_via_password_wrong_content_type() {
524 let mock = Mock::given(matchers::method("POST"))
526 .and(matchers::path("identity/connect/token"))
527 .respond_with(
528 ResponseTemplate::new(200)
529 .set_body_string("<html><body>Error</body></html>")
530 .insert_header("content-type", "text/html"),
531 );
532
533 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
534 let login_client = make_login_client(&mock_server);
535
536 let request = make_password_login_request(TestKdfType::Pbkdf2);
537 let result = login_client.login_via_password(request).await;
538
539 assert!(result.is_err());
541 let error = result.unwrap_err();
542 assert!(
543 matches!(error, PasswordLoginError::Unknown(_)),
544 "Expected Unknown error for wrong content type, got: {:?}",
545 error
546 );
547 }
548
549 #[tokio::test]
550 async fn test_login_via_password_unexpected_status_code() {
551 let mock = Mock::given(matchers::method("POST"))
553 .and(matchers::path("identity/connect/token"))
554 .respond_with(ResponseTemplate::new(418).set_body_string("I'm a teapot"));
555
556 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
557 let login_client = make_login_client(&mock_server);
558
559 let request = make_password_login_request(TestKdfType::Pbkdf2);
560 let result = login_client.login_via_password(request).await;
561
562 assert!(result.is_err());
564 let error = result.unwrap_err();
565 assert!(
566 matches!(error, PasswordLoginError::Unknown(_)),
567 "Expected Unknown error for unexpected status code, got: {:?}",
568 error
569 );
570 }
571}