Skip to content

Commit

Permalink
Add methods and events for email verification and password reset
Browse files Browse the repository at this point in the history
  • Loading branch information
blairlunceford committed May 23, 2024
1 parent 5789a21 commit 65d28b7
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 5 deletions.
55 changes: 51 additions & 4 deletions tests/test_user_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import workos

from tests.utils.fixtures.mock_auth_factor_totp import MockAuthFactorTotp
from tests.utils.fixtures.mock_email_verification import MockEmailVerification
from tests.utils.fixtures.mock_invitation import MockInvitation
from tests.utils.fixtures.mock_magic_auth import MockMagicAuth
from tests.utils.fixtures.mock_organization_membership import MockOrganizationMembership
from tests.utils.fixtures.mock_password_reset import MockPasswordReset
from tests.utils.fixtures.mock_session import MockSession
from tests.utils.fixtures.mock_user import MockUser
from workos.user_management import UserManagement
Expand Down Expand Up @@ -213,10 +215,18 @@ def mock_auth_factors(self):
}
return dict_response

@pytest.fixture
def mock_email_verification(self):
return MockEmailVerification("email_verification_ABCDE").to_dict()

@pytest.fixture
def mock_magic_auth(self):
return MockMagicAuth("magic_auth_ABCDE").to_dict()

@pytest.fixture
def mock_password_reset(self):
return MockPasswordReset("password_reset_ABCDE").to_dict()

@pytest.fixture
def mock_invitation(self):
return MockInvitation("invitation_ABCDE").to_dict()
Expand Down Expand Up @@ -869,16 +879,37 @@ def test_get_logout_url(self):

assert expected == result

def test_get_password_reset(self, mock_password_reset, capture_and_mock_request):
url, request_kwargs = capture_and_mock_request("get", mock_password_reset, 200)

password_reset = self.user_management.get_password_reset("password_reset_ABCDE")

assert url[0].endswith("user_management/password_reset/password_reset_ABCDE")
assert password_reset["id"] == "password_reset_ABCDE"

def test_create_password_reset(self, capture_and_mock_request, mock_password_reset):
email = "[email protected]"
url, _ = capture_and_mock_request("post", mock_password_reset, 201)

password_reset = self.user_management.create_password_reset(email=email)

assert url[0].endswith("user_management/password_reset")
assert password_reset["email"] == email

def test_send_password_reset_email(self, capture_and_mock_request):
email = "[email protected]"
password_reset_url = "https://foo-corp.com/reset-password"

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

response = self.user_management.send_password_reset_email(
email=email,
password_reset_url=password_reset_url,
)
with pytest.warns(
DeprecationWarning,
match="'send_password_reset_email' is deprecated. Please use 'create_password_reset' instead. This method will be removed in a future major version.",
):
response = self.user_management.send_password_reset_email(
email=email,
password_reset_url=password_reset_url,
)

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

def test_get_email_verification(
self, mock_email_verification, capture_and_mock_request
):
url, request_kwargs = capture_and_mock_request(
"get", mock_email_verification, 200
)

email_verification = self.user_management.get_email_verification(
"email_verification_ABCDE"
)

assert url[0].endswith(
"user_management/email_verification/email_verification_ABCDE"
)
assert email_verification["id"] == "email_verification_ABCDE"

def test_send_verification_email(self, capture_and_mock_request, mock_user):
user_id = "user_01H7ZGXFP5C6BBQY6Z7277ZCT0"

Expand Down
23 changes: 23 additions & 0 deletions tests/utils/fixtures/mock_email_verification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import datetime
from workos.resources.base import WorkOSBaseResource


class MockEmailVerification(WorkOSBaseResource):
def __init__(self, id):
self.id = id
self.user_id = "user_01HWZBQAY251RZ9BKB4RZW4D4A"
self.email = "[email protected]"
self.expires_at = datetime.datetime.now()
self.code = "123456"
self.created_at = datetime.datetime.now()
self.updated_at = datetime.datetime.now()

OBJECT_FIELDS = [
"id",
"user_id",
"email",
"expires_at",
"code",
"created_at",
"updated_at",
]
4 changes: 3 additions & 1 deletion tests/utils/fixtures/mock_invitation.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ def __init__(self, id):
self.expires_at = datetime.datetime.now()
self.token = "Z1uX3RbwcIl5fIGJJJCXXisdI"
self.accept_invitation_url = (
"https://myauthkit.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI"
"https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI"
)
self.organization_id = "org_12345"
self.inviter_user_id = "user_123"
self.created_at = datetime.datetime.now()
self.updated_at = datetime.datetime.now()

Expand All @@ -28,6 +29,7 @@ def __init__(self, id):
"token",
"accept_invitation_url",
"organization_id",
"inviter_user_id",
"created_at",
"updated_at",
]
25 changes: 25 additions & 0 deletions tests/utils/fixtures/mock_password_reset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import datetime
from workos.resources.base import WorkOSBaseResource


class MockPasswordReset(WorkOSBaseResource):
def __init__(self, id):
self.id = id
self.user_id = "user_01HWZBQAY251RZ9BKB4RZW4D4A"
self.email = "[email protected]"
self.password_reset_token = "Z1uX3RbwcIl5fIGJJJCXXisdI"
self.password_reset_url = (
"https://your-app.com/reset-password?token=Z1uX3RbwcIl5fIGJJJCXXisdI"
)
self.expires_at = datetime.datetime.now()
self.created_at = datetime.datetime.now()

