torii_storage_postgres/
password.rs1use crate::PostgresStorage;
2use async_trait::async_trait;
3use torii_core::UserId;
4use torii_core::error::StorageError;
5use torii_core::storage::PasswordStorage;
6
7#[async_trait]
8impl PasswordStorage for PostgresStorage {
9 async fn set_password_hash(
10 &self,
11 user_id: &UserId,
12 hash: &str,
13 ) -> Result<(), torii_core::Error> {
14 sqlx::query("UPDATE users SET password_hash = $1 WHERE id = $2")
15 .bind(hash)
16 .bind(user_id.as_str())
17 .execute(&self.pool)
18 .await
19 .map_err(|_| StorageError::Database("Failed to set password hash".to_string()))?;
20 Ok(())
21 }
22
23 async fn get_password_hash(
24 &self,
25 user_id: &UserId,
26 ) -> Result<Option<String>, torii_core::Error> {
27 let result = sqlx::query_scalar("SELECT password_hash FROM users WHERE id = $1")
28 .bind(user_id.as_str())
29 .fetch_optional(&self.pool)
30 .await
31 .map_err(|_| StorageError::Database("Failed to get password hash".to_string()))?;
32 Ok(result)
33 }
34}
35
36#[cfg(test)]
37mod tests {
38 use super::*;
39 use crate::PostgresStorage;
40 use sqlx::PgPool;
41
42 use tokio::sync::OnceCell;
43 use torii_core::UserId;
44
45 static TEST_POOL: OnceCell<PgPool> = OnceCell::const_new();
46
47 async fn get_test_pool() -> &'static PgPool {
48 TEST_POOL
49 .get_or_init(|| async {
50 let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {
51 "postgres://postgres:postgres@localhost/torii_test".to_string()
52 });
53
54 PgPool::connect(&database_url)
55 .await
56 .expect("Failed to connect to test database")
57 })
58 .await
59 }
60
61 async fn setup_test_storage() -> PostgresStorage {
62 let pool = get_test_pool().await.clone();
63
64 sqlx::query(
66 r#"
67 CREATE TABLE IF NOT EXISTS users (
68 id TEXT PRIMARY KEY,
69 email TEXT NOT NULL UNIQUE,
70 name TEXT,
71 password_hash TEXT,
72 created_at TIMESTAMPTZ DEFAULT NOW(),
73 updated_at TIMESTAMPTZ DEFAULT NOW()
74 )
75 "#,
76 )
77 .execute(&pool)
78 .await
79 .expect("Failed to create test table");
80
81 PostgresStorage::new(pool)
82 }
83
84 async fn cleanup_test_user(storage: &PostgresStorage, user_id: &UserId) {
85 let _ = sqlx::query("DELETE FROM users WHERE id = $1")
86 .bind(user_id.as_str())
87 .execute(&storage.pool)
88 .await;
89 }
90
91 #[tokio::test]
92 #[ignore = "requires database"]
93 async fn test_set_and_get_password_hash() {
94 let storage = setup_test_storage().await;
95 let user_id = UserId::new_random();
96 let password_hash = "hashed_password_123";
97
98 sqlx::query("INSERT INTO users (id, email) VALUES ($1, $2)")
100 .bind(user_id.as_str())
101 .bind("test@example.com")
102 .execute(&storage.pool)
103 .await
104 .expect("Failed to insert test user");
105
106 let result = storage.set_password_hash(&user_id, password_hash).await;
108 assert!(result.is_ok());
109
110 let result = storage.get_password_hash(&user_id).await;
112 assert!(result.is_ok());
113 assert_eq!(result.unwrap(), Some(password_hash.to_string()));
114
115 cleanup_test_user(&storage, &user_id).await;
117 }
118
119 #[tokio::test]
120 #[ignore = "requires database"]
121 async fn test_get_password_hash_not_found() {
122 let storage = setup_test_storage().await;
123 let user_id = UserId::new_random();
124
125 let result = storage.get_password_hash(&user_id).await;
126 assert!(result.is_ok());
127 assert_eq!(result.unwrap(), None);
128 }
129
130 #[tokio::test]
131 #[ignore = "requires database"]
132 async fn test_set_password_hash_nonexistent_user() {
133 let storage = setup_test_storage().await;
134 let user_id = UserId::new_random();
135 let password_hash = "hashed_password_123";
136
137 let result = storage.set_password_hash(&user_id, password_hash).await;
138 assert!(result.is_ok());
140 }
141}