1use bitwarden_core::key_management::MasterPasswordAuthenticationData;
2
3use crate::login::{
4 LoginClient,
5 api::{request::LoginApiRequest, send_login_request},
6 login_via_password::{PasswordLoginApiRequest, PasswordLoginError, PasswordLoginRequest},
7 models::LoginResponse,
8};
9
10#[bitwarden_ffi::wasm_export]
11impl LoginClient {
12 pub async fn login_via_password(
17 &self,
18 request: PasswordLoginRequest,
19 ) -> Result<LoginResponse, PasswordLoginError> {
20 let master_password_authentication = MasterPasswordAuthenticationData::derive(
21 &request.password,
22 &request.prelogin_response.kdf,
23 &request.prelogin_response.salt,
24 )?;
25
26 let api_request: LoginApiRequest<PasswordLoginApiRequest> =
27 (request, master_password_authentication).into();
28
29 let api_configs = self.client.internal.get_api_configurations();
30
31 let response = send_login_request(&api_configs.identity_config, &api_request).await;
32
33 response.map_err(Into::into)
34 }
35}
36
37#[cfg(test)]
38mod tests {
39 use bitwarden_core::{
40 Client, ClientSettings, DeviceType,
41 key_management::account_cryptographic_state::WrappedAccountCryptographicState,
42 };
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(Client::new(Some(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 match &success_response.wrapped_account_crypto_state {
212 Some(WrappedAccountCryptographicState::V1 { private_key }) => {
213 assert_eq!(private_key.to_string(), TEST_PRIVATE_KEY);
214 }
215 other => panic!(
216 "Expected Some(WrappedAccountCryptographicState::V1), got: {other:?}"
217 ),
218 }
219 }
220 }
221 }
222
223 #[tokio::test]
224 async fn test_login_via_password_success() {
225 let kdf_types = [TestKdfType::Pbkdf2, TestKdfType::Argon2id];
226
227 for kdf_type in kdf_types {
228 let raw_success = make_mock_success_response();
229
230 let mock = add_standard_login_headers(
231 Mock::given(matchers::method("POST")).and(matchers::path("identity/connect/token")),
232 )
233 .respond_with(ResponseTemplate::new(200).set_body_json(raw_success));
234
235 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
236 let login_client = make_login_client(&mock_server);
237
238 let request = make_password_login_request(kdf_type);
239 let result = login_client.login_via_password(request).await;
240
241 assert!(result.is_ok(), "Failed for KDF type: {kdf_type:?}");
242 let login_response = result.unwrap();
243 assert_login_success_response(&login_response);
244 }
245 }
246
247 #[tokio::test]
248 async fn test_login_via_password_success_without_account_keys() {
249 let mut raw_success = make_mock_success_response();
250 raw_success
251 .as_object_mut()
252 .expect("mock response is a JSON object")
253 .remove("AccountKeys");
254
255 let mock = add_standard_login_headers(
256 Mock::given(matchers::method("POST")).and(matchers::path("identity/connect/token")),
257 )
258 .respond_with(ResponseTemplate::new(200).set_body_json(raw_success));
259
260 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
261 let login_client = make_login_client(&mock_server);
262
263 let request = make_password_login_request(TestKdfType::Pbkdf2);
264 let result = login_client.login_via_password(request).await;
265
266 assert!(result.is_ok());
267 let LoginResponse::Authenticated(success_response) = result.unwrap();
268 assert!(success_response.wrapped_account_crypto_state.is_none());
269 }
270
271 #[tokio::test]
272 async fn test_login_via_password_malformed_account_keys() {
273 let mut raw_success = make_mock_success_response();
274 raw_success["AccountKeys"] = serde_json::json!({
275 "publicKeyEncryptionKeyPair": {
276 "wrappedPrivateKey": "not-a-valid-encstring",
277 "publicKey": TEST_PUBLIC_KEY,
278 "Object": "publicKeyEncryptionKeyPair"
279 },
280 "Object": "privateKeys"
281 });
282
283 let mock = add_standard_login_headers(
284 Mock::given(matchers::method("POST")).and(matchers::path("identity/connect/token")),
285 )
286 .respond_with(ResponseTemplate::new(200).set_body_json(raw_success));
287
288 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
289 let login_client = make_login_client(&mock_server);
290
291 let request = make_password_login_request(TestKdfType::Pbkdf2);
292 let result = login_client.login_via_password(request).await;
293
294 assert!(result.is_err());
295 match result.unwrap_err() {
296 PasswordLoginError::Unknown(msg) => {
297 assert!(
298 msg.contains("AccountKeys"),
299 "Expected error message to reference AccountKeys, got: {msg}"
300 );
301 }
302 other => panic!("Expected Unknown error, got: {other:?}"),
303 }
304 }
305
306 #[tokio::test]
307 async fn test_login_via_password_invalid_credentials() {
308 let error_response = serde_json::json!({
309 "error": "invalid_grant",
310 "error_description": "invalid_username_or_password"
311 });
312
313 let mock = Mock::given(matchers::method("POST"))
314 .and(matchers::path("identity/connect/token"))
315 .respond_with(ResponseTemplate::new(400).set_body_json(error_response));
316
317 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
318 let login_client = make_login_client(&mock_server);
319
320 let request = make_password_login_request(TestKdfType::Pbkdf2);
321 let result = login_client.login_via_password(request).await;
322
323 assert!(result.is_err());
324 let error = result.unwrap_err();
325
326 assert!(matches!(
327 error,
328 PasswordLoginError::InvalidUsernameOrPassword
329 ));
330 }
331
332 #[tokio::test]
333 async fn test_login_via_password_invalid_request() {
334 let error_response = serde_json::json!({
335 "error": "invalid_request",
336 "error_description": "Missing required parameter"
337 });
338
339 let mock = Mock::given(matchers::method("POST"))
340 .and(matchers::path("identity/connect/token"))
341 .respond_with(ResponseTemplate::new(400).set_body_json(error_response));
342
343 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
344 let login_client = make_login_client(&mock_server);
345
346 let request = make_password_login_request(TestKdfType::Pbkdf2);
347 let result = login_client.login_via_password(request).await;
348
349 assert!(result.is_err());
350 let error = result.unwrap_err();
351
352 match error {
353 PasswordLoginError::Unknown(msg) => {
354 assert!(msg.contains("Invalid request"));
355 assert!(msg.contains("Missing required parameter"));
356 }
357 _ => panic!("Expected Unknown error variant"),
358 }
359 }
360
361 #[tokio::test]
362 async fn test_login_via_password_invalid_client() {
363 let error_response = serde_json::json!({
364 "error": "invalid_client",
365 "error_description": "Client authentication failed"
366 });
367
368 let mock = Mock::given(matchers::method("POST"))
369 .and(matchers::path("identity/connect/token"))
370 .respond_with(ResponseTemplate::new(401).set_body_json(error_response));
371
372 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
373 let login_client = make_login_client(&mock_server);
374
375 let request = make_password_login_request(TestKdfType::Pbkdf2);
376 let result = login_client.login_via_password(request).await;
377
378 assert!(result.is_err());
379 let error = result.unwrap_err();
380
381 match error {
382 PasswordLoginError::Unknown(msg) => {
383 assert!(msg.contains("Invalid client"));
384 assert!(msg.contains("Client authentication failed"));
385 }
386 _ => panic!("Expected Unknown error variant"),
387 }
388 }
389
390 #[tokio::test]
391 async fn test_login_via_password_unexpected_error() {
392 let error_response = serde_json::json!({
393 "unexpected_field": "unexpected_value"
394 });
395
396 let mock = Mock::given(matchers::method("POST"))
397 .and(matchers::path("identity/connect/token"))
398 .respond_with(ResponseTemplate::new(500).set_body_json(error_response));
399
400 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
401 let login_client = make_login_client(&mock_server);
402
403 let request = make_password_login_request(TestKdfType::Pbkdf2);
404 let result = login_client.login_via_password(request).await;
405
406 assert!(result.is_err());
407 let error = result.unwrap_err();
408
409 match error {
410 PasswordLoginError::Unknown(msg) => {
411 assert!(msg.contains("Unexpected error"));
412 }
413 _ => panic!("Expected Unknown error variant"),
414 }
415 }
416
417 #[tokio::test]
418 async fn test_login_via_password_invalid_kdf_configuration() {
419 let (mock_server, _api_config) = start_api_mock(vec![]).await;
421 let login_client = make_login_client(&mock_server);
422
423 let request = PasswordLoginRequest {
426 login_request: LoginRequest {
427 client_id: TEST_CLIENT_ID.to_string(),
428 device: LoginDeviceRequest {
429 device_type: DeviceType::SDK,
430 device_identifier: TEST_DEVICE_IDENTIFIER.to_string(),
431 device_name: TEST_DEVICE_NAME.to_string(),
432 device_push_token: Some(TEST_PUSH_TOKEN.to_string()),
433 },
434 },
435 email: TEST_EMAIL.to_string(),
436 password: TEST_PASSWORD.to_string(),
437 prelogin_response: PasswordPreloginResponse {
438 kdf: Kdf::PBKDF2 {
439 iterations: std::num::NonZeroU32::new(100).unwrap(), },
441 salt: TEST_SALT.to_string(),
442 },
443 };
444
445 let result = login_client.login_via_password(request).await;
446
447 assert!(result.is_err());
448 let error = result.unwrap_err();
449
450 assert!(
452 matches!(
453 error,
454 PasswordLoginError::PasswordAuthenticationDataDerivation(_)
455 ),
456 "Expected PasswordAuthenticationDataDerivation error, got: {:?}",
457 error
458 );
459 }
460
461 #[tokio::test]
464 async fn test_login_via_password_connection_refused() {
465 let settings = ClientSettings {
467 identity_url: "http://127.0.0.1:1".to_string(), api_url: "http://127.0.0.1:1".to_string(),
469 user_agent: "Bitwarden Rust-SDK [TEST]".into(),
470 device_type: DeviceType::SDK,
471 device_identifier: None,
472 bitwarden_client_version: None,
473 bitwarden_package_type: None,
474 };
475 let login_client = LoginClient::new(Client::new(Some(settings)));
476
477 let request = make_password_login_request(TestKdfType::Pbkdf2);
478 let result = login_client.login_via_password(request).await;
479
480 assert!(result.is_err());
482 let error = result.unwrap_err();
483 assert!(
484 matches!(error, PasswordLoginError::Unknown(_)),
485 "Expected Unknown error for connection refused, got: {:?}",
486 error
487 );
488 }
489
490 #[tokio::test]
491 async fn test_login_via_password_dns_failure() {
492 let settings = ClientSettings {
494 identity_url: "http://this-domain-definitely-does-not-exist-12345.invalid".to_string(),
495 api_url: "http://this-domain-definitely-does-not-exist-12345.invalid".to_string(),
496 user_agent: "Bitwarden Rust-SDK [TEST]".into(),
497 device_type: DeviceType::SDK,
498 device_identifier: None,
499 bitwarden_client_version: None,
500 bitwarden_package_type: None,
501 };
502 let login_client = LoginClient::new(Client::new(Some(settings)));
503
504 let request = make_password_login_request(TestKdfType::Pbkdf2);
505 let result = login_client.login_via_password(request).await;
506
507 assert!(result.is_err());
509 let error = result.unwrap_err();
510 assert!(
511 matches!(error, PasswordLoginError::Unknown(_)),
512 "Expected Unknown error for DNS failure, got: {:?}",
513 error
514 );
515 }
516
517 #[tokio::test]
520 async fn test_login_via_password_empty_response_body() {
521 let mock = Mock::given(matchers::method("POST"))
523 .and(matchers::path("identity/connect/token"))
524 .respond_with(ResponseTemplate::new(200).set_body_string(""));
525
526 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
527 let login_client = make_login_client(&mock_server);
528
529 let request = make_password_login_request(TestKdfType::Pbkdf2);
530 let result = login_client.login_via_password(request).await;
531
532 assert!(result.is_err());
534 let error = result.unwrap_err();
535 assert!(
536 matches!(error, PasswordLoginError::Unknown(_)),
537 "Expected Unknown error for empty response, got: {:?}",
538 error
539 );
540 }
541
542 #[tokio::test]
543 async fn test_login_via_password_malformed_json() {
544 let mock = Mock::given(matchers::method("POST"))
546 .and(matchers::path("identity/connect/token"))
547 .respond_with(ResponseTemplate::new(200).set_body_string("{invalid json"));
548
549 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
550 let login_client = make_login_client(&mock_server);
551
552 let request = make_password_login_request(TestKdfType::Pbkdf2);
553 let result = login_client.login_via_password(request).await;
554
555 assert!(result.is_err());
557 let error = result.unwrap_err();
558 assert!(
559 matches!(error, PasswordLoginError::Unknown(_)),
560 "Expected Unknown error for malformed JSON, got: {:?}",
561 error
562 );
563 }
564
565 #[tokio::test]
566 async fn test_login_via_password_incomplete_success_response() {
567 let incomplete_response = serde_json::json!({
569 "access_token": TEST_ACCESS_TOKEN,
570 });
572
573 let mock = Mock::given(matchers::method("POST"))
574 .and(matchers::path("identity/connect/token"))
575 .respond_with(ResponseTemplate::new(200).set_body_json(incomplete_response));
576
577 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
578 let login_client = make_login_client(&mock_server);
579
580 let request = make_password_login_request(TestKdfType::Pbkdf2);
581 let result = login_client.login_via_password(request).await;
582
583 assert!(result.is_err());
585 let error = result.unwrap_err();
586 assert!(
587 matches!(error, PasswordLoginError::Unknown(_)),
588 "Expected Unknown error for incomplete response, got: {:?}",
589 error
590 );
591 }
592
593 #[tokio::test]
594 async fn test_login_via_password_wrong_content_type() {
595 let mock = Mock::given(matchers::method("POST"))
597 .and(matchers::path("identity/connect/token"))
598 .respond_with(
599 ResponseTemplate::new(200)
600 .set_body_string("<html><body>Error</body></html>")
601 .insert_header("content-type", "text/html"),
602 );
603
604 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
605 let login_client = make_login_client(&mock_server);
606
607 let request = make_password_login_request(TestKdfType::Pbkdf2);
608 let result = login_client.login_via_password(request).await;
609
610 assert!(result.is_err());
612 let error = result.unwrap_err();
613 assert!(
614 matches!(error, PasswordLoginError::Unknown(_)),
615 "Expected Unknown error for wrong content type, got: {:?}",
616 error
617 );
618 }
619
620 #[tokio::test]
621 async fn test_login_via_password_unexpected_status_code() {
622 let mock = Mock::given(matchers::method("POST"))
624 .and(matchers::path("identity/connect/token"))
625 .respond_with(ResponseTemplate::new(418).set_body_string("I'm a teapot"));
626
627 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
628 let login_client = make_login_client(&mock_server);
629
630 let request = make_password_login_request(TestKdfType::Pbkdf2);
631 let result = login_client.login_via_password(request).await;
632
633 assert!(result.is_err());
635 let error = result.unwrap_err();
636 assert!(
637 matches!(error, PasswordLoginError::Unknown(_)),
638 "Expected Unknown error for unexpected status code, got: {:?}",
639 error
640 );
641 }
642}