OBJECT_FIELDS = [
"id",
"user_id",
"email",
"password_reset_token",
"password_reset_url",
"expires_at",
"created_at",
]
37 changes: 37 additions & 0 deletions workos/resources/user_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ def to_dict(self):
return authentication_response_dict


class WorkOSEmailVerification(WorkOSBaseResource):
"""Representation of a EmailVerification object as returned by WorkOS through User Management features.
Attributes:
OBJECT_FIELDS (list): List of fields a WorkOSEmailVerification comprises.
"""

OBJECT_FIELDS = [
"id",
"user_id",
"email",
"expires_at",
"code",
"created_at",
"updated_at",
]


class WorkOSInvitation(WorkOSBaseResource):
"""Representation of an Invitation as returned by WorkOS through User Management features.
Expand All @@ -92,6 +110,7 @@ class WorkOSInvitation(WorkOSBaseResource):
"token",
"accept_invitation_url",
"organization_id",
"inviter_user_id",
"created_at",
"updated_at",
]
Expand All @@ -115,6 +134,24 @@ class WorkOSMagicAuth(WorkOSBaseResource):
]


class WorkOSPasswordReset(WorkOSBaseResource):
"""Representation of a PasswordReset object as returned by WorkOS through User Management features.
Attributes:
OBJECT_FIELDS (list): List of fields a WorkOSPasswordReset comprises.
"""

OBJECT_FIELDS = [
"id",
"user_id",
"email",
"password_reset_token",
"password_reset_url",
"expires_at",
"created_at",
]


class WorkOSOrganizationMembership(WorkOSBaseResource):
"""Representation of an Organization Membership as returned by WorkOS through User Management features.
Expand Down
80 changes: 80 additions & 0 deletions workos/user_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
from workos.resources.user_management import (
WorkOSAuthenticationResponse,
WorkOSRefreshTokenAuthenticationResponse,
WorkOSEmailVerification,
WorkOSInvitation,
WorkOSMagicAuth,
WorkOSPasswordReset,
WorkOSOrganizationMembership,
WorkOSPasswordChallengeResponse,
WorkOSUser,
Expand Down Expand Up @@ -44,9 +46,12 @@
MAGIC_AUTH_PATH = "user_management/magic_auth"
USER_SEND_MAGIC_AUTH_PATH = "user_management/magic_auth/send"
USER_AUTH_FACTORS_PATH = "user_management/users/{0}/auth_factors"
EMAIL_VERIFICATION_DETAIL_PATH = "user_management/email_verification/{0}"
INVITATION_PATH = "user_management/invitations"
INVITATION_DETAIL_PATH = "user_management/invitations/{0}"
INVITATION_REVOKE_PATH = "user_management/invitations/{0}/revoke"
PASSWORD_RESET_PATH = "user_management/password_reset"
PASSWORD_RESET_DETAIL_PATH = "user_management/password_reset/{0}"

RESPONSE_LIMIT = 10

Expand Down Expand Up @@ -829,18 +834,73 @@ def get_logout_url(self, session_id):
session_id,
)

def get_password_reset(self, password_reset_id):
"""Get the details of a password reset object.
Args:
password_reset_id (str) - The unique ID of the password reset object.
Returns:
dict: PasswordReset response from WorkOS.
"""
headers = {}

response = self.request_helper.request(
PASSWORD_RESET_DETAIL_PATH.format(password_reset_id),
method=REQUEST_METHOD_GET,
headers=headers,
token=workos.api_key,
)

return WorkOSPasswordReset.construct_from_response(response).to_dict()

def create_password_reset(
self,
email,
):
"""Creates a password reset token that can be sent to a user's email to reset the password.
Args:
email: The email address of the user.
Returns:
dict: PasswordReset response from WorkOS.
"""
headers = {}

params = {
"email": email,
}

response = self.request_helper.request(
PASSWORD_RESET_PATH,
method=REQUEST_METHOD_POST,
params=params,
headers=headers,
token=workos.api_key,
)

return WorkOSPasswordReset.construct_from_response(response).to_dict()

def send_password_reset_email(
self,
email,
password_reset_url,
):
"""Sends a password reset email to a user.
Deprecated: Please use `create_password_reset` instead. This method will be removed in a future major version.
Kwargs:
email (str): The email of the user that wishes to reset their password.
password_reset_url (str): The URL that will be linked to in the email.
"""

warn(
"'send_password_reset_email' is deprecated. Please use 'create_password_reset' instead. This method will be removed in a future major version.",
DeprecationWarning,
)

headers = {}

payload = {
Expand Down Expand Up @@ -888,6 +948,26 @@ def reset_password(

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

def get_email_verification(self, email_verification_id):
"""Get the details of an email verification object.
Args:
email_verificationh_id (str) - The unique ID of the email verification object.
Returns:
dict: EmailVerification response from WorkOS.
"""
headers = {}

response = self.request_helper.request(
EMAIL_VERIFICATION_DETAIL_PATH.format(email_verification_id),
method=REQUEST_METHOD_GET,
headers=headers,
token=workos.api_key,
)

return WorkOSEmailVerification.construct_from_response(response).to_dict()

def send_verification_email(
self,
user_id,
Expand Down

0 comments on commit 65d28b7

Please sign in to comment.