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