cot/auth/db.rs
1//! Database-backed user authentication backend.
2//!
3//! This module provides a user type and an authentication backend that stores
4//! the user data in a database using the Cot ORM.
5
6use std::any::Any;
7use std::borrow::Cow;
8use std::fmt::{Display, Formatter};
9use std::sync::Arc;
10
11use async_trait::async_trait;
12// Importing `Auto` from `cot` instead of `crate` so that the migration generator
13// can figure out it's an autogenerated field
14use cot::db::Auto;
15use cot_macros::AdminModel;
16use hmac::{Hmac, Mac};
17use sha2::Sha512;
18use thiserror::Error;
19
20use crate::App;
21use crate::admin::{AdminModelManager, DefaultAdminModelManager};
22use crate::auth::{
23 AuthBackend, AuthError, Password, PasswordHash, PasswordVerificationResult, Result,
24 SessionAuthHash, User, UserId,
25};
26use crate::config::SecretKey;
27use crate::db::migrations::SyncDynMigration;
28use crate::db::{Database, DatabaseBackend, LimitedString, Model, model, query};
29use crate::form::Form;
30
31pub mod migrations;
32
33pub(crate) const MAX_USERNAME_LENGTH: u32 = 255;
34
35/// A user stored in the database.
36#[derive(Debug, Clone, Form, AdminModel)]
37#[model]
38pub struct DatabaseUser {
39 #[model(primary_key)]
40 id: Auto<i64>,
41 #[model(unique)]
42 username: LimitedString<MAX_USERNAME_LENGTH>,
43 password: PasswordHash,
44}
45
46/// An error that occurs when creating a user.
47#[derive(Debug, Clone, Error)]
48#[non_exhaustive]
49pub enum CreateUserError {
50 /// The username is too long.
51 #[error("username is too long (max {MAX_USERNAME_LENGTH} characters, got {0})")]
52 UsernameTooLong(usize),
53}
54
55impl DatabaseUser {
56 #[must_use]
57 fn new(
58 id: Auto<i64>,
59 username: LimitedString<MAX_USERNAME_LENGTH>,
60 password: &Password,
61 ) -> Self {
62 Self {
63 id,
64 username,
65 password: PasswordHash::from_password(password),
66 }
67 }
68
69 /// Create a new user and save it to the database.
70 ///
71 /// # Errors
72 ///
73 /// Returns an error if the user could not be saved to the database.
74 ///
75 /// # Example
76 ///
77 /// ```
78 /// use cot::auth::Password;
79 /// use cot::auth::db::DatabaseUser;
80 /// use cot::request::{Request, RequestExt};
81 /// use cot::response::{Response, ResponseExt};
82 /// use cot::{Body, StatusCode};
83 ///
84 /// async fn view(request: &Request) -> cot::Result<Response> {
85 /// let user = DatabaseUser::create_user(
86 /// request.db(),
87 /// "testuser".to_string(),
88 /// &Password::new("password123"),
89 /// )
90 /// .await?;
91 ///
92 /// Ok(Response::new_html(
93 /// StatusCode::OK,
94 /// Body::fixed("User created!"),
95 /// ))
96 /// }
97 ///
98 /// # #[tokio::main]
99 /// # async fn main() -> cot::Result<()> {
100 /// # use cot::test::{TestDatabase, TestRequestBuilder};
101 /// # let mut test_database = TestDatabase::new_sqlite().await?;
102 /// # test_database.with_auth().run_migrations().await;
103 /// # let request = TestRequestBuilder::get("/")
104 /// # .with_db_auth(test_database.database())
105 /// # .await
106 /// # .build();
107 /// # view(&request).await?;
108 /// # test_database.cleanup().await?;
109 /// # Ok(())
110 /// # }
111 /// ```
112 pub async fn create_user<DB: DatabaseBackend, T: Into<String>, U: Into<Password>>(
113 db: &DB,
114 username: T,
115 password: U,
116 ) -> Result<Self> {
117 let username = username.into();
118 let username_length = username.len();
119 let username = LimitedString::<MAX_USERNAME_LENGTH>::new(username).map_err(|_| {
120 AuthError::backend_error(CreateUserError::UsernameTooLong(username_length))
121 })?;
122
123 let mut user = Self::new(Auto::auto(), username, &password.into());
124 user.insert(db).await.map_err(AuthError::backend_error)?;
125
126 Ok(user)
127 }
128
129 /// Get a user by their integer ID. Returns [`None`] if the user does not
130 /// exist.
131 ///
132 /// # Errors
133 ///
134 /// Returns an error if there was an error querying the database.
135 ///
136 /// # Panics
137 ///
138 /// Panics if the user ID provided is not an integer.
139 ///
140 /// # Example
141 ///
142 /// ```
143 /// use cot::auth::db::DatabaseUser;
144 /// use cot::auth::{Password, UserId};
145 /// use cot::request::{Request, RequestExt};
146 /// use cot::response::{Response, ResponseExt};
147 /// use cot::{Body, StatusCode};
148 ///
149 /// async fn view(request: &Request) -> cot::Result<Response> {
150 /// let user = DatabaseUser::create_user(
151 /// request.db(),
152 /// "testuser".to_string(),
153 /// &Password::new("password123"),
154 /// )
155 /// .await?;
156 ///
157 /// let user_from_db = DatabaseUser::get_by_id(request.db(), user.id()).await?;
158 ///
159 /// Ok(Response::new_html(
160 /// StatusCode::OK,
161 /// Body::fixed("User created!"),
162 /// ))
163 /// }
164 ///
165 /// # #[tokio::main]
166 /// # async fn main() -> cot::Result<()> {
167 /// # use cot::test::{TestDatabase, TestRequestBuilder};
168 /// # let mut test_database = TestDatabase::new_sqlite().await?;
169 /// # test_database.with_auth().run_migrations().await;
170 /// # let request = TestRequestBuilder::get("/")
171 /// # .with_db_auth(test_database.database())
172 /// # .await
173 /// # .build();
174 /// # view(&request).await?;
175 /// # test_database.cleanup().await?;
176 /// # Ok(())
177 /// # }
178 /// ```
179 pub async fn get_by_id<DB: DatabaseBackend>(db: &DB, id: i64) -> Result<Option<Self>> {
180 let db_user = query!(DatabaseUser, $id == id)
181 .get(db)
182 .await
183 .map_err(AuthError::backend_error)?;
184
185 Ok(db_user)
186 }
187
188 /// Get a user by their username. Returns [`None`] if the user does not
189 /// exist.
190 ///
191 /// # Errors
192 ///
193 /// Returns an error if there was an error querying the database.
194 ///
195 /// # Example
196 ///
197 /// ```
198 /// use cot::auth::db::DatabaseUser;
199 /// use cot::auth::{Password, UserId};
200 /// use cot::request::{Request, RequestExt};
201 /// use cot::response::{Response, ResponseExt};
202 /// use cot::{Body, StatusCode};
203 ///
204 /// async fn view(request: &Request) -> cot::Result<Response> {
205 /// let user = DatabaseUser::create_user(
206 /// request.db(),
207 /// "testuser".to_string(),
208 /// &Password::new("password123"),
209 /// )
210 /// .await?;
211 ///
212 /// let user_from_db = DatabaseUser::get_by_username(request.db(), "testuser").await?;
213 ///
214 /// Ok(Response::new_html(
215 /// StatusCode::OK,
216 /// Body::fixed("User created!"),
217 /// ))
218 /// }
219 ///
220 /// # #[tokio::main]
221 /// # async fn main() -> cot::Result<()> {
222 /// # use cot::test::{TestDatabase, TestRequestBuilder};
223 /// # let mut test_database = TestDatabase::new_sqlite().await?;
224 /// # test_database.with_auth().run_migrations().await;
225 /// # let request = TestRequestBuilder::get("/")
226 /// # .with_db_auth(test_database.database())
227 /// # .await
228 /// # .build();
229 /// # view(&request).await?;
230 /// # test_database.cleanup().await?;
231 /// # Ok(())
232 /// # }
233 /// ```
234 pub async fn get_by_username<DB: DatabaseBackend>(
235 db: &DB,
236 username: &str,
237 ) -> Result<Option<Self>> {
238 let username = LimitedString::<MAX_USERNAME_LENGTH>::new(username).map_err(|_| {
239 AuthError::backend_error(CreateUserError::UsernameTooLong(username.len()))
240 })?;
241 let db_user = query!(DatabaseUser, $username == username)
242 .get(db)
243 .await
244 .map_err(AuthError::backend_error)?;
245
246 Ok(db_user)
247 }
248
249 /// Authenticate a user.
250 ///
251 /// # Errors
252 ///
253 /// Returns an error if there was an error querying the database.
254 pub async fn authenticate<DB: DatabaseBackend>(
255 db: &DB,
256 credentials: &DatabaseUserCredentials,
257 ) -> Result<Option<Self>> {
258 let username = credentials.username();
259 let username_limited = LimitedString::<MAX_USERNAME_LENGTH>::new(username.to_string())
260 .map_err(|_| {
261 AuthError::backend_error(CreateUserError::UsernameTooLong(username.len()))
262 })?;
263 let user = query!(DatabaseUser, $username == username_limited)
264 .get(db)
265 .await
266 .map_err(AuthError::backend_error)?;
267
268 if let Some(mut user) = user {
269 let password_hash = &user.password;
270 match password_hash.verify(credentials.password()) {
271 PasswordVerificationResult::Ok => Ok(Some(user)),
272 PasswordVerificationResult::OkObsolete(new_hash) => {
273 user.password = new_hash;
274 user.save(db).await.map_err(AuthError::backend_error)?;
275 Ok(Some(user))
276 }
277 PasswordVerificationResult::Invalid => Ok(None),
278 }
279 } else {
280 // SECURITY: If no user was found, run the same hashing function to prevent
281 // timing attacks from being used to determine if a user exists. Additionally,
282 // do something with the result to prevent the compiler from optimizing out the
283 // operation.
284 // TODO: benchmark this to make sure it works as expected
285 let dummy_hash = PasswordHash::from_password(credentials.password());
286 if let PasswordVerificationResult::Invalid = dummy_hash.verify(credentials.password()) {
287 unreachable!(
288 "Password hash verification should never fail for a newly generated hash"
289 );
290 }
291 Ok(None)
292 }
293 }
294
295 /// Get the ID of the user.
296 ///
297 /// # Example
298 ///
299 /// ```
300 /// use cot::auth::db::DatabaseUser;
301 /// use cot::auth::{Password, UserId};
302 /// use cot::request::{Request, RequestExt};
303 /// use cot::response::{Response, ResponseExt};
304 /// use cot::{Body, StatusCode};
305 ///
306 /// async fn view(request: &Request) -> cot::Result<Response> {
307 /// let user = DatabaseUser::create_user(
308 /// request.db(),
309 /// "testuser".to_string(),
310 /// &Password::new("password123"),
311 /// )
312 /// .await?;
313 ///
314 /// Ok(Response::new_html(
315 /// StatusCode::OK,
316 /// Body::fixed(format!("User ID: {}", user.id())),
317 /// ))
318 /// }
319 ///
320 /// # #[tokio::main]
321 /// # async fn main() -> cot::Result<()> {
322 /// # use cot::test::{TestDatabase, TestRequestBuilder};
323 /// # let mut test_database = TestDatabase::new_sqlite().await?;
324 /// # test_database.with_auth().run_migrations().await;
325 /// # let request = TestRequestBuilder::get("/")
326 /// # .with_db_auth(test_database.database())
327 /// # .await
328 /// # .build();
329 /// # view(&request).await?;
330 /// # test_database.cleanup().await?;
331 /// # Ok(())
332 /// # }
333 /// ```
334 #[must_use]
335 pub fn id(&self) -> i64 {
336 match self.id {
337 Auto::Fixed(id) => id,
338 Auto::Auto => unreachable!("DatabaseUser constructed with an unknown ID"),
339 }
340 }
341
342 /// Get the username of the user.
343 ///
344 /// # Example
345 ///
346 /// ```
347 /// use cot::auth::db::DatabaseUser;
348 /// use cot::auth::{Password, UserId};
349 /// use cot::request::{Request, RequestExt};
350 /// use cot::response::{Response, ResponseExt};
351 /// use cot::{Body, StatusCode};
352 ///
353 /// async fn view(request: &Request) -> cot::Result<Response> {
354 /// let user = DatabaseUser::create_user(
355 /// request.db(),
356 /// "testuser".to_string(),
357 /// &Password::new("password123"),
358 /// )
359 /// .await?;
360 ///
361 /// Ok(Response::new_html(
362 /// StatusCode::OK,
363 /// Body::fixed(user.username().to_string()),
364 /// ))
365 /// }
366 ///
367 /// # #[tokio::main]
368 /// # async fn main() -> cot::Result<()> {
369 /// # use cot::test::{TestDatabase, TestRequestBuilder};
370 /// # let mut test_database = TestDatabase::new_sqlite().await?;
371 /// # test_database.with_auth().run_migrations().await;
372 /// # let request = TestRequestBuilder::get("/")
373 /// # .with_db_auth(test_database.database())
374 /// # .await
375 /// # .build();
376 /// # view(&request).await?;
377 /// # test_database.cleanup().await?;
378 /// # Ok(())
379 /// # }
380 /// ```
381 #[must_use]
382 pub fn username(&self) -> &str {
383 &self.username
384 }
385}
386
387type SessionAuthHmac = Hmac<Sha512>;
388
389impl User for DatabaseUser {
390 fn id(&self) -> Option<UserId> {
391 Some(UserId::Int(self.id()))
392 }
393
394 fn username(&self) -> Option<Cow<'_, str>> {
395 Some(Cow::from(self.username.as_str()))
396 }
397
398 fn is_active(&self) -> bool {
399 true
400 }
401
402 fn is_authenticated(&self) -> bool {
403 true
404 }
405
406 fn session_auth_hash(&self, secret_key: &SecretKey) -> Option<SessionAuthHash> {
407 let mut mac = SessionAuthHmac::new_from_slice(secret_key.as_bytes())
408 .expect("HMAC can take key of any size");
409 mac.update(self.password.as_str().as_bytes());
410 let hmac_data = mac.finalize().into_bytes();
411
412 Some(SessionAuthHash::new(&hmac_data))
413 }
414}
415
416impl Display for DatabaseUser {
417 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
418 write!(f, "{}", self.username)
419 }
420}
421
422/// Credentials for authenticating a user stored in the database.
423///
424/// This struct is used to authenticate a user stored in the database. It
425/// contains the username and password of the user.
426///
427/// Can be passed to
428/// [`AuthRequestExt::authenticate`](crate::auth::AuthRequestExt::authenticate)
429/// to authenticate a user when using the [`DatabaseUserBackend`].
430#[derive(Debug, Clone)]
431pub struct DatabaseUserCredentials {
432 username: String,
433 password: Password,
434}
435
436impl DatabaseUserCredentials {
437 /// Create a new instance of the database user credentials.
438 ///
439 /// # Example
440 ///
441 /// ```
442 /// use cot::auth::Password;
443 /// use cot::auth::db::DatabaseUserCredentials;
444 ///
445 /// let credentials =
446 /// DatabaseUserCredentials::new(String::from("testuser"), Password::new("password123"));
447 /// ```
448 #[must_use]
449 pub fn new(username: String, password: Password) -> Self {
450 Self { username, password }
451 }
452
453 /// Get the username of the user.
454 ///
455 /// # Example
456 ///
457 /// ```
458 /// use cot::auth::Password;
459 /// use cot::auth::db::DatabaseUserCredentials;
460 ///
461 /// let credentials =
462 /// DatabaseUserCredentials::new(String::from("testuser"), Password::new("password123"));
463 /// assert_eq!(credentials.username(), "testuser");
464 /// ```
465 #[must_use]
466 pub fn username(&self) -> &str {
467 &self.username
468 }
469
470 /// Get the password of the user.
471 ///
472 /// # Example
473 ///
474 /// ```
475 /// use cot::auth::Password;
476 /// use cot::auth::db::DatabaseUserCredentials;
477 ///
478 /// let credentials =
479 /// DatabaseUserCredentials::new(String::from("testuser"), Password::new("password123"));
480 /// assert!(!credentials.password().as_str().is_empty());
481 /// ```
482 #[must_use]
483 pub fn password(&self) -> &Password {
484 &self.password
485 }
486}
487
488/// The authentication backend for users stored in the database.
489///
490/// This is the default authentication backend for Cot. It authenticates
491/// users stored in the database using the [`DatabaseUser`] model.
492///
493/// This backend supports authenticating users using the
494/// [`DatabaseUserCredentials`] struct and ignores all other credential types.
495#[derive(Debug, Clone)]
496pub struct DatabaseUserBackend {
497 database: Arc<Database>,
498}
499
500impl DatabaseUserBackend {
501 /// Create a new instance of the database user authentication backend.
502 ///
503 /// # Example
504 ///
505 /// ```
506 /// use std::sync::Arc;
507 ///
508 /// use cot::auth::AuthBackend;
509 /// use cot::auth::db::DatabaseUserBackend;
510 /// use cot::config::ProjectConfig;
511 /// use cot::project::{AuthBackendContext, WithApps};
512 /// use cot::{Project, ProjectContext};
513 ///
514 /// struct HelloProject;
515 /// impl Project for HelloProject {
516 /// fn auth_backend(&self, context: &AuthBackendContext) -> Arc<dyn AuthBackend> {
517 /// Arc::new(DatabaseUserBackend::new(context.database().clone()))
518 /// // note that it's usually better to just set the auth backend in the config
519 /// }
520 /// }
521 /// ```
522 #[must_use]
523 pub fn new(database: Arc<Database>) -> Self {
524 Self { database }
525 }
526}
527
528#[async_trait]
529impl AuthBackend for DatabaseUserBackend {
530 async fn authenticate(
531 &self,
532 credentials: &(dyn Any + Send + Sync),
533 ) -> Result<Option<Box<dyn User + Send + Sync>>> {
534 if let Some(credentials) = credentials.downcast_ref::<DatabaseUserCredentials>() {
535 #[expect(trivial_casts)] // Upcast to the correct Box type
536 Ok(DatabaseUser::authenticate(&self.database, credentials)
537 .await
538 .map(|user| user.map(|user| Box::new(user) as Box<dyn User + Send + Sync>))?)
539 } else {
540 Err(AuthError::CredentialsTypeNotSupported)
541 }
542 }
543
544 async fn get_by_id(&self, id: UserId) -> Result<Option<Box<dyn User + Send + Sync>>> {
545 let UserId::Int(id) = id else {
546 return Err(AuthError::UserIdTypeNotSupported);
547 };
548
549 #[expect(trivial_casts)] // Upcast to the correct Box type
550 Ok(DatabaseUser::get_by_id(&self.database, id)
551 .await?
552 .map(|user| Box::new(user) as Box<dyn User + Send + Sync>))
553 }
554}
555
556/// An app that provides authentication via a user model stored in the database.
557#[derive(Debug, Copy, Clone)]
558pub struct DatabaseUserApp;
559
560impl Default for DatabaseUserApp {
561 fn default() -> Self {
562 Self::new()
563 }
564}
565
566impl DatabaseUserApp {
567 /// Create a new instance of the database user authentication app.
568 ///
569 /// # Example
570 ///
571 /// ```no_run
572 /// use cot::auth::db::DatabaseUserApp;
573 /// use cot::config::{DatabaseConfig, ProjectConfig};
574 /// use cot::project::RegisterAppsContext;
575 /// use cot::{App, AppBuilder, Project};
576 ///
577 /// struct HelloProject;
578 /// impl Project for HelloProject {
579 /// fn config(&self, config_name: &str) -> cot::Result<ProjectConfig> {
580 /// Ok(ProjectConfig::builder()
581 /// .database(DatabaseConfig::builder().url("sqlite::memory:").build())
582 /// .build())
583 /// }
584 ///
585 /// fn register_apps(&self, apps: &mut AppBuilder, _context: &RegisterAppsContext) {
586 /// use cot::project::{RegisterAppsContext, WithConfig};
587 /// apps.register_with_views(DatabaseUserApp::new(), "");
588 /// }
589 /// }
590 ///
591 /// #[cot::main]
592 /// fn main() -> impl Project {
593 /// HelloProject
594 /// }
595 /// ```
596 #[must_use]
597 pub fn new() -> Self {
598 Self {}
599 }
600}
601
602impl App for DatabaseUserApp {
603 fn name(&self) -> &'static str {
604 "cot_db_user"
605 }
606
607 fn admin_model_managers(&self) -> Vec<Box<dyn AdminModelManager>> {
608 vec![Box::new(DefaultAdminModelManager::<DatabaseUser>::new())]
609 }
610
611 fn migrations(&self) -> Vec<Box<SyncDynMigration>> {
612 cot::db::migrations::wrap_migrations(migrations::MIGRATIONS)
613 }
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619 use crate::config::SecretKey;
620 use crate::db::MockDatabaseBackend;
621
622 #[test]
623 #[cfg_attr(miri, ignore)]
624 fn session_auth_hash() {
625 let user = DatabaseUser::new(
626 Auto::fixed(1),
627 LimitedString::new("testuser").unwrap(),
628 &Password::new("password123"),
629 );
630 let secret_key = SecretKey::new(b"supersecretkey");
631
632 let hash = user.session_auth_hash(&secret_key);
633 assert!(hash.is_some());
634 }
635
636 #[test]
637 #[cfg_attr(miri, ignore)]
638 fn database_user_traits() {
639 let user = DatabaseUser::new(
640 Auto::fixed(1),
641 LimitedString::new("testuser").unwrap(),
642 &Password::new("password123"),
643 );
644 let user_ref: &dyn User = &user;
645 assert_eq!(user_ref.id(), Some(UserId::Int(1)));
646 assert_eq!(user_ref.username(), Some(Cow::from("testuser")));
647 assert!(user_ref.is_active());
648 assert!(user_ref.is_authenticated());
649 assert!(
650 user_ref
651 .session_auth_hash(&SecretKey::new(b"supersecretkey"))
652 .is_some()
653 );
654 }
655
656 #[cot::test]
657 #[cfg_attr(miri, ignore)]
658 async fn create_user() {
659 let mut mock_db = MockDatabaseBackend::new();
660 mock_db
661 .expect_insert::<DatabaseUser>()
662 .returning(|_| Ok(()));
663
664 let username = "testuser".to_string();
665 let password = Password::new("password123");
666
667 let user = DatabaseUser::create_user(&mock_db, username.clone(), &password)
668 .await
669 .unwrap();
670 assert_eq!(user.username(), username);
671 }
672
673 #[cot::test]
674 #[cfg_attr(miri, ignore)]
675 async fn get_by_id() {
676 let mut mock_db = MockDatabaseBackend::new();
677 let user = DatabaseUser::new(
678 Auto::fixed(1),
679 LimitedString::new("testuser").unwrap(),
680 &Password::new("password123"),
681 );
682
683 mock_db
684 .expect_get::<DatabaseUser>()
685 .returning(move |_| Ok(Some(user.clone())));
686
687 let result = DatabaseUser::get_by_id(&mock_db, 1).await.unwrap();
688 assert!(result.is_some());
689 assert_eq!(result.unwrap().username(), "testuser");
690 }
691
692 #[cot::test]
693 #[cfg_attr(miri, ignore)]
694 async fn authenticate() {
695 let mut mock_db = MockDatabaseBackend::new();
696 let user = DatabaseUser::new(
697 Auto::fixed(1),
698 LimitedString::new("testuser").unwrap(),
699 &Password::new("password123"),
700 );
701
702 mock_db
703 .expect_get::<DatabaseUser>()
704 .returning(move |_| Ok(Some(user.clone())));
705
706 let credentials =
707 DatabaseUserCredentials::new("testuser".to_string(), Password::new("password123"));
708 let result = DatabaseUser::authenticate(&mock_db, &credentials)
709 .await
710 .unwrap();
711 assert!(result.is_some());
712 assert_eq!(result.unwrap().username(), "testuser");
713 }
714
715 #[cot::test]
716 #[cfg_attr(miri, ignore)]
717 async fn authenticate_non_existing() {
718 let mut mock_db = MockDatabaseBackend::new();
719
720 mock_db
721 .expect_get::<DatabaseUser>()
722 .returning(move |_| Ok(None));
723
724 let credentials =
725 DatabaseUserCredentials::new("testuser".to_string(), Password::new("password123"));
726 let result = DatabaseUser::authenticate(&mock_db, &credentials)
727 .await
728 .unwrap();
729 assert!(result.is_none());
730 }
731
732 #[cot::test]
733 #[cfg_attr(miri, ignore)]
734 async fn authenticate_invalid_password() {
735 let mut mock_db = MockDatabaseBackend::new();
736 let user = DatabaseUser::new(
737 Auto::fixed(1),
738 LimitedString::new("testuser").unwrap(),
739 &Password::new("password123"),
740 );
741
742 mock_db
743 .expect_get::<DatabaseUser>()
744 .returning(move |_| Ok(Some(user.clone())));
745
746 let credentials =
747 DatabaseUserCredentials::new("testuser".to_string(), Password::new("invalid"));
748 let result = DatabaseUser::authenticate(&mock_db, &credentials)
749 .await
750 .unwrap();
751 assert!(result.is_none());
752 }
753}