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::RngCore;
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 usage_limit: Option<i32>,
51 times_used: i32,
52 created_at: DateTime<Utc>,
53 last_used_at: Option<DateTime<Utc>>,
54 expires_at: Option<DateTime<Utc>>,
55 revoked_at: Option<DateTime<Utc>>,
56}
57
58impl Node<Ulid> for UserRegistrationTokenLookup {
59 fn cursor(&self) -> Ulid {
60 self.user_registration_token_id.into()
61 }
62}
63
64impl Filter for UserRegistrationTokenFilter {
65 fn generate_condition(&self, _has_joins: bool) -> impl sea_query::IntoCondition {
66 sea_query::Condition::all()
67 .add_option(self.has_been_used().map(|has_been_used| {
68 if has_been_used {
69 Expr::col((
70 UserRegistrationTokens::Table,
71 UserRegistrationTokens::TimesUsed,
72 ))
73 .gt(0)
74 } else {
75 Expr::col((
76 UserRegistrationTokens::Table,
77 UserRegistrationTokens::TimesUsed,
78 ))
79 .eq(0)
80 }
81 }))
82 .add_option(self.is_revoked().map(|is_revoked| {
83 if is_revoked {
84 Expr::col((
85 UserRegistrationTokens::Table,
86 UserRegistrationTokens::RevokedAt,
87 ))
88 .is_not_null()
89 } else {
90 Expr::col((
91 UserRegistrationTokens::Table,
92 UserRegistrationTokens::RevokedAt,
93 ))
94 .is_null()
95 }
96 }))
97 .add_option(self.is_expired().map(|is_expired| {
98 if is_expired {
99 Condition::all()
100 .add(
101 Expr::col((
102 UserRegistrationTokens::Table,
103 UserRegistrationTokens::ExpiresAt,
104 ))
105 .is_not_null(),
106 )
107 .add(
108 Expr::col((
109 UserRegistrationTokens::Table,
110 UserRegistrationTokens::ExpiresAt,
111 ))
112 .lt(Expr::val(self.now())),
113 )
114 } else {
115 Condition::any()
116 .add(
117 Expr::col((
118 UserRegistrationTokens::Table,
119 UserRegistrationTokens::ExpiresAt,
120 ))
121 .is_null(),
122 )
123 .add(
124 Expr::col((
125 UserRegistrationTokens::Table,
126 UserRegistrationTokens::ExpiresAt,
127 ))
128 .gte(Expr::val(self.now())),
129 )
130 }
131 }))
132 .add_option(self.is_valid().map(|is_valid| {
133 let valid = Condition::all()
134 .add(
136 Condition::any()
137 .add(
138 Expr::col((
139 UserRegistrationTokens::Table,
140 UserRegistrationTokens::UsageLimit,
141 ))
142 .is_null(),
143 )
144 .add(
145 Expr::col((
146 UserRegistrationTokens::Table,
147 UserRegistrationTokens::TimesUsed,
148 ))
149 .lt(Expr::col((
150 UserRegistrationTokens::Table,
151 UserRegistrationTokens::UsageLimit,
152 ))),
153 ),
154 )
155 .add(
157 Expr::col((
158 UserRegistrationTokens::Table,
159 UserRegistrationTokens::RevokedAt,
160 ))
161 .is_null(),
162 )
163 .add(
165 Condition::any()
166 .add(
167 Expr::col((
168 UserRegistrationTokens::Table,
169 UserRegistrationTokens::ExpiresAt,
170 ))
171 .is_null(),
172 )
173 .add(
174 Expr::col((
175 UserRegistrationTokens::Table,
176 UserRegistrationTokens::ExpiresAt,
177 ))
178 .gte(Expr::val(self.now())),
179 ),
180 );
181
182 if is_valid { valid } else { valid.not() }
183 }))
184 }
185}
186
187impl TryFrom<UserRegistrationTokenLookup> for UserRegistrationToken {
188 type Error = DatabaseInconsistencyError;
189
190 fn try_from(res: UserRegistrationTokenLookup) -> Result<Self, Self::Error> {
191 let id = Ulid::from(res.user_registration_token_id);
192
193 let usage_limit = res
194 .usage_limit
195 .map(u32::try_from)
196 .transpose()
197 .map_err(|e| {
198 DatabaseInconsistencyError::on("user_registration_tokens")
199 .column("usage_limit")
200 .row(id)
201 .source(e)
202 })?;
203
204 let times_used = res.times_used.try_into().map_err(|e| {
205 DatabaseInconsistencyError::on("user_registration_tokens")
206 .column("times_used")
207 .row(id)
208 .source(e)
209 })?;
210
211 Ok(UserRegistrationToken {
212 id,
213 token: res.token,
214 usage_limit,
215 times_used,
216 created_at: res.created_at,
217 last_used_at: res.last_used_at,
218 expires_at: res.expires_at,
219 revoked_at: res.revoked_at,
220 })
221 }
222}
223
224#[async_trait]
225impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> {
226 type Error = DatabaseError;
227
228 #[tracing::instrument(
229 name = "db.user_registration_token.list",
230 skip_all,
231 fields(
232 db.query.text,
233 ),
234 err,
235 )]
236 async fn list(
237 &mut self,
238 filter: UserRegistrationTokenFilter,
239 pagination: Pagination,
240 ) -> Result<Page<UserRegistrationToken>, Self::Error> {
241 let (sql, arguments) = Query::select()
242 .expr_as(
243 Expr::col((
244 UserRegistrationTokens::Table,
245 UserRegistrationTokens::UserRegistrationTokenId,
246 )),
247 UserRegistrationTokenLookupIden::UserRegistrationTokenId,
248 )
249 .expr_as(
250 Expr::col((UserRegistrationTokens::Table, UserRegistrationTokens::Token)),
251 UserRegistrationTokenLookupIden::Token,
252 )
253 .expr_as(
254 Expr::col((
255 UserRegistrationTokens::Table,
256 UserRegistrationTokens::UsageLimit,
257 )),
258 UserRegistrationTokenLookupIden::UsageLimit,
259 )
260 .expr_as(
261 Expr::col((
262 UserRegistrationTokens::Table,
263 UserRegistrationTokens::TimesUsed,
264 )),
265 UserRegistrationTokenLookupIden::TimesUsed,
266 )
267 .expr_as(
268 Expr::col((
269 UserRegistrationTokens::Table,
270 UserRegistrationTokens::CreatedAt,
271 )),
272 UserRegistrationTokenLookupIden::CreatedAt,
273 )
274 .expr_as(
275 Expr::col((
276 UserRegistrationTokens::Table,
277 UserRegistrationTokens::LastUsedAt,
278 )),
279 UserRegistrationTokenLookupIden::LastUsedAt,
280 )
281 .expr_as(
282 Expr::col((
283 UserRegistrationTokens::Table,
284 UserRegistrationTokens::ExpiresAt,
285 )),
286 UserRegistrationTokenLookupIden::ExpiresAt,
287 )
288 .expr_as(
289 Expr::col((
290 UserRegistrationTokens::Table,
291 UserRegistrationTokens::RevokedAt,
292 )),
293 UserRegistrationTokenLookupIden::RevokedAt,
294 )
295 .from(UserRegistrationTokens::Table)
296 .apply_filter(filter)
297 .generate_pagination(
298 (
299 UserRegistrationTokens::Table,
300 UserRegistrationTokens::UserRegistrationTokenId,
301 ),
302 pagination,
303 )
304 .build_sqlx(PostgresQueryBuilder);
305
306 let edges: Vec<UserRegistrationTokenLookup> = sqlx::query_as_with(&sql, arguments)
307 .traced()
308 .fetch_all(&mut *self.conn)
309 .await?;
310
311 let page = pagination
312 .process(edges)
313 .try_map(UserRegistrationToken::try_from)?;
314
315 Ok(page)
316 }
317
318 #[tracing::instrument(
319 name = "db.user_registration_token.count",
320 skip_all,
321 fields(
322 db.query.text,
323 user_registration_token.filter = ?filter,
324 ),
325 err,
326 )]
327 async fn count(&mut self, filter: UserRegistrationTokenFilter) -> Result<usize, Self::Error> {
328 let (sql, values) = Query::select()
329 .expr(
330 Expr::col((
331 UserRegistrationTokens::Table,
332 UserRegistrationTokens::UserRegistrationTokenId,
333 ))
334 .count(),
335 )
336 .from(UserRegistrationTokens::Table)
337 .apply_filter(filter)
338 .build_sqlx(PostgresQueryBuilder);
339
340 let count: i64 = sqlx::query_scalar_with(&sql, values)
341 .traced()
342 .fetch_one(&mut *self.conn)
343 .await?;
344
345 count
346 .try_into()
347 .map_err(DatabaseError::to_invalid_operation)
348 }
349
350 #[tracing::instrument(
351 name = "db.user_registration_token.lookup",
352 skip_all,
353 fields(
354 db.query.text,
355 user_registration_token.id = %id,
356 ),
357 err,
358 )]
359 async fn lookup(&mut self, id: Ulid) -> Result<Option<UserRegistrationToken>, Self::Error> {
360 let res = sqlx::query_as!(
361 UserRegistrationTokenLookup,
362 r#"
363 SELECT user_registration_token_id,
364 token,
365 usage_limit,
366 times_used,
367 created_at,
368 last_used_at,
369 expires_at,
370 revoked_at
371 FROM user_registration_tokens
372 WHERE user_registration_token_id = $1
373 "#,
374 Uuid::from(id)
375 )
376 .traced()
377 .fetch_optional(&mut *self.conn)
378 .await?;
379
380 let Some(res) = res else {
381 return Ok(None);
382 };
383
384 Ok(Some(res.try_into()?))
385 }
386
387 #[tracing::instrument(
388 name = "db.user_registration_token.find_by_token",
389 skip_all,
390 fields(
391 db.query.text,
392 token = %token,
393 ),
394 err,
395 )]
396 async fn find_by_token(
397 &mut self,
398 token: &str,
399 ) -> Result<Option<UserRegistrationToken>, Self::Error> {
400 let res = sqlx::query_as!(
401 UserRegistrationTokenLookup,
402 r#"
403 SELECT user_registration_token_id,
404 token,
405 usage_limit,
406 times_used,
407 created_at,
408 last_used_at,
409 expires_at,
410 revoked_at
411 FROM user_registration_tokens
412 WHERE token = $1
413 "#,
414 token
415 )
416 .traced()
417 .fetch_optional(&mut *self.conn)
418 .await?;
419
420 let Some(res) = res else {
421 return Ok(None);
422 };
423
424 Ok(Some(res.try_into()?))
425 }
426
427 #[tracing::instrument(
428 name = "db.user_registration_token.add",
429 skip_all,
430 fields(
431 db.query.text,
432 user_registration_token.token = %token,
433 ),
434 err,
435 )]
436 async fn add(
437 &mut self,
438 rng: &mut (dyn RngCore + Send),
439 clock: &dyn mas_data_model::Clock,
440 token: String,
441 usage_limit: Option<u32>,
442 expires_at: Option<DateTime<Utc>>,
443 ) -> Result<UserRegistrationToken, Self::Error> {
444 let created_at = clock.now();
445 let id = Ulid::from_datetime_with_rng(created_at, rng);
446
447 let usage_limit_i32 = usage_limit
448 .map(i32::try_from)
449 .transpose()
450 .map_err(DatabaseError::to_invalid_operation)?;
451
452 sqlx::query!(
453 r#"
454 INSERT INTO user_registration_tokens
455 (user_registration_token_id, token, usage_limit, created_at, expires_at)
456 VALUES ($1, $2, $3, $4, $5)
457 "#,
458 Uuid::from(id),
459 &token,
460 usage_limit_i32,
461 created_at,
462 expires_at,
463 )
464 .traced()
465 .execute(&mut *self.conn)
466 .await?;
467
468 Ok(UserRegistrationToken {
469 id,
470 token,
471 usage_limit,
472 times_used: 0,
473 created_at,
474 last_used_at: None,
475 expires_at,
476 revoked_at: None,
477 })
478 }
479
480 #[tracing::instrument(
481 name = "db.user_registration_token.use_token",
482 skip_all,
483 fields(
484 db.query.text,
485 user_registration_token.id = %token.id,
486 ),
487 err,
488 )]
489 async fn use_token(
490 &mut self,
491 clock: &dyn Clock,
492 token: UserRegistrationToken,
493 ) -> Result<UserRegistrationToken, Self::Error> {
494 let now = clock.now();
495 let new_times_used = sqlx::query_scalar!(
496 r#"
497 UPDATE user_registration_tokens
498 SET times_used = times_used + 1,
499 last_used_at = $2
500 WHERE user_registration_token_id = $1 AND revoked_at IS NULL
501 RETURNING times_used
502 "#,
503 Uuid::from(token.id),
504 now,
505 )
506 .traced()
507 .fetch_one(&mut *self.conn)
508 .await?;
509
510 let new_times_used = new_times_used
511 .try_into()
512 .map_err(DatabaseError::to_invalid_operation)?;
513
514 Ok(UserRegistrationToken {
515 times_used: new_times_used,
516 last_used_at: Some(now),
517 ..token
518 })
519 }
520
521 #[tracing::instrument(
522 name = "db.user_registration_token.revoke",
523 skip_all,
524 fields(
525 db.query.text,
526 user_registration_token.id = %token.id,
527 ),
528 err,
529 )]
530 async fn revoke(
531 &mut self,
532 clock: &dyn Clock,
533 mut token: UserRegistrationToken,
534 ) -> Result<UserRegistrationToken, Self::Error> {
535 let revoked_at = clock.now();
536 let res = sqlx::query!(
537 r#"
538 UPDATE user_registration_tokens
539 SET revoked_at = $2
540 WHERE user_registration_token_id = $1
541 "#,
542 Uuid::from(token.id),
543 revoked_at,
544 )
545 .traced()
546 .execute(&mut *self.conn)
547 .await?;
548
549 DatabaseError::ensure_affected_rows(&res, 1)?;
550
551 token.revoked_at = Some(revoked_at);
552
553 Ok(token)
554 }
555
556 #[tracing::instrument(
557 name = "db.user_registration_token.unrevoke",
558 skip_all,
559 fields(
560 db.query.text,
561 user_registration_token.id = %token.id,
562 ),
563 err,
564 )]
565 async fn unrevoke(
566 &mut self,
567 mut token: UserRegistrationToken,
568 ) -> Result<UserRegistrationToken, Self::Error> {
569 let res = sqlx::query!(
570 r#"
571 UPDATE user_registration_tokens
572 SET revoked_at = NULL
573 WHERE user_registration_token_id = $1
574 "#,
575 Uuid::from(token.id),
576 )
577 .traced()
578 .execute(&mut *self.conn)
579 .await?;
580
581 DatabaseError::ensure_affected_rows(&res, 1)?;
582
583 token.revoked_at = None;
584
585 Ok(token)
586 }
587
588 #[tracing::instrument(
589 name = "db.user_registration_token.set_expiry",
590 skip_all,
591 fields(
592 db.query.text,
593 user_registration_token.id = %token.id,
594 ),
595 err,
596 )]
597 async fn set_expiry(
598 &mut self,
599 mut token: UserRegistrationToken,
600 expires_at: Option<DateTime<Utc>>,
601 ) -> Result<UserRegistrationToken, Self::Error> {
602 let res = sqlx::query!(
603 r#"
604 UPDATE user_registration_tokens
605 SET expires_at = $2
606 WHERE user_registration_token_id = $1
607 "#,
608 Uuid::from(token.id),
609 expires_at,
610 )
611 .traced()
612 .execute(&mut *self.conn)
613 .await?;
614
615 DatabaseError::ensure_affected_rows(&res, 1)?;
616
617 token.expires_at = expires_at;
618
619 Ok(token)
620 }
621
622 #[tracing::instrument(
623 name = "db.user_registration_token.set_usage_limit",
624 skip_all,
625 fields(
626 db.query.text,
627 user_registration_token.id = %token.id,
628 ),
629 err,
630 )]
631 async fn set_usage_limit(
632 &mut self,
633 mut token: UserRegistrationToken,
634 usage_limit: Option<u32>,
635 ) -> Result<UserRegistrationToken, Self::Error> {
636 let usage_limit_i32 = usage_limit
637 .map(i32::try_from)
638 .transpose()
639 .map_err(DatabaseError::to_invalid_operation)?;
640
641 let res = sqlx::query!(
642 r#"
643 UPDATE user_registration_tokens
644 SET usage_limit = $2
645 WHERE user_registration_token_id = $1
646 "#,
647 Uuid::from(token.id),
648 usage_limit_i32,
649 )
650 .traced()
651 .execute(&mut *self.conn)
652 .await?;
653
654 DatabaseError::ensure_affected_rows(&res, 1)?;
655
656 token.usage_limit = usage_limit;
657
658 Ok(token)
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use chrono::Duration;
665 use mas_data_model::{Clock as _, clock::MockClock};
666 use mas_storage::{Pagination, user::UserRegistrationTokenFilter};
667 use rand::SeedableRng;
668 use rand_chacha::ChaChaRng;
669 use sqlx::PgPool;
670
671 use crate::PgRepository;
672
673 #[sqlx::test(migrator = "crate::MIGRATOR")]
674 async fn test_unrevoke(pool: PgPool) {
675 let mut rng = ChaChaRng::seed_from_u64(42);
676 let clock = MockClock::default();
677
678 let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
679
680 let token = repo
682 .user_registration_token()
683 .add(&mut rng, &clock, "test_token".to_owned(), None, None)
684 .await
685 .unwrap();
686
687 let revoked_token = repo
689 .user_registration_token()
690 .revoke(&clock, token)
691 .await
692 .unwrap();
693
694 assert!(revoked_token.revoked_at.is_some());
696
697 let unrevoked_token = repo
699 .user_registration_token()
700 .unrevoke(revoked_token)
701 .await
702 .unwrap();
703
704 assert!(unrevoked_token.revoked_at.is_none());
706
707 let non_revoked_filter = UserRegistrationTokenFilter::new(clock.now()).with_revoked(false);
709 let page = repo
710 .user_registration_token()
711 .list(non_revoked_filter, Pagination::first(10))
712 .await
713 .unwrap();
714
715 assert!(page.edges.iter().any(|t| t.node.id == unrevoked_token.id));
716 }
717
718 #[sqlx::test(migrator = "crate::MIGRATOR")]
719 async fn test_set_expiry(pool: PgPool) {
720 let mut rng = ChaChaRng::seed_from_u64(42);
721 let clock = MockClock::default();
722
723 let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
724
725 let token = repo
727 .user_registration_token()
728 .add(&mut rng, &clock, "test_token_expiry".to_owned(), None, None)
729 .await
730 .unwrap();
731
732 assert!(token.expires_at.is_none());
734
735 let future_time = clock.now() + Duration::days(30);
737 let updated_token = repo
738 .user_registration_token()
739 .set_expiry(token, Some(future_time))
740 .await
741 .unwrap();
742
743 assert_eq!(updated_token.expires_at, Some(future_time));
745
746 let final_token = repo
748 .user_registration_token()
749 .set_expiry(updated_token, None)
750 .await
751 .unwrap();
752
753 assert!(final_token.expires_at.is_none());
755 }
756
757 #[sqlx::test(migrator = "crate::MIGRATOR")]
758 async fn test_set_usage_limit(pool: PgPool) {
759 let mut rng = ChaChaRng::seed_from_u64(42);
760 let clock = MockClock::default();
761
762 let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
763
764 let token = repo
766 .user_registration_token()
767 .add(&mut rng, &clock, "test_token_limit".to_owned(), None, None)
768 .await
769 .unwrap();
770
771 assert!(token.usage_limit.is_none());
773
774 let updated_token = repo
776 .user_registration_token()
777 .set_usage_limit(token, Some(5))
778 .await
779 .unwrap();
780
781 assert_eq!(updated_token.usage_limit, Some(5));
783
784 let changed_token = repo
786 .user_registration_token()
787 .set_usage_limit(updated_token, Some(10))
788 .await
789 .unwrap();
790
791 assert_eq!(changed_token.usage_limit, Some(10));
793
794 let final_token = repo
796 .user_registration_token()
797 .set_usage_limit(changed_token, None)
798 .await
799 .unwrap();
800
801 assert!(final_token.usage_limit.is_none());
803 }
804
805 #[sqlx::test(migrator = "crate::MIGRATOR")]
806 async fn test_list_and_count(pool: PgPool) {
807 let mut rng = ChaChaRng::seed_from_u64(42);
808 let clock = MockClock::default();
809
810 let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
811
812 let _token1 = repo
815 .user_registration_token()
816 .add(&mut rng, &clock, "token1".to_owned(), None, None)
817 .await
818 .unwrap();
819
820 let token2 = repo
822 .user_registration_token()
823 .add(&mut rng, &clock, "token2".to_owned(), None, None)
824 .await
825 .unwrap();
826 let token2 = repo
827 .user_registration_token()
828 .use_token(&clock, token2)
829 .await
830 .unwrap();
831
832 let past_time = clock.now() - Duration::days(1);
834 let token3 = repo
835 .user_registration_token()
836 .add(&mut rng, &clock, "token3".to_owned(), None, Some(past_time))
837 .await
838 .unwrap();
839
840 let token4 = repo
842 .user_registration_token()
843 .add(&mut rng, &clock, "token4".to_owned(), None, None)
844 .await
845 .unwrap();
846 let token4 = repo
847 .user_registration_token()
848 .revoke(&clock, token4)
849 .await
850 .unwrap();
851
852 let empty_filter = UserRegistrationTokenFilter::new(clock.now());
854 let page = repo
855 .user_registration_token()
856 .list(empty_filter, Pagination::first(10))
857 .await
858 .unwrap();
859 assert_eq!(page.edges.len(), 4);
860
861 let count = repo
863 .user_registration_token()
864 .count(empty_filter)
865 .await
866 .unwrap();
867 assert_eq!(count, 4);
868
869 let used_filter = UserRegistrationTokenFilter::new(clock.now()).with_been_used(true);
871 let page = repo
872 .user_registration_token()
873 .list(used_filter, Pagination::first(10))
874 .await
875 .unwrap();
876 assert_eq!(page.edges.len(), 1);
877 assert_eq!(page.edges[0].node.id, token2.id);
878
879 let unused_filter = UserRegistrationTokenFilter::new(clock.now()).with_been_used(false);
881 let page = repo
882 .user_registration_token()
883 .list(unused_filter, Pagination::first(10))
884 .await
885 .unwrap();
886 assert_eq!(page.edges.len(), 3);
887
888 let expired_filter = UserRegistrationTokenFilter::new(clock.now()).with_expired(true);
890 let page = repo
891 .user_registration_token()
892 .list(expired_filter, Pagination::first(10))
893 .await
894 .unwrap();
895 assert_eq!(page.edges.len(), 1);
896 assert_eq!(page.edges[0].node.id, token3.id);
897
898 let not_expired_filter = UserRegistrationTokenFilter::new(clock.now()).with_expired(false);
899 let page = repo
900 .user_registration_token()
901 .list(not_expired_filter, Pagination::first(10))
902 .await
903 .unwrap();
904 assert_eq!(page.edges.len(), 3);
905
906 let revoked_filter = UserRegistrationTokenFilter::new(clock.now()).with_revoked(true);
908 let page = repo
909 .user_registration_token()
910 .list(revoked_filter, Pagination::first(10))
911 .await
912 .unwrap();
913 assert_eq!(page.edges.len(), 1);
914 assert_eq!(page.edges[0].node.id, token4.id);
915
916 let not_revoked_filter = UserRegistrationTokenFilter::new(clock.now()).with_revoked(false);
917 let page = repo
918 .user_registration_token()
919 .list(not_revoked_filter, Pagination::first(10))
920 .await
921 .unwrap();
922 assert_eq!(page.edges.len(), 3);
923
924 let valid_filter = UserRegistrationTokenFilter::new(clock.now()).with_valid(true);
926 let page = repo
927 .user_registration_token()
928 .list(valid_filter, Pagination::first(10))
929 .await
930 .unwrap();
931 assert_eq!(page.edges.len(), 2);
932
933 let invalid_filter = UserRegistrationTokenFilter::new(clock.now()).with_valid(false);
934 let page = repo
935 .user_registration_token()
936 .list(invalid_filter, Pagination::first(10))
937 .await
938 .unwrap();
939 assert_eq!(page.edges.len(), 2);
940
941 let combined_filter = UserRegistrationTokenFilter::new(clock.now())
943 .with_been_used(false)
944 .with_revoked(true);
945 let page = repo
946 .user_registration_token()
947 .list(combined_filter, Pagination::first(10))
948 .await
949 .unwrap();
950 assert_eq!(page.edges.len(), 1);
951 assert_eq!(page.edges[0].node.id, token4.id);
952
953 let page = repo
955 .user_registration_token()
956 .list(empty_filter, Pagination::first(2))
957 .await
958 .unwrap();
959 assert_eq!(page.edges.len(), 2);
960 }
961}