Skip to content

Commit

Permalink
Type webhook methods (#307)
Browse files Browse the repository at this point in the history
* Type inputs to webhook verification methods

* Return well defined types from webhook verification

* mypy fixes

* Remove some unneeded tests

* Update test fixtures with new values

* Add explicit None
  • Loading branch information
tribble authored Jul 31, 2024
1 parent adc73ff commit 37f36c6
Show file tree
Hide file tree
Showing 3 changed files with 325 additions and 42 deletions.
28 changes: 5 additions & 23 deletions tests/test_webhooks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import json
from os import error
from workos.webhooks import Webhooks
Expand All @@ -16,47 +17,28 @@ def setup(self, set_api_key):

@pytest.fixture
def mock_event_body(self):
return '{"id":"wh_01FG9JXJ9C9S052FX59JVG4EG1","data":{"id":"conn_01EHWNC0FCBHZ3BJ7EGKYXK0E6","name":"Foo Corp\'s Connection","state":"active","object":"connection","domains":[{"id":"conn_domain_01EHWNFTAFCF3CQAE5A9Q0P1YB","domain":"foo-corp.com","object":"connection_domain"}],"connection_type":"OktaSAML","organization_id":"org_01EHWNCE74X7JSDV0X3SZ3KJNY"},"event":"connection.activated"}'
return '{"id":"event_01J44T8116Q5M0RYCFA6KWNXN9","data":{"id":"conn_01EHWNC0FCBHZ3BJ7EGKYXK0E6","name":"Foo Corp\'s Connection","state":"active","object":"connection","status":"linked","domains":[{"id":"conn_domain_01EHWNFTAFCF3CQAE5A9Q0P1YB","domain":"foo-corp.com","object":"connection_domain"}],"created_at":"2021-06-25T19:07:33.155Z","updated_at":"2021-06-25T19:07:33.155Z","external_key":"3QMR4u0Tok6SgwY2AWG6u6mkQ","connection_type":"OktaSAML","organization_id":"org_01EHWNCE74X7JSDV0X3SZ3KJNY"},"event":"connection.activated","created_at":"2021-06-25T19:07:33.155Z"}'

@pytest.fixture
def mock_header(self):
return "t=1632409405772, v1=67612f0e74f008b436a13b00266f90ef5c13f9cbcf6262206f5f4a539ff61702"
return "t=1722443701539, v1=bd54a3768f461461c8439c2f97ab0d646ef3976f84d5d5b132d18f2fa89cdad5"

@pytest.fixture
def mock_secret(self):
return "1lyKDzhJjuCkIscIWqkSe4YsQ"
return "2sAZJlbjP8Ce3rwkKEv2GfKef"

@pytest.fixture
def mock_bad_secret(self):
return "this_is_not_it_123"

@pytest.fixture
def mock_header_no_timestamp(self):
return "v1=67612f0e74f008b436a13b00266f90ef5c13f9cbcf6262206f5f4a539ff61702"
return "v1=bd54a3768f461461c8439c2f97ab0d646ef3976f84d5d5b132d18f2fa89cdad5"

@pytest.fixture
def mock_sig_hash(self):
return "df25b6efdd39d82e7b30e75ea19655b306860ad5cde3eeaeb6f1dfea029ea259"

def test_missing_body(self, mock_header, mock_secret):
with pytest.raises(ValueError) as err:
self.webhooks.verify_event(None, mock_header, mock_secret)
assert "Payload body is missing and is a required parameter" in str(err.value)

def test_missing_header(self, mock_event_body, mock_secret):
with pytest.raises(ValueError) as err:
self.webhooks.verify_event(
mock_event_body.encode("utf-8"), None, mock_secret
)
assert "Payload signature missing and is a required parameter" in str(err.value)

def test_missing_secret(self, mock_event_body, mock_header):
with pytest.raises(ValueError) as err:
self.webhooks.verify_event(
mock_event_body.encode("utf-8"), mock_header, None
)
assert "Secret is missing and is a required parameter" in str(err.value)

def test_unable_to_extract_timestamp(
self, mock_event_body, mock_header_no_timestamp, mock_secret
):
Expand Down
283 changes: 283 additions & 0 deletions workos/resources/webhooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
from typing import Generic, Literal, Union
from pydantic import Field
from typing_extensions import Annotated
from workos.resources.directory_sync import DirectoryGroup
from workos.resources.events import EventPayload
from workos.resources.user_management import OrganizationMembership, User
from workos.resources.workos_model import WorkOSModel
from workos.types.directory_sync.directory_user import DirectoryUser
from workos.types.events.authentication_payload import (
AuthenticationEmailVerificationSucceededPayload,
AuthenticationMagicAuthFailedPayload,
AuthenticationMagicAuthSucceededPayload,
AuthenticationMfaSucceededPayload,
AuthenticationOauthSucceededPayload,
AuthenticationPasswordFailedPayload,
AuthenticationPasswordSucceededPayload,
AuthenticationSsoSucceededPayload,
)
from workos.types.events.connection_payload_with_legacy_fields import (
ConnectionPayloadWithLegacyFields,
)
from workos.types.events.directory_group_membership_payload import (
DirectoryGroupMembershipPayload,
)
from workos.types.events.directory_group_with_previous_attributes import (
DirectoryGroupWithPreviousAttributes,
)
from workos.types.events.directory_payload import DirectoryPayload
from workos.types.events.directory_payload_with_legacy_fields import (
DirectoryPayloadWithLegacyFields,
)
from workos.types.events.directory_user_with_previous_attributes import (
DirectoryUserWithPreviousAttributes,
)
from workos.types.events.organization_domain_verification_failed_payload import (
OrganizationDomainVerificationFailedPayload,
)
from workos.types.events.session_created_payload import SessionCreatedPayload
from workos.types.organizations.organization_common import OrganizationCommon
from workos.types.organizations.organization_domain import OrganizationDomain
from workos.types.roles.role import Role
from workos.types.sso.connection import Connection
from workos.types.user_management.email_verification_common import (
EmailVerificationCommon,
)
from workos.types.user_management.invitation_common import InvitationCommon
from workos.types.user_management.magic_auth_common import MagicAuthCommon
from workos.types.user_management.password_reset_common import PasswordResetCommon


class WebhookModel(WorkOSModel, Generic[EventPayload]):
"""Representation of an Webhook delivered via Webhook.
Attributes:
OBJECT_FIELDS (list): List of fields an Webhook is comprised of.
"""

id: str
data: EventPayload
created_at: str


class AuthenticationEmailVerificationSucceededWebhook(
WebhookModel[AuthenticationEmailVerificationSucceededPayload,]
):
event: Literal["authentication.email_verification_succeeded"]


class AuthenticationMagicAuthFailedWebhook(
WebhookModel[AuthenticationMagicAuthFailedPayload,]
):
event: Literal["authentication.magic_auth_failed"]


class AuthenticationMagicAuthSucceededWebhook(
WebhookModel[AuthenticationMagicAuthSucceededPayload,]
):
event: Literal["authentication.magic_auth_succeeded"]


class AuthenticationMfaSucceededWebhook(
WebhookModel[AuthenticationMfaSucceededPayload]
):
event: Literal["authentication.mfa_succeeded"]


class AuthenticationOauthSucceededWebhook(
WebhookModel[AuthenticationOauthSucceededPayload]
):
event: Literal["authentication.oauth_succeeded"]


class AuthenticationPasswordFailedWebhook(
WebhookModel[AuthenticationPasswordFailedPayload]
):
event: Literal["authentication.password_failed"]


class AuthenticationPasswordSucceededWebhook(
WebhookModel[AuthenticationPasswordSucceededPayload,]
):
event: Literal["authentication.password_succeeded"]


class AuthenticationSsoSucceededWebhook(
WebhookModel[AuthenticationSsoSucceededPayload]
):
event: Literal["authentication.sso_succeeded"]


class ConnectionActivatedWebhook(WebhookModel[ConnectionPayloadWithLegacyFields]):
event: Literal["connection.activated"]


class ConnectionDeactivatedWebhook(WebhookModel[ConnectionPayloadWithLegacyFields]):
event: Literal["connection.deactivated"]


class ConnectionDeletedWebhook(WebhookModel[Connection]):
event: Literal["connection.deleted"]


class DirectoryActivatedWebhook(WebhookModel[DirectoryPayloadWithLegacyFields]):
event: Literal["dsync.activated"]


class DirectoryDeletedWebhook(WebhookModel[DirectoryPayload]):
event: Literal["dsync.deleted"]


class DirectoryGroupCreatedWebhook(WebhookModel[DirectoryGroup]):
event: Literal["dsync.group.created"]


class DirectoryGroupDeletedWebhook(WebhookModel[DirectoryGroup]):
event: Literal["dsync.group.deleted"]


class DirectoryGroupUpdatedWebhook(WebhookModel[DirectoryGroupWithPreviousAttributes]):
event: Literal["dsync.group.updated"]


class DirectoryUserCreatedWebhook(WebhookModel[DirectoryUser]):
event: Literal["dsync.user.created"]


class DirectoryUserDeletedWebhook(WebhookModel[DirectoryUser]):
event: Literal["dsync.user.deleted"]


class DirectoryUserUpdatedWebhook(WebhookModel[DirectoryUserWithPreviousAttributes]):
event: Literal["dsync.user.updated"]


class DirectoryUserAddedToGroupWebhook(WebhookModel[DirectoryGroupMembershipPayload]):
event: Literal["dsync.group.user_added"]


class DirectoryUserRemovedFromGroupWebhook(
WebhookModel[DirectoryGroupMembershipPayload]
):
event: Literal["dsync.group.user_removed"]


class EmailVerificationCreatedWebhook(WebhookModel[EmailVerificationCommon]):
event: Literal["email_verification.created"]


class InvitationCreatedWebhook(WebhookModel[InvitationCommon]):
event: Literal["invitation.created"]


class MagicAuthCreatedWebhook(WebhookModel[MagicAuthCommon]):
event: Literal["magic_auth.created"]


class OrganizationCreatedWebhook(WebhookModel[OrganizationCommon]):
event: Literal["organization.created"]


class OrganizationDeletedWebhook(WebhookModel[OrganizationCommon]):
event: Literal["organization.deleted"]


class OrganizationUpdatedWebhook(WebhookModel[OrganizationCommon]):
event: Literal["organization.updated"]


class OrganizationDomainVerificationFailedWebhook(
WebhookModel[OrganizationDomainVerificationFailedPayload,]
):
event: Literal["organization_domain.verification_failed"]


class OrganizationDomainVerifiedWebhook(WebhookModel[OrganizationDomain]):
event: Literal["organization_domain.verified"]


class OrganizationMembershipCreatedWebhook(WebhookModel[OrganizationMembership]):
event: Literal["organization_membership.created"]


class OrganizationMembershipDeletedWebhook(WebhookModel[OrganizationMembership]):
event: Literal["organization_membership.deleted"]


class OrganizationMembershipUpdatedWebhook(WebhookModel[OrganizationMembership]):
event: Literal["organization_membership.updated"]


class PasswordResetCreatedWebhook(WebhookModel[PasswordResetCommon]):
event: Literal["password_reset.created"]


class RoleCreatedWebhook(WebhookModel[Role]):
event: Literal["role.created"]


class RoleDeletedWebhook(WebhookModel[Role]):
event: Literal["role.deleted"]


class RoleUpdatedWebhook(WebhookModel[Role]):
event: Literal["role.updated"]


class SessionCreatedWebhook(WebhookModel[SessionCreatedPayload]):
event: Literal["session.created"]


class UserCreatedWebhook(WebhookModel[User]):
event: Literal["user.created"]


class UserDeletedWebhook(WebhookModel[User]):
event: Literal["user.deleted"]


class UserUpdatedWebhook(WebhookModel[User]):
event: Literal["user.updated"]


Webhook = Annotated[
Union[
AuthenticationEmailVerificationSucceededWebhook,
AuthenticationMagicAuthFailedWebhook,
AuthenticationMagicAuthSucceededWebhook,
AuthenticationMfaSucceededWebhook,
AuthenticationOauthSucceededWebhook,
AuthenticationPasswordFailedWebhook,
AuthenticationPasswordSucceededWebhook,
AuthenticationSsoSucceededWebhook,
ConnectionActivatedWebhook,
ConnectionDeactivatedWebhook,
ConnectionDeletedWebhook,
DirectoryActivatedWebhook,
DirectoryDeletedWebhook,
DirectoryGroupCreatedWebhook,
DirectoryGroupDeletedWebhook,
DirectoryGroupUpdatedWebhook,
DirectoryUserCreatedWebhook,
DirectoryUserDeletedWebhook,
DirectoryUserUpdatedWebhook,
DirectoryUserAddedToGroupWebhook,
DirectoryUserRemovedFromGroupWebhook,
EmailVerificationCreatedWebhook,
InvitationCreatedWebhook,
MagicAuthCreatedWebhook,
OrganizationCreatedWebhook,
OrganizationDeletedWebhook,
OrganizationUpdatedWebhook,
OrganizationDomainVerificationFailedWebhook,
OrganizationDomainVerifiedWebhook,
PasswordResetCreatedWebhook,
RoleCreatedWebhook,
RoleDeletedWebhook,
RoleUpdatedWebhook,
SessionCreatedWebhook,
UserCreatedWebhook,
UserDeletedWebhook,
UserUpdatedWebhook,
],
Field(..., discriminator="event"),
]
Loading

0 comments on commit 37f36c6

Please sign in to comment.