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 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 )]
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 )]
325 async fn count(&mut self, filter: UserRegistrationTokenFilter) -> Result<usize, Self::Error> {
326 let (sql, values) = Query::select()
327 .expr(
328 Expr::col((
329 UserRegistrationTokens::Table,
330 UserRegistrationTokens::UserRegistrationTokenId,
331 ))
332 .count(),
333 )
334 .from(UserRegistrationTokens::Table)
335 .apply_filter(filter)
336 .build_sqlx(PostgresQueryBuilder);
337
338 let count: i64 = sqlx::query_scalar_with(&sql, values)
339 .traced()
340 .fetch_one(&mut *self.conn)
341 .await?;
342
343 count
344 .try_into()
345 .map_err(DatabaseError::to_invalid_operation)
346 }
347
348 #[tracing::instrument(
349 name = "db.user_registration_token.lookup",
350 skip_all,
351 fields(
352 db.query.text,
353 user_registration_token.id = %id,
354 ),
355 )]
356 async fn lookup(&mut self, id: Ulid) -> Result<Option<UserRegistrationToken>, Self::Error> {
357 let res = sqlx::query_as!(
358 UserRegistrationTokenLookup,
359 r#"
360 SELECT user_registration_token_id,
361 token,
362 usage_limit,
363 times_used,
364 created_at,
365 last_used_at,
366 expires_at,
367 revoked_at
368 FROM user_registration_tokens
369 WHERE user_registration_token_id = $1
370 "#,
371 Uuid::from(id)
372 )
373 .traced()
374 .fetch_optional(&mut *self.conn)
375 .await?;
376
377 let Some(res) = res else {
378 return Ok(None);
379 };
380
381 Ok(Some(res.try_into()?))
382 }
383
384 #[tracing::instrument(
385 name = "db.user_registration_token.find_by_token",
386 skip_all,
387 fields(
388 db.query.text,
389 token = %token,
390 ),
391 )]
392 async fn find_by_token(
393 &mut self,
394 token: &str,
395 ) -> Result<Option<UserRegistrationToken>, Self::Error> {
396 let res = sqlx::query_as!(
397 UserRegistrationTokenLookup,
398 r#"
399 SELECT user_registration_token_id,
400 token,
401 usage_limit,
402 times_used,
403 created_at,
404 last_used_at,
405 expires_at,
406 revoked_at
407 FROM user_registration_tokens
408 WHERE token = $1
409 "#,
410 token
411 )
412 .traced()
413 .fetch_optional(&mut *self.conn)
414 .await?;
415
416 let Some(res) = res else {
417 return Ok(None);
418 };
419
420 Ok(Some(res.try_into()?))
421 }
422
423 #[tracing::instrument(
424 name = "db.user_registration_token.add",
425 skip_all,
426 fields(
427 db.query.text,
428 user_registration_token.token = %token,
429 ),
430 )]
431 async fn add(
432 &mut self,
433 rng: &mut (dyn Rng + Send),
434 clock: &dyn mas_data_model::Clock,
435 token: String,
436 usage_limit: Option<u32>,
437 expires_at: Option<DateTime<Utc>>,
438 ) -> Result<UserRegistrationToken, Self::Error> {
439 let created_at = clock.now();
440 let id = Ulid::from_datetime_with_rng(created_at, rng);
441
442 let usage_limit_i32 = usage_limit
443 .map(i32::try_from)
444 .transpose()
445 .map_err(DatabaseError::to_invalid_operation)?;
446
447 sqlx::query!(
448 r#"
449 INSERT INTO user_registration_tokens
450 (user_registration_token_id, token, usage_limit, created_at, expires_at)
451 VALUES ($1, $2, $3, $4, $5)
452 "#,
453 Uuid::from(id),
454 &token,
455 usage_limit_i32,
456 created_at,
457 expires_at,
458 )
459 .traced()
460 .execute(&mut *self.conn)
461 .await?;
462
463 Ok(UserRegistrationToken {
464 id,
465 token,
466 usage_limit,
467 times_used: 0,
468 created_at,
469 last_used_at: None,
470 expires_at,
471 revoked_at: None,
472 })
473 }
474
475 #[tracing::instrument(
476 name = "db.user_registration_token.use_token",
477 skip_all,
478 fields(
479 db.query.text,
480 user_registration_token.id = %token.id,
481 ),
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 )]
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 )]
557 async fn unrevoke(
558 &mut self,
559 mut token: UserRegistrationToken,
560 ) -> Result<UserRegistrationToken, Self::Error> {
561 let res = sqlx::query!(
562 r#"
563 UPDATE user_registration_tokens
564 SET revoked_at = NULL
565 WHERE user_registration_token_id = $1
566 "#,
567 Uuid::from(token.id),
568 )
569 .traced()
570 .execute(&mut *self.conn)
571 .await?;
572
573 DatabaseError::ensure_affected_rows(&res, 1)?;
574
575 token.revoked_at = None;
576
577 Ok(token)
578 }
579
580 #[tracing::instrument(
581 name = "db.user_registration_token.set_expiry",
582 skip_all,
583 fields(
584 db.query.text,
585 user_registration_token.id = %token.id,
586 ),
587 )]
588 async fn set_expiry(
589 &mut self,
590 mut token: UserRegistrationToken,
591 expires_at: Option<DateTime<Utc>>,
592 ) -> Result<UserRegistrationToken, Self::Error> {
593 let res = sqlx::query!(
594 r#"
595 UPDATE user_registration_tokens
596 SET expires_at = $2
597 WHERE user_registration_token_id = $1
598 "#,
599 Uuid::from(token.id),
600 expires_at,
601 )
602 .traced()
603 .execute(&mut *self.conn)
604 .await?;
605
606 DatabaseError::ensure_affected_rows(&res, 1)?;
607
608 token.expires_at = expires_at;
609
610 Ok(token)
611 }
612
613 #[tracing::instrument(
614 name = "db.user_registration_token.set_usage_limit",
615 skip_all,
616 fields(
617 db.query.text,
618 user_registration_token.id = %token.id,
619 ),
620 )]
621 async fn set_usage_limit(
622 &mut self,
623 mut token: UserRegistrationToken,
624 usage_limit: Option<u32>,
625 ) -> Result<UserRegistrationToken, Self::Error> {
626 let usage_limit_i32 = usage_limit
627 .map(i32::try_from)
628 .transpose()
629 .map_err(DatabaseError::to_invalid_operation)?;
630
631 let res = sqlx::query!(
632 r#"
633 UPDATE user_registration_tokens
634 SET usage_limit = $2
635 WHERE user_registration_token_id = $1
636 "#,
637 Uuid::from(token.id),
638 usage_limit_i32,
639 )
640 .traced()
641 .execute(&mut *self.conn)
642 .await?;
643
644 DatabaseError::ensure_affected_rows(&res, 1)?;
645
646 token.usage_limit = usage_limit;
647
648 Ok(token)
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use chrono::Duration;
655 use mas_data_model::{Clock as _, clock::MockClock};
656 use mas_storage::{Pagination, user::UserRegistrationTokenFilter};
657 use rand::SeedableRng;
658 use rand_chacha::ChaChaRng;
659 use sqlx::PgPool;
660
661 use crate::PgRepository;
662
663 #[sqlx::test(migrator = "crate::MIGRATOR")]
664 async fn test_unrevoke(pool: PgPool) {
665 let mut rng = ChaChaRng::seed_from_u64(42);
666 let clock = MockClock::default();
667
668 let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
669
670 let token = repo
672 .user_registration_token()
673 .add(&mut rng, &clock, "test_token".to_owned(), None, None)
674 .await
675 .unwrap();
676
677 let revoked_token = repo
679 .user_registration_token()
680 .revoke(&clock, token)
681 .await
682 .unwrap();
683
684 assert!(revoked_token.revoked_at.is_some());
686
687 let unrevoked_token = repo
689 .user_registration_token()
690 .unrevoke(revoked_token)
691 .await
692 .unwrap();
693
694 assert!(unrevoked_token.revoked_at.is_none());
696
697 let non_revoked_filter = UserRegistrationTokenFilter::new(clock.now()).with_revoked(false);
699 let page = repo
700 .user_registration_token()
701 .list(non_revoked_filter, Pagination::first(10))
702 .await
703 .unwrap();
704
705 assert!(page.edges.iter().any(|t| t.node.id == unrevoked_token.id));
706 }
707
708 #[sqlx::test(migrator = "crate::MIGRATOR")]
709 async fn test_set_expiry(pool: PgPool) {
710 let mut rng = ChaChaRng::seed_from_u64(42);
711 let clock = MockClock::default();
712
713 let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
714
715 let token = repo
717 .user_registration_token()
718 .add(&mut rng, &clock, "test_token_expiry".to_owned(), None, None)
719 .await
720 .unwrap();
721
722 assert!(token.expires_at.is_none());
724
725 let future_time = clock.now() + Duration::days(30);
727 let updated_token = repo
728 .user_registration_token()
729 .set_expiry(token, Some(future_time))
730 .await
731 .unwrap();
732
733 assert_eq!(updated_token.expires_at, Some(future_time));
735
736 let final_token = repo
738 .user_registration_token()
739 .set_expiry(updated_token, None)
740 .await
741 .unwrap();
742
743 assert!(final_token.expires_at.is_none());
745 }
746
747 #[sqlx::test(migrator = "crate::MIGRATOR")]
748 async fn test_set_usage_limit(pool: PgPool) {
749 let mut rng = ChaChaRng::seed_from_u64(42);
750 let clock = MockClock::default();
751
752 let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
753
754 let token = repo
756 .user_registration_token()
757 .add(&mut rng, &clock, "test_token_limit".to_owned(), None, None)
758 .await
759 .unwrap();
760
761 assert!(token.usage_limit.is_none());
763
764 let updated_token = repo
766 .user_registration_token()
767 .set_usage_limit(token, Some(5))
768 .await
769 .unwrap();
770
771 assert_eq!(updated_token.usage_limit, Some(5));
773
774 let changed_token = repo
776 .user_registration_token()
777 .set_usage_limit(updated_token, Some(10))
778 .await
779 .unwrap();
780
781 assert_eq!(changed_token.usage_limit, Some(10));
783
784 let final_token = repo
786 .user_registration_token()
787 .set_usage_limit(changed_token, None)
788 .await
789 .unwrap();
790
791 assert!(final_token.usage_limit.is_none());
793 }
794
795 #[sqlx::test(migrator = "crate::MIGRATOR")]
796 async fn test_list_and_count(pool: PgPool) {
797 let mut rng = ChaChaRng::seed_from_u64(42);
798 let clock = MockClock::default();
799
800 let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
801
802 let _token1 = repo
805 .user_registration_token()
806 .add(&mut rng, &clock, "token1".to_owned(), None, None)
807 .await
808 .unwrap();
809
810 let token2 = repo
812 .user_registration_token()
813 .add(&mut rng, &clock, "token2".to_owned(), None, None)
814 .await
815 .unwrap();
816 let token2 = repo
817 .user_registration_token()
818 .use_token(&clock, token2)
819 .await
820 .unwrap();
821
822 let past_time = clock.now() - Duration::days(1);
824 let token3 = repo
825 .user_registration_token()
826 .add(&mut rng, &clock, "token3".to_owned(), None, Some(past_time))
827 .await
828 .unwrap();
829
830 let token4 = repo
832 .user_registration_token()
833 .add(&mut rng, &clock, "token4".to_owned(), None, None)
834 .await
835 .unwrap();
836 let token4 = repo
837 .user_registration_token()
838 .revoke(&clock, token4)
839 .await
840 .unwrap();
841
842 let empty_filter = UserRegistrationTokenFilter::new(clock.now());
844 let page = repo
845 .user_registration_token()
846 .list(empty_filter, Pagination::first(10))
847 .await
848 .unwrap();
849 assert_eq!(page.edges.len(), 4);
850
851 let count = repo
853 .user_registration_token()
854 .count(empty_filter)
855 .await
856 .unwrap();
857 assert_eq!(count, 4);
858
859 let used_filter = UserRegistrationTokenFilter::new(clock.now()).with_been_used(true);
861 let page = repo
862 .user_registration_token()
863 .list(used_filter, Pagination::first(10))
864 .await
865 .unwrap();
866 assert_eq!(page.edges.len(), 1);
867 assert_eq!(page.edges[0].node.id, token2.id);
868
869 let unused_filter = UserRegistrationTokenFilter::new(clock.now()).with_been_used(false);
871 let page = repo
872 .user_registration_token()
873 .list(unused_filter, Pagination::first(10))
874 .await
875 .unwrap();
876 assert_eq!(page.edges.len(), 3);
877
878 let expired_filter = UserRegistrationTokenFilter::new(clock.now()).with_expired(true);
880 let page = repo
881 .user_registration_token()
882 .list(expired_filter, Pagination::first(10))
883 .await
884 .unwrap();
885 assert_eq!(page.edges.len(), 1);
886 assert_eq!(page.edges[0].node.id, token3.id);
887
888 let not_expired_filter = UserRegistrationTokenFilter::new(clock.now()).with_expired(false);
889 let page = repo
890 .user_registration_token()
891 .list(not_expired_filter, Pagination::first(10))
892 .await
893 .unwrap();
894 assert_eq!(page.edges.len(), 3);
895
896 let revoked_filter = UserRegistrationTokenFilter::new(clock.now()).with_revoked(true);
898 let page = repo
899 .user_registration_token()
900 .list(revoked_filter, Pagination::first(10))
901 .await
902 .unwrap();
903 assert_eq!(page.edges.len(), 1);
904 assert_eq!(page.edges[0].node.id, token4.id);
905
906 let not_revoked_filter = UserRegistrationTokenFilter::new(clock.now()).with_revoked(false);
907 let page = repo
908 .user_registration_token()
909 .list(not_revoked_filter, Pagination::first(10))
910 .await
911 .unwrap();
912 assert_eq!(page.edges.len(), 3);
913
914 let valid_filter = UserRegistrationTokenFilter::new(clock.now()).with_valid(true);
916 let page = repo
917 .user_registration_token()
918 .list(valid_filter, Pagination::first(10))
919 .await
920 .unwrap();
921 assert_eq!(page.edges.len(), 2);
922
923 let invalid_filter = UserRegistrationTokenFilter::new(clock.now()).with_valid(false);
924 let page = repo
925 .user_registration_token()
926 .list(invalid_filter, Pagination::first(10))
927 .await
928 .unwrap();
929 assert_eq!(page.edges.len(), 2);
930
931 let combined_filter = UserRegistrationTokenFilter::new(clock.now())
933 .with_been_used(false)
934 .with_revoked(true);
935 let page = repo
936 .user_registration_token()
937 .list(combined_filter, Pagination::first(10))
938 .await
939 .unwrap();
940 assert_eq!(page.edges.len(), 1);
941 assert_eq!(page.edges[0].node.id, token4.id);
942
943 let page = repo
945 .user_registration_token()
946 .list(empty_filter, Pagination::first(2))
947 .await
948 .unwrap();
949 assert_eq!(page.edges.len(), 2);
950 }
951}