1use async_trait::async_trait;
8use chrono::{DateTime, Utc};
9use mas_data_model::{Clock, UlidExt as _, UserRegistrationToken};
10use mas_storage::{
11 Page, Pagination,
12 pagination::Node,
13 user::{UserRegistrationTokenFilter, UserRegistrationTokenRepository},
14};
15use rand::Rng;
16use sea_query::{Condition, Expr, ExprTrait, PostgresQueryBuilder, Query, enum_def};
17use sea_query_sqlx::SqlxBinder;
18use sqlx::PgConnection;
19use ulid::Ulid;
20use uuid::Uuid;
21
22use crate::{
23 DatabaseInconsistencyError,
24 errors::DatabaseError,
25 filter::{Filter, StatementExt},
26 iden::UserRegistrationTokens,
27 pagination::QueryBuilderExt,
28 tracing::ExecuteExt,
29};
30
31pub struct PgUserRegistrationTokenRepository<'c> {
34 conn: &'c mut PgConnection,
35}
36
37impl<'c> PgUserRegistrationTokenRepository<'c> {
38 pub fn new(conn: &'c mut PgConnection) -> Self {
41 Self { conn }
42 }
43}
44
45#[derive(Debug, Clone, sqlx::FromRow)]
46#[enum_def]
47struct UserRegistrationTokenLookup {
48 user_registration_token_id: Uuid,
49 token: String,
50 username: Option<String>,
51 email: Option<String>,
52 passwordless: bool,
53 usage_limit: Option<i32>,
54 times_used: i32,
55 created_at: DateTime<Utc>,
56 last_used_at: Option<DateTime<Utc>>,
57 expires_at: Option<DateTime<Utc>>,
58 revoked_at: Option<DateTime<Utc>>,
59}
60
61impl Node<Ulid> for UserRegistrationTokenLookup {
62 fn cursor(&self) -> Ulid {
63 self.user_registration_token_id.into()
64 }
65}
66
67impl Filter for UserRegistrationTokenFilter {
68 fn generate_condition(&self, _has_joins: bool) -> impl sea_query::IntoCondition {
69 sea_query::Condition::all()
70 .add_option(self.has_been_used().map(|has_been_used| {
71 if has_been_used {
72 Expr::col((
73 UserRegistrationTokens::Table,
74 UserRegistrationTokens::TimesUsed,
75 ))
76 .gt(0)
77 } else {
78 Expr::col((
79 UserRegistrationTokens::Table,
80 UserRegistrationTokens::TimesUsed,
81 ))
82 .eq(0)
83 }
84 }))
85 .add_option(self.is_revoked().map(|is_revoked| {
86 if is_revoked {
87 Expr::col((
88 UserRegistrationTokens::Table,
89 UserRegistrationTokens::RevokedAt,
90 ))
91 .is_not_null()
92 } else {
93 Expr::col((
94 UserRegistrationTokens::Table,
95 UserRegistrationTokens::RevokedAt,
96 ))
97 .is_null()
98 }
99 }))
100 .add_option(self.is_expired().map(|is_expired| {
101 if is_expired {
102 Condition::all()
103 .add(
104 Expr::col((
105 UserRegistrationTokens::Table,
106 UserRegistrationTokens::ExpiresAt,
107 ))
108 .is_not_null(),
109 )
110 .add(
111 Expr::col((
112 UserRegistrationTokens::Table,
113 UserRegistrationTokens::ExpiresAt,
114 ))
115 .lt(Expr::val(self.now())),
116 )
117 } else {
118 Condition::any()
119 .add(
120 Expr::col((
121 UserRegistrationTokens::Table,
122 UserRegistrationTokens::ExpiresAt,
123 ))
124 .is_null(),
125 )
126 .add(
127 Expr::col((
128 UserRegistrationTokens::Table,
129 UserRegistrationTokens::ExpiresAt,
130 ))
131 .gte(Expr::val(self.now())),
132 )
133 }
134 }))
135 .add_option(self.is_valid().map(|is_valid| {
136 let valid = Condition::all()
137 .add(
139 Condition::any()
140 .add(
141 Expr::col((
142 UserRegistrationTokens::Table,
143 UserRegistrationTokens::UsageLimit,
144 ))
145 .is_null(),
146 )
147 .add(
148 Expr::col((
149 UserRegistrationTokens::Table,
150 UserRegistrationTokens::TimesUsed,
151 ))
152 .lt(Expr::col((
153 UserRegistrationTokens::Table,
154 UserRegistrationTokens::UsageLimit,
155 ))),
156 ),
157 )
158 .add(
160 Expr::col((
161 UserRegistrationTokens::Table,
162 UserRegistrationTokens::RevokedAt,
163 ))
164 .is_null(),
165 )
166 .add(
168 Condition::any()
169 .add(
170 Expr::col((
171 UserRegistrationTokens::Table,
172 UserRegistrationTokens::ExpiresAt,
173 ))
174 .is_null(),
175 )
176 .add(
177 Expr::col((
178 UserRegistrationTokens::Table,
179 UserRegistrationTokens::ExpiresAt,
180 ))
181 .gte(Expr::val(self.now())),
182 ),
183 );
184
185 if is_valid { valid } else { valid.not() }
186 }))
187 }
188}
189
190impl TryFrom<UserRegistrationTokenLookup> for UserRegistrationToken {
191 type Error = DatabaseInconsistencyError;
192
193 fn try_from(res: UserRegistrationTokenLookup) -> Result<Self, Self::Error> {
194 let id = Ulid::from(res.user_registration_token_id);
195
196 let usage_limit = res
197 .usage_limit
198 .map(u32::try_from)
199 .transpose()
200 .map_err(|e| {
201 DatabaseInconsistencyError::on("user_registration_tokens")
202 .column("usage_limit")
203 .row(id)
204 .source(e)
205 })?;
206
207 let times_used = res.times_used.try_into().map_err(|e| {
208 DatabaseInconsistencyError::on("user_registration_tokens")
209 .column("times_used")
210 .row(id)
211 .source(e)
212 })?;
213
214 Ok(UserRegistrationToken {
215 id,
216 token: res.token,
217 username: res.username,
218 email: res.email,
219 passwordless: res.passwordless,
220 usage_limit,
221 times_used,
222 created_at: res.created_at,
223 last_used_at: res.last_used_at,
224 expires_at: res.expires_at,
225 revoked_at: res.revoked_at,
226 })
227 }
228}
229
230#[async_trait]
231impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> {
232 type Error = DatabaseError;
233
234 #[tracing::instrument(
235 name = "db.user_registration_token.list",
236 skip_all,
237 fields(
238 db.query.text,
239 ),
240 err,
241 )]
242 async fn list(
243 &mut self,
244 filter: UserRegistrationTokenFilter,
245 pagination: Pagination,
246 ) -> Result<Page<UserRegistrationToken>, Self::Error> {
247 let (sql, arguments) = Query::select()
248 .expr_as(
249 Expr::col((
250 UserRegistrationTokens::Table,
251 UserRegistrationTokens::UserRegistrationTokenId,
252 )),
253 UserRegistrationTokenLookupIden::UserRegistrationTokenId,
254 )
255 .expr_as(
256 Expr::col((UserRegistrationTokens::Table, UserRegistrationTokens::Token)),
257 UserRegistrationTokenLookupIden::Token,
258 )
259 .expr_as(
260 Expr::col((
261 UserRegistrationTokens::Table,
262 UserRegistrationTokens::Username,
263 )),
264 UserRegistrationTokenLookupIden::Username,
265 )
266 .expr_as(
267 Expr::col((UserRegistrationTokens::Table, UserRegistrationTokens::Email)),
268 UserRegistrationTokenLookupIden::Email,
269 )
270 .expr_as(
271 Expr::col((
272 UserRegistrationTokens::Table,
273 UserRegistrationTokens::Passwordless,
274 )),
275 UserRegistrationTokenLookupIden::Passwordless,
276 )
277 .expr_as(
278 Expr::col((
279 UserRegistrationTokens::Table,
280 UserRegistrationTokens::UsageLimit,
281 )),
282 UserRegistrationTokenLookupIden::UsageLimit,
283 )
284 .expr_as(
285 Expr::col((
286 UserRegistrationTokens::Table,
287 UserRegistrationTokens::TimesUsed,
288 )),
289 UserRegistrationTokenLookupIden::TimesUsed,
290 )
291 .expr_as(
292 Expr::col((
293 UserRegistrationTokens::Table,
294 UserRegistrationTokens::CreatedAt,
295 )),
296 UserRegistrationTokenLookupIden::CreatedAt,
297 )
298 .expr_as(
299 Expr::col((
300 UserRegistrationTokens::Table,
301 UserRegistrationTokens::LastUsedAt,
302 )),
303 UserRegistrationTokenLookupIden::LastUsedAt,
304 )
305 .expr_as(
306 Expr::col((
307 UserRegistrationTokens::Table,
308 UserRegistrationTokens::ExpiresAt,
309 )),
310 UserRegistrationTokenLookupIden::ExpiresAt,
311 )
312 .expr_as(
313 Expr::col((
314 UserRegistrationTokens::Table,
315 UserRegistrationTokens::RevokedAt,
316 )),
317 UserRegistrationTokenLookupIden::RevokedAt,
318 )
319 .from(UserRegistrationTokens::Table)
320 .apply_filter(filter)
321 .generate_pagination(
322 (
323 UserRegistrationTokens::Table,
324 UserRegistrationTokens::UserRegistrationTokenId,
325 ),
326 pagination,
327 )
328 .build_sqlx(PostgresQueryBuilder);
329
330 let edges: Vec<UserRegistrationTokenLookup> = sqlx::query_as_with(&sql, arguments)
331 .traced()
332 .fetch_all(&mut *self.conn)
333 .await?;
334
335 let page = pagination
336 .process(edges)
337 .try_map(UserRegistrationToken::try_from)?;
338
339 Ok(page)
340 }
341
342 #[tracing::instrument(
343 name = "db.user_registration_token.count",
344 skip_all,
345 fields(
346 db.query.text,
347 user_registration_token.filter = ?filter,
348 ),
349 err,
350 )]
351 async fn count(&mut self, filter: UserRegistrationTokenFilter) -> Result<usize, Self::Error> {
352 let (sql, values) = Query::select()
353 .expr(
354 Expr::col((
355 UserRegistrationTokens::Table,
356 UserRegistrationTokens::UserRegistrationTokenId,
357 ))
358 .count(),
359 )
360 .from(UserRegistrationTokens::Table)
361 .apply_filter(filter)
362 .build_sqlx(PostgresQueryBuilder);
363
364 let count: i64 = sqlx::query_scalar_with(&sql, values)
365 .traced()
366 .fetch_one(&mut *self.conn)
367 .await?;
368
369 count
370 .try_into()
371 .map_err(DatabaseError::to_invalid_operation)
372 }
373
374 #[tracing::instrument(
375 name = "db.user_registration_token.lookup",
376 skip_all,
377 fields(
378 db.query.text,
379 user_registration_token.id = %id,
380 ),
381 err,
382 )]
383 async fn lookup(&mut self, id: Ulid) -> Result<Option<UserRegistrationToken>, Self::Error> {
384 let res = sqlx::query_as!(
385 UserRegistrationTokenLookup,
386 r#"
387 SELECT user_registration_token_id,
388 token,
389 username,
390 email,
391 passwordless,
392 usage_limit,
393 times_used,
394 created_at,
395 last_used_at,
396 expires_at,
397 revoked_at
398 FROM user_registration_tokens
399 WHERE user_registration_token_id = $1
400 "#,
401 Uuid::from(id)
402 )
403 .traced()
404 .fetch_optional(&mut *self.conn)
405 .await?;
406
407 let Some(res) = res else {
408 return Ok(None);
409 };
410
411 Ok(Some(res.try_into()?))
412 }
413
414 #[tracing::instrument(
415 name = "db.user_registration_token.find_by_token",
416 skip_all,
417 fields(
418 db.query.text,
419 token = %token,
420 ),
421 err,
422 )]
423 async fn find_by_token(
424 &mut self,
425 token: &str,
426 ) -> Result<Option<UserRegistrationToken>, Self::Error> {
427 let res = sqlx::query_as!(
428 UserRegistrationTokenLookup,
429 r#"
430 SELECT user_registration_token_id,
431 token,
432 username,
433 email,
434 passwordless,
435 usage_limit,
436 times_used,
437 created_at,
438 last_used_at,
439 expires_at,
440 revoked_at
441 FROM user_registration_tokens
442 WHERE token = $1
443 "#,
444 token
445 )
446 .traced()
447 .fetch_optional(&mut *self.conn)
448 .await?;
449
450 let Some(res) = res else {
451 return Ok(None);
452 };
453
454 Ok(Some(res.try_into()?))
455 }
456
457 #[tracing::instrument(
458 name = "db.user_registration_token.add",
459 skip_all,
460 fields(
461 db.query.text,
462 user_registration_token.token = %token,
463 ),
464 err,
465 )]
466 async fn add(
467 &mut self,
468 rng: &mut (dyn Rng + Send),
469 clock: &dyn mas_data_model::Clock,
470 token: String,
471 usage_limit: Option<u32>,
472 expires_at: Option<DateTime<Utc>>,
473 username: Option<String>,
474 email: Option<String>,
475 passwordless: bool,
476 ) -> Result<UserRegistrationToken, Self::Error> {
477 let created_at = clock.now();
478 let id = Ulid::from_datetime_with_rng(created_at, rng);
479
480 let usage_limit_i32 = usage_limit
481 .map(i32::try_from)
482 .transpose()
483 .map_err(DatabaseError::to_invalid_operation)?;
484
485 sqlx::query!(
486 r#"
487 INSERT INTO user_registration_tokens
488 (user_registration_token_id, token, usage_limit, created_at, expires_at, username, email, passwordless)
489 VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
490 "#,
491 Uuid::from(id),
492 &token,
493 usage_limit_i32,
494 created_at,
495 expires_at,
496 username.as_deref(),
497 email.as_deref(),
498 passwordless,
499 )
500 .traced()
501 .execute(&mut *self.conn)
502 .await?;
503
504 Ok(UserRegistrationToken {
505 id,
506 token,
507 username,
508 email,
509 passwordless,
510 usage_limit,
511 times_used: 0,
512 created_at,
513 last_used_at: None,
514 expires_at,
515 revoked_at: None,
516 })
517 }
518
519 #[tracing::instrument(
520 name = "db.user_registration_token.use_token",
521 skip_all,
522 fields(
523 db.query.text,
524 user_registration_token.id = %token.id,
525 ),
526 err,
527 )]
528 async fn use_token(
529 &mut self,
530 clock: &dyn Clock,
531 token: UserRegistrationToken,
532 ) -> Result<UserRegistrationToken, Self::Error> {
533 let now = clock.now();
534 let new_times_used = sqlx::query_scalar!(
535 r#"
536 UPDATE user_registration_tokens
537 SET times_used = times_used + 1,
538 last_used_at = $2
539 WHERE user_registration_token_id = $1 AND revoked_at IS NULL
540 RETURNING times_used
541 "#,
542 Uuid::from(token.id),
543 now,
544 )
545 .traced()
546 .fetch_one(&mut *self.conn)
547 .await?;
548
549 let new_times_used = new_times_used
550 .try_into()
551 .map_err(DatabaseError::to_invalid_operation)?;
552
553 Ok(UserRegistrationToken {
554 times_used: new_times_used,
555 last_used_at: Some(now),
556 ..token
557 })
558 }
559
560 #[tracing::instrument(
561 name = "db.user_registration_token.revoke",
562 skip_all,
563 fields(
564 db.query.text,
565 user_registration_token.id = %token.id,
566 ),
567 err,
568 )]
569 async fn revoke(
570 &mut self,
571 clock: &dyn Clock,
572 mut token: UserRegistrationToken,
573 ) -> Result<UserRegistrationToken, Self::Error> {
574 let revoked_at = clock.now();
575 let res = sqlx::query!(
576 r#"
577 UPDATE user_registration_tokens
578 SET revoked_at = $2
579 WHERE user_registration_token_id = $1
580 "#,
581 Uuid::from(token.id),
582 revoked_at,
583 )
584 .traced()
585 .execute(&mut *self.conn)
586 .await?;
587
588 DatabaseError::ensure_affected_rows(&res, 1)?;
589
590 token.revoked_at = Some(revoked_at);
591
592 Ok(token)
593 }
594
595 #[tracing::instrument(
596 name = "db.user_registration_token.unrevoke",
597 skip_all,
598 fields(
599 db.query.text,
600 user_registration_token.id = %token.id,
601 ),
602 err,
603 )]
604 async fn unrevoke(
605 &mut self,
606 mut token: UserRegistrationToken,
607 ) -> Result<UserRegistrationToken, Self::Error> {
608 let res = sqlx::query!(
609 r#"
610 UPDATE user_registration_tokens
611 SET revoked_at = NULL
612 WHERE user_registration_token_id = $1
613 "#,
614 Uuid::from(token.id),
615 )
616 .traced()
617 .execute(&mut *self.conn)
618 .await?;
619
620 DatabaseError::ensure_affected_rows(&res, 1)?;
621
622 token.revoked_at = None;
623
624 Ok(token)
625 }
626
627 #[tracing::instrument(
628 name = "db.user_registration_token.set_expiry",
629 skip_all,
630 fields(
631 db.query.text,
632 user_registration_token.id = %token.id,
633 ),
634 err,
635 )]
636 async fn set_expiry(
637 &mut self,
638 mut token: UserRegistrationToken,
639 expires_at: Option<DateTime<Utc>>,
640 ) -> Result<UserRegistrationToken, Self::Error> {
641 let res = sqlx::query!(
642 r#"
643 UPDATE user_registration_tokens
644 SET expires_at = $2
645 WHERE user_registration_token_id = $1
646 "#,
647 Uuid::from(token.id),
648 expires_at,
649 )
650 .traced()
651 .execute(&mut *self.conn)
652 .await?;
653
654 DatabaseError::ensure_affected_rows(&res, 1)?;
655
656 token.expires_at = expires_at;
657
658 Ok(token)
659 }
660
661 #[tracing::instrument(
662 name = "db.user_registration_token.set_usage_limit",
663 skip_all,
664 fields(
665 db.query.text,
666 user_registration_token.id = %token.id,
667 ),
668 err,
669 )]
670 async fn set_usage_limit(
671 &mut self,
672 mut token: UserRegistrationToken,
673 usage_limit: Option<u32>,
674 ) -> Result<UserRegistrationToken, Self::Error> {
675 let usage_limit_i32 = usage_limit
676 .map(i32::try_from)
677 .transpose()
678 .map_err(DatabaseError::to_invalid_operation)?;
679
680 let res = sqlx::query!(
681 r#"
682 UPDATE user_registration_tokens
683 SET usage_limit = $2
684 WHERE user_registration_token_id = $1
685 "#,
686 Uuid::from(token.id),
687 usage_limit_i32,
688 )
689 .traced()
690 .execute(&mut *self.conn)
691 .await?;
692
693 DatabaseError::ensure_affected_rows(&res, 1)?;
694
695 token.usage_limit = usage_limit;
696
697 Ok(token)
698 }
699
700 #[tracing::instrument(
701 name = "db.user_registration_token.set_username",
702 skip_all,
703 fields(
704 db.query.text,
705 user_registration_token.id = %token.id,
706 ),
707 err,
708 )]
709 async fn set_username(
710 &mut self,
711 mut token: UserRegistrationToken,
712 username: Option<String>,
713 ) -> Result<UserRegistrationToken, Self::Error> {
714 let res = sqlx::query!(
715 r#"
716 UPDATE user_registration_tokens
717 SET username = $2
718 WHERE user_registration_token_id = $1
719 "#,
720 Uuid::from(token.id),
721 username.as_deref(),
722 )
723 .traced()
724 .execute(&mut *self.conn)
725 .await?;
726
727 DatabaseError::ensure_affected_rows(&res, 1)?;
728
729 token.username = username;
730
731 Ok(token)
732 }
733
734 #[tracing::instrument(
735 name = "db.user_registration_token.set_email",
736 skip_all,
737 fields(
738 db.query.text,
739 user_registration_token.id = %token.id,
740 ),
741 err,
742 )]
743 async fn set_email(
744 &mut self,
745 mut token: UserRegistrationToken,
746 email: Option<String>,
747 ) -> Result<UserRegistrationToken, Self::Error> {
748 let res = sqlx::query!(
749 r#"
750 UPDATE user_registration_tokens
751 SET email = $2
752 WHERE user_registration_token_id = $1
753 "#,
754 Uuid::from(token.id),
755 email.as_deref(),
756 )
757 .traced()
758 .execute(&mut *self.conn)
759 .await?;
760
761 DatabaseError::ensure_affected_rows(&res, 1)?;
762
763 token.email = email;
764
765 Ok(token)
766 }
767}
768
769#[cfg(test)]
770mod tests {
771 use chrono::Duration;
772 use mas_data_model::{Clock as _, clock::MockClock};
773 use mas_storage::{Pagination, user::UserRegistrationTokenFilter};
774 use rand::SeedableRng;
775 use rand_chacha::ChaChaRng;
776 use sqlx::PgPool;
777
778 use crate::PgRepository;
779
780 #[sqlx::test(migrator = "crate::MIGRATOR")]
781 async fn test_unrevoke(pool: PgPool) {
782 let mut rng = ChaChaRng::seed_from_u64(42);
783 let clock = MockClock::default();
784
785 let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
786
787 let token = repo
789 .user_registration_token()
790 .add(
791 &mut rng,
792 &clock,
793 "test_token".to_owned(),
794 None,
795 None,
796 None,
797 None,
798 false,
799 )
800 .await
801 .unwrap();
802
803 let revoked_token = repo
805 .user_registration_token()
806 .revoke(&clock, token)
807 .await
808 .unwrap();
809
810 assert!(revoked_token.revoked_at.is_some());
812
813 let unrevoked_token = repo
815 .user_registration_token()
816 .unrevoke(revoked_token)
817 .await
818 .unwrap();
819
820 assert!(unrevoked_token.revoked_at.is_none());
822
823 let non_revoked_filter = UserRegistrationTokenFilter::new(clock.now()).with_revoked(false);
825 let page = repo
826 .user_registration_token()
827 .list(non_revoked_filter, Pagination::first(10))
828 .await
829 .unwrap();
830
831 assert!(page.edges.iter().any(|t| t.node.id == unrevoked_token.id));
832 }
833
834 #[sqlx::test(migrator = "crate::MIGRATOR")]
835 async fn test_set_expiry(pool: PgPool) {
836 let mut rng = ChaChaRng::seed_from_u64(42);
837 let clock = MockClock::default();
838
839 let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
840
841 let token = repo
843 .user_registration_token()
844 .add(
845 &mut rng,
846 &clock,
847 "test_token_expiry".to_owned(),
848 None,
849 None,
850 None,
851 None,
852 false,
853 )
854 .await
855 .unwrap();
856
857 assert!(token.expires_at.is_none());
859
860 let future_time = clock.now() + Duration::days(30);
862 let updated_token = repo
863 .user_registration_token()
864 .set_expiry(token, Some(future_time))
865 .await
866 .unwrap();
867
868 assert_eq!(updated_token.expires_at, Some(future_time));
870
871 let final_token = repo
873 .user_registration_token()
874 .set_expiry(updated_token, None)
875 .await
876 .unwrap();
877
878 assert!(final_token.expires_at.is_none());
880 }
881
882 #[sqlx::test(migrator = "crate::MIGRATOR")]
883 async fn test_set_usage_limit(pool: PgPool) {
884 let mut rng = ChaChaRng::seed_from_u64(42);
885 let clock = MockClock::default();
886
887 let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
888
889 let token = repo
891 .user_registration_token()
892 .add(
893 &mut rng,
894 &clock,
895 "test_token_limit".to_owned(),
896 None,
897 None,
898 None,
899 None,
900 false,
901 )
902 .await
903 .unwrap();
904
905 assert!(token.usage_limit.is_none());
907
908 let updated_token = repo
910 .user_registration_token()
911 .set_usage_limit(token, Some(5))
912 .await
913 .unwrap();
914
915 assert_eq!(updated_token.usage_limit, Some(5));
917
918 let changed_token = repo
920 .user_registration_token()
921 .set_usage_limit(updated_token, Some(10))
922 .await
923 .unwrap();
924
925 assert_eq!(changed_token.usage_limit, Some(10));
927
928 let final_token = repo
930 .user_registration_token()
931 .set_usage_limit(changed_token, None)
932 .await
933 .unwrap();
934
935 assert!(final_token.usage_limit.is_none());
937 }
938
939 #[sqlx::test(migrator = "crate::MIGRATOR")]
940 async fn test_list_and_count(pool: PgPool) {
941 let mut rng = ChaChaRng::seed_from_u64(42);
942 let clock = MockClock::default();
943
944 let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
945
946 let _token1 = repo
949 .user_registration_token()
950 .add(
951 &mut rng,
952 &clock,
953 "token1".to_owned(),
954 None,
955 None,
956 None,
957 None,
958 false,
959 )
960 .await
961 .unwrap();
962
963 let token2 = repo
965 .user_registration_token()
966 .add(
967 &mut rng,
968 &clock,
969 "token2".to_owned(),
970 None,
971 None,
972 None,
973 None,
974 false,
975 )
976 .await
977 .unwrap();
978 let token2 = repo
979 .user_registration_token()
980 .use_token(&clock, token2)
981 .await
982 .unwrap();
983
984 let past_time = clock.now() - Duration::days(1);
986 let token3 = repo
987 .user_registration_token()
988 .add(
989 &mut rng,
990 &clock,
991 "token3".to_owned(),
992 None,
993 Some(past_time),
994 None,
995 None,
996 false,
997 )
998 .await
999 .unwrap();
1000
1001 let token4 = repo
1003 .user_registration_token()
1004 .add(
1005 &mut rng,
1006 &clock,
1007 "token4".to_owned(),
1008 None,
1009 None,
1010 None,
1011 None,
1012 false,
1013 )
1014 .await
1015 .unwrap();
1016 let token4 = repo
1017 .user_registration_token()
1018 .revoke(&clock, token4)
1019 .await
1020 .unwrap();
1021
1022 let empty_filter = UserRegistrationTokenFilter::new(clock.now());
1024 let page = repo
1025 .user_registration_token()
1026 .list(empty_filter, Pagination::first(10))
1027 .await
1028 .unwrap();
1029 assert_eq!(page.edges.len(), 4);
1030
1031 let count = repo
1033 .user_registration_token()
1034 .count(empty_filter)
1035 .await
1036 .unwrap();
1037 assert_eq!(count, 4);
1038
1039 let used_filter = UserRegistrationTokenFilter::new(clock.now()).with_been_used(true);
1041 let page = repo
1042 .user_registration_token()
1043 .list(used_filter, Pagination::first(10))
1044 .await
1045 .unwrap();
1046 assert_eq!(page.edges.len(), 1);
1047 assert_eq!(page.edges[0].node.id, token2.id);
1048
1049 let unused_filter = UserRegistrationTokenFilter::new(clock.now()).with_been_used(false);
1051 let page = repo
1052 .user_registration_token()
1053 .list(unused_filter, Pagination::first(10))
1054 .await
1055 .unwrap();
1056 assert_eq!(page.edges.len(), 3);
1057
1058 let expired_filter = UserRegistrationTokenFilter::new(clock.now()).with_expired(true);
1060 let page = repo
1061 .user_registration_token()
1062 .list(expired_filter, Pagination::first(10))
1063 .await
1064 .unwrap();
1065 assert_eq!(page.edges.len(), 1);
1066 assert_eq!(page.edges[0].node.id, token3.id);
1067
1068 let not_expired_filter = UserRegistrationTokenFilter::new(clock.now()).with_expired(false);
1069 let page = repo
1070 .user_registration_token()
1071 .list(not_expired_filter, Pagination::first(10))
1072 .await
1073 .unwrap();
1074 assert_eq!(page.edges.len(), 3);
1075
1076 let revoked_filter = UserRegistrationTokenFilter::new(clock.now()).with_revoked(true);
1078 let page = repo
1079 .user_registration_token()
1080 .list(revoked_filter, Pagination::first(10))
1081 .await
1082 .unwrap();
1083 assert_eq!(page.edges.len(), 1);
1084 assert_eq!(page.edges[0].node.id, token4.id);
1085
1086 let not_revoked_filter = UserRegistrationTokenFilter::new(clock.now()).with_revoked(false);
1087 let page = repo
1088 .user_registration_token()
1089 .list(not_revoked_filter, Pagination::first(10))
1090 .await
1091 .unwrap();
1092 assert_eq!(page.edges.len(), 3);
1093
1094 let valid_filter = UserRegistrationTokenFilter::new(clock.now()).with_valid(true);
1096 let page = repo
1097 .user_registration_token()
1098 .list(valid_filter, Pagination::first(10))
1099 .await
1100 .unwrap();
1101 assert_eq!(page.edges.len(), 2);
1102
1103 let invalid_filter = UserRegistrationTokenFilter::new(clock.now()).with_valid(false);
1104 let page = repo
1105 .user_registration_token()
1106 .list(invalid_filter, Pagination::first(10))
1107 .await
1108 .unwrap();
1109 assert_eq!(page.edges.len(), 2);
1110
1111 let combined_filter = UserRegistrationTokenFilter::new(clock.now())
1113 .with_been_used(false)
1114 .with_revoked(true);
1115 let page = repo
1116 .user_registration_token()
1117 .list(combined_filter, Pagination::first(10))
1118 .await
1119 .unwrap();
1120 assert_eq!(page.edges.len(), 1);
1121 assert_eq!(page.edges[0].node.id, token4.id);
1122
1123 let page = repo
1125 .user_registration_token()
1126 .list(empty_filter, Pagination::first(2))
1127 .await
1128 .unwrap();
1129 assert_eq!(page.edges.len(), 2);
1130 }
1131}