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