Skip to main content

authx_plugins/password_reset/
mod.rs

1mod service;
2
3pub use service::{PasswordResetRequest, PasswordResetService};
4
5#[cfg(test)]
6mod tests {
7    use super::service::{PasswordResetRequest, PasswordResetService};
8    use authx_core::{error::AuthError, events::EventBus, models::CreateUser};
9    use authx_storage::{memory::MemoryStore, ports::UserRepository};
10
11    fn setup() -> (MemoryStore, EventBus) {
12        (MemoryStore::new(), EventBus::new())
13    }
14
15    async fn make_user(store: &MemoryStore) -> uuid::Uuid {
16        UserRepository::create(
17            store,
18            CreateUser {
19                email: "reset@example.com".into(),
20                username: None,
21                metadata: None,
22            },
23        )
24        .await
25        .unwrap()
26        .id
27    }
28
29    #[tokio::test]
30    async fn request_reset_returns_none_for_unknown_email() {
31        let (store, events) = setup();
32        let svc = PasswordResetService::new(store, events);
33        let tok = svc.request_reset("nobody@x.com").await.unwrap();
34        assert!(tok.is_none());
35    }
36
37    #[tokio::test]
38    async fn request_reset_returns_token_for_known_email() {
39        let (store, events) = setup();
40        make_user(&store).await;
41        let svc = PasswordResetService::new(store, events);
42        let tok = svc.request_reset("reset@example.com").await.unwrap();
43        assert!(tok.is_some());
44        assert_eq!(tok.unwrap().len(), 64); // 32 bytes hex
45    }
46
47    #[tokio::test]
48    async fn reset_password_fails_with_bad_token() {
49        let (store, events) = setup();
50        let svc = PasswordResetService::new(store, events);
51        let err = svc
52            .reset_password(PasswordResetRequest {
53                token: "bad_token".into(),
54                new_password: "newpassword123".into(),
55            })
56            .await
57            .unwrap_err();
58        assert!(matches!(err, AuthError::InvalidToken));
59    }
60
61    #[tokio::test]
62    async fn reset_password_succeeds_with_valid_token() {
63        use authx_core::crypto::hash_password;
64        use authx_core::models::{CreateCredential, CredentialKind};
65        use authx_storage::ports::CredentialRepository;
66
67        let (store, events) = setup();
68        let uid = make_user(&store).await;
69
70        // Give the user an initial password.
71        let old_hash = hash_password("oldpass123").unwrap();
72        CredentialRepository::create(
73            &store,
74            CreateCredential {
75                user_id: uid,
76                kind: CredentialKind::Password,
77                credential_hash: old_hash,
78                metadata: None,
79            },
80        )
81        .await
82        .unwrap();
83
84        let svc = PasswordResetService::new(store.clone(), events);
85        let token = svc
86            .request_reset("reset@example.com")
87            .await
88            .unwrap()
89            .unwrap();
90
91        svc.reset_password(PasswordResetRequest {
92            token,
93            new_password: "newpass456".into(),
94        })
95        .await
96        .unwrap();
97
98        // New credential should allow sign-in with new password.
99        let new_hash = CredentialRepository::find_password_hash(&store, uid)
100            .await
101            .unwrap()
102            .unwrap();
103        assert!(authx_core::crypto::verify_password(&new_hash, "newpass456").unwrap());
104    }
105
106    #[tokio::test]
107    async fn reset_password_rejects_same_password() {
108        use authx_core::crypto::hash_password;
109        use authx_core::models::{CreateCredential, CredentialKind};
110        use authx_storage::ports::CredentialRepository;
111
112        let (store, events) = setup();
113        let uid = make_user(&store).await;
114
115        let old_hash = hash_password("samepass123").unwrap();
116        CredentialRepository::create(
117            &store,
118            CreateCredential {
119                user_id: uid,
120                kind: CredentialKind::Password,
121                credential_hash: old_hash,
122                metadata: None,
123            },
124        )
125        .await
126        .unwrap();
127
128        let svc = PasswordResetService::new(store.clone(), events);
129        let token = svc
130            .request_reset("reset@example.com")
131            .await
132            .unwrap()
133            .unwrap();
134
135        let err = svc
136            .reset_password(PasswordResetRequest {
137                token,
138                new_password: "samepass123".into(),
139            })
140            .await
141            .unwrap_err();
142
143        assert!(matches!(err, AuthError::Internal(_)));
144    }
145}