LCOV - code coverage report
Current view: top level - service/src/identity/repo - accounts.rs (source / functions) Coverage Total Hit
Test: Rust Backend Coverage Lines: 84.4 % 77 65
Test Date: 2025-12-20 21:58:40 Functions: 46.4 % 28 13

            Line data    Source code
       1              : //! Account repository for database operations
       2              : 
       3              : use async_trait::async_trait;
       4              : use sqlx::PgPool;
       5              : use uuid::Uuid;
       6              : 
       7              : /// Account creation result
       8              : #[derive(Debug, Clone)]
       9              : pub struct CreatedAccount {
      10              :     pub id: Uuid,
      11              :     pub root_kid: String,
      12              : }
      13              : 
      14              : /// Error types for account operations
      15              : #[derive(Debug, thiserror::Error)]
      16              : pub enum AccountRepoError {
      17              :     #[error("username already taken")]
      18              :     DuplicateUsername,
      19              :     #[error("public key already registered")]
      20              :     DuplicateKey,
      21              :     #[error("database error: {0}")]
      22              :     Database(#[from] sqlx::Error),
      23              : }
      24              : 
      25              : /// Repository trait for account operations
      26              : ///
      27              : /// This trait abstracts database operations to enable unit testing
      28              : /// handlers with mock implementations.
      29              : #[async_trait]
      30              : pub trait AccountRepo: Send + Sync {
      31              :     /// Create a new account with the given credentials
      32              :     ///
      33              :     /// # Errors
      34              :     ///
      35              :     /// Returns `AccountRepoError::DuplicateUsername` if username is taken.
      36              :     /// Returns `AccountRepoError::DuplicateKey` if public key is already registered.
      37              :     async fn create(
      38              :         &self,
      39              :         username: &str,
      40              :         root_pubkey: &str,
      41              :         root_kid: &str,
      42              :     ) -> Result<CreatedAccount, AccountRepoError>;
      43              : }
      44              : 
      45              : /// `PostgreSQL` implementation of [`AccountRepo`]
      46              : pub struct PgAccountRepo {
      47              :     pool: PgPool,
      48              : }
      49              : 
      50              : impl PgAccountRepo {
      51              :     #[must_use]
      52            0 :     pub const fn new(pool: PgPool) -> Self {
      53            0 :         Self { pool }
      54            0 :     }
      55              : }
      56              : 
      57              : #[async_trait]
      58              : impl AccountRepo for PgAccountRepo {
      59              :     async fn create(
      60              :         &self,
      61              :         username: &str,
      62              :         root_pubkey: &str,
      63              :         root_kid: &str,
      64            0 :     ) -> Result<CreatedAccount, AccountRepoError> {
      65              :         create_account(&self.pool, username, root_pubkey, root_kid).await
      66            0 :     }
      67              : }
      68              : 
      69              : /// Shared implementation for account creation that works with any executor.
      70              : /// This allows tests to use transactions for isolation.
      71            9 : async fn create_account<'e, E>(
      72            9 :     executor: E,
      73            9 :     username: &str,
      74            9 :     root_pubkey: &str,
      75            9 :     root_kid: &str,
      76            9 : ) -> Result<CreatedAccount, AccountRepoError>
      77            9 : where
      78            9 :     E: sqlx::Executor<'e, Database = sqlx::Postgres>,
      79            9 : {
      80            9 :     let id = Uuid::new_v4();
      81              : 
      82            9 :     let result = sqlx::query(
      83            9 :         r"
      84            9 :         INSERT INTO accounts (id, username, root_pubkey, root_kid)
      85            9 :         VALUES ($1, $2, $3, $4)
      86            9 :         ",
      87            9 :     )
      88            9 :     .bind(id)
      89            9 :     .bind(username)
      90            9 :     .bind(root_pubkey)
      91            9 :     .bind(root_kid)
      92            9 :     .execute(executor)
      93            9 :     .await;
      94              : 
      95            9 :     match result {
      96            7 :         Ok(_) => Ok(CreatedAccount {
      97            7 :             id,
      98            7 :             root_kid: root_kid.to_string(),
      99            7 :         }),
     100            2 :         Err(e) => {
     101            2 :             if let sqlx::Error::Database(db_err) = &e {
     102            2 :                 if let Some(constraint) = db_err.constraint() {
     103            2 :                     match constraint {
     104            2 :                         "accounts_username_key" => return Err(AccountRepoError::DuplicateUsername),
     105            1 :                         "accounts_root_kid_key" => return Err(AccountRepoError::DuplicateKey),
     106            0 :                         _ => {}
     107              :                     }
     108            0 :                 }
     109            0 :             }
     110            0 :             Err(AccountRepoError::Database(e))
     111              :         }
     112              :     }
     113            9 : }
     114              : 
     115              : /// Create an account using any executor (pool, connection, or transaction).
     116              : /// Useful for tests that need transaction isolation.
     117              : ///
     118              : /// # Errors
     119              : ///
     120              : /// Returns `AccountRepoError::DuplicateUsername` if username is taken.
     121              : /// Returns `AccountRepoError::DuplicateKey` if public key is already registered.
     122            9 : pub async fn create_account_with_executor<'e, E>(
     123            9 :     executor: E,
     124            9 :     username: &str,
     125            9 :     root_pubkey: &str,
     126            9 :     root_kid: &str,
     127            9 : ) -> Result<CreatedAccount, AccountRepoError>
     128            9 : where
     129            9 :     E: sqlx::Executor<'e, Database = sqlx::Postgres>,
     130            9 : {
     131            9 :     create_account(executor, username, root_pubkey, root_kid).await
     132            9 : }
     133              : 
     134              : #[cfg(any(test, feature = "test-utils"))]
     135              : #[allow(clippy::expect_used)]
     136              : pub mod mock {
     137              :     //! Mock implementation for testing
     138              : 
     139              :     use super::{async_trait, AccountRepo, AccountRepoError, CreatedAccount, Uuid};
     140              :     use std::sync::Mutex;
     141              : 
     142              :     /// Mock account repository for unit tests.
     143              :     pub struct MockAccountRepo {
     144              :         /// Preset result to return from `create()`.
     145              :         pub create_result: Mutex<Option<Result<CreatedAccount, AccountRepoError>>>,
     146              :         /// Captured calls for verification
     147              :         pub calls: Mutex<Vec<(String, String, String)>>,
     148              :     }
     149              : 
     150              :     impl MockAccountRepo {
     151              :         /// Create a new mock repository.
     152              :         #[must_use]
     153           22 :         pub const fn new() -> Self {
     154           22 :             Self {
     155           22 :                 create_result: Mutex::new(None),
     156           22 :                 calls: Mutex::new(Vec::new()),
     157           22 :             }
     158           22 :         }
     159              : 
     160              :         /// Set the result that `create()` will return.
     161              :         ///
     162              :         /// # Panics
     163              :         ///
     164              :         /// Panics if the internal mutex is poisoned.
     165            4 :         pub fn set_create_result(&self, result: Result<CreatedAccount, AccountRepoError>) {
     166            4 :             *self.create_result.lock().expect("lock poisoned") = Some(result);
     167            4 :         }
     168              : 
     169              :         /// Retrieve all recorded calls
     170              :         ///
     171              :         /// # Panics
     172              :         ///
     173              :         /// Panics if the internal mutex is poisoned.
     174            1 :         pub fn calls(&self) -> Vec<(String, String, String)> {
     175            1 :             self.calls.lock().expect("lock poisoned").clone()
     176            1 :         }
     177              :     }
     178              : 
     179              :     impl Default for MockAccountRepo {
     180            0 :         fn default() -> Self {
     181            0 :             Self::new()
     182            0 :         }
     183              :     }
     184              : 
     185              :     #[async_trait]
     186              :     impl AccountRepo for MockAccountRepo {
     187              :         async fn create(
     188              :             &self,
     189              :             username: &str,
     190              :             root_pubkey: &str,
     191              :             root_kid: &str,
     192            8 :         ) -> Result<CreatedAccount, AccountRepoError> {
     193              :             self.calls.lock().expect("lock poisoned").push((
     194              :                 username.to_string(),
     195              :                 root_pubkey.to_string(),
     196              :                 root_kid.to_string(),
     197              :             ));
     198              :             self.create_result
     199              :                 .lock()
     200              :                 .expect("lock poisoned")
     201              :                 .take()
     202            4 :                 .unwrap_or_else(|| {
     203            4 :                     Ok(CreatedAccount {
     204            4 :                         id: Uuid::new_v4(),
     205            4 :                         root_kid: root_kid.to_string(),
     206            4 :                     })
     207            4 :                 })
     208            8 :         }
     209              :     }
     210              : }
        

Generated by: LCOV version 2.0-1