Skip to main content

bitwarden_auth/login/login_via_password/
login_via_password_impl.rs

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