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