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