tinycongress_api/identity/repo/
accounts.rs1use async_trait::async_trait;
4use sqlx::PgPool;
5use uuid::Uuid;
6
7#[derive(Debug, Clone)]
9pub struct CreatedAccount {
10 pub id: Uuid,
11 pub root_kid: String,
12}
13
14#[derive(Debug, thiserror::Error)]
16pub 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#[async_trait]
30pub trait AccountRepo: Send + Sync {
31 async fn create(
38 &self,
39 username: &str,
40 root_pubkey: &str,
41 root_kid: &str,
42 ) -> Result<CreatedAccount, AccountRepoError>;
43}
44
45pub struct PgAccountRepo {
47 pool: PgPool,
48}
49
50impl PgAccountRepo {
51 #[must_use]
52 pub const fn new(pool: PgPool) -> Self {
53 Self { pool }
54 }
55}
56
57#[async_trait]
58impl AccountRepo for PgAccountRepo {
59 async fn create(
60 &self,
61 username: &str,
62 root_pubkey: &str,
63 root_kid: &str,
64 ) -> Result<CreatedAccount, AccountRepoError> {
65 create_account(&self.pool, username, root_pubkey, root_kid).await
66 }
67}
68
69async fn create_account<'e, E>(
72 executor: E,
73 username: &str,
74 root_pubkey: &str,
75 root_kid: &str,
76) -> Result<CreatedAccount, AccountRepoError>
77where
78 E: sqlx::Executor<'e, Database = sqlx::Postgres>,
79{
80 let id = Uuid::new_v4();
81
82 let result = sqlx::query(
83 r"
84 INSERT INTO accounts (id, username, root_pubkey, root_kid)
85 VALUES ($1, $2, $3, $4)
86 ",
87 )
88 .bind(id)
89 .bind(username)
90 .bind(root_pubkey)
91 .bind(root_kid)
92 .execute(executor)
93 .await;
94
95 match result {
96 Ok(_) => Ok(CreatedAccount {
97 id,
98 root_kid: root_kid.to_string(),
99 }),
100 Err(e) => {
101 if let sqlx::Error::Database(db_err) = &e {
102 if let Some(constraint) = db_err.constraint() {
103 match constraint {
104 "accounts_username_key" => return Err(AccountRepoError::DuplicateUsername),
105 "accounts_root_kid_key" => return Err(AccountRepoError::DuplicateKey),
106 _ => {}
107 }
108 }
109 }
110 Err(AccountRepoError::Database(e))
111 }
112 }
113}
114
115pub async fn create_account_with_executor<'e, E>(
123 executor: E,
124 username: &str,
125 root_pubkey: &str,
126 root_kid: &str,
127) -> Result<CreatedAccount, AccountRepoError>
128where
129 E: sqlx::Executor<'e, Database = sqlx::Postgres>,
130{
131 create_account(executor, username, root_pubkey, root_kid).await
132}
133
134#[cfg(any(test, feature = "test-utils"))]
135#[allow(clippy::expect_used)]
136pub mod mock {
137 use super::{async_trait, AccountRepo, AccountRepoError, CreatedAccount, Uuid};
140 use std::sync::Mutex;
141
142 pub struct MockAccountRepo {
144 pub create_result: Mutex<Option<Result<CreatedAccount, AccountRepoError>>>,
146 pub calls: Mutex<Vec<(String, String, String)>>,
148 }
149
150 impl MockAccountRepo {
151 #[must_use]
153 pub const fn new() -> Self {
154 Self {
155 create_result: Mutex::new(None),
156 calls: Mutex::new(Vec::new()),
157 }
158 }
159
160 pub fn set_create_result(&self, result: Result<CreatedAccount, AccountRepoError>) {
166 *self.create_result.lock().expect("lock poisoned") = Some(result);
167 }
168
169 pub fn calls(&self) -> Vec<(String, String, String)> {
175 self.calls.lock().expect("lock poisoned").clone()
176 }
177 }
178
179 impl Default for MockAccountRepo {
180 fn default() -> Self {
181 Self::new()
182 }
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 ) -> 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 .unwrap_or_else(|| {
203 Ok(CreatedAccount {
204 id: Uuid::new_v4(),
205 root_kid: root_kid.to_string(),
206 })
207 })
208 }
209 }
210}