Skip to content

Commit 45b6a63

Browse files
authored
Add methods and events for email verification and password reset (#268)
1 parent 5789a21 commit 45b6a63

File tree

6 files changed

+219
-5
lines changed

6 files changed

+219
-5
lines changed

tests/test_user_management.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
import workos
55

66
from tests.utils.fixtures.mock_auth_factor_totp import MockAuthFactorTotp
7+
from tests.utils.fixtures.mock_email_verification import MockEmailVerification
78
from tests.utils.fixtures.mock_invitation import MockInvitation
89
from tests.utils.fixtures.mock_magic_auth import MockMagicAuth
910
from tests.utils.fixtures.mock_organization_membership import MockOrganizationMembership
11+
from tests.utils.fixtures.mock_password_reset import MockPasswordReset
1012
from tests.utils.fixtures.mock_session import MockSession
1113
from tests.utils.fixtures.mock_user import MockUser
1214
from workos.user_management import UserManagement
@@ -213,10 +215,18 @@ def mock_auth_factors(self):
213215
}
214216
return dict_response
215217

218+
@pytest.fixture
219+
def mock_email_verification(self):
220+
return MockEmailVerification("email_verification_ABCDE").to_dict()
221+
216222
@pytest.fixture
217223
def mock_magic_auth(self):
218224
return MockMagicAuth("magic_auth_ABCDE").to_dict()
219225

226+
@pytest.fixture
227+
def mock_password_reset(self):
228+
return MockPasswordReset("password_reset_ABCDE").to_dict()
229+
220230
@pytest.fixture
221231
def mock_invitation(self):
222232
return MockInvitation("invitation_ABCDE").to_dict()
@@ -869,16 +879,37 @@ def test_get_logout_url(self):
869879

870880
assert expected == result
871881

882+
def test_get_password_reset(self, mock_password_reset, capture_and_mock_request):
883+
url, request_kwargs = capture_and_mock_request("get", mock_password_reset, 200)
884+
885+
password_reset = self.user_management.get_password_reset("password_reset_ABCDE")
886+
887+
assert url[0].endswith("user_management/password_reset/password_reset_ABCDE")
888+
assert password_reset["id"] == "password_reset_ABCDE"
889+
890+
def test_create_password_reset(self, capture_and_mock_request, mock_password_reset):
891+
892+
url, _ = capture_and_mock_request("post", mock_password_reset, 201)
893+
894+
password_reset = self.user_management.create_password_reset(email=email)
895+
896+
assert url[0].endswith("user_management/password_reset")
897+
assert password_reset["email"] == email
898+
872899
def test_send_password_reset_email(self, capture_and_mock_request):
873900
874901
password_reset_url = "https://foo-corp.com/reset-password"
875902

876903
url, request = capture_and_mock_request("post", None, 200)
877904

878-
response = self.user_management.send_password_reset_email(
879-
email=email,
880-
password_reset_url=password_reset_url,
881-
)
905+
with pytest.warns(
906+
DeprecationWarning,
907+
match="'send_password_reset_email' is deprecated. Please use 'create_password_reset' instead. This method will be removed in a future major version.",
908+
):
909+
response = self.user_management.send_password_reset_email(
910+
email=email,
911+
password_reset_url=password_reset_url,
912+
)
882913

883914
assert url[0].endswith("user_management/password_reset/send")
884915
assert request["json"]["email"] == email
@@ -901,6 +932,22 @@ def test_reset_password(self, capture_and_mock_request, mock_user):
901932
assert request["json"]["token"] == token
902933
assert request["json"]["new_password"] == new_password
903934

935+
def test_get_email_verification(
936+
self, mock_email_verification, capture_and_mock_request
937+
):
938+
url, request_kwargs = capture_and_mock_request(
939+
"get", mock_email_verification, 200
940+
)
941+
942+
email_verification = self.user_management.get_email_verification(
943+
"email_verification_ABCDE"
944+
)
945+
946+
assert url[0].endswith(
947+
"user_management/email_verification/email_verification_ABCDE"
948+
)
949+
assert email_verification["id"] == "email_verification_ABCDE"
950+
904951
def test_send_verification_email(self, capture_and_mock_request, mock_user):
905952
user_id = "user_01H7ZGXFP5C6BBQY6Z7277ZCT0"
906953

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import datetime
2+
from workos.resources.base import WorkOSBaseResource
3+
4+
5+
class MockEmailVerification(WorkOSBaseResource):
6+
def __init__(self, id):
7+
self.id = id
8+
self.user_id = "user_01HWZBQAY251RZ9BKB4RZW4D4A"
9+
self.email = "[email protected]"
10+
self.expires_at = datetime.datetime.now()
11+
self.code = "123456"
12+
self.created_at = datetime.datetime.now()
13+
self.updated_at = datetime.datetime.now()
14+
15+
OBJECT_FIELDS = [
16+
"id",
17+
"user_id",
18+
"email",
19+
"expires_at",
20+
"code",
21+
"created_at",
22+
"updated_at",
23+
]

