Skip to content

Commit fc6b0e7

Browse files
committed
fixup! Data model and repository for user registration tokens
1 parent e53e266 commit fc6b0e7

File tree

2 files changed

+192
-13
lines changed

2 files changed

+192
-13
lines changed

crates/storage-pg/src/user/registration_token.rs

Lines changed: 143 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use mas_storage::{
1111
user::{UserRegistrationTokenFilter, UserRegistrationTokenRepository},
1212
};
1313
use rand::RngCore;
14-
use sea_query::{Expr, PostgresQueryBuilder, Query, enum_def};
14+
use sea_query::{Condition, Expr, PostgresQueryBuilder, Query, enum_def};
1515
use sea_query_binder::SqlxBinder;
1616
use sqlx::PgConnection;
1717
use ulid::Ulid;
@@ -54,6 +54,7 @@ struct UserRegistrationTokenLookup {
5454
}
5555

5656
impl Filter for UserRegistrationTokenFilter {
57+
#[expect(clippy::too_many_lines)]
5758
fn generate_condition(&self, _has_joins: bool) -> impl sea_query::IntoCondition {
5859
sea_query::Condition::all()
5960
.add_option(self.has_been_used().map(|has_been_used| {
@@ -86,6 +87,93 @@ impl Filter for UserRegistrationTokenFilter {
8687
.is_null()
8788
}
8889
}))
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+
// Has not reached its usage limit
128+
.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+
// Has not been revoked
149+
.add(
150+
Expr::col((
151+
UserRegistrationTokens::Table,
152+
UserRegistrationTokens::RevokedAt,
153+
))
154+
.is_null(),
155+
)
156+
// Has not expired
157+
.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+
}))
89177
}
90178
}
91179

@@ -462,8 +550,10 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> {
462550

463551
#[cfg(test)]
464552
mod tests {
465-
use chrono::{DateTime, Utc};
466-
use mas_storage::{Pagination, clock::MockClock, user::UserRegistrationTokenFilter};
553+
use chrono::Duration;
554+
use mas_storage::{
555+
Clock as _, Pagination, clock::MockClock, user::UserRegistrationTokenFilter,
556+
};
467557
use rand::SeedableRng;
468558
use rand_chacha::ChaChaRng;
469559
use sqlx::PgPool;
@@ -498,8 +588,8 @@ mod tests {
498588
.unwrap();
499589

500590
// 3. A token that is expired
501-
let past_time = DateTime::<Utc>::from_timestamp(0, 0).unwrap();
502-
let _token3 = repo
591+
let past_time = clock.now() - Duration::days(1);
592+
let token3 = repo
503593
.user_registration_token()
504594
.add(&mut rng, &clock, "token3".to_owned(), None, Some(past_time))
505595
.await
@@ -518,7 +608,7 @@ mod tests {
518608
.unwrap();
519609

520610
// Test list with empty filter
521-
let empty_filter = UserRegistrationTokenFilter::new();
611+
let empty_filter = UserRegistrationTokenFilter::new(clock.now());
522612
let page = repo
523613
.user_registration_token()
524614
.list(empty_filter, Pagination::first(10))
@@ -535,7 +625,7 @@ mod tests {
535625
assert_eq!(count, 4);
536626

537627
// Test has_been_used filter
538-
let used_filter = UserRegistrationTokenFilter::new().with_been_used(true);
628+
let used_filter = UserRegistrationTokenFilter::new(clock.now()).with_been_used(true);
539629
let page = repo
540630
.user_registration_token()
541631
.list(used_filter, Pagination::first(10))
@@ -545,16 +635,34 @@ mod tests {
545635
assert_eq!(page.edges[0].id, token2.id);
546636

547637
// Test unused filter
548-
let unused_filter = UserRegistrationTokenFilter::new().with_been_used(false);
638+
let unused_filter = UserRegistrationTokenFilter::new(clock.now()).with_been_used(false);
549639
let page = repo
550640
.user_registration_token()
551641
.list(unused_filter, Pagination::first(10))
552642
.await
553643
.unwrap();
554644
assert_eq!(page.edges.len(), 3);
555645

646+
// Test is_expired filter
647+
let expired_filter = UserRegistrationTokenFilter::new(clock.now()).with_expired(true);
648+
let page = repo
649+
.user_registration_token()
650+
.list(expired_filter, Pagination::first(10))
651+
.await
652+
.unwrap();
653+
assert_eq!(page.edges.len(), 1);
654+
assert_eq!(page.edges[0].id, token3.id);
655+
656+
let not_expired_filter = UserRegistrationTokenFilter::new(clock.now()).with_expired(false);
657+
let page = repo
658+
.user_registration_token()
659+
.list(not_expired_filter, Pagination::first(10))
660+
.await
661+
.unwrap();
662+
assert_eq!(page.edges.len(), 3);
663+
556664
// Test is_revoked filter
557-
let revoked_filter = UserRegistrationTokenFilter::new().with_revoked(true);
665+
let revoked_filter = UserRegistrationTokenFilter::new(clock.now()).with_revoked(true);
558666
let page = repo
559667
.user_registration_token()
560668
.list(revoked_filter, Pagination::first(10))
@@ -563,8 +671,33 @@ mod tests {
563671
assert_eq!(page.edges.len(), 1);
564672
assert_eq!(page.edges[0].id, token4.id);
565673

674+
let not_revoked_filter = UserRegistrationTokenFilter::new(clock.now()).with_revoked(false);
675+
let page = repo
676+
.user_registration_token()
677+
.list(not_revoked_filter, Pagination::first(10))
678+
.await
679+
.unwrap();
680+
assert_eq!(page.edges.len(), 3);
681+
682+
// Test is_valid filter
683+
let valid_filter = UserRegistrationTokenFilter::new(clock.now()).with_valid(true);
684+
let page = repo
685+
.user_registration_token()
686+
.list(valid_filter, Pagination::first(10))
687+
.await
688+
.unwrap();
689+
assert_eq!(page.edges.len(), 2);
690+
691+
let invalid_filter = UserRegistrationTokenFilter::new(clock.now()).with_valid(false);
692+
let page = repo
693+
.user_registration_token()
694+
.list(invalid_filter, Pagination::first(10))
695+
.await
696+
.unwrap();
697+
assert_eq!(page.edges.len(), 2);
698+
566699
// Test combined filters
567-
let combined_filter = UserRegistrationTokenFilter::new()
700+
let combined_filter = UserRegistrationTokenFilter::new(clock.now())
568701
.with_been_used(false)
569702
.with_revoked(true);
570703
let page = repo

crates/storage/src/user/registration_token.rs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,26 @@ use ulid::Ulid;
1212
use crate::{Clock, repository_impl};
1313

1414
/// A filter to apply when listing [`UserRegistrationToken`]s
15-
#[derive(Debug, Default, Clone, Copy)]
15+
#[derive(Debug, Clone, Copy)]
1616
pub struct UserRegistrationTokenFilter {
17+
now: DateTime<Utc>,
1718
has_been_used: Option<bool>,
1819
is_revoked: Option<bool>,
20+
is_expired: Option<bool>,
21+
is_valid: Option<bool>,
1922
}
2023

2124
impl UserRegistrationTokenFilter {
2225
/// Create a new empty filter
2326
#[must_use]
24-
pub fn new() -> Self {
25-
Self::default()
27+
pub fn new(now: DateTime<Utc>) -> Self {
28+
Self {
29+
now,
30+
has_been_used: None,
31+
is_revoked: None,
32+
is_expired: None,
33+
is_valid: None,
34+
}
2635
}
2736

2837
/// Filter by whether the token has been used at least once
@@ -39,6 +48,21 @@ impl UserRegistrationTokenFilter {
3948
self
4049
}
4150

51+
/// Filter by expired status
52+
#[must_use]
53+
pub fn with_expired(mut self, is_expired: bool) -> Self {
54+
self.is_expired = Some(is_expired);
55+
self
56+
}
57+
58+
/// Filter by valid status (meaning: not expired, not revoked, and still
59+
/// with uses left)
60+
#[must_use]
61+
pub fn with_valid(mut self, is_valid: bool) -> Self {
62+
self.is_valid = Some(is_valid);
63+
self
64+
}
65+
4266
/// Get the used status filter
4367
///
4468
/// Returns [`None`] if no used status filter was set
@@ -54,6 +78,28 @@ impl UserRegistrationTokenFilter {
5478
pub fn is_revoked(&self) -> Option<bool> {
5579
self.is_revoked
5680
}
81+
82+
/// Get the expired status filter
83+
///
84+
/// Returns [`None`] if no expired status filter was set
85+
#[must_use]
86+
pub fn is_expired(&self) -> Option<bool> {
87+
self.is_expired
88+
}
89+
90+
/// Get the valid status filter
91+
///
92+
/// Returns [`None`] if no valid status filter was set
93+
#[must_use]
94+
pub fn is_valid(&self) -> Option<bool> {
95+
self.is_valid
96+
}
97+
98+
/// Get the current time for this filter evaluation
99+
#[must_use]
100+
pub fn now(&self) -> DateTime<Utc> {
101+
self.now
102+
}
57103
}
58104

59105
/// A [`UserRegistrationTokenRepository`] helps interacting with

0 commit comments

Comments
 (0)