etopay_sdk/core/
user.rs

1//! This module defines methods for interacting with user-related functionality,
2//! such as getting user state, creating a new user, deleting a user, and more.
3
4use super::Sdk;
5use crate::backend;
6use crate::backend::kyc::check_kyc_status;
7use crate::error::Result;
8use crate::types::newtypes::AccessToken;
9use crate::types::newtypes::EncryptionPin;
10use crate::types::newtypes::EncryptionSalt;
11use crate::types::users::{ActiveUser, KycType, UserEntity};
12use log::{debug, info, warn};
13
14impl Sdk {
15    /// Get user entity
16    ///
17    /// # Returns
18    ///
19    /// Returns a `Result` containing the user entity (`UserEntity`) if successful, or an `Error` if an error occurs.
20    ///
21    /// # Errors
22    ///
23    /// Returns an `Error` if there is an issue initializing the user or accessing the repository.
24    pub async fn get_user(&self) -> Result<UserEntity> {
25        debug!("Getting the user");
26        let Some(repo) = &self.repo else {
27            return Err(crate::Error::UserRepoNotInitialized);
28        };
29
30        // load active user
31        let Some(active_user) = &self.active_user else {
32            return Err(crate::Error::UserNotInitialized);
33        };
34
35        // load user state
36        Ok(repo.get(active_user.username.as_str())?)
37    }
38
39    /// Create a new user
40    ///
41    /// # Arguments
42    ///
43    /// * `username` - The username of the new user.
44    ///
45    /// # Returns
46    ///
47    /// Returns `Ok(())` if the user is created successfully, or an `Error` if an error occurs.
48    ///
49    /// # Errors
50    ///
51    /// Returns an `Error` if there is an issue validating the configuration, initializing the repository, or creating the user.
52    pub async fn create_new_user(&mut self, username: &str) -> Result<()> {
53        info!("Creating a new user");
54        let Some(repo) = &mut self.repo else {
55            return Err(crate::Error::UserRepoNotInitialized);
56        };
57
58        let salt = EncryptionSalt::generate();
59        let user = UserEntity {
60            user_id: None,
61            username: username.into(),
62            encrypted_password: None,
63            salt,
64            is_kyc_verified: false,
65            kyc_type: KycType::Undefined,
66            viviswap_state: Option::None,
67            local_share: None,
68            wallet_transactions: Vec::new(),
69            wallet_transactions_versioned: Vec::new(),
70        };
71
72        repo.create(&user)?;
73
74        Ok(())
75    }
76
77    /// Delete the currently active user and their wallet
78    ///
79    /// # Arguments
80    ///
81    /// * `pin` - The PIN of the user to be deleted.
82    ///
83    /// # Returns
84    ///
85    /// Returns `Ok(())` if the user is deleted successfully, or an `Error` if an error occurs.
86    ///
87    /// # Errors
88    ///
89    /// Returns an `Error` if there is an issue verifying the PIN, initializing the repository, initiliazing the user, deleting the user, or deleting the wallet.
90    pub async fn delete_user(&mut self, pin: Option<&EncryptionPin>) -> Result<()> {
91        let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
92
93        let user_entity = self.get_user().await?;
94
95        // make sure the pin is correct before continuing, only if the wallet exists
96        if user_entity.encrypted_password.is_some() {
97            let pin = pin.ok_or(crate::Error::Wallet(crate::WalletError::WrongPinOrPassword))?;
98            self.verify_pin(pin).await?;
99            info!("Pin verified");
100        }
101
102        let Some(active_user) = &mut self.active_user else {
103            return Err(crate::Error::UserNotInitialized);
104        };
105
106        warn!("Deleting an existing user");
107
108        let Some(repo) = &mut self.repo else {
109            return Err(crate::Error::UserRepoNotInitialized);
110        };
111
112        let username = &active_user.username;
113
114        // Call the delete endpoint on the backend first to avoid ending up in an inconsistent state
115        let access_token = self
116            .access_token
117            .as_ref()
118            .ok_or(crate::error::Error::MissingAccessToken)?;
119        crate::backend::user::delete_user_account(config, access_token).await?;
120
121        // Delete the user in the repo
122        repo.delete(username)?;
123
124        // take the wallet out of the Option and try to delete it, it will then be dropped
125        if let Err(e) = active_user
126            .wallet_manager
127            .delete_wallet(config, &self.access_token, repo)
128            .await
129        {
130            log::error!("Error deleting the wallet: {e:?}");
131        }
132
133        Ok(())
134    }
135
136    /// Initialize an user
137    ///
138    /// # Arguments
139    ///
140    /// * `username` - The username of the user to initialize.
141    ///
142    /// # Returns
143    ///
144    /// Returns `Ok(())` if the user is initialized successfully, or an `Error` if an error occurs.
145    ///
146    /// # Errors
147    ///
148    /// Returns an `Error` if there is an issue validating the configuration, initializing the repository, or checking the KYC status.
149    pub async fn init_user(&mut self, username: &str) -> Result<()> {
150        info!("Initializing user {username}");
151        let Some(repo) = &mut self.repo else {
152            return Err(crate::Error::UserRepoNotInitialized);
153        };
154        let user = repo.get(username)?;
155        let active_user = ActiveUser::from(user);
156
157        if let Some(access_token) = &self.access_token {
158            let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
159            let status = check_kyc_status(config, access_token, username).await?;
160            repo.set_kyc_state(username, status.is_verified)?;
161        }
162
163        self.active_user = Some(active_user);
164
165        Ok(())
166    }
167
168    /// Refresh access token
169    ///
170    /// # Arguments
171    ///
172    /// * `access_token` - The new access token to be set. Or `None` to unset it.
173    ///
174    /// # Returns
175    ///
176    /// Returns `Ok(())` if the access token is refreshed successfully, or an `Error` if an error occurs.
177    ///
178    /// # Errors
179    ///
180    /// Returns an `Error` if there is an issue validating the configuration.
181    pub async fn refresh_access_token(&mut self, access_token: Option<AccessToken>) -> Result<()> {
182        self.access_token = access_token;
183
184        Ok(())
185    }
186
187    /// Check if KYC status is verified.
188    ///
189    /// # Arguments
190    ///
191    /// * `username` - The username of the user to check KYC status for.
192    ///
193    /// # Returns
194    ///
195    /// Returns `Ok(true)` if the KYC status is verified, or `Ok(false)` if it is not verified.
196    ///
197    /// # Errors
198    ///
199    /// Returns an `Error` if there is an issue validating the configuration, initializing the repository, or checking the KYC status.
200    pub async fn is_kyc_status_verified(&mut self, username: &str) -> Result<bool> {
201        info!("Checking KYC status of user {username}");
202        let Some(repo) = &mut self.repo else {
203            return Err(crate::Error::UserRepoNotInitialized);
204        };
205        let access_token = self
206            .access_token
207            .as_ref()
208            .ok_or(crate::error::Error::MissingAccessToken)?;
209
210        let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
211        let status = check_kyc_status(config, access_token, username).await?;
212        let is_verified = status.is_verified;
213
214        // Store user state in db if is verified
215        if is_verified {
216            repo.set_kyc_state(username, is_verified)?;
217        }
218
219        Ok(is_verified)
220    }
221
222    /// Set the user preferred network
223    pub async fn set_preferred_network(&mut self, network_key: Option<String>) -> Result<()> {
224        let Some(_user) = &self.active_user else {
225            return Err(crate::Error::UserNotInitialized);
226        };
227        let access_token = self
228            .access_token
229            .as_ref()
230            .ok_or(crate::error::Error::MissingAccessToken)?;
231        let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
232        backend::user::set_preferred_network(config, access_token, network_key).await?;
233        Ok(())
234    }
235
236    /// Get the user preferred network
237    pub async fn get_preferred_network(&self) -> Result<Option<String>> {
238        let Some(_user) = &self.active_user else {
239            return Err(crate::Error::UserNotInitialized);
240        };
241        let access_token = self
242            .access_token
243            .as_ref()
244            .ok_or(crate::error::Error::MissingAccessToken)?;
245        let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
246        let preferred_network = backend::user::get_preferred_network(config, access_token).await?;
247        Ok(preferred_network)
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::core::core_testing_utils::handle_error_test_cases;
255    use crate::testing_utils::{
256        AUTH_PROVIDER, HEADER_X_APP_NAME, IOTA_NETWORK_KEY, TOKEN, USERNAME, example_get_user, set_config,
257    };
258    use crate::{core::Sdk, user::MockUserRepo, wallet_manager::MockWalletManager};
259    use api_types::api::kyc::KycStatusResponse;
260    use api_types::api::viviswap::detail::SwapPaymentDetailKey;
261    use rstest::rstest;
262
263    #[rstest]
264    #[case::success(Ok(example_get_user(SwapPaymentDetailKey::Iota, false, 0, KycType::Undefined)))]
265    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
266    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
267    #[case::user_not_found(Err(crate::Error::UserRepository(crate::user::error::UserKvStorageError::UserNotFound { username: USERNAME.to_string() })))]
268    #[tokio::test]
269    async fn test_get_user(#[case] expected: Result<MockUserRepo>) {
270        // Arrange
271        let (_srv, config, _cleanup) = set_config().await;
272        let mut sdk = Sdk::new(config).unwrap();
273
274        match &expected {
275            Ok(_) => {
276                let mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 1, KycType::Undefined);
277                sdk.repo = Some(Box::new(mock_user_repo));
278
279                sdk.active_user = Some(crate::types::users::ActiveUser {
280                    username: USERNAME.into(),
281                    wallet_manager: Box::new(MockWalletManager::new()),
282                    mnemonic_derivation_options: Default::default(),
283                });
284            }
285            Err(error) => {
286                handle_error_test_cases(error, &mut sdk, 0, 0).await;
287            }
288        }
289
290        // Act
291        let response = sdk.get_user().await;
292
293        // Assert
294        match expected {
295            Ok(_resp) => {
296                assert!(response.is_ok());
297            }
298            Err(ref expected_err) => {
299                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
300            }
301        }
302    }
303
304    #[rstest]
305    #[case::success(Ok(()))]
306    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
307    #[tokio::test]
308    async fn test_create_new_user(#[case] expected: Result<()>) {
309        // Arrange
310        let (_srv, config, _cleanup) = set_config().await;
311        let mut sdk = Sdk::new(config).unwrap();
312
313        match &expected {
314            Ok(_) => {
315                let mut mock_user_repo = MockUserRepo::new();
316                mock_user_repo.expect_create().times(1).returning(|_| Ok(()));
317
318                sdk.repo = Some(Box::new(mock_user_repo));
319
320                sdk.active_user = Some(crate::types::users::ActiveUser {
321                    username: USERNAME.into(),
322                    wallet_manager: Box::new(MockWalletManager::new()),
323                    mnemonic_derivation_options: Default::default(),
324                });
325            }
326            Err(error) => {
327                handle_error_test_cases(error, &mut sdk, 0, 0).await;
328            }
329        }
330
331        // Act
332        let response = sdk.create_new_user("new_user").await;
333
334        // Assert
335        match expected {
336            Ok(_) => response.unwrap(),
337            Err(ref expected_err) => {
338                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
339            }
340        }
341    }
342
343    #[rstest]
344    #[case::success(Ok(()))]
345    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
346    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
347    #[case::unauthorized(Err(crate::Error::MissingAccessToken))]
348    #[case::missing_config(Err(crate::Error::MissingConfig))]
349    #[tokio::test]
350    async fn test_delete_user(#[case] expected: Result<()>) {
351        // Arrange
352        let (mut srv, config, _cleanup) = set_config().await;
353        let mut sdk = Sdk::new(config).unwrap();
354
355        let pin = EncryptionPin::try_from_string("123456").unwrap();
356
357        let mut mock_server = None;
358        match &expected {
359            Ok(_) => {
360                let mut mock_wallet_user = MockWalletManager::new();
361                mock_wallet_user
362                    .expect_delete_wallet()
363                    .once()
364                    .returning(|_, _, _| Ok(()));
365
366                sdk.active_user = Some(crate::types::users::ActiveUser {
367                    username: USERNAME.into(),
368                    wallet_manager: Box::new(mock_wallet_user),
369                    mnemonic_derivation_options: Default::default(),
370                });
371
372                sdk.access_token = Some(TOKEN.clone());
373
374                let mut mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 3, KycType::Undefined);
375                mock_user_repo.expect_update().once().returning(|_| Ok(()));
376                mock_user_repo.expect_delete().once().returning(|uname| {
377                    assert_eq!(uname, USERNAME);
378                    Ok(())
379                });
380                sdk.repo = Some(Box::new(mock_user_repo));
381
382                let new_pin = EncryptionPin::try_from_string("123456").unwrap();
383                sdk.change_pin(&pin, &new_pin).await.unwrap();
384
385                mock_server = Some(
386                    srv.mock("DELETE", "/api/user")
387                        .match_header(HEADER_X_APP_NAME, AUTH_PROVIDER)
388                        .match_header("authorization", format!("Bearer {}", TOKEN.as_str()).as_str())
389                        .with_status(202) // Accepted
390                        .expect(1)
391                        .create(),
392                );
393            }
394            Err(error) => {
395                handle_error_test_cases(error, &mut sdk, 0, 2).await;
396            }
397        }
398
399        // Act
400        let response = sdk.delete_user(Some(&pin)).await;
401
402        // Assert
403        match expected {
404            Ok(_) => response.unwrap(),
405            Err(ref expected_err) => {
406                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
407            }
408        }
409        if let Some(m) = mock_server {
410            m.assert();
411        }
412    }
413
414    #[rstest]
415    #[case::success(true, Ok(()))]
416    #[case::repo_init_error(false, Err(crate::Error::UserRepoNotInitialized))]
417    #[case::missing_config(false, Err(crate::Error::MissingConfig))]
418    #[tokio::test]
419    async fn test_init_user(#[case] _access_token: bool, #[case] expected: Result<()>) {
420        // Arrange
421        let (mut srv, config, _cleanup) = set_config().await;
422        let mut sdk = Sdk::new(config).unwrap();
423
424        let mut mock_server = None;
425        match &expected {
426            Ok(_) => {
427                let mut mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 1, KycType::Undefined);
428                mock_user_repo.expect_set_kyc_state().times(1).returning(|_, _| Ok(()));
429                sdk.repo = Some(Box::new(mock_user_repo));
430
431                sdk.active_user = Some(crate::types::users::ActiveUser {
432                    username: USERNAME.into(),
433                    wallet_manager: Box::new(MockWalletManager::new()),
434                    mnemonic_derivation_options: Default::default(),
435                });
436
437                sdk.access_token = Some(TOKEN.clone());
438
439                let mock_response = KycStatusResponse {
440                    username: USERNAME.into(),
441                    is_verified: true,
442                };
443                let body = serde_json::to_string(&mock_response).unwrap();
444
445                mock_server = Some(
446                    srv.mock("GET", "/api/kyc/check-status")
447                        .match_header(HEADER_X_APP_NAME, AUTH_PROVIDER)
448                        .match_header("authorization", format!("Bearer {}", TOKEN.as_str()).as_str())
449                        .with_status(200)
450                        .with_body(body)
451                        .create(),
452                );
453            }
454            Err(error) => {
455                handle_error_test_cases(error, &mut sdk, 1, 0).await;
456            }
457        }
458
459        // Act
460        let response = sdk.init_user(USERNAME).await;
461
462        // Assert
463        match expected {
464            Ok(_) => response.unwrap(),
465            Err(ref expected_err) => {
466                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
467            }
468        }
469        if let Some(m) = mock_server {
470            m.assert();
471        }
472    }
473
474    #[rstest]
475    #[case::success(Ok(true))]
476    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
477    #[case::unauthorized(Err(crate::Error::MissingAccessToken))]
478    #[case::missing_config(Err(crate::Error::MissingConfig))]
479    #[tokio::test]
480    async fn test_is_kyc_verified(#[case] expected: Result<bool>) {
481        // Arrange
482        let (mut srv, config, _cleanup) = set_config().await;
483        let mut sdk = Sdk::new(config).unwrap();
484        let mut mock_server = None;
485
486        match &expected {
487            Ok(_) => {
488                let mut mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 0, KycType::Undefined);
489                mock_user_repo.expect_set_kyc_state().times(1).returning(|_, _| Ok(()));
490                sdk.repo = Some(Box::new(mock_user_repo));
491
492                sdk.active_user = Some(crate::types::users::ActiveUser {
493                    username: USERNAME.into(),
494                    wallet_manager: Box::new(MockWalletManager::new()),
495                    mnemonic_derivation_options: Default::default(),
496                });
497
498                sdk.access_token = Some(TOKEN.clone());
499
500                let mock_response = KycStatusResponse {
501                    username: USERNAME.into(),
502                    is_verified: true,
503                };
504                let body = serde_json::to_string(&mock_response).unwrap();
505
506                mock_server = Some(
507                    srv.mock("GET", "/api/kyc/check-status")
508                        .match_header(HEADER_X_APP_NAME, AUTH_PROVIDER)
509                        .match_header("authorization", format!("Bearer {}", TOKEN.as_str()).as_str())
510                        .with_status(200)
511                        .with_body(body)
512                        .create(),
513                );
514            }
515            Err(error) => {
516                handle_error_test_cases(error, &mut sdk, 0, 0).await;
517            }
518        }
519
520        // Act
521        let response = sdk.is_kyc_status_verified(USERNAME).await;
522
523        // Assert
524        match expected {
525            Ok(verified) => assert!(verified),
526            Err(ref expected_err) => {
527                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
528            }
529        }
530        if let Some(m) = mock_server {
531            m.assert();
532        }
533    }
534
535    #[rstest]
536    #[case::success(Ok(()))]
537    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
538    #[case::unauthorized(Err(crate::Error::MissingAccessToken))]
539    #[case::missing_config(Err(crate::Error::MissingConfig))]
540    #[tokio::test]
541    async fn test_set_preferred_network(#[case] expected: Result<()>) {
542        // Arrange
543        let (mut srv, config, _cleanup) = set_config().await;
544        let mut sdk = Sdk::new(config).unwrap();
545        let mut mock_server = None;
546
547        match &expected {
548            Ok(_) => {
549                sdk.active_user = Some(crate::types::users::ActiveUser {
550                    username: USERNAME.into(),
551                    wallet_manager: Box::new(MockWalletManager::new()),
552                    mnemonic_derivation_options: Default::default(),
553                });
554                sdk.access_token = Some(TOKEN.clone());
555
556                mock_server = Some(
557                    srv.mock("PUT", "/api/user/network")
558                        .match_header(HEADER_X_APP_NAME, AUTH_PROVIDER)
559                        .match_header("authorization", format!("Bearer {}", TOKEN.as_str()).as_str())
560                        .with_status(202)
561                        .expect(1)
562                        .create(),
563                );
564            }
565            Err(error) => {
566                handle_error_test_cases(error, &mut sdk, 0, 0).await;
567            }
568        }
569
570        // Act
571        let response = sdk.set_preferred_network(Some(IOTA_NETWORK_KEY.to_string())).await;
572
573        // Assert
574        match expected {
575            Ok(()) => response.unwrap(),
576            Err(ref expected_err) => {
577                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
578            }
579        }
580        if let Some(m) = mock_server {
581            m.assert();
582        }
583    }
584
585    #[rstest]
586    #[case::success(Ok(Some(IOTA_NETWORK_KEY.to_string())))]
587    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
588    #[case::unauthorized(Err(crate::Error::MissingAccessToken))]
589    #[case::missing_config(Err(crate::Error::MissingConfig))]
590    #[tokio::test]
591    async fn test_get_preferred_network(#[case] expected: Result<Option<String>>) {
592        // Arrange
593        let (mut srv, config, _cleanup) = set_config().await;
594        let mut sdk = Sdk::new(config).unwrap();
595        let mut mock_server = None;
596
597        match &expected {
598            Ok(_) => {
599                sdk.active_user = Some(crate::types::users::ActiveUser {
600                    username: USERNAME.into(),
601                    wallet_manager: Box::new(MockWalletManager::new()),
602                    mnemonic_derivation_options: Default::default(),
603                });
604                sdk.access_token = Some(TOKEN.clone());
605
606                mock_server = Some(
607                    srv.mock("GET", "/api/user/network")
608                        .match_header(HEADER_X_APP_NAME, AUTH_PROVIDER)
609                        .match_header("authorization", format!("Bearer {}", TOKEN.as_str()).as_str())
610                        .with_status(200)
611                        .with_header("content-type", "application/json")
612                        .with_body("{\"network_key\":\"IOTA\"}")
613                        .expect(1)
614                        .create(),
615                );
616            }
617            Err(error) => {
618                handle_error_test_cases(error, &mut sdk, 0, 0).await;
619            }
620        }
621
622        // Act
623        let response = sdk.get_preferred_network().await;
624
625        // Assert
626        match expected {
627            Ok(network_key) => {
628                assert_eq!(response.unwrap(), network_key)
629            }
630            Err(ref expected_err) => {
631                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
632            }
633        }
634        if let Some(m) = mock_server {
635            m.assert();
636        }
637    }
638}