tests/utils/fixtures/mock_invitation.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ def __init__(self, id):
1212
self.expires_at = datetime.datetime.now()
1313
self.token = "Z1uX3RbwcIl5fIGJJJCXXisdI"
1414
self.accept_invitation_url = (
15-
"https://myauthkit.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI"
15+
"https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI"
1616
)
1717
self.organization_id = "org_12345"
18+
self.inviter_user_id = "user_123"
1819
self.created_at = datetime.datetime.now()
1920
self.updated_at = datetime.datetime.now()
2021

@@ -28,6 +29,7 @@ def __init__(self, id):
2829
"token",
2930
"accept_invitation_url",
3031
"organization_id",
32+
"inviter_user_id",
3133
"created_at",
3234
"updated_at",
3335
]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import datetime
2+
from workos.resources.base import WorkOSBaseResource
3+
4+
5+
class MockPasswordReset(WorkOSBaseResource):
6+
def __init__(self, id):
7+
self.id = id
8+
self.user_id = "user_01HWZBQAY251RZ9BKB4RZW4D4A"
9+
self.email = "[email protected]"
10+
self.password_reset_token = "Z1uX3RbwcIl5fIGJJJCXXisdI"
11+
self.password_reset_url = (
12+
"https://your-app.com/reset-password?token=Z1uX3RbwcIl5fIGJJJCXXisdI"
13+
)
14+
self.expires_at = datetime.datetime.now()
15+
self.created_at = datetime.datetime.now()
16+
17+
OBJECT_FIELDS = [
18+
"id",
19+
"user_id",
20+
"email",
21+
"password_reset_token",
22+
"password_reset_url",
23+
"expires_at",
24+
"created_at",
25+
]

workos/resources/user_management.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,24 @@ def to_dict(self):
7575
return authentication_response_dict
7676

7777

78+
class WorkOSEmailVerification(WorkOSBaseResource):
79+
"""Representation of a EmailVerification object as returned by WorkOS through User Management features.
80+
81+
Attributes:
82+
OBJECT_FIELDS (list): List of fields a WorkOSEmailVerification comprises.
83+
"""
84+
85+
OBJECT_FIELDS = [
86+
"id",
87+
"user_id",
88+
"email",
89+
"expires_at",
90+
"code",
91+
"created_at",
92+
"updated_at",
93+
]
94+
95+
7896
class WorkOSInvitation(WorkOSBaseResource):
7997
"""Representation of an Invitation as returned by WorkOS through User Management features.
8098
@@ -92,6 +110,7 @@ class WorkOSInvitation(WorkOSBaseResource):
92110
"token",
93111
"accept_invitation_url",
94112
"organization_id",
113+
"inviter_user_id",
95114
"created_at",
96115
"updated_at",
97116
]
@@ -115,6 +134,24 @@ class WorkOSMagicAuth(WorkOSBaseResource):
115134
]
116135

117136

137+
class WorkOSPasswordReset(WorkOSBaseResource):
138+
"""Representation of a PasswordReset object as returned by WorkOS through User Management features.
139+
140+
Attributes:
141+
OBJECT_FIELDS (list): List of fields a WorkOSPasswordReset comprises.
142+
"""
143+
144+
OBJECT_FIELDS = [
145+
"id",
146+
"user_id",
147+
"email",
148+
"password_reset_token",
149+
"password_reset_url",
150+
"expires_at",
151+
"created_at",
152+
]
153+
154+
118155
class WorkOSOrganizationMembership(WorkOSBaseResource):
119156
"""Representation of an Organization Membership as returned by WorkOS through User Management features.
120157

workos/user_management.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
from workos.resources.user_management import (
77
WorkOSAuthenticationResponse,
88
WorkOSRefreshTokenAuthenticationResponse,
9+
WorkOSEmailVerification,
910
WorkOSInvitation,
1011
WorkOSMagicAuth,
12+
WorkOSPasswordReset,
1113
WorkOSOrganizationMembership,
1214
WorkOSPasswordChallengeResponse,
1315
WorkOSUser,
@@ -44,9 +46,12 @@
4446
MAGIC_AUTH_PATH = "user_management/magic_auth"
4547
USER_SEND_MAGIC_AUTH_PATH = "user_management/magic_auth/send"
4648
USER_AUTH_FACTORS_PATH = "user_management/users/{0}/auth_factors"
49+
EMAIL_VERIFICATION_DETAIL_PATH = "user_management/email_verification/{0}"
4750
INVITATION_PATH = "user_management/invitations"
4851
INVITATION_DETAIL_PATH = "user_management/invitations/{0}"
4952
INVITATION_REVOKE_PATH = "user_management/invitations/{0}/revoke"
53+
PASSWORD_RESET_PATH = "user_management/password_reset"
54+
PASSWORD_RESET_DETAIL_PATH = "user_management/password_reset/{0}"
5055

5156
RESPONSE_LIMIT = 10
5257

@@ -829,18 +834,73 @@ def get_logout_url(self, session_id):
829834
session_id,
830835
)
831836

837+
def get_password_reset(self, password_reset_id):
838+
"""Get the details of a password reset object.
839+
840+
Args:
841+
password_reset_id (str) - The unique ID of the password reset object.
842+
843+
Returns:
844+
dict: PasswordReset response from WorkOS.
845+
"""
846+
headers = {}
847+
848+
response = self.request_helper.request(
849+
PASSWORD_RESET_DETAIL_PATH.format(password_reset_id),
850+
method=REQUEST_METHOD_GET,
851+
headers=headers,
852+
token=workos.api_key,
853+
)
854+
855+
return WorkOSPasswordReset.construct_from_response(response).to_dict()
856+
857+
def create_password_reset(
858+
self,
859+
email,
860+
):
861+
"""Creates a password reset token that can be sent to a user's email to reset the password.
862+
863+
Args:
864+
email: The email address of the user.
865+
866+
Returns:
867+
dict: PasswordReset response from WorkOS.
868+
"""
869+
headers = {}
870+
871+
params = {
872+
"email": email,
873+
}
874+
875+
response = self.request_helper.request(
876+
PASSWORD_RESET_PATH,
877+
method=REQUEST_METHOD_POST,
878+
params=params,
879+
headers=headers,
880+
token=workos.api_key,
881+
)
882+
883+
return WorkOSPasswordReset.construct_from_response(response).to_dict()
884+
832885
def send_password_reset_email(
833886
self,
834887
email,
835888
password_reset_url,
836889
):
837890
"""Sends a password reset email to a user.
838891
892+
Deprecated: Please use `create_password_reset` instead. This method will be removed in a future major version.
893+
839894
Kwargs:
840895
email (str): The email of the user that wishes to reset their password.
841896
password_reset_url (str): The URL that will be linked to in the email.
842897
"""
843898

899+
warn(
900+
"'send_password_reset_email' is deprecated. Please use 'create_password_reset' instead. This method will be removed in a future major version.",
901+
DeprecationWarning,
902+
)
903+
844904
headers = {}
845905

846906
payload = {
@@ -888,6 +948,26 @@ def reset_password(
888948

889949
return WorkOSUser.construct_from_response(response["user"]).to_dict()
890950

951+
def get_email_verification(self, email_verification_id):
952+
"""Get the details of an email verification object.
953+
954+
Args:
955+
email_verificationh_id (str) - The unique ID of the email verification object.
956+
957+
Returns:
958+
dict: EmailVerification response from WorkOS.
959+
"""
960+
headers = {}
961+
962+
response = self.request_helper.request(
963+
EMAIL_VERIFICATION_DETAIL_PATH.format(email_verification_id),
964+
method=REQUEST_METHOD_GET,
965+
headers=headers,
966+
token=workos.api_key,
967+
)
968+
969+
return WorkOSEmailVerification.construct_from_response(response).to_dict()
970+
891971
def send_verification_email(
892972
self,
893973
user_id,

0 commit comments

Comments
 (0)