Skip to main content

bitwarden_auth/login/login_via_password/
login_via_password_impl.rs

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    /// Authenticates a user via email and master password.
15    ///
16    /// Derives the master password hash using KDF settings from prelogin, then sends
17    /// the authentication request to obtain access tokens and vault keys.
18    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    // Mock success response constants (using real-world valid encrypted data format)
69    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                // Verify user decryption options
199                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                // Verify master password policy is present
209                assert!(success_response.master_password_policy.is_some());
210
211                // TEST_PRIVATE_KEY is an AesCbc256_HmacSha256_B64 EncString, so the conversion
212                // produces the V1 variant rather than V2.
213                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        // No mock server needed - error occurs during KDF derivation before API call
422        let (mock_server, _api_config) = start_api_mock(vec![]).await;
423        let login_client = make_login_client(&mock_server);
424
425        // Create a request with PBKDF2 iterations below the minimum (5000)
426        // This will cause derive() to fail with InsufficientKdfParameters
427        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(), // Below minimum of 5000
442                },
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        // Verify it's the PasswordAuthenticationDataDerivation error variant
453        assert!(
454            matches!(
455                error,
456                PasswordLoginError::PasswordAuthenticationDataDerivation(_)
457            ),
458            "Expected PasswordAuthenticationDataDerivation error, got: {:?}",
459            error
460        );
461    }
462
463    // ==================== Network Error Tests ====================
464
465    #[tokio::test]
466    async fn test_login_via_password_connection_refused() {
467        // Use an invalid port that will refuse connections
468        let settings = ClientSettings {
469            identity_url: "http://127.0.0.1:1".to_string(), // Port 1 will be refused
470            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        // Should fail with Unknown error due to connection refused
483        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        // Use a domain that doesn't exist
495        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        // Should fail with Unknown error due to DNS failure
510        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    // ==================== Malformed Response Tests ====================
520
521    #[tokio::test]
522    async fn test_login_via_password_empty_response_body() {
523        // Server returns 200 but with empty body
524        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        // Should fail with Unknown error due to empty body
535        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        // Server returns 200 but with invalid JSON
547        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        // Should fail with Unknown error due to malformed JSON
558        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        // Server returns 200 with valid JSON but missing required fields
570        let incomplete_response = serde_json::json!({
571            "access_token": TEST_ACCESS_TOKEN,
572            // Missing expires_in, token_type, and other required fields
573        });
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        // Should fail with Unknown error due to missing required fields
586        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        // Server returns HTML instead of JSON
598        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        // Should fail with Unknown error due to wrong content type
613        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        // Server returns 418 I'm a teapot (unexpected status code)
625        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        // Should fail with Unknown error due to unexpected status code
636        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}