1use std::sync::Arc;
2
3use chrono::{DateTime, Duration, Utc};
4use sqlx::SqlitePool;
5
6use crate::db::Db;
7use crate::email::{EmailMessage, EmailSender, EmailTemplate, NoopEmailSender, fallback_username};
8use crate::error::AuthError;
9use crate::event_sink::{AuthEvent, EventSink, NoopEventSink};
10use crate::sessions::{self, SessionConfig};
11use crate::types::{Email, SessionToken, User, UserId};
12
13pub type OnUserActive = Arc<dyn Fn(UserId, DateTime<Utc>) + Send + Sync>;
22
23pub struct LoginOutcome {
25 pub user: User,
26 pub token: SessionToken,
27 pub set_cookie: String,
29}
30
31#[derive(Debug, thiserror::Error)]
33pub enum BuildError {
34 #[error("database error: {0}")]
36 Database(#[from] AuthError),
37
38 #[error("invalid configuration: {0}")]
41 InvalidConfig(&'static str),
42}
43
44enum PoolSource {
45 Url(String),
46 Pool(SqlitePool),
47}
48
49pub struct AllowThemBuilder {
51 pool_source: PoolSource,
52 session_ttl: Option<Duration>,
53 cookie_name: Option<&'static str>,
54 cookie_secure: Option<bool>,
55 cookie_domain: String,
56 mfa_key: Option<[u8; 32]>,
57 signing_key: Option<[u8; 32]>,
58 csrf_key: Option<[u8; 32]>,
59 base_url: Option<String>,
60 on_user_active: Option<OnUserActive>,
61 email_sender: Option<Box<dyn EmailSender>>,
62 event_sink: Option<Box<dyn EventSink>>,
63}
64
65impl AllowThemBuilder {
66 pub fn new(url: impl Into<String>) -> Self {
71 Self {
72 pool_source: PoolSource::Url(url.into()),
73 session_ttl: None,
74 cookie_name: None,
75 cookie_secure: None,
76 cookie_domain: String::new(),
77 mfa_key: None,
78 signing_key: None,
79 csrf_key: None,
80 base_url: None,
81 on_user_active: None,
82 email_sender: None,
83 event_sink: None,
84 }
85 }
86
87 pub fn with_pool(pool: SqlitePool) -> Self {
92 Self {
93 pool_source: PoolSource::Pool(pool),
94 session_ttl: None,
95 cookie_name: None,
96 cookie_secure: None,
97 cookie_domain: String::new(),
98 mfa_key: None,
99 signing_key: None,
100 csrf_key: None,
101 base_url: None,
102 on_user_active: None,
103 email_sender: None,
104 event_sink: None,
105 }
106 }
107
108 pub fn session_ttl(mut self, ttl: Duration) -> Self {
110 self.session_ttl = Some(ttl);
111 self
112 }
113
114 pub fn cookie_name(mut self, name: &'static str) -> Self {
116 self.cookie_name = Some(name);
117 self
118 }
119
120 pub fn cookie_secure(mut self, secure: bool) -> Self {
124 self.cookie_secure = Some(secure);
125 self
126 }
127
128 pub fn cookie_domain(mut self, domain: impl Into<String>) -> Self {
133 self.cookie_domain = domain.into();
134 self
135 }
136
137 pub fn mfa_key(mut self, key: [u8; 32]) -> Self {
142 self.mfa_key = Some(key);
143 self
144 }
145
146 pub fn signing_key(mut self, key: [u8; 32]) -> Self {
151 self.signing_key = Some(key);
152 self
153 }
154
155 pub fn base_url(mut self, url: impl Into<String>) -> Self {
161 self.base_url = Some(url.into());
162 self
163 }
164
165 pub fn csrf_key(mut self, key: [u8; 32]) -> Self {
171 self.csrf_key = Some(key);
172 self
173 }
174
175 pub fn on_user_active(mut self, callback: OnUserActive) -> Self {
188 self.on_user_active = Some(callback);
189 self
190 }
191
192 pub fn email_sender(mut self, sender: Box<dyn EmailSender>) -> Self {
204 self.email_sender = Some(sender);
205 self
206 }
207
208 pub fn event_sink(mut self, sink: Box<dyn EventSink>) -> Self {
216 self.event_sink = Some(sink);
217 self
218 }
219
220 pub async fn build(self) -> Result<AllowThem, BuildError> {
225 let db = match self.pool_source {
226 PoolSource::Url(url) => Db::connect(&url).await?,
227 PoolSource::Pool(pool) => Db::new(pool).await?,
228 };
229
230 let defaults = SessionConfig::default();
231 let session_config = SessionConfig {
232 ttl: self.session_ttl.unwrap_or(defaults.ttl),
233 cookie_name: self.cookie_name.unwrap_or(defaults.cookie_name),
234 secure: self.cookie_secure.unwrap_or(defaults.secure),
235 };
236
237 let email_sender = self.email_sender.unwrap_or_else(|| {
238 tracing::warn!(
239 "no email_sender configured; defaulting to NoopEmailSender — \
240 outgoing emails (password reset, verification, invitation, \
241 MFA recovery) will be silently dropped",
242 );
243 Box::new(NoopEmailSender)
244 });
245
246 let event_sink = self.event_sink.unwrap_or_else(|| Box::new(NoopEventSink));
247
248 Ok(AllowThem {
249 inner: Arc::new(Inner {
250 db,
251 session_config,
252 cookie_domain: self.cookie_domain,
253 mfa_key: self.mfa_key,
254 signing_key: self.signing_key,
255 csrf_key: self.csrf_key,
256 base_url: self.base_url,
257 on_user_active: self.on_user_active,
258 email_sender,
259 event_sink,
260 }),
261 })
262 }
263}
264
265struct Inner {
266 db: Db,
267 session_config: SessionConfig,
268 cookie_domain: String,
269 mfa_key: Option<[u8; 32]>,
270 signing_key: Option<[u8; 32]>,
271 csrf_key: Option<[u8; 32]>,
272 base_url: Option<String>,
273 on_user_active: Option<OnUserActive>,
274 email_sender: Box<dyn EmailSender>,
275 event_sink: Box<dyn EventSink>,
276}
277
278#[derive(Clone)]
284pub struct AllowThem {
285 inner: Arc<Inner>,
286}
287
288impl AllowThem {
289 pub fn db(&self) -> &Db {
294 &self.inner.db
295 }
296
297 pub fn session_config(&self) -> &SessionConfig {
299 &self.inner.session_config
300 }
301
302 pub fn session_cookie(&self, token: &SessionToken) -> String {
307 sessions::session_cookie(token, &self.inner.session_config, &self.inner.cookie_domain)
308 }
309
310 pub(crate) fn mfa_key(&self) -> Result<&[u8; 32], AuthError> {
312 self.inner
313 .mfa_key
314 .as_ref()
315 .ok_or(AuthError::MfaNotConfigured)
316 }
317
318 pub(crate) fn signing_key(&self) -> Result<&[u8; 32], AuthError> {
320 self.inner
321 .signing_key
322 .as_ref()
323 .ok_or(AuthError::SigningKeyNotConfigured)
324 }
325
326 pub fn base_url(&self) -> Result<&str, AuthError> {
328 self.inner
329 .base_url
330 .as_deref()
331 .ok_or(AuthError::BaseUrlNotConfigured)
332 }
333
334 pub fn csrf_key(&self) -> Result<&[u8; 32], AuthError> {
335 self.inner
336 .csrf_key
337 .as_ref()
338 .ok_or(AuthError::CsrfKeyNotConfigured)
339 }
340
341 pub fn on_user_active(&self) -> Option<&OnUserActive> {
347 self.inner.on_user_active.as_ref()
348 }
349
350 pub fn email_sender(&self) -> &dyn EmailSender {
355 &*self.inner.email_sender
356 }
357
358 pub fn event_sink(&self) -> &dyn EventSink {
363 &*self.inner.event_sink
364 }
365
366 pub async fn emit_event(&self, event: AuthEvent) {
372 self.event_sink().emit(&event).await;
373 }
374
375 pub async fn send_password_reset_email(&self, email: &Email) -> Result<(), AuthError> {
388 let raw_token = match self.db().create_password_reset(email).await? {
389 None => return Ok(()),
390 Some(t) => t,
391 };
392
393 let username = match self.db().get_user_by_email(email).await {
394 Ok(user) => fallback_username(&user),
395 Err(_) => email
396 .as_str()
397 .split('@')
398 .next()
399 .unwrap_or("there")
400 .to_owned(),
401 };
402
403 let reset_url = format!(
404 "{}/auth/reset-password?token={}",
405 self.base_url()?,
406 raw_token
407 );
408 let message = EmailMessage {
409 to: email.as_str().to_owned(),
410 subject: "Reset your password".to_owned(),
411 template: EmailTemplate::PasswordReset {
412 url: reset_url,
413 username,
414 },
415 };
416
417 self.email_sender()
418 .send(&message)
419 .await
420 .map_err(|e| AuthError::Email(e.to_string()))
421 }
422
423 pub async fn send_verification_email(
431 &self,
432 user_id: UserId,
433 email: &Email,
434 ) -> Result<(), AuthError> {
435 let raw_token = self.db().create_email_verification(user_id).await?;
436
437 let username = match self.db().get_user(user_id).await {
438 Ok(user) => fallback_username(&user),
439 Err(_) => email
440 .as_str()
441 .split('@')
442 .next()
443 .unwrap_or("there")
444 .to_owned(),
445 };
446
447 let verify_url = format!("{}/auth/verify-email?token={}", self.base_url()?, raw_token);
448 let message = EmailMessage {
449 to: email.as_str().to_owned(),
450 subject: "Verify your email address".to_owned(),
451 template: EmailTemplate::EmailVerification {
452 url: verify_url,
453 username,
454 },
455 };
456
457 self.email_sender()
458 .send(&message)
459 .await
460 .map_err(|e| AuthError::Email(e.to_string()))
461 }
462
463 pub async fn send_invitation_email(
473 &self,
474 email: &Email,
475 invitation_url: &str,
476 invited_by: UserId,
477 expires_at: chrono::DateTime<chrono::Utc>,
478 ) -> Result<(), AuthError> {
479 self.db()
480 .create_invitation(Some(email), None, Some(invited_by), expires_at)
481 .await?;
482
483 let inviter_name = match self.db().get_user(invited_by).await {
484 Ok(user) => fallback_username(&user),
485 Err(_) => "your team".to_owned(),
486 };
487
488 let message = EmailMessage {
489 to: email.as_str().to_owned(),
490 subject: format!("You've been invited by {inviter_name}"),
491 template: EmailTemplate::Invitation {
492 url: invitation_url.to_owned(),
493 invited_by: inviter_name,
494 },
495 };
496
497 self.email_sender()
498 .send(&message)
499 .await
500 .map_err(|e| AuthError::Email(e.to_string()))
501 }
502
503 pub async fn send_mfa_recovery_email(
510 &self,
511 user_id: UserId,
512 codes: Vec<String>,
513 ) -> Result<(), AuthError> {
514 let user = self.db().get_user(user_id).await?;
515 let username = fallback_username(&user);
516
517 let message = EmailMessage {
518 to: user.email.as_str().to_owned(),
519 subject: "Your MFA recovery codes".to_owned(),
520 template: EmailTemplate::MfaRecovery { codes, username },
521 };
522
523 self.email_sender()
524 .send(&message)
525 .await
526 .map_err(|e| AuthError::Email(e.to_string()))
527 }
528
529 pub fn notify_user_active(&self, user_id: UserId) {
535 let Some(cb) = self.inner.on_user_active.as_ref() else {
536 return;
537 };
538 let now = Utc::now();
539 let cb = cb.clone();
540 if let Err(_payload) =
541 std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || cb(user_id, now)))
542 {
543 tracing::error!(user_id = %user_id, "on_user_active callback panicked");
544 }
545 }
546
547 pub async fn get_decrypted_signing_key(
552 &self,
553 ) -> Result<(crate::signing_keys::SigningKey, String), AuthError> {
554 let enc_key = self.signing_key()?;
555 let key = self.db().get_active_signing_key().await?;
556 let pem = crate::signing_keys::decrypt_private_key(&key, enc_key)?;
557 Ok((key, pem))
558 }
559
560 pub fn clear_session_cookie(&self) -> String {
566 sessions::clear_session_cookie(&self.inner.session_config, &self.inner.cookie_domain)
567 }
568
569 pub fn parse_session_cookie(&self, cookie_header: &str) -> Option<SessionToken> {
573 sessions::parse_session_cookie(cookie_header, self.inner.session_config.cookie_name)
574 }
575
576 pub async fn login(&self, identifier: &str, password: &str) -> Result<LoginOutcome, AuthError> {
586 use crate::audit::AuditEvent;
587 use crate::password::verify_password;
588
589 let user = self
590 .db()
591 .find_for_login(identifier)
592 .await
593 .map_err(|e| match e {
594 AuthError::NotFound => AuthError::InvalidCredentials,
595 other => other,
596 })?;
597
598 if !user.is_active {
599 return Err(AuthError::InvalidCredentials);
600 }
601
602 let hash = user
603 .password_hash
604 .as_ref()
605 .ok_or(AuthError::InvalidCredentials)?;
606
607 if !verify_password(password, hash)? {
608 return Err(AuthError::InvalidCredentials);
609 }
610
611 let token = sessions::generate_token();
612 let token_hash = sessions::hash_token(&token);
613 let expires_at = Utc::now() + self.inner.session_config.ttl;
614 self.db()
615 .create_session(user.id, token_hash, None, None, expires_at)
616 .await?;
617
618 let _ = self
619 .db()
620 .log_audit(AuditEvent::Login, Some(&user.id), None, None, None, None)
621 .await;
622
623 self.notify_user_active(user.id);
624 self.emit_event(AuthEvent::new(
625 "session.created",
626 Some(user.id),
627 serde_json::json!({ "user_id": user.id }),
628 ))
629 .await;
630
631 let set_cookie = self.session_cookie(&token);
632 Ok(LoginOutcome {
633 user,
634 token,
635 set_cookie,
636 })
637 }
638
639 pub async fn create_session_cookie(&self, user_id: UserId) -> Result<LoginOutcome, AuthError> {
645 let user = self.db().get_user(user_id).await?;
646 let token = sessions::generate_token();
647 let token_hash = sessions::hash_token(&token);
648 let expires_at = Utc::now() + self.inner.session_config.ttl;
649 self.db()
650 .create_session(user_id, token_hash, None, None, expires_at)
651 .await?;
652
653 self.notify_user_active(user_id);
654 self.emit_event(AuthEvent::new(
655 "session.created",
656 Some(user_id),
657 serde_json::json!({ "user_id": user_id }),
658 ))
659 .await;
660
661 let set_cookie = self.session_cookie(&token);
662 Ok(LoginOutcome {
663 user,
664 token,
665 set_cookie,
666 })
667 }
668}
669
670#[cfg(test)]
671mod tests {
672 use sqlx::sqlite::SqliteConnectOptions;
673 use std::str::FromStr;
674
675 use super::*;
676 use crate::sessions::generate_token;
677 use crate::types::Email;
678
679 #[tokio::test]
680 async fn build_with_url_defaults() {
681 let ath = AllowThemBuilder::new("sqlite::memory:")
682 .build()
683 .await
684 .unwrap();
685
686 let config = ath.session_config();
687 assert_eq!(config.ttl, Duration::hours(24));
688 assert_eq!(config.cookie_name, "allowthem_session");
689 assert!(config.secure);
690
691 let token = generate_token();
692 let cookie = ath.session_cookie(&token);
693 assert!(!cookie.contains("; Domain="));
694 }
695
696 #[tokio::test]
697 async fn build_with_pool() {
698 let opts = SqliteConnectOptions::from_str("sqlite::memory:")
699 .unwrap()
700 .pragma("foreign_keys", "ON");
701 let pool = sqlx::SqlitePool::connect_with(opts).await.unwrap();
702
703 let ath = AllowThemBuilder::with_pool(pool).build().await.unwrap();
704
705 let email = Email::new("test@example.com".into()).unwrap();
706 let user = ath.db().create_user(email, "password123", None, None).await;
707 assert!(user.is_ok());
708 }
709
710 #[tokio::test]
711 async fn build_with_overrides() {
712 let ath = AllowThemBuilder::new("sqlite::memory:")
713 .session_ttl(Duration::hours(48))
714 .cookie_name("my_session")
715 .cookie_secure(false)
716 .cookie_domain("example.com")
717 .build()
718 .await
719 .unwrap();
720
721 let config = ath.session_config();
722 assert_eq!(config.ttl, Duration::hours(48));
723 assert_eq!(config.cookie_name, "my_session");
724 assert!(!config.secure);
725 }
726
727 #[tokio::test]
728 async fn session_cookie_uses_config() {
729 let ath = AllowThemBuilder::new("sqlite::memory:")
730 .cookie_name("custom")
731 .cookie_secure(false)
732 .cookie_domain("example.com")
733 .build()
734 .await
735 .unwrap();
736
737 let token = generate_token();
738 let cookie = ath.session_cookie(&token);
739
740 assert!(cookie.contains("custom="));
741 assert!(cookie.contains("; Domain=example.com"));
742 assert!(!cookie.contains("; Secure"));
743 }
744
745 #[tokio::test]
746 async fn clear_session_cookie_defaults() {
747 let ath = AllowThemBuilder::new("sqlite::memory:")
748 .build()
749 .await
750 .unwrap();
751
752 let cookie = ath.clear_session_cookie();
753 assert!(cookie.starts_with("allowthem_session=;"));
754 assert!(cookie.contains("; Max-Age=0"));
755 assert!(!cookie.contains("; Domain="));
756 assert!(cookie.contains("; Secure"));
757 }
758
759 #[tokio::test]
760 async fn clear_session_cookie_name_matches_session_cookie() {
761 let ath = AllowThemBuilder::new("sqlite::memory:")
762 .cookie_name("app_session")
763 .build()
764 .await
765 .unwrap();
766
767 let token = generate_token();
768 let set = ath.session_cookie(&token);
769 let clear = ath.clear_session_cookie();
770
771 assert!(set.starts_with("app_session="));
773 assert!(clear.starts_with("app_session=;"));
774 assert!(clear.contains("; Path=/"));
775 assert!(clear.contains("; Max-Age=0"));
776 }
777
778 #[tokio::test]
779 async fn clear_session_cookie_with_domain_and_no_secure() {
780 let ath = AllowThemBuilder::new("sqlite::memory:")
781 .cookie_name("my_session")
782 .cookie_secure(false)
783 .cookie_domain("example.com")
784 .build()
785 .await
786 .unwrap();
787
788 let cookie = ath.clear_session_cookie();
789 assert!(cookie.starts_with("my_session=;"));
790 assert!(cookie.contains("; Max-Age=0"));
791 assert!(cookie.contains("; Domain=example.com"));
792 assert!(!cookie.contains("; Secure"));
793 }
794
795 #[tokio::test]
796 async fn parse_session_cookie_uses_config() {
797 let ath = AllowThemBuilder::new("sqlite::memory:")
798 .cookie_name("custom")
799 .build()
800 .await
801 .unwrap();
802
803 let header = "custom=abc123; other=xyz";
804 let result = ath.parse_session_cookie(header);
805
806 assert!(result.is_some());
807 assert_eq!(result.unwrap().as_str(), "abc123");
808 }
809
810 #[tokio::test]
811 async fn build_with_bad_url_fails() {
812 let result = AllowThemBuilder::new("not-a-url").build().await;
813
814 assert!(result.is_err());
815 assert!(matches!(result.err().unwrap(), BuildError::Database(_)));
816 }
817
818 #[tokio::test]
819 async fn clone_shares_state() {
820 let ath = AllowThemBuilder::new("sqlite::memory:")
821 .build()
822 .await
823 .unwrap();
824 let ath2 = ath.clone();
825
826 let email = Email::new("shared@example.com".into()).unwrap();
827 let user = ath
828 .db()
829 .create_user(email, "password123", None, None)
830 .await
831 .unwrap();
832
833 let found = ath2.db().get_user(user.id).await;
834 assert!(found.is_ok());
835 assert_eq!(found.unwrap().id, user.id);
836 }
837
838 #[tokio::test]
839 async fn signing_key_not_configured_returns_error() {
840 let ath = AllowThemBuilder::new("sqlite::memory:")
841 .build()
842 .await
843 .unwrap();
844 let result = ath.signing_key();
845 assert!(matches!(
846 result,
847 Err(crate::error::AuthError::SigningKeyNotConfigured)
848 ));
849 }
850
851 #[tokio::test]
852 async fn base_url_not_configured_returns_error() {
853 let ath = AllowThemBuilder::new("sqlite::memory:")
854 .build()
855 .await
856 .unwrap();
857 let result = ath.base_url();
858 assert!(matches!(
859 result,
860 Err(crate::error::AuthError::BaseUrlNotConfigured)
861 ));
862 }
863
864 #[tokio::test]
865 async fn base_url_configured_returns_value() {
866 let ath = AllowThemBuilder::new("sqlite::memory:")
867 .base_url("https://auth.example.com")
868 .build()
869 .await
870 .unwrap();
871 let result = ath.base_url();
872 assert!(matches!(result, Ok("https://auth.example.com")));
873 }
874
875 #[tokio::test]
876 async fn login_success() {
877 let ath = AllowThemBuilder::new("sqlite::memory:")
878 .cookie_secure(false)
879 .build()
880 .await
881 .unwrap();
882
883 let email = Email::new("login@example.com".into()).unwrap();
884 ath.db()
885 .create_user(email, "secret", None, None)
886 .await
887 .unwrap();
888
889 let outcome = ath.login("login@example.com", "secret").await.unwrap();
890 assert_eq!(outcome.user.email.as_str(), "login@example.com");
891 assert!(!outcome.token.as_str().is_empty());
892 assert!(outcome.set_cookie.contains("allowthem_session="));
893 }
894
895 #[tokio::test]
896 async fn login_wrong_password() {
897 let ath = AllowThemBuilder::new("sqlite::memory:")
898 .build()
899 .await
900 .unwrap();
901
902 let email = Email::new("wp@example.com".into()).unwrap();
903 ath.db()
904 .create_user(email, "correct", None, None)
905 .await
906 .unwrap();
907
908 let result = ath.login("wp@example.com", "wrong").await;
909 assert!(matches!(result, Err(AuthError::InvalidCredentials)));
910 }
911
912 #[tokio::test]
913 async fn login_unknown_identifier() {
914 let ath = AllowThemBuilder::new("sqlite::memory:")
915 .build()
916 .await
917 .unwrap();
918
919 let result = ath.login("nobody@example.com", "any").await;
920 assert!(matches!(result, Err(AuthError::InvalidCredentials)));
921 }
922
923 #[tokio::test]
924 async fn login_inactive_user() {
925 let ath = AllowThemBuilder::new("sqlite::memory:")
926 .build()
927 .await
928 .unwrap();
929
930 let email = Email::new("inactive@example.com".into()).unwrap();
931 let user = ath
932 .db()
933 .create_user(email, "secret", None, None)
934 .await
935 .unwrap();
936 ath.db().update_user_active(user.id, false).await.unwrap();
937
938 let result = ath.login("inactive@example.com", "secret").await;
939 assert!(matches!(result, Err(AuthError::InvalidCredentials)));
940 }
941
942 #[tokio::test]
943 async fn login_no_password_hash() {
944 use crate::types::UserId;
945
946 let ath = AllowThemBuilder::new("sqlite::memory:")
947 .build()
948 .await
949 .unwrap();
950
951 let id = UserId::new();
954 let now = chrono::Utc::now()
955 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
956 .to_string();
957 sqlx::query(
958 "INSERT INTO allowthem_users \
959 (id, email, username, password_hash, email_verified, is_active, created_at, updated_at) \
960 VALUES (?, 'sso@example.com', NULL, NULL, 1, 1, ?, ?)",
961 )
962 .bind(id)
963 .bind(&now)
964 .bind(&now)
965 .execute(ath.db().pool())
966 .await
967 .unwrap();
968
969 let result = ath.login("sso@example.com", "any").await;
970 assert!(matches!(result, Err(AuthError::InvalidCredentials)));
971 }
972
973 #[tokio::test]
974 async fn create_session_cookie_success() {
975 let ath = AllowThemBuilder::new("sqlite::memory:")
976 .cookie_secure(false)
977 .build()
978 .await
979 .unwrap();
980
981 let email = Email::new("sess@example.com".into()).unwrap();
982 let user = ath
983 .db()
984 .create_user(email, "secret", None, None)
985 .await
986 .unwrap();
987
988 let outcome = ath.create_session_cookie(user.id).await.unwrap();
989 assert_eq!(outcome.user.id, user.id);
990 assert!(!outcome.token.as_str().is_empty());
991 assert!(outcome.set_cookie.contains("allowthem_session="));
992
993 let session = ath.db().lookup_session(&outcome.token).await.unwrap();
995 assert!(session.is_some());
996 }
997
998 #[tokio::test]
999 async fn create_session_cookie_unknown_user() {
1000 use crate::types::UserId;
1001
1002 let ath = AllowThemBuilder::new("sqlite::memory:")
1003 .build()
1004 .await
1005 .unwrap();
1006
1007 let result = ath.create_session_cookie(UserId::new()).await;
1008 assert!(matches!(result, Err(AuthError::NotFound)));
1009 }
1010
1011 #[tokio::test]
1014 async fn on_user_active_default_is_none() {
1015 let ath = AllowThemBuilder::new("sqlite::memory:")
1016 .build()
1017 .await
1018 .unwrap();
1019 assert!(ath.on_user_active().is_none());
1020 }
1021
1022 #[tokio::test]
1023 async fn on_user_active_builder_stores_callback() {
1024 use std::sync::Arc;
1025 use std::sync::atomic::{AtomicU64, Ordering};
1026
1027 let counter = Arc::new(AtomicU64::new(0));
1028 let c = counter.clone();
1029 let cb: OnUserActive = Arc::new(move |_uid, _ts| {
1030 c.fetch_add(1, Ordering::Relaxed);
1031 });
1032
1033 let ath = AllowThemBuilder::new("sqlite::memory:")
1034 .on_user_active(cb)
1035 .build()
1036 .await
1037 .unwrap();
1038
1039 assert!(ath.on_user_active().is_some());
1040 }
1041
1042 #[tokio::test]
1043 async fn on_user_active_fires_on_login_success() {
1044 use std::sync::{Arc, Mutex};
1045
1046 let captured: Arc<Mutex<Vec<(UserId, DateTime<Utc>)>>> = Arc::new(Mutex::new(Vec::new()));
1047 let cap = captured.clone();
1048 let cb: OnUserActive = Arc::new(move |uid, ts| {
1049 cap.lock().unwrap().push((uid, ts));
1050 });
1051
1052 let ath = AllowThemBuilder::new("sqlite::memory:")
1053 .cookie_secure(false)
1054 .on_user_active(cb)
1055 .build()
1056 .await
1057 .unwrap();
1058
1059 let email = Email::new("active@example.com".into()).unwrap();
1060 let user = ath
1061 .db()
1062 .create_user(email, "hunter2", None, None)
1063 .await
1064 .unwrap();
1065
1066 let before = Utc::now();
1067 ath.login("active@example.com", "hunter2").await.unwrap();
1068 let after = Utc::now();
1069
1070 let events = captured.lock().unwrap();
1071 assert_eq!(events.len(), 1, "callback must fire exactly once");
1072 assert_eq!(events[0].0, user.id, "callback receives correct UserId");
1073 assert!(
1074 events[0].1 >= before && events[0].1 <= after,
1075 "callback timestamp must be within the test window"
1076 );
1077 }
1078
1079 #[tokio::test]
1080 async fn on_user_active_no_fire_on_login_wrong_password() {
1081 use std::sync::Arc;
1082 use std::sync::atomic::{AtomicU64, Ordering};
1083
1084 let counter = Arc::new(AtomicU64::new(0));
1085 let c = counter.clone();
1086 let cb: OnUserActive = Arc::new(move |_uid, _ts| {
1087 c.fetch_add(1, Ordering::Relaxed);
1088 });
1089
1090 let ath = AllowThemBuilder::new("sqlite::memory:")
1091 .on_user_active(cb)
1092 .build()
1093 .await
1094 .unwrap();
1095
1096 let email = Email::new("wrongpw@example.com".into()).unwrap();
1097 ath.db()
1098 .create_user(email, "correct", None, None)
1099 .await
1100 .unwrap();
1101
1102 let _ = ath.login("wrongpw@example.com", "wrong").await;
1103 assert_eq!(counter.load(Ordering::Relaxed), 0);
1104 }
1105
1106 #[tokio::test]
1107 async fn on_user_active_no_fire_on_login_unknown_identifier() {
1108 use std::sync::Arc;
1109 use std::sync::atomic::{AtomicU64, Ordering};
1110
1111 let counter = Arc::new(AtomicU64::new(0));
1112 let c = counter.clone();
1113 let cb: OnUserActive = Arc::new(move |_uid, _ts| {
1114 c.fetch_add(1, Ordering::Relaxed);
1115 });
1116
1117 let ath = AllowThemBuilder::new("sqlite::memory:")
1118 .on_user_active(cb)
1119 .build()
1120 .await
1121 .unwrap();
1122
1123 let _ = ath.login("nobody@example.com", "any").await;
1124 assert_eq!(counter.load(Ordering::Relaxed), 0);
1125 }
1126
1127 #[tokio::test]
1128 async fn on_user_active_no_fire_on_login_inactive_user() {
1129 use std::sync::Arc;
1130 use std::sync::atomic::{AtomicU64, Ordering};
1131
1132 let counter = Arc::new(AtomicU64::new(0));
1133 let c = counter.clone();
1134 let cb: OnUserActive = Arc::new(move |_uid, _ts| {
1135 c.fetch_add(1, Ordering::Relaxed);
1136 });
1137
1138 let ath = AllowThemBuilder::new("sqlite::memory:")
1139 .on_user_active(cb)
1140 .build()
1141 .await
1142 .unwrap();
1143
1144 let email = Email::new("inact@example.com".into()).unwrap();
1145 let user = ath
1146 .db()
1147 .create_user(email, "secret", None, None)
1148 .await
1149 .unwrap();
1150 ath.db().update_user_active(user.id, false).await.unwrap();
1151
1152 let _ = ath.login("inact@example.com", "secret").await;
1153 assert_eq!(counter.load(Ordering::Relaxed), 0);
1154 }
1155
1156 #[tokio::test]
1157 async fn on_user_active_no_fire_on_login_no_password_hash() {
1158 use std::sync::Arc;
1159 use std::sync::atomic::{AtomicU64, Ordering};
1160
1161 let counter = Arc::new(AtomicU64::new(0));
1162 let c = counter.clone();
1163 let cb: OnUserActive = Arc::new(move |_uid, _ts| {
1164 c.fetch_add(1, Ordering::Relaxed);
1165 });
1166
1167 let ath = AllowThemBuilder::new("sqlite::memory:")
1168 .on_user_active(cb)
1169 .build()
1170 .await
1171 .unwrap();
1172
1173 let id = UserId::new();
1174 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
1175 sqlx::query(
1176 "INSERT INTO allowthem_users \
1177 (id, email, username, password_hash, email_verified, is_active, created_at, updated_at) \
1178 VALUES (?, 'sso2@example.com', NULL, NULL, 1, 1, ?, ?)",
1179 )
1180 .bind(id)
1181 .bind(&now)
1182 .bind(&now)
1183 .execute(ath.db().pool())
1184 .await
1185 .unwrap();
1186
1187 let _ = ath.login("sso2@example.com", "any").await;
1188 assert_eq!(counter.load(Ordering::Relaxed), 0);
1189 }
1190
1191 #[tokio::test]
1192 async fn on_user_active_fires_on_create_session_cookie() {
1193 use std::sync::Arc;
1194 use std::sync::atomic::{AtomicU64, Ordering};
1195
1196 let counter = Arc::new(AtomicU64::new(0));
1197 let c = counter.clone();
1198 let cb: OnUserActive = Arc::new(move |_uid, _ts| {
1199 c.fetch_add(1, Ordering::Relaxed);
1200 });
1201
1202 let ath = AllowThemBuilder::new("sqlite::memory:")
1203 .cookie_secure(false)
1204 .on_user_active(cb)
1205 .build()
1206 .await
1207 .unwrap();
1208
1209 let email = Email::new("csc@example.com".into()).unwrap();
1210 let user = ath
1211 .db()
1212 .create_user(email, "pass", None, None)
1213 .await
1214 .unwrap();
1215
1216 ath.create_session_cookie(user.id).await.unwrap();
1217 assert_eq!(counter.load(Ordering::Relaxed), 1);
1218 }
1219
1220 #[tokio::test]
1221 async fn on_user_active_no_fire_on_create_session_cookie_unknown_user() {
1222 use std::sync::Arc;
1223 use std::sync::atomic::{AtomicU64, Ordering};
1224
1225 let counter = Arc::new(AtomicU64::new(0));
1226 let c = counter.clone();
1227 let cb: OnUserActive = Arc::new(move |_uid, _ts| {
1228 c.fetch_add(1, Ordering::Relaxed);
1229 });
1230
1231 let ath = AllowThemBuilder::new("sqlite::memory:")
1232 .on_user_active(cb)
1233 .build()
1234 .await
1235 .unwrap();
1236
1237 let _ = ath.create_session_cookie(UserId::new()).await;
1238 assert_eq!(counter.load(Ordering::Relaxed), 0);
1239 }
1240
1241 #[tokio::test]
1242 async fn on_user_active_no_fire_on_session_validation() {
1243 use std::sync::Arc;
1244 use std::sync::atomic::{AtomicU64, Ordering};
1245
1246 let counter = Arc::new(AtomicU64::new(0));
1247 let c = counter.clone();
1248 let cb: OnUserActive = Arc::new(move |_uid, _ts| {
1249 c.fetch_add(1, Ordering::Relaxed);
1250 });
1251
1252 let ath = AllowThemBuilder::new("sqlite::memory:")
1253 .cookie_secure(false)
1254 .on_user_active(cb)
1255 .build()
1256 .await
1257 .unwrap();
1258
1259 let email = Email::new("passive@example.com".into()).unwrap();
1260 ath.db()
1261 .create_user(email, "pass", None, None)
1262 .await
1263 .unwrap();
1264
1265 let outcome = ath.login("passive@example.com", "pass").await.unwrap();
1266 assert_eq!(counter.load(Ordering::Relaxed), 1);
1268
1269 let _ = ath
1271 .db()
1272 .validate_session(&outcome.token, Duration::hours(24))
1273 .await
1274 .unwrap();
1275 assert_eq!(
1276 counter.load(Ordering::Relaxed),
1277 1,
1278 "session validation must not fire callback"
1279 );
1280 }
1281
1282 #[tokio::test]
1283 async fn on_user_active_panic_does_not_propagate() {
1284 let cb: OnUserActive = Arc::new(|_uid, _ts| {
1285 panic!("intentional test panic in on_user_active callback");
1286 });
1287
1288 let ath = AllowThemBuilder::new("sqlite::memory:")
1289 .cookie_secure(false)
1290 .on_user_active(cb)
1291 .build()
1292 .await
1293 .unwrap();
1294
1295 let email = Email::new("panic@example.com".into()).unwrap();
1296 ath.db()
1297 .create_user(email, "pass", None, None)
1298 .await
1299 .unwrap();
1300
1301 let prev_hook = std::panic::take_hook();
1303 std::panic::set_hook(Box::new(|_| {}));
1304 let result = ath.login("panic@example.com", "pass").await;
1305 std::panic::set_hook(prev_hook);
1306
1307 assert!(
1308 result.is_ok(),
1309 "panic in callback must not propagate to caller"
1310 );
1311 }
1312
1313 struct CapturingSender(std::sync::Arc<std::sync::Mutex<Vec<crate::email::EmailMessage>>>);
1317
1318 impl crate::email::EmailSender for CapturingSender {
1319 fn send<'a>(
1320 &'a self,
1321 message: &'a crate::email::EmailMessage,
1322 ) -> std::pin::Pin<
1323 Box<dyn std::future::Future<Output = Result<(), crate::error::AuthError>> + Send + 'a>,
1324 > {
1325 self.0.lock().unwrap().push(message.clone());
1326 Box::pin(std::future::ready(Ok(())))
1327 }
1328 }
1329
1330 fn capturing_sender() -> (
1331 Box<dyn crate::email::EmailSender>,
1332 std::sync::Arc<std::sync::Mutex<Vec<crate::email::EmailMessage>>>,
1333 ) {
1334 let captured = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1335 let sender = CapturingSender(captured.clone());
1336 (Box::new(sender), captured)
1337 }
1338
1339 struct FailingSender;
1341
1342 impl crate::email::EmailSender for FailingSender {
1343 fn send<'a>(
1344 &'a self,
1345 _message: &'a crate::email::EmailMessage,
1346 ) -> std::pin::Pin<
1347 Box<dyn std::future::Future<Output = Result<(), crate::error::AuthError>> + Send + 'a>,
1348 > {
1349 Box::pin(std::future::ready(Err(crate::error::AuthError::Email(
1350 "injected failure".into(),
1351 ))))
1352 }
1353 }
1354
1355 async fn make_user_with_username(ath: &AllowThem, email_str: &str, username: Option<&str>) {
1356 let email = Email::new(email_str.into()).unwrap();
1357 ath.db()
1358 .create_user(
1359 email,
1360 "password",
1361 username.map(|s| crate::types::Username::new(s)),
1362 None,
1363 )
1364 .await
1365 .unwrap();
1366 }
1367
1368 #[tokio::test]
1369 async fn email_sender_default_is_noop_and_succeeds() {
1370 let ath = AllowThemBuilder::new("sqlite::memory:")
1372 .base_url("https://example.com")
1373 .build()
1374 .await
1375 .unwrap();
1376
1377 let msg = crate::email::EmailMessage {
1379 to: "nobody@example.com".into(),
1380 subject: "test".into(),
1381 template: crate::email::EmailTemplate::PasswordReset {
1382 url: "https://example.com/reset".into(),
1383 username: "nobody".into(),
1384 },
1385 };
1386 let result = ath.email_sender().send(&msg).await;
1387 assert!(result.is_ok(), "NoopEmailSender must return Ok");
1388 }
1389
1390 #[tokio::test]
1391 async fn email_sender_custom_sender_installed() {
1392 let (sender_box, captured) = capturing_sender();
1393 let ath = AllowThemBuilder::new("sqlite::memory:")
1394 .base_url("https://example.com")
1395 .email_sender(sender_box)
1396 .build()
1397 .await
1398 .unwrap();
1399
1400 let msg = crate::email::EmailMessage {
1401 to: "test@example.com".into(),
1402 subject: "subject".into(),
1403 template: crate::email::EmailTemplate::PasswordReset {
1404 url: "https://example.com/reset".into(),
1405 username: "test".into(),
1406 },
1407 };
1408 ath.email_sender().send(&msg).await.unwrap();
1409 assert_eq!(captured.lock().unwrap().len(), 1);
1410 }
1411
1412 #[tokio::test]
1413 async fn send_password_reset_email_builds_correct_template() {
1414 let (sender_box, captured) = capturing_sender();
1415 let ath = AllowThemBuilder::new("sqlite::memory:")
1416 .base_url("https://example.com")
1417 .email_sender(sender_box)
1418 .build()
1419 .await
1420 .unwrap();
1421 make_user_with_username(&ath, "reset@example.com", Some("alice")).await;
1422
1423 let email = Email::new("reset@example.com".into()).unwrap();
1424 ath.send_password_reset_email(&email).await.unwrap();
1425
1426 let msgs = captured.lock().unwrap();
1427 assert_eq!(msgs.len(), 1);
1428 let msg = &msgs[0];
1429 assert_eq!(msg.to, "reset@example.com");
1430 assert_eq!(msg.subject, "Reset your password");
1431 match &msg.template {
1432 crate::email::EmailTemplate::PasswordReset { url, username } => {
1433 assert!(
1434 url.contains("https://example.com"),
1435 "URL must contain base_url"
1436 );
1437 assert!(
1438 url.contains("/auth/reset-password?token="),
1439 "URL must have path"
1440 );
1441 assert_eq!(username, "alice");
1442 }
1443 other => panic!("expected PasswordReset template, got {other:?}"),
1444 }
1445 }
1446
1447 #[tokio::test]
1448 async fn send_verification_email_builds_correct_template() {
1449 let (sender_box, captured) = capturing_sender();
1450 let ath = AllowThemBuilder::new("sqlite::memory:")
1451 .base_url("https://example.com")
1452 .email_sender(sender_box)
1453 .build()
1454 .await
1455 .unwrap();
1456 let email = Email::new("verify@example.com".into()).unwrap();
1457 let user = ath
1458 .db()
1459 .create_user(
1460 email.clone(),
1461 "pass",
1462 Some(crate::types::Username::new("bob")),
1463 None,
1464 )
1465 .await
1466 .unwrap();
1467
1468 ath.send_verification_email(user.id, &email).await.unwrap();
1469
1470 let msgs = captured.lock().unwrap();
1471 assert_eq!(msgs.len(), 1);
1472 let msg = &msgs[0];
1473 assert_eq!(msg.to, "verify@example.com");
1474 assert_eq!(msg.subject, "Verify your email address");
1475 match &msg.template {
1476 crate::email::EmailTemplate::EmailVerification { url, username } => {
1477 assert!(url.contains("https://example.com"));
1478 assert!(url.contains("/auth/verify-email?token="));
1479 assert_eq!(username, "bob");
1480 }
1481 other => panic!("expected EmailVerification template, got {other:?}"),
1482 }
1483 }
1484
1485 #[tokio::test]
1486 async fn send_password_reset_email_username_fallback_uses_email_local_part() {
1487 let (sender_box, captured) = capturing_sender();
1488 let ath = AllowThemBuilder::new("sqlite::memory:")
1489 .base_url("https://example.com")
1490 .email_sender(sender_box)
1491 .build()
1492 .await
1493 .unwrap();
1494 make_user_with_username(&ath, "noname@example.com", None).await;
1496
1497 let email = Email::new("noname@example.com".into()).unwrap();
1498 ath.send_password_reset_email(&email).await.unwrap();
1499
1500 let msgs = captured.lock().unwrap();
1501 let msg = &msgs[0];
1502 match &msg.template {
1503 crate::email::EmailTemplate::PasswordReset { username, .. } => {
1504 assert_eq!(username, "noname", "must fall back to email local part");
1505 }
1506 other => panic!("expected PasswordReset, got {other:?}"),
1507 }
1508 }
1509
1510 #[tokio::test]
1511 async fn sender_error_propagates_as_auth_error_email() {
1512 let ath = AllowThemBuilder::new("sqlite::memory:")
1513 .base_url("https://example.com")
1514 .email_sender(Box::new(FailingSender))
1515 .build()
1516 .await
1517 .unwrap();
1518 make_user_with_username(&ath, "fail@example.com", Some("fail")).await;
1519
1520 let email = Email::new("fail@example.com".into()).unwrap();
1521 let result = ath.send_password_reset_email(&email).await;
1522 assert!(
1523 matches!(result, Err(crate::error::AuthError::Email(_))),
1524 "sender error must surface as AuthError::Email"
1525 );
1526 }
1527
1528 #[tokio::test]
1529 async fn send_password_reset_email_silent_on_unknown_email() {
1530 let (sender_box, captured) = capturing_sender();
1531 let ath = AllowThemBuilder::new("sqlite::memory:")
1532 .base_url("https://example.com")
1533 .email_sender(sender_box)
1534 .build()
1535 .await
1536 .unwrap();
1537
1538 let email = Email::new("ghost@example.com".into()).unwrap();
1539 let result = ath.send_password_reset_email(&email).await;
1540 assert!(result.is_ok(), "must return Ok for unknown email");
1541 assert!(
1542 captured.lock().unwrap().is_empty(),
1543 "no email must be sent for unknown address"
1544 );
1545 }
1546
1547 #[tokio::test]
1548 async fn send_invitation_email_creates_invitation_and_sends() {
1549 use crate::types::UserId;
1550
1551 let (sender_box, captured) = capturing_sender();
1552 let ath = AllowThemBuilder::new("sqlite::memory:")
1553 .base_url("https://example.com")
1554 .email_sender(sender_box)
1555 .build()
1556 .await
1557 .unwrap();
1558
1559 let inviter_email = Email::new("inviter@example.com".into()).unwrap();
1561 let inviter = ath
1562 .db()
1563 .create_user(
1564 inviter_email,
1565 "pass",
1566 Some(crate::types::Username::new("Alice")),
1567 None,
1568 )
1569 .await
1570 .unwrap();
1571 let inviter_id: UserId = inviter.id;
1572
1573 let invitee = Email::new("invitee@example.com".into()).unwrap();
1574 let invite_url = "https://example.com/invite/tok123";
1575 let expires_at = chrono::Utc::now() + chrono::Duration::hours(48);
1576
1577 ath.send_invitation_email(&invitee, invite_url, inviter_id, expires_at)
1578 .await
1579 .unwrap();
1580
1581 let msgs = captured.lock().unwrap();
1583 assert_eq!(msgs.len(), 1);
1584 let msg = &msgs[0];
1585 assert_eq!(msg.to, "invitee@example.com");
1586 match &msg.template {
1587 crate::email::EmailTemplate::Invitation { url, invited_by } => {
1588 assert_eq!(url, invite_url);
1589 assert_eq!(invited_by, "Alice");
1590 }
1591 other => panic!("expected Invitation template, got {other:?}"),
1592 }
1593 }
1594
1595 #[tokio::test]
1596 async fn send_mfa_recovery_email_sends_codes_without_db_write() {
1597 let (sender_box, captured) = capturing_sender();
1598 let ath = AllowThemBuilder::new("sqlite::memory:")
1599 .base_url("https://example.com")
1600 .email_sender(sender_box)
1601 .build()
1602 .await
1603 .unwrap();
1604
1605 let email = Email::new("mfa@example.com".into()).unwrap();
1606 let user = ath
1607 .db()
1608 .create_user(
1609 email,
1610 "pass",
1611 Some(crate::types::Username::new("carol")),
1612 None,
1613 )
1614 .await
1615 .unwrap();
1616
1617 let codes = vec!["code-1".into(), "code-2".into(), "code-3".into()];
1618 ath.send_mfa_recovery_email(user.id, codes.clone())
1619 .await
1620 .unwrap();
1621
1622 let msgs = captured.lock().unwrap();
1623 assert_eq!(msgs.len(), 1);
1624 let msg = &msgs[0];
1625 assert_eq!(msg.to, "mfa@example.com");
1626 assert_eq!(msg.subject, "Your MFA recovery codes");
1627 match &msg.template {
1628 crate::email::EmailTemplate::MfaRecovery {
1629 codes: sent_codes,
1630 username,
1631 } => {
1632 assert_eq!(sent_codes, &codes, "codes must be forwarded as-is");
1633 assert_eq!(username, "carol");
1634 }
1635 other => panic!("expected MfaRecovery template, got {other:?}"),
1636 }
1637 }
1638
1639 struct CapturingSink(std::sync::Arc<std::sync::Mutex<Vec<crate::event_sink::AuthEvent>>>);
1643
1644 impl crate::event_sink::EventSink for CapturingSink {
1645 fn emit<'a>(
1646 &'a self,
1647 event: &'a crate::event_sink::AuthEvent,
1648 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'a>> {
1649 self.0.lock().unwrap().push(event.clone());
1650 Box::pin(std::future::ready(()))
1651 }
1652 }
1653
1654 fn capturing_sink() -> (
1655 Box<dyn crate::event_sink::EventSink>,
1656 std::sync::Arc<std::sync::Mutex<Vec<crate::event_sink::AuthEvent>>>,
1657 ) {
1658 let captured = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1659 let sink = CapturingSink(captured.clone());
1660 (Box::new(sink), captured)
1661 }
1662
1663 async fn ath_with_sink() -> (
1664 AllowThem,
1665 std::sync::Arc<std::sync::Mutex<Vec<crate::event_sink::AuthEvent>>>,
1666 ) {
1667 let (sink_box, captured) = capturing_sink();
1668 let ath = AllowThemBuilder::new("sqlite::memory:")
1669 .base_url("https://example.com")
1670 .event_sink(sink_box)
1671 .build()
1672 .await
1673 .unwrap();
1674 (ath, captured)
1675 }
1676
1677 #[tokio::test]
1678 async fn create_user_emits_user_created() {
1679 let (ath, captured) = ath_with_sink().await;
1680 let email = crate::types::Email::new("ev@example.com".into()).unwrap();
1681 ath.create_user(email, "pass1234", None, None)
1682 .await
1683 .unwrap();
1684
1685 let events = captured.lock().unwrap();
1686 assert_eq!(events.len(), 1);
1687 assert_eq!(events[0].event_type, "user.created");
1688 assert!(events[0].user_id.is_some());
1689 }
1690
1691 #[tokio::test]
1692 async fn login_emits_session_created() {
1693 let (ath, captured) = ath_with_sink().await;
1694 let email = crate::types::Email::new("ev@example.com".into()).unwrap();
1695 ath.create_user(email.clone(), "pass1234", None, None)
1696 .await
1697 .unwrap();
1698 captured.lock().unwrap().clear(); ath.login("ev@example.com", "pass1234").await.unwrap();
1701
1702 let events = captured.lock().unwrap();
1703 assert_eq!(events.len(), 1);
1704 assert_eq!(events[0].event_type, "session.created");
1705 }
1706
1707 #[tokio::test]
1708 async fn delete_session_emits_session_destroyed() {
1709 let (ath, captured) = ath_with_sink().await;
1710 let email = crate::types::Email::new("ev@example.com".into()).unwrap();
1711 ath.create_user(email.clone(), "pass1234", None, None)
1712 .await
1713 .unwrap();
1714 let token = ath.login("ev@example.com", "pass1234").await.unwrap().token;
1715 captured.lock().unwrap().clear();
1716
1717 ath.delete_session(&token).await.unwrap();
1718
1719 let events = captured.lock().unwrap();
1720 assert_eq!(events.len(), 1);
1721 assert_eq!(events[0].event_type, "session.destroyed");
1722 }
1723
1724 #[tokio::test]
1725 async fn update_user_email_emits_user_updated() {
1726 let (ath, captured) = ath_with_sink().await;
1727 let email = crate::types::Email::new("ev@example.com".into()).unwrap();
1728 let user = ath
1729 .create_user(email, "pass1234", None, None)
1730 .await
1731 .unwrap();
1732 captured.lock().unwrap().clear();
1733
1734 let new_email = crate::types::Email::new("new@example.com".into()).unwrap();
1735 ath.update_user_email(user.id, new_email).await.unwrap();
1736
1737 let events = captured.lock().unwrap();
1738 assert_eq!(events.len(), 1);
1739 assert_eq!(events[0].event_type, "user.updated");
1740 assert_eq!(events[0].data["field"], "email");
1741 }
1742
1743 #[tokio::test]
1744 async fn update_user_active_false_emits_user_blocked() {
1745 let (ath, captured) = ath_with_sink().await;
1746 let email = crate::types::Email::new("ev@example.com".into()).unwrap();
1747 let user = ath
1748 .create_user(email, "pass1234", None, None)
1749 .await
1750 .unwrap();
1751 captured.lock().unwrap().clear();
1752
1753 ath.update_user_active(user.id, false).await.unwrap();
1754
1755 let events = captured.lock().unwrap();
1756 assert_eq!(events.len(), 1);
1757 assert_eq!(events[0].event_type, "user.blocked");
1758 }
1759
1760 #[tokio::test]
1761 async fn delete_user_emits_user_deleted() {
1762 let (ath, captured) = ath_with_sink().await;
1763 let email = crate::types::Email::new("ev@example.com".into()).unwrap();
1764 let user = ath
1765 .create_user(email, "pass1234", None, None)
1766 .await
1767 .unwrap();
1768 captured.lock().unwrap().clear();
1769
1770 ath.delete_user(user.id).await.unwrap();
1771
1772 let events = captured.lock().unwrap();
1773 assert_eq!(events.len(), 1);
1774 assert_eq!(events[0].event_type, "user.deleted");
1775 }
1776
1777 #[tokio::test]
1778 async fn noop_sink_is_default_and_no_events_captured() {
1779 let ath = AllowThemBuilder::new("sqlite::memory:")
1781 .base_url("https://example.com")
1782 .build()
1783 .await
1784 .unwrap();
1785 let email = crate::types::Email::new("ev@example.com".into()).unwrap();
1786 ath.create_user(email, "pass1234", None, None)
1788 .await
1789 .unwrap();
1790 }
1791}