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