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