1use base64ct::{Base64UrlUnpadded, Encoding};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::db::Db;
7use crate::error::AuthError;
8use crate::event_sink::AuthEvent;
9use crate::handle::AllowThem;
10use crate::password::hash_password;
11use crate::types::{Email, User, UserId, Username};
12
13pub(crate) fn map_unique_violation(err: sqlx::Error) -> AuthError {
18 if let sqlx::Error::Database(ref db_err) = err {
19 let msg = db_err.message();
20 if msg.contains("UNIQUE constraint failed") {
21 if msg.contains("email") {
22 return AuthError::Conflict("email already exists".into());
23 }
24 if msg.contains("username") {
25 return AuthError::Conflict("username already exists".into());
26 }
27 return AuthError::Conflict(msg.to_string());
28 }
29 }
30 AuthError::Database(err)
31}
32
33pub struct SearchUsersParams<'a> {
35 pub query: Option<&'a str>,
36 pub is_active: Option<bool>,
37 pub has_mfa: Option<bool>,
38 pub email_verified: Option<bool>,
42 pub limit: u32,
43 pub offset: u32,
44}
45
46#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
48pub struct UserListEntry {
49 pub id: UserId,
50 pub email: Email,
51 pub username: Option<Username>,
52 pub is_active: bool,
53 pub has_mfa: bool,
54 pub created_at: DateTime<Utc>,
55}
56
57pub struct SearchUsersResult {
59 pub users: Vec<UserListEntry>,
60 pub total: u32,
61}
62
63pub struct UserCursor {
67 pub created_at: DateTime<Utc>,
68 pub id: UserId,
69}
70
71#[derive(Serialize, Deserialize)]
72struct RawUserCursor {
73 ca: String,
74 id: String,
75}
76
77impl UserCursor {
78 pub fn from_entry(entry: &UserListEntry) -> Self {
79 Self {
80 created_at: entry.created_at,
81 id: entry.id,
82 }
83 }
84
85 pub fn encode(&self) -> String {
86 let raw = RawUserCursor {
87 ca: self.created_at.to_rfc3339(),
88 id: self.id.to_string(),
89 };
90 let json = serde_json::to_string(&raw).expect("RawUserCursor serializes");
91 Base64UrlUnpadded::encode_string(json.as_bytes())
92 }
93
94 pub fn decode(s: &str) -> Option<Self> {
95 let bytes = Base64UrlUnpadded::decode_vec(s).ok()?;
96 let raw: RawUserCursor = serde_json::from_slice(&bytes).ok()?;
97 let created_at = chrono::DateTime::parse_from_rfc3339(&raw.ca)
98 .ok()?
99 .with_timezone(&Utc);
100 let id = raw.id.parse::<uuid::Uuid>().ok().map(UserId::from_uuid)?;
101 Some(Self { created_at, id })
102 }
103}
104
105impl Db {
106 pub async fn count_users(&self) -> Result<u64, AuthError> {
109 let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM allowthem_users")
110 .fetch_one(self.pool())
111 .await
112 .map_err(AuthError::Database)?;
113 Ok(n as u64)
114 }
115
116 pub async fn create_user(
121 &self,
122 email: Email,
123 password: &str,
124 username: Option<Username>,
125 custom_data: Option<&Value>,
126 ) -> Result<User, AuthError> {
127 let id = UserId::new();
128 let pw_hash = hash_password(password)?;
129 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
130
131 sqlx::query(
132 "INSERT INTO allowthem_users \
133 (id, email, username, password_hash, email_verified, is_active, created_at, updated_at, custom_data) \
134 VALUES (?1, ?2, ?3, ?4, 0, 1, ?5, ?5, ?6)",
135 )
136 .bind(id)
137 .bind(&email)
138 .bind(&username)
139 .bind(&pw_hash)
140 .bind(&now)
141 .bind(custom_data.map(sqlx::types::Json))
142 .execute(self.pool())
143 .await
144 .map_err(map_unique_violation)?;
145
146 self.get_user(id).await
147 }
148
149 pub async fn create_user_with_hash(
152 &self,
153 email: Email,
154 password_hash: &str,
155 username: Option<Username>,
156 custom_data: Option<&Value>,
157 ) -> Result<User, AuthError> {
158 let id = UserId::new();
159 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
160
161 sqlx::query(
162 "INSERT INTO allowthem_users (id, email, username, password_hash, email_verified, is_active, created_at, updated_at, custom_data)
163 VALUES (?1, ?2, ?3, ?4, 0, 1, ?5, ?5, ?6)",
164 )
165 .bind(id)
166 .bind(&email)
167 .bind(&username)
168 .bind(password_hash)
169 .bind(&now)
170 .bind(custom_data.map(sqlx::types::Json))
171 .execute(self.pool())
172 .await
173 .map_err(map_unique_violation)?;
174
175 self.get_user(id).await
176 }
177
178 pub async fn get_user(&self, id: UserId) -> Result<User, AuthError> {
180 sqlx::query_as::<_, User>(
181 "SELECT id, email, username, NULL as password_hash, \
182 email_verified, is_active, created_at, updated_at, custom_data \
183 FROM allowthem_users WHERE id = ?",
184 )
185 .bind(id)
186 .fetch_optional(self.pool())
187 .await?
188 .ok_or(AuthError::NotFound)
189 }
190
191 pub async fn get_user_by_email(&self, email: &Email) -> Result<User, AuthError> {
193 sqlx::query_as::<_, User>(
194 "SELECT id, email, username, NULL as password_hash, \
195 email_verified, is_active, created_at, updated_at, custom_data \
196 FROM allowthem_users WHERE email = ?",
197 )
198 .bind(email)
199 .fetch_optional(self.pool())
200 .await?
201 .ok_or(AuthError::NotFound)
202 }
203
204 pub async fn get_user_by_username(&self, username: &Username) -> Result<User, AuthError> {
206 sqlx::query_as::<_, User>(
207 "SELECT id, email, username, NULL as password_hash, \
208 email_verified, is_active, created_at, updated_at, custom_data \
209 FROM allowthem_users WHERE username = ?",
210 )
211 .bind(username)
212 .fetch_optional(self.pool())
213 .await?
214 .ok_or(AuthError::NotFound)
215 }
216
217 pub async fn find_for_login(&self, identifier: &str) -> Result<User, AuthError> {
222 sqlx::query_as::<_, User>(
223 "SELECT id, email, username, password_hash, \
224 email_verified, is_active, created_at, updated_at, custom_data \
225 FROM allowthem_users WHERE email = ?1 OR username = ?1",
226 )
227 .bind(identifier)
228 .fetch_optional(self.pool())
229 .await?
230 .ok_or(AuthError::NotFound)
231 }
232
233 pub async fn update_user_email(&self, id: UserId, email: Email) -> Result<(), AuthError> {
235 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
236 let result =
237 sqlx::query("UPDATE allowthem_users SET email = ?1, updated_at = ?2 WHERE id = ?3")
238 .bind(&email)
239 .bind(&now)
240 .bind(id)
241 .execute(self.pool())
242 .await
243 .map_err(map_unique_violation)?;
244
245 if result.rows_affected() == 0 {
246 return Err(AuthError::NotFound);
247 }
248 Ok(())
249 }
250
251 pub async fn update_user_username(
253 &self,
254 id: UserId,
255 username: Option<Username>,
256 ) -> Result<(), AuthError> {
257 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
258 let result =
259 sqlx::query("UPDATE allowthem_users SET username = ?1, updated_at = ?2 WHERE id = ?3")
260 .bind(&username)
261 .bind(&now)
262 .bind(id)
263 .execute(self.pool())
264 .await
265 .map_err(map_unique_violation)?;
266
267 if result.rows_affected() == 0 {
268 return Err(AuthError::NotFound);
269 }
270 Ok(())
271 }
272
273 pub async fn update_user_active(&self, id: UserId, is_active: bool) -> Result<(), AuthError> {
275 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
276 let result =
277 sqlx::query("UPDATE allowthem_users SET is_active = ?1, updated_at = ?2 WHERE id = ?3")
278 .bind(is_active)
279 .bind(&now)
280 .bind(id)
281 .execute(self.pool())
282 .await?;
283
284 if result.rows_affected() == 0 {
285 return Err(AuthError::NotFound);
286 }
287 Ok(())
288 }
289
290 pub async fn set_email_verified(&self, id: UserId, verified: bool) -> Result<(), AuthError> {
298 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
299 let result = sqlx::query(
300 "UPDATE allowthem_users SET email_verified = ?1, updated_at = ?2 WHERE id = ?3",
301 )
302 .bind(verified)
303 .bind(&now)
304 .bind(id)
305 .execute(self.pool())
306 .await?;
307
308 if result.rows_affected() == 0 {
309 return Err(AuthError::NotFound);
310 }
311 Ok(())
312 }
313
314 pub async fn delete_user(&self, id: UserId) -> Result<(), AuthError> {
316 let result = sqlx::query("DELETE FROM allowthem_users WHERE id = ?")
317 .bind(id)
318 .execute(self.pool())
319 .await?;
320
321 if result.rows_affected() == 0 {
322 return Err(AuthError::NotFound);
323 }
324 Ok(())
325 }
326
327 pub async fn list_users(&self) -> Result<Vec<User>, AuthError> {
329 sqlx::query_as::<_, User>(
330 "SELECT id, email, username, NULL as password_hash, \
331 email_verified, is_active, created_at, updated_at, custom_data \
332 FROM allowthem_users ORDER BY created_at ASC",
333 )
334 .fetch_all(self.pool())
335 .await
336 .map_err(AuthError::Database)
337 }
338
339 pub async fn list_users_paginated(
344 &self,
345 limit: u32,
346 cursor: Option<&UserCursor>,
347 ) -> Result<Vec<UserListEntry>, AuthError> {
348 let limit = (limit as i64).min(200);
349 match cursor {
350 None => sqlx::query_as::<_, UserListEntry>(
351 "SELECT u.id, u.email, u.username, u.is_active, \
352 EXISTS (SELECT 1 FROM allowthem_mfa_secrets \
353 WHERE user_id = u.id AND enabled = 1) AS has_mfa, \
354 u.created_at \
355 FROM allowthem_users u \
356 ORDER BY u.created_at ASC, u.id ASC \
357 LIMIT ?",
358 )
359 .bind(limit)
360 .fetch_all(self.pool())
361 .await
362 .map_err(AuthError::Database),
363 Some(c) => {
364 let ca = c.created_at.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
365 sqlx::query_as::<_, UserListEntry>(
366 "SELECT u.id, u.email, u.username, u.is_active, \
367 EXISTS (SELECT 1 FROM allowthem_mfa_secrets \
368 WHERE user_id = u.id AND enabled = 1) AS has_mfa, \
369 u.created_at \
370 FROM allowthem_users u \
371 WHERE (u.created_at > ?1 OR (u.created_at = ?1 AND u.id > ?2)) \
372 ORDER BY u.created_at ASC, u.id ASC \
373 LIMIT ?3",
374 )
375 .bind(&ca)
376 .bind(c.id)
377 .bind(limit)
378 .fetch_all(self.pool())
379 .await
380 .map_err(AuthError::Database)
381 }
382 }
383 }
384
385 pub async fn search_users(
391 &self,
392 params: SearchUsersParams<'_>,
393 ) -> Result<SearchUsersResult, AuthError> {
394 let mut where_clauses: Vec<String> = Vec::new();
395 let mut bind_values: Vec<String> = Vec::new();
396
397 if let Some(q) = params.query {
398 let trimmed = q.trim();
399 if !trimmed.is_empty() {
400 let escaped = trimmed
401 .replace('\\', "\\\\")
402 .replace('%', "\\%")
403 .replace('_', "\\_");
404 let pattern = format!("%{escaped}%");
405 where_clauses
406 .push("(u.email LIKE ? ESCAPE '\\' OR u.username LIKE ? ESCAPE '\\')".into());
407 bind_values.push(pattern.clone());
408 bind_values.push(pattern);
409 }
410 }
411
412 if let Some(active) = params.is_active {
413 where_clauses.push("u.is_active = ?".into());
414 bind_values.push(if active { "1".into() } else { "0".into() });
415 }
416
417 if let Some(has_mfa) = params.has_mfa {
418 let exists = if has_mfa { "EXISTS" } else { "NOT EXISTS" };
419 where_clauses.push(format!(
420 "{exists} (SELECT 1 FROM allowthem_mfa_secrets WHERE user_id = u.id AND enabled = 1)"
421 ));
422 }
423
424 if let Some(verified) = params.email_verified {
425 where_clauses.push("u.email_verified = ?".into());
426 bind_values.push(if verified { "1".into() } else { "0".into() });
427 }
428
429 let where_sql = if where_clauses.is_empty() {
430 String::new()
431 } else {
432 format!("WHERE {}", where_clauses.join(" AND "))
433 };
434
435 let count_sql: &'static str = Box::leak(
436 format!("SELECT COUNT(*) FROM allowthem_users u {where_sql}").into_boxed_str(),
437 );
438 let mut count_query = sqlx::query_scalar::<_, i64>(count_sql);
439 for val in &bind_values {
440 count_query = count_query.bind(val);
441 }
442 let total = count_query
443 .fetch_one(self.pool())
444 .await
445 .map_err(AuthError::Database)? as u32;
446
447 let data_sql: &'static str = Box::leak(
448 format!(
449 "SELECT u.id, u.email, u.username, u.is_active, \
450 EXISTS (SELECT 1 FROM allowthem_mfa_secrets \
451 WHERE user_id = u.id AND enabled = 1) as has_mfa, \
452 u.created_at \
453 FROM allowthem_users u {where_sql} \
454 ORDER BY u.created_at ASC \
455 LIMIT ? OFFSET ?"
456 )
457 .into_boxed_str(),
458 );
459 let mut data_query = sqlx::query_as::<_, UserListEntry>(data_sql);
460 for val in &bind_values {
461 data_query = data_query.bind(val);
462 }
463 data_query = data_query.bind(params.limit).bind(params.offset);
464
465 let users = data_query
466 .fetch_all(self.pool())
467 .await
468 .map_err(AuthError::Database)?;
469
470 Ok(SearchUsersResult { users, total })
471 }
472
473 pub async fn update_user_password(
477 &self,
478 id: UserId,
479 new_password: &str,
480 ) -> Result<(), AuthError> {
481 let pw_hash = hash_password(new_password)?;
482 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
483 let result = sqlx::query(
484 "UPDATE allowthem_users SET password_hash = ?1, updated_at = ?2 WHERE id = ?3",
485 )
486 .bind(&pw_hash)
487 .bind(&now)
488 .bind(id)
489 .execute(self.pool())
490 .await?;
491
492 if result.rows_affected() == 0 {
493 return Err(AuthError::NotFound);
494 }
495 Ok(())
496 }
497
498 pub async fn clear_password_hash(&self, id: UserId) -> Result<(), AuthError> {
504 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
505 let result = sqlx::query(
506 "UPDATE allowthem_users SET password_hash = NULL, updated_at = ? WHERE id = ?",
507 )
508 .bind(&now)
509 .bind(id)
510 .execute(self.pool())
511 .await?;
512
513 if result.rows_affected() == 0 {
514 return Err(AuthError::NotFound);
515 }
516 Ok(())
517 }
518
519 pub async fn get_custom_data(&self, id: &UserId) -> Result<Option<Value>, AuthError> {
524 let row: Option<(Option<Value>,)> =
525 sqlx::query_as("SELECT custom_data FROM allowthem_users WHERE id = ?")
526 .bind(id)
527 .fetch_optional(self.pool())
528 .await?;
529
530 match row {
531 None => Err(AuthError::NotFound),
532 Some((data,)) => Ok(data),
533 }
534 }
535
536 pub async fn set_custom_data(&self, id: &UserId, data: &Value) -> Result<(), AuthError> {
540 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
541 let result = sqlx::query(
542 "UPDATE allowthem_users SET custom_data = ?1, updated_at = ?2 WHERE id = ?3",
543 )
544 .bind(sqlx::types::Json(data))
545 .bind(&now)
546 .bind(id)
547 .execute(self.pool())
548 .await?;
549
550 if result.rows_affected() == 0 {
551 return Err(AuthError::NotFound);
552 }
553 Ok(())
554 }
555
556 pub async fn delete_custom_data(&self, id: &UserId) -> Result<(), AuthError> {
560 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
561 sqlx::query("UPDATE allowthem_users SET custom_data = NULL, updated_at = ?1 WHERE id = ?2")
562 .bind(&now)
563 .bind(id)
564 .execute(self.pool())
565 .await?;
566
567 Ok(())
568 }
569}
570
571impl AllowThem {
574 pub async fn create_user(
576 &self,
577 email: Email,
578 password: &str,
579 username: Option<Username>,
580 custom_data: Option<&Value>,
581 ) -> Result<User, AuthError> {
582 let user = self
583 .db()
584 .create_user(email, password, username, custom_data)
585 .await?;
586 self.emit_event(AuthEvent::new(
587 "user.created",
588 Some(user.id),
589 serde_json::json!({
590 "user_id": user.id,
591 "email": user.email,
592 "username": user.username,
593 }),
594 ))
595 .await;
596 Ok(user)
597 }
598
599 pub async fn update_user_email(&self, id: UserId, email: Email) -> Result<(), AuthError> {
601 self.db().update_user_email(id, email).await?;
602 self.emit_event(AuthEvent::new(
603 "user.updated",
604 Some(id),
605 serde_json::json!({ "user_id": id, "field": "email" }),
606 ))
607 .await;
608 Ok(())
609 }
610
611 pub async fn update_user_username(
613 &self,
614 id: UserId,
615 username: Option<Username>,
616 ) -> Result<(), AuthError> {
617 self.db().update_user_username(id, username).await?;
618 self.emit_event(AuthEvent::new(
619 "user.updated",
620 Some(id),
621 serde_json::json!({ "user_id": id, "field": "username" }),
622 ))
623 .await;
624 Ok(())
625 }
626
627 pub async fn update_user_password(
629 &self,
630 id: UserId,
631 new_password: &str,
632 ) -> Result<(), AuthError> {
633 self.db().update_user_password(id, new_password).await?;
634 self.emit_event(AuthEvent::new(
635 "password.changed",
636 Some(id),
637 serde_json::json!({ "user_id": id }),
638 ))
639 .await;
640 Ok(())
641 }
642
643 pub async fn delete_user(&self, id: UserId) -> Result<(), AuthError> {
645 self.db().delete_user(id).await?;
646 self.emit_event(AuthEvent::new(
647 "user.deleted",
648 Some(id),
649 serde_json::json!({ "user_id": id }),
650 ))
651 .await;
652 Ok(())
653 }
654
655 pub async fn update_user_active(&self, id: UserId, is_active: bool) -> Result<(), AuthError> {
657 self.db().update_user_active(id, is_active).await?;
658 let event_type = if is_active {
659 "user.unblocked"
660 } else {
661 "user.blocked"
662 };
663 self.emit_event(AuthEvent::new(
664 event_type,
665 Some(id),
666 serde_json::json!({ "user_id": id }),
667 ))
668 .await;
669 Ok(())
670 }
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676 use crate::handle::{AllowThem, AllowThemBuilder};
677
678 async fn setup() -> AllowThem {
679 AllowThemBuilder::new("sqlite::memory:")
680 .cookie_secure(false)
681 .build()
682 .await
683 .unwrap()
684 }
685
686 async fn make_user(db: &Db, tag: u32) -> crate::types::User {
687 let email = Email::new(format!("user{tag}@example.com")).unwrap();
688 db.create_user(email, "pw123456", None, None).await.unwrap()
689 }
690
691 #[tokio::test]
692 async fn user_cursor_encode_decode_roundtrip() {
693 let ath = setup().await;
694 let db = ath.db();
695 let user = make_user(db, 1).await;
696 let entries = db.list_users_paginated(10, None).await.unwrap();
697 assert_eq!(entries.len(), 1);
698 let cursor = UserCursor::from_entry(&entries[0]);
699 let encoded = cursor.encode();
700 let decoded = UserCursor::decode(&encoded).unwrap();
701 assert_eq!(decoded.id, user.id);
702 }
703
704 #[tokio::test]
705 async fn list_users_paginated_returns_first_page() {
706 let ath = setup().await;
707 let db = ath.db();
708 for i in 0..5 {
709 make_user(db, i).await;
710 }
711 let page = db.list_users_paginated(3, None).await.unwrap();
712 assert_eq!(page.len(), 3);
713 }
714
715 #[tokio::test]
716 async fn set_email_verified_toggles_flag() {
717 let ath = setup().await;
718 let db = ath.db();
719 let user = make_user(db, 99).await;
720 assert!(!user.email_verified);
721
722 db.set_email_verified(user.id, true).await.unwrap();
723 let after = db.get_user(user.id).await.unwrap();
724 assert!(after.email_verified);
725
726 db.set_email_verified(user.id, false).await.unwrap();
727 let after = db.get_user(user.id).await.unwrap();
728 assert!(!after.email_verified);
729 }
730
731 #[tokio::test]
732 async fn set_email_verified_unknown_id_returns_not_found() {
733 let ath = setup().await;
734 let db = ath.db();
735 let err = db
736 .set_email_verified(UserId::new(), true)
737 .await
738 .unwrap_err();
739 assert!(matches!(err, AuthError::NotFound));
740 }
741
742 #[tokio::test]
743 async fn list_users_paginated_cursor_advances() {
744 let ath = setup().await;
745 let db = ath.db();
746 for i in 0..5 {
747 make_user(db, i + 10).await;
748 }
749 let page1 = db.list_users_paginated(3, None).await.unwrap();
750 assert_eq!(page1.len(), 3);
751 let cursor = UserCursor::from_entry(page1.last().unwrap());
752 let page2 = db.list_users_paginated(3, Some(&cursor)).await.unwrap();
753 assert_eq!(page2.len(), 2);
754 assert!(!page2.iter().any(|u| page1.iter().any(|v| v.id == u.id)));
755 }
756
757 fn unfiltered(limit: u32) -> SearchUsersParams<'static> {
758 SearchUsersParams {
759 query: None,
760 is_active: None,
761 has_mfa: None,
762 email_verified: None,
763 limit,
764 offset: 0,
765 }
766 }
767
768 #[tokio::test]
769 async fn search_users_filter_email_verified_true() {
770 let ath = setup().await;
771 let db = ath.db();
772 let u1 = make_user(db, 1).await;
773 let _u2 = make_user(db, 2).await;
774 db.set_email_verified(u1.id, true).await.unwrap();
775
776 let result = db
777 .search_users(SearchUsersParams {
778 email_verified: Some(true),
779 ..unfiltered(10)
780 })
781 .await
782 .unwrap();
783 assert_eq!(result.total, 1);
784 assert_eq!(result.users.len(), 1);
785 assert_eq!(result.users[0].id, u1.id);
786 }
787
788 #[tokio::test]
789 async fn search_users_filter_email_verified_false() {
790 let ath = setup().await;
791 let db = ath.db();
792 let u1 = make_user(db, 1).await;
793 let u2 = make_user(db, 2).await;
794 db.set_email_verified(u1.id, true).await.unwrap();
795
796 let result = db
797 .search_users(SearchUsersParams {
798 email_verified: Some(false),
799 ..unfiltered(10)
800 })
801 .await
802 .unwrap();
803 assert_eq!(result.total, 1);
804 assert_eq!(result.users[0].id, u2.id);
805 }
806
807 #[tokio::test]
808 async fn search_users_filter_email_verified_none_includes_both() {
809 let ath = setup().await;
810 let db = ath.db();
811 let u1 = make_user(db, 1).await;
812 let _u2 = make_user(db, 2).await;
813 db.set_email_verified(u1.id, true).await.unwrap();
814
815 let result = db.search_users(unfiltered(10)).await.unwrap();
816 assert_eq!(result.total, 2);
817 assert_eq!(result.users.len(), 2);
818 }
819
820 #[tokio::test]
823 async fn count_users_zero_on_empty_db() {
824 let ath = setup().await;
825 let n = ath.db().count_users().await.expect("count_users");
826 assert_eq!(n, 0);
827 }
828
829 #[tokio::test]
830 async fn count_users_after_create() {
831 let ath = setup().await;
832 let db = ath.db();
833 make_user(db, 1).await;
834 make_user(db, 2).await;
835 make_user(db, 3).await;
836 let n = db.count_users().await.expect("count_users");
837 assert_eq!(n, 3);
838 }
839}