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