torii_storage_postgres/
password.rs

1use 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        // Create test user table if it doesn't exist
65        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        // First insert a test user
99        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        // Test setting password hash
107        let result = storage.set_password_hash(&user_id, password_hash).await;
108        assert!(result.is_ok());
109
110        // Test getting password hash
111        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
116        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        // Should succeed but not actually update anything
139        assert!(result.is_ok());
140    }
141}