From df2ff2d2472601189d28b250bbf37d8b7a310621 Mon Sep 17 00:00:00 2001 From: Ben Lobaugh Date: Tue, 23 Sep 2025 12:01:11 -0700 Subject: [PATCH 1/2] Added integration with the Events API endpoints --- lib/Events.php | 312 +++++++++++++++++++++++++ lib/Resource/Event.php | 174 ++++++++++++++ lib/WorkOS.php | 1 + tests/WorkOS/EventsTest.php | 451 ++++++++++++++++++++++++++++++++++++ 4 files changed, 938 insertions(+) create mode 100644 lib/Events.php create mode 100644 lib/Resource/Event.php create mode 100644 tests/WorkOS/EventsTest.php diff --git a/lib/Events.php b/lib/Events.php new file mode 100644 index 0000000..bbb5047 --- /dev/null +++ b/lib/Events.php @@ -0,0 +1,312 @@ + + */ + public function listEvents($params = []) + { + $eventsPath = "events"; + + // Validate and sanitize input parameters + $params = $this->validateAndSanitizeParams($params); + + // Handle events parameter - convert array to comma-separated string + if (isset($params['events']) && is_array($params['events'])) { + $params['events'] = implode(',', $params['events']); + } + + // Note: The WorkOS Events API requires at least one event type to be specified + // If no events parameter is provided, the API will return a 400 error + // Consider using getValidEventTypes() to get available event types + + return Client::request(Client::METHOD_GET, $eventsPath, null, $params, true); + } + + + + /** + * Get available event types. + * + * @return array Array of valid event types + */ + public function getValidEventTypes() + { + return [ + // Authentication Events + 'authentication.email_verification_succeeded', + 'authentication.magic_auth_failed', + 'authentication.magic_auth_succeeded', + 'authentication.mfa_failed', + 'authentication.mfa_succeeded', + 'authentication.oauth_failed', + 'authentication.oauth_succeeded', + 'authentication.password_failed', + 'authentication.password_succeeded', + 'authentication.passkey_failed', + 'authentication.passkey_succeeded', + 'authentication.sso_failed', + 'authentication.sso_succeeded', + 'authentication.radar_risk_detected', + + // Connection Events + 'connection.activated', + 'connection.deactivated', + 'connection.deleted', + 'connection.saml_certificate_renewed', + 'connection.saml_certificate_renewal_required', + + // DSync Events + 'dsync.activated', + 'dsync.deleted', + 'dsync.group.created', + 'dsync.group.deleted', + 'dsync.group.updated', + 'dsync.group.user_added', + 'dsync.group.user_removed', + 'dsync.user.created', + 'dsync.user.deleted', + 'dsync.user.updated', + + // Email Verification Events + 'email_verification.created', + + // Flag Events + 'flag.created', + 'flag.updated', + 'flag.deleted', + 'flag.rule_updated', + + // Invitation Events + 'invitation.accepted', + 'invitation.created', + 'invitation.revoked', + + // Organization Events + 'organization.created', + 'organization.updated', + 'organization.deleted', + 'organization_domain.created', + 'organization_domain.updated', + 'organization_domain.deleted', + 'organization_domain.verified', + 'organization_domain.verification_failed', + 'organization_membership.created', + 'organization_membership.deleted', + 'organization_membership.updated', + + // Password Reset Events + 'password_reset.created', + 'password_reset.succeeded', + + // Role Events + 'role.created', + 'role.deleted', + 'role.updated', + + // Session Events + 'session.created', + 'session.revoked', + + // User Events + 'user.created', + 'user.deleted', + 'user.updated', + ]; + } + + /** + * Validate event types against the list of valid types. + * + * @param string|array $eventTypes Event type(s) to validate + * + * @return bool True if all event types are valid + */ + public function validateEventTypes($eventTypes) + { + // Handle null or empty input + if (empty($eventTypes)) { + return false; + } + + $validTypes = $this->getValidEventTypes(); + + if (is_string($eventTypes)) { + $eventTypes = explode(',', $eventTypes); + } + + // Ensure we have an array + if (!is_array($eventTypes)) { + return false; + } + + foreach ($eventTypes as $eventType) { + $eventType = trim($eventType); + + // Skip empty strings after trimming + if (empty($eventType)) { + continue; + } + + // Validate against whitelist + if (!in_array($eventType, $validTypes, true)) { + return false; + } + } + + return true; + } + + /** + * Get events filtered by type. + * + * @param string|array $eventTypes Required event types to filter by (comma-separated string or array) + * @param array $params Optional parameters (same as listEvents) + * + * @throws Exception\BadRequestException if invalid event types are provided + * @throws Exception\WorkOSException + * + * @return array Events filtered by the specified types + */ + public function getEventsByType($eventTypes, $params = []) + { + // Validate event types before proceeding + if (!$this->validateEventTypes($eventTypes)) { + throw new Exception\BadRequestException( + new Resource\Response(json_encode(['error' => 'One or more event types are invalid']), [], 400) + ); + } + + // Add the required events parameter + $params['events'] = $eventTypes; + + return $this->listEvents($params); + } + + /** + * Validate and sanitize input parameters for API requests. + * + * @param array $params Raw input parameters + * + * @return array Sanitized parameters + * + * @throws Exception\BadRequestException if invalid parameters are provided + */ + private function validateAndSanitizeParams($params) + { + $sanitized = []; + + // Validate limit parameter + if (isset($params['limit'])) { + $limit = filter_var($params['limit'], FILTER_VALIDATE_INT); + if ($limit === false || $limit < 1 || $limit > self::MAX_EVENTS_LIMIT) { + throw new Exception\BadRequestException( + new Resource\Response(json_encode(['error' => 'Limit must be an integer between 1 and ' . self::MAX_EVENTS_LIMIT]), [], 400) + ); + } + $sanitized['limit'] = $limit; + } + + // Validate order parameter + if (isset($params['order'])) { + $order = strtolower(trim($params['order'])); + if (!in_array($order, ['asc', 'desc'], true)) { + throw new Exception\BadRequestException( + new Resource\Response(json_encode(['error' => 'Order must be "asc" or "desc"']), [], 400) + ); + } + $sanitized['order'] = $order; + } + + // Validate organization_id parameter + if (isset($params['organization_id'])) { + $orgId = trim($params['organization_id']); + if (empty($orgId) || strlen($orgId) > 255) { + throw new Exception\BadRequestException( + new Resource\Response(json_encode(['error' => 'Organization ID must be a non-empty string up to 255 characters']), [], 400) + ); + } + $sanitized['organization_id'] = $orgId; + } + + // Validate after/before cursor parameters + if (isset($params['after'])) { + $after = trim($params['after']); + if (strlen($after) > 255) { + throw new Exception\BadRequestException( + new Resource\Response(json_encode(['error' => 'After cursor must be a string up to 255 characters']), [], 400) + ); + } + $sanitized['after'] = $after; + } + + if (isset($params['before'])) { + $before = trim($params['before']); + if (strlen($before) > 255) { + throw new Exception\BadRequestException( + new Resource\Response(json_encode(['error' => 'Before cursor must be a string up to 255 characters']), [], 400) + ); + } + $sanitized['before'] = $before; + } + + // Validate events parameter + if (isset($params['events'])) { + $events = $params['events']; + + // Convert string to array for validation + if (is_string($events)) { + $events = explode(',', $events); + } + + if (!is_array($events) || empty($events)) { + throw new Exception\BadRequestException( + new Resource\Response(json_encode(['error' => 'Events parameter must be a non-empty array or comma-separated string']), [], 400) + ); + } + + // Validate each event type + if (!$this->validateEventTypes($events)) { + throw new Exception\BadRequestException( + new Resource\Response(json_encode(['error' => 'One or more event types are invalid']), [], 400) + ); + } + + $sanitized['events'] = $events; + } + + return $sanitized; + } + +} diff --git a/lib/Resource/Event.php b/lib/Resource/Event.php new file mode 100644 index 0000000..5ede978 --- /dev/null +++ b/lib/Resource/Event.php @@ -0,0 +1,174 @@ + "id", + "object" => "object", + "event" => "event", + "data" => "data", + "created_at" => "createdAt", + ]; + + /** + * Get the event ID. + * + * @return string + */ + public function getId() + { + return $this->values["id"]; + } + + /** + * Get the event object type. + * + * @return string + */ + public function getObject() + { + return $this->values["object"]; + } + + /** + * Get the event type. + * + * @return string + */ + public function getEvent() + { + return $this->values["event"]; + } + + /** + * Get the event data. + * + * @return array + */ + public function getData() + { + return $this->values["data"]; + } + + /** + * Get the event creation timestamp. + * + * @return string + */ + public function getCreatedAt() + { + return $this->values["createdAt"]; + } + + /** + * Get formatted creation date. + * + * @param string $format PHP date format (default: 'Y-m-d H:i:s') + * + * @return string + */ + public function getFormattedCreatedAt($format = 'Y-m-d H:i:s') + { + return date($format, strtotime($this->values["createdAt"])); + } + + /** + * Check if event is of a specific type. + * + * @param string $eventType The event type to check + * + * @return bool + */ + public function isEventType($eventType) + { + return $this->values["event"] === $eventType; + } + + /** + * Check if event is an authentication event. + * + * @return bool + */ + public function isAuthenticationEvent() + { + return strpos($this->values["event"], 'authentication.') === 0; + } + + /** + * Check if event is a user event. + * + * @return bool + */ + public function isUserEvent() + { + return strpos($this->values["event"], 'user.') === 0; + } + + /** + * Check if event is an organization event. + * + * @return bool + */ + public function isOrganizationEvent() + { + return strpos($this->values["event"], 'organization') === 0; + } + + /** + * Check if event is a DSync event. + * + * @return bool + */ + public function isDSyncEvent() + { + return strpos($this->values["event"], 'dsync.') === 0; + } + + /** + * Get a specific data field from the event data. + * + * @param string $key The data key to retrieve + * @param mixed $default Default value if key doesn't exist + * + * @return mixed + */ + public function getDataField($key, $default = null) + { + $data = $this->values["data"]; + + if (is_array($data) && array_key_exists($key, $data)) { + return $data[$key]; + } + + return $default; + } + + /** + * Get the event data as JSON string. + * + * @param int $options JSON encoding options + * + * @return string + */ + public function getDataAsJson($options = JSON_PRETTY_PRINT) + { + return json_encode($this->values["data"], $options); + } +} diff --git a/lib/WorkOS.php b/lib/WorkOS.php index 935e32e..d4a2775 100644 --- a/lib/WorkOS.php +++ b/lib/WorkOS.php @@ -162,4 +162,5 @@ private static function getEnvVariable($key) return false; } + } diff --git a/tests/WorkOS/EventsTest.php b/tests/WorkOS/EventsTest.php new file mode 100644 index 0000000..e2719e7 --- /dev/null +++ b/tests/WorkOS/EventsTest.php @@ -0,0 +1,451 @@ +traitSetUp(); + $this->withApiKeyAndClientId(); + } + + public function testListEvents() + { + $events = new Events(); + + $this->mockRequest( + Client::METHOD_GET, + "events", + null, + ['events' => 'user.created'], + true, + json_encode([ + 'object' => 'list', + 'data' => [ + [ + 'id' => 'event_123', + 'event' => 'user.created', + 'object' => 'event', + 'data' => ['user' => ['id' => 'user_123']], + 'created_at' => '2023-01-01T00:00:00Z' + ] + ], + 'list_metadata' => ['after' => null] + ]) + ); + + $response = $events->listEvents(['events' => ['user.created']]); + + $this->assertIsArray($response); + $this->assertArrayHasKey('object', $response); + $this->assertEquals('list', $response['object']); + $this->assertArrayHasKey('data', $response); + $this->assertIsArray($response['data']); + $this->assertCount(1, $response['data']); + } + + public function testListEventsWithFilters() + { + $events = new Events(); + + $this->mockRequest( + Client::METHOD_GET, + "events", + null, + ['limit' => 10, 'events' => 'user.created,user.updated'], + true, + json_encode([ + 'object' => 'list', + 'data' => [ + [ + 'id' => 'event_123', + 'event' => 'user.created', + 'object' => 'event', + 'data' => ['user' => ['id' => 'user_123']], + 'created_at' => '2023-01-01T00:00:00Z' + ] + ], + 'list_metadata' => ['after' => null] + ]) + ); + + $response = $events->listEvents([ + 'events' => 'user.created,user.updated', + 'limit' => 10 + ]); + + $this->assertIsArray($response); + $this->assertArrayHasKey('data', $response); + $this->assertCount(1, $response['data']); + $this->assertEquals('user.created', $response['data'][0]['event']); + } + + public function testListEventsWithArrayFilter() + { + $events = new Events(); + + $this->mockRequest( + Client::METHOD_GET, + "events", + null, + ['limit' => 5, 'events' => 'user.created,user.updated'], + true, + json_encode([ + 'object' => 'list', + 'data' => [], + 'list_metadata' => ['after' => null] + ]) + ); + + $response = $events->listEvents([ + 'events' => ['user.created', 'user.updated'], + 'limit' => 5 + ]); + + $this->assertIsArray($response); + $this->assertArrayHasKey('data', $response); + } + + public function testListEventsWithAllParameters() + { + $events = new Events(); + + $this->mockRequest( + Client::METHOD_GET, + "events", + null, + [ + 'limit' => 25, + 'order' => 'asc', + 'organization_id' => 'org_123', + 'after' => 'cursor_after', + 'before' => 'cursor_before', + 'events' => 'user.created,user.updated' + ], + true, + json_encode([ + 'object' => 'list', + 'data' => [], + 'list_metadata' => ['after' => null] + ]) + ); + + $response = $events->listEvents([ + 'events' => ['user.created', 'user.updated'], + 'limit' => 25, + 'order' => 'asc', + 'organization_id' => 'org_123', + 'after' => 'cursor_after', + 'before' => 'cursor_before' + ]); + + $this->assertIsArray($response); + $this->assertArrayHasKey('data', $response); + } + + // Parameter validation tests + public function testListEventsWithInvalidLimit() + { + $events = new Events(); + + $this->expectException(\WorkOS\Exception\BadRequestException::class); + $events->listEvents(['events' => ['user.created'], 'limit' => 'invalid']); + } + + public function testListEventsWithLimitTooLow() + { + $events = new Events(); + + $this->expectException(\WorkOS\Exception\BadRequestException::class); + $events->listEvents(['events' => ['user.created'], 'limit' => 0]); + } + + public function testListEventsWithLimitTooHigh() + { + $events = new Events(); + + $this->expectException(\WorkOS\Exception\BadRequestException::class); + $events->listEvents(['events' => ['user.created'], 'limit' => 101]); + } + + public function testListEventsWithInvalidOrder() + { + $events = new Events(); + + $this->expectException(\WorkOS\Exception\BadRequestException::class); + $events->listEvents(['events' => ['user.created'], 'order' => 'invalid']); + } + + public function testListEventsWithEmptyOrganizationId() + { + $events = new Events(); + + $this->expectException(\WorkOS\Exception\BadRequestException::class); + $events->listEvents(['events' => ['user.created'], 'organization_id' => '']); + } + + public function testListEventsWithLongOrganizationId() + { + $events = new Events(); + + $this->expectException(\WorkOS\Exception\BadRequestException::class); + $events->listEvents(['events' => ['user.created'], 'organization_id' => str_repeat('a', 256)]); + } + + public function testListEventsWithLongAfterCursor() + { + $events = new Events(); + + $this->expectException(\WorkOS\Exception\BadRequestException::class); + $events->listEvents(['events' => ['user.created'], 'after' => str_repeat('a', 256)]); + } + + public function testListEventsWithLongBeforeCursor() + { + $events = new Events(); + + $this->expectException(\WorkOS\Exception\BadRequestException::class); + $events->listEvents(['events' => ['user.created'], 'before' => str_repeat('a', 256)]); + } + + public function testListEventsWithInvalidEventsParameter() + { + $events = new Events(); + + $this->expectException(\WorkOS\Exception\BadRequestException::class); + $events->listEvents(['events' => []]); + } + + public function testListEventsWithInvalidEventTypes() + { + $events = new Events(); + + $this->expectException(\WorkOS\Exception\BadRequestException::class); + $events->listEvents(['events' => ['invalid.event.type']]); + } + + public function testGetValidEventTypes() + { + $events = new Events(); + + $validTypes = $events->getValidEventTypes(); + + $this->assertIsArray($validTypes); + $this->assertNotEmpty($validTypes); + + // Test some specific event types exist + $this->assertContains('user.created', $validTypes); + $this->assertContains('user.updated', $validTypes); + $this->assertContains('organization.created', $validTypes); + $this->assertContains('authentication.sso_succeeded', $validTypes); + $this->assertContains('dsync.user.created', $validTypes); + } + + public function testValidateEventTypes() + { + $events = new Events(); + + // Test valid event types + $this->assertTrue($events->validateEventTypes('user.created')); + $this->assertTrue($events->validateEventTypes(['user.created', 'user.updated'])); + $this->assertTrue($events->validateEventTypes('user.created,user.updated')); + + // Test invalid event types + $this->assertFalse($events->validateEventTypes('invalid.event.type')); + $this->assertFalse($events->validateEventTypes(['user.created', 'invalid.event.type'])); + } + + public function testValidateEventTypesWithNull() + { + $events = new Events(); + + $this->assertFalse($events->validateEventTypes(null)); + } + + public function testValidateEventTypesWithEmptyString() + { + $events = new Events(); + + $this->assertFalse($events->validateEventTypes('')); + } + + public function testValidateEventTypesWithEmptyArray() + { + $events = new Events(); + + $this->assertFalse($events->validateEventTypes([])); + } + + public function testValidateEventTypesWithWhitespace() + { + $events = new Events(); + + $this->assertTrue($events->validateEventTypes(' user.created ')); + $this->assertTrue($events->validateEventTypes('user.created , user.updated')); + } + + public function testValidateEventTypesWithMixedValidInvalid() + { + $events = new Events(); + + $this->assertFalse($events->validateEventTypes(['user.created', 'invalid.event.type'])); + } + + public function testGetEventsByType() + { + $events = new Events(); + + $this->mockRequest( + Client::METHOD_GET, + "events", + null, + ['limit' => 20, 'events' => 'user.created,user.updated'], + true, + json_encode([ + 'object' => 'list', + 'data' => [ + [ + 'id' => 'event_123', + 'event' => 'user.created', + 'object' => 'event', + 'data' => ['user' => ['id' => 'user_123']], + 'created_at' => '2023-01-01T00:00:00Z' + ] + ], + 'list_metadata' => ['after' => null] + ]) + ); + + $response = $events->getEventsByType(['user.created', 'user.updated'], ['limit' => 20]); + + $this->assertIsArray($response); + $this->assertArrayHasKey('data', $response); + $this->assertCount(1, $response['data']); + $this->assertEquals('user.created', $response['data'][0]['event']); + } + + public function testGetEventsByTypeWithInvalidTypes() + { + $events = new Events(); + + // Test with invalid event types + $this->expectException(\WorkOS\Exception\BadRequestException::class); + $events->getEventsByType(['invalid.event.type'], ['limit' => 20]); + } + + public function testGetEventsByTypeWithEmptyTypes() + { + $events = new Events(); + + // Test with empty event types + $this->expectException(\WorkOS\Exception\BadRequestException::class); + $events->getEventsByType([]); + } + + public function testEventResourceCreation() + { + $events = new Events(); + + $eventData = [ + 'id' => 'event_123', + 'event' => 'user.created', + 'object' => 'event', + 'data' => ['user' => ['id' => 'user_123']], + 'created_at' => '2023-01-01T00:00:00Z' + ]; + + $this->mockRequest( + Client::METHOD_GET, + "events", + null, + ['limit' => 1, 'events' => 'user.created'], + true, + json_encode([ + 'object' => 'list', + 'data' => [$eventData], + 'list_metadata' => ['after' => null] + ]) + ); + + $response = $events->listEvents(['events' => ['user.created'], 'limit' => 1]); + + $this->assertNotEmpty($response['data']); + $eventData = $response['data'][0]; + + // Test creating Event resource + $event = \WorkOS\Resource\Event::constructFromResponse($eventData); + + $this->assertInstanceOf(\WorkOS\Resource\Event::class, $event); + $this->assertEquals($eventData['id'], $event->getId()); + $this->assertEquals($eventData['event'], $event->getEvent()); + $this->assertEquals($eventData['data'], $event->getData()); + + // Test helper methods + $this->assertTrue($event->isUserEvent()); + + // Test JSON formatting + $jsonData = $event->getDataAsJson(); + $this->assertIsString($jsonData); + $this->assertJson($jsonData); + } + + public function testEventResourceHelperMethods() + { + $eventData = [ + 'id' => 'event_123', + 'event' => 'authentication.sso_succeeded', + 'object' => 'event', + 'data' => ['user' => ['id' => 'user_123']], + 'created_at' => '2023-01-01T00:00:00Z' + ]; + + $event = \WorkOS\Resource\Event::constructFromResponse($eventData); + + // Test event type checks + $this->assertTrue($event->isAuthenticationEvent()); + $this->assertFalse($event->isUserEvent()); + $this->assertFalse($event->isOrganizationEvent()); + $this->assertFalse($event->isDSyncEvent()); + + // Test specific event type + $this->assertTrue($event->isEventType('authentication.sso_succeeded')); + $this->assertFalse($event->isEventType('user.created')); + } + + public function testEventResourceDataAccess() + { + $eventData = [ + 'id' => 'event_123', + 'event' => 'user.created', + 'object' => 'event', + 'data' => [ + 'user' => [ + 'id' => 'user_123', + 'email' => 'test@example.com' + ] + ], + 'created_at' => '2023-01-01T00:00:00Z' + ]; + + $event = \WorkOS\Resource\Event::constructFromResponse($eventData); + + // Test data field access + $userData = $event->getDataField('user'); + $this->assertIsArray($userData); + $this->assertEquals('user_123', $userData['id']); + $this->assertEquals('test@example.com', $userData['email']); + $this->assertNull($event->getDataField('nonexistent')); + + // Test formatted date + $formattedDate = $event->getFormattedCreatedAt('Y-m-d H:i:s'); + $this->assertIsString($formattedDate); + $this->assertStringContainsString('2023-01-01', $formattedDate); + } +} \ No newline at end of file From 1e262397105c1b8f11f0e37d8d34b217a6de1464 Mon Sep 17 00:00:00 2001 From: Ben Lobaugh Date: Fri, 5 Dec 2025 09:22:05 -0800 Subject: [PATCH 2/2] Updates per review --- lib/EventTypes.php | 91 +++++++++ lib/Events.php | 288 +---------------------------- lib/Resource/Event.php | 8 +- tests/WorkOS/EventsTest.php | 360 +++++++++++------------------------- 4 files changed, 211 insertions(+), 536 deletions(-) create mode 100644 lib/EventTypes.php diff --git a/lib/EventTypes.php b/lib/EventTypes.php new file mode 100644 index 0000000..c981980 --- /dev/null +++ b/lib/EventTypes.php @@ -0,0 +1,91 @@ + + * @return array{null, string|null, array} Returns [before, after, events] where before is always null for Events API */ public function listEvents($params = []) { $eventsPath = "events"; - - // Validate and sanitize input parameters - $params = $this->validateAndSanitizeParams($params); - + // Handle events parameter - convert array to comma-separated string if (isset($params['events']) && is_array($params['events'])) { $params['events'] = implode(',', $params['events']); } - - // Note: The WorkOS Events API requires at least one event type to be specified - // If no events parameter is provided, the API will return a 400 error - // Consider using getValidEventTypes() to get available event types - - return Client::request(Client::METHOD_GET, $eventsPath, null, $params, true); - } - - - - /** - * Get available event types. - * - * @return array Array of valid event types - */ - public function getValidEventTypes() - { - return [ - // Authentication Events - 'authentication.email_verification_succeeded', - 'authentication.magic_auth_failed', - 'authentication.magic_auth_succeeded', - 'authentication.mfa_failed', - 'authentication.mfa_succeeded', - 'authentication.oauth_failed', - 'authentication.oauth_succeeded', - 'authentication.password_failed', - 'authentication.password_succeeded', - 'authentication.passkey_failed', - 'authentication.passkey_succeeded', - 'authentication.sso_failed', - 'authentication.sso_succeeded', - 'authentication.radar_risk_detected', - - // Connection Events - 'connection.activated', - 'connection.deactivated', - 'connection.deleted', - 'connection.saml_certificate_renewed', - 'connection.saml_certificate_renewal_required', - - // DSync Events - 'dsync.activated', - 'dsync.deleted', - 'dsync.group.created', - 'dsync.group.deleted', - 'dsync.group.updated', - 'dsync.group.user_added', - 'dsync.group.user_removed', - 'dsync.user.created', - 'dsync.user.deleted', - 'dsync.user.updated', - - // Email Verification Events - 'email_verification.created', - - // Flag Events - 'flag.created', - 'flag.updated', - 'flag.deleted', - 'flag.rule_updated', - - // Invitation Events - 'invitation.accepted', - 'invitation.created', - 'invitation.revoked', - - // Organization Events - 'organization.created', - 'organization.updated', - 'organization.deleted', - 'organization_domain.created', - 'organization_domain.updated', - 'organization_domain.deleted', - 'organization_domain.verified', - 'organization_domain.verification_failed', - 'organization_membership.created', - 'organization_membership.deleted', - 'organization_membership.updated', - - // Password Reset Events - 'password_reset.created', - 'password_reset.succeeded', - - // Role Events - 'role.created', - 'role.deleted', - 'role.updated', - - // Session Events - 'session.created', - 'session.revoked', - - // User Events - 'user.created', - 'user.deleted', - 'user.updated', - ]; - } - /** - * Validate event types against the list of valid types. - * - * @param string|array $eventTypes Event type(s) to validate - * - * @return bool True if all event types are valid - */ - public function validateEventTypes($eventTypes) - { - // Handle null or empty input - if (empty($eventTypes)) { - return false; - } - - $validTypes = $this->getValidEventTypes(); - - if (is_string($eventTypes)) { - $eventTypes = explode(',', $eventTypes); + $response = Client::request(Client::METHOD_GET, $eventsPath, null, $params, true); + $events = []; + foreach ($response["data"] as $responseData) { + \array_push($events, Resource\Event::constructFromResponse($responseData)); } - - // Ensure we have an array - if (!is_array($eventTypes)) { - return false; - } - - foreach ($eventTypes as $eventType) { - $eventType = trim($eventType); - - // Skip empty strings after trimming - if (empty($eventType)) { - continue; - } - - // Validate against whitelist - if (!in_array($eventType, $validTypes, true)) { - return false; - } - } - - return true; + $after = $response["list_metadata"]["after"] ?? null; + return [null, $after, $events]; } - - /** - * Get events filtered by type. - * - * @param string|array $eventTypes Required event types to filter by (comma-separated string or array) - * @param array $params Optional parameters (same as listEvents) - * - * @throws Exception\BadRequestException if invalid event types are provided - * @throws Exception\WorkOSException - * - * @return array Events filtered by the specified types - */ - public function getEventsByType($eventTypes, $params = []) - { - // Validate event types before proceeding - if (!$this->validateEventTypes($eventTypes)) { - throw new Exception\BadRequestException( - new Resource\Response(json_encode(['error' => 'One or more event types are invalid']), [], 400) - ); - } - - // Add the required events parameter - $params['events'] = $eventTypes; - - return $this->listEvents($params); - } - - /** - * Validate and sanitize input parameters for API requests. - * - * @param array $params Raw input parameters - * - * @return array Sanitized parameters - * - * @throws Exception\BadRequestException if invalid parameters are provided - */ - private function validateAndSanitizeParams($params) - { - $sanitized = []; - - // Validate limit parameter - if (isset($params['limit'])) { - $limit = filter_var($params['limit'], FILTER_VALIDATE_INT); - if ($limit === false || $limit < 1 || $limit > self::MAX_EVENTS_LIMIT) { - throw new Exception\BadRequestException( - new Resource\Response(json_encode(['error' => 'Limit must be an integer between 1 and ' . self::MAX_EVENTS_LIMIT]), [], 400) - ); - } - $sanitized['limit'] = $limit; - } - - // Validate order parameter - if (isset($params['order'])) { - $order = strtolower(trim($params['order'])); - if (!in_array($order, ['asc', 'desc'], true)) { - throw new Exception\BadRequestException( - new Resource\Response(json_encode(['error' => 'Order must be "asc" or "desc"']), [], 400) - ); - } - $sanitized['order'] = $order; - } - - // Validate organization_id parameter - if (isset($params['organization_id'])) { - $orgId = trim($params['organization_id']); - if (empty($orgId) || strlen($orgId) > 255) { - throw new Exception\BadRequestException( - new Resource\Response(json_encode(['error' => 'Organization ID must be a non-empty string up to 255 characters']), [], 400) - ); - } - $sanitized['organization_id'] = $orgId; - } - - // Validate after/before cursor parameters - if (isset($params['after'])) { - $after = trim($params['after']); - if (strlen($after) > 255) { - throw new Exception\BadRequestException( - new Resource\Response(json_encode(['error' => 'After cursor must be a string up to 255 characters']), [], 400) - ); - } - $sanitized['after'] = $after; - } - - if (isset($params['before'])) { - $before = trim($params['before']); - if (strlen($before) > 255) { - throw new Exception\BadRequestException( - new Resource\Response(json_encode(['error' => 'Before cursor must be a string up to 255 characters']), [], 400) - ); - } - $sanitized['before'] = $before; - } - - // Validate events parameter - if (isset($params['events'])) { - $events = $params['events']; - - // Convert string to array for validation - if (is_string($events)) { - $events = explode(',', $events); - } - - if (!is_array($events) || empty($events)) { - throw new Exception\BadRequestException( - new Resource\Response(json_encode(['error' => 'Events parameter must be a non-empty array or comma-separated string']), [], 400) - ); - } - - // Validate each event type - if (!$this->validateEventTypes($events)) { - throw new Exception\BadRequestException( - new Resource\Response(json_encode(['error' => 'One or more event types are invalid']), [], 400) - ); - } - - $sanitized['events'] = $events; - } - - return $sanitized; - } - } diff --git a/lib/Resource/Event.php b/lib/Resource/Event.php index 5ede978..120bb12 100644 --- a/lib/Resource/Event.php +++ b/lib/Resource/Event.php @@ -16,12 +16,12 @@ class Event extends BaseWorkOSResource "object", "event", "data", - "created_at", + "createdAt", ]; public const RESPONSE_TO_RESOURCE_KEY = [ "id" => "id", - "object" => "object", + "object" => "object", "event" => "event", "data" => "data", "created_at" => "createdAt", @@ -152,11 +152,11 @@ public function isDSyncEvent() public function getDataField($key, $default = null) { $data = $this->values["data"]; - + if (is_array($data) && array_key_exists($key, $data)) { return $data[$key]; } - + return $default; } diff --git a/tests/WorkOS/EventsTest.php b/tests/WorkOS/EventsTest.php index e2719e7..7ee3d7a 100644 --- a/tests/WorkOS/EventsTest.php +++ b/tests/WorkOS/EventsTest.php @@ -19,7 +19,7 @@ protected function setUp(): void public function testListEvents() { $events = new Events(); - + $this->mockRequest( Client::METHOD_GET, "events", @@ -40,26 +40,27 @@ public function testListEvents() 'list_metadata' => ['after' => null] ]) ); - - $response = $events->listEvents(['events' => ['user.created']]); - - $this->assertIsArray($response); - $this->assertArrayHasKey('object', $response); - $this->assertEquals('list', $response['object']); - $this->assertArrayHasKey('data', $response); - $this->assertIsArray($response['data']); - $this->assertCount(1, $response['data']); + + list($before, $after, $eventsList) = $events->listEvents(['events' => ['user.created']]); + + $this->assertNull($before); + $this->assertNull($after); + $this->assertIsArray($eventsList); + $this->assertCount(1, $eventsList); + $this->assertInstanceOf(\WorkOS\Resource\Event::class, $eventsList[0]); + $this->assertEquals('event_123', $eventsList[0]->getId()); + $this->assertEquals('user.created', $eventsList[0]->getEvent()); } public function testListEventsWithFilters() { $events = new Events(); - + $this->mockRequest( Client::METHOD_GET, "events", null, - ['limit' => 10, 'events' => 'user.created,user.updated'], + ['events' => 'user.created,user.updated', 'limit' => 10], true, json_encode([ 'object' => 'list', @@ -75,27 +76,29 @@ public function testListEventsWithFilters() 'list_metadata' => ['after' => null] ]) ); - - $response = $events->listEvents([ + + list($before, $after, $eventsList) = $events->listEvents([ 'events' => 'user.created,user.updated', 'limit' => 10 ]); - - $this->assertIsArray($response); - $this->assertArrayHasKey('data', $response); - $this->assertCount(1, $response['data']); - $this->assertEquals('user.created', $response['data'][0]['event']); + + $this->assertNull($before); + $this->assertNull($after); + $this->assertIsArray($eventsList); + $this->assertCount(1, $eventsList); + $this->assertInstanceOf(\WorkOS\Resource\Event::class, $eventsList[0]); + $this->assertEquals('user.created', $eventsList[0]->getEvent()); } public function testListEventsWithArrayFilter() { $events = new Events(); - + $this->mockRequest( Client::METHOD_GET, "events", null, - ['limit' => 5, 'events' => 'user.created,user.updated'], + ['events' => 'user.created,user.updated', 'limit' => 5], true, json_encode([ 'object' => 'list', @@ -103,41 +106,43 @@ public function testListEventsWithArrayFilter() 'list_metadata' => ['after' => null] ]) ); - - $response = $events->listEvents([ + + list($before, $after, $eventsList) = $events->listEvents([ 'events' => ['user.created', 'user.updated'], 'limit' => 5 ]); - - $this->assertIsArray($response); - $this->assertArrayHasKey('data', $response); + + $this->assertNull($before); + $this->assertNull($after); + $this->assertIsArray($eventsList); + $this->assertCount(0, $eventsList); } public function testListEventsWithAllParameters() { $events = new Events(); - + $this->mockRequest( Client::METHOD_GET, "events", null, [ + 'events' => 'user.created,user.updated', 'limit' => 25, 'order' => 'asc', 'organization_id' => 'org_123', 'after' => 'cursor_after', - 'before' => 'cursor_before', - 'events' => 'user.created,user.updated' + 'before' => 'cursor_before' ], true, json_encode([ 'object' => 'list', 'data' => [], - 'list_metadata' => ['after' => null] + 'list_metadata' => ['after' => 'cursor_next'] ]) ); - - $response = $events->listEvents([ + + list($before, $after, $eventsList) = $events->listEvents([ 'events' => ['user.created', 'user.updated'], 'limit' => 25, 'order' => 'asc', @@ -145,214 +150,17 @@ public function testListEventsWithAllParameters() 'after' => 'cursor_after', 'before' => 'cursor_before' ]); - - $this->assertIsArray($response); - $this->assertArrayHasKey('data', $response); - } - - // Parameter validation tests - public function testListEventsWithInvalidLimit() - { - $events = new Events(); - - $this->expectException(\WorkOS\Exception\BadRequestException::class); - $events->listEvents(['events' => ['user.created'], 'limit' => 'invalid']); - } - - public function testListEventsWithLimitTooLow() - { - $events = new Events(); - - $this->expectException(\WorkOS\Exception\BadRequestException::class); - $events->listEvents(['events' => ['user.created'], 'limit' => 0]); - } - - public function testListEventsWithLimitTooHigh() - { - $events = new Events(); - - $this->expectException(\WorkOS\Exception\BadRequestException::class); - $events->listEvents(['events' => ['user.created'], 'limit' => 101]); - } - - public function testListEventsWithInvalidOrder() - { - $events = new Events(); - - $this->expectException(\WorkOS\Exception\BadRequestException::class); - $events->listEvents(['events' => ['user.created'], 'order' => 'invalid']); - } - - public function testListEventsWithEmptyOrganizationId() - { - $events = new Events(); - - $this->expectException(\WorkOS\Exception\BadRequestException::class); - $events->listEvents(['events' => ['user.created'], 'organization_id' => '']); - } - - public function testListEventsWithLongOrganizationId() - { - $events = new Events(); - - $this->expectException(\WorkOS\Exception\BadRequestException::class); - $events->listEvents(['events' => ['user.created'], 'organization_id' => str_repeat('a', 256)]); - } - - public function testListEventsWithLongAfterCursor() - { - $events = new Events(); - - $this->expectException(\WorkOS\Exception\BadRequestException::class); - $events->listEvents(['events' => ['user.created'], 'after' => str_repeat('a', 256)]); - } - - public function testListEventsWithLongBeforeCursor() - { - $events = new Events(); - - $this->expectException(\WorkOS\Exception\BadRequestException::class); - $events->listEvents(['events' => ['user.created'], 'before' => str_repeat('a', 256)]); - } - - public function testListEventsWithInvalidEventsParameter() - { - $events = new Events(); - - $this->expectException(\WorkOS\Exception\BadRequestException::class); - $events->listEvents(['events' => []]); - } - - public function testListEventsWithInvalidEventTypes() - { - $events = new Events(); - - $this->expectException(\WorkOS\Exception\BadRequestException::class); - $events->listEvents(['events' => ['invalid.event.type']]); - } - - public function testGetValidEventTypes() - { - $events = new Events(); - - $validTypes = $events->getValidEventTypes(); - - $this->assertIsArray($validTypes); - $this->assertNotEmpty($validTypes); - - // Test some specific event types exist - $this->assertContains('user.created', $validTypes); - $this->assertContains('user.updated', $validTypes); - $this->assertContains('organization.created', $validTypes); - $this->assertContains('authentication.sso_succeeded', $validTypes); - $this->assertContains('dsync.user.created', $validTypes); - } - - public function testValidateEventTypes() - { - $events = new Events(); - - // Test valid event types - $this->assertTrue($events->validateEventTypes('user.created')); - $this->assertTrue($events->validateEventTypes(['user.created', 'user.updated'])); - $this->assertTrue($events->validateEventTypes('user.created,user.updated')); - - // Test invalid event types - $this->assertFalse($events->validateEventTypes('invalid.event.type')); - $this->assertFalse($events->validateEventTypes(['user.created', 'invalid.event.type'])); - } - - public function testValidateEventTypesWithNull() - { - $events = new Events(); - - $this->assertFalse($events->validateEventTypes(null)); - } - - public function testValidateEventTypesWithEmptyString() - { - $events = new Events(); - - $this->assertFalse($events->validateEventTypes('')); - } - - public function testValidateEventTypesWithEmptyArray() - { - $events = new Events(); - - $this->assertFalse($events->validateEventTypes([])); - } - - public function testValidateEventTypesWithWhitespace() - { - $events = new Events(); - - $this->assertTrue($events->validateEventTypes(' user.created ')); - $this->assertTrue($events->validateEventTypes('user.created , user.updated')); - } - public function testValidateEventTypesWithMixedValidInvalid() - { - $events = new Events(); - - $this->assertFalse($events->validateEventTypes(['user.created', 'invalid.event.type'])); - } - - public function testGetEventsByType() - { - $events = new Events(); - - $this->mockRequest( - Client::METHOD_GET, - "events", - null, - ['limit' => 20, 'events' => 'user.created,user.updated'], - true, - json_encode([ - 'object' => 'list', - 'data' => [ - [ - 'id' => 'event_123', - 'event' => 'user.created', - 'object' => 'event', - 'data' => ['user' => ['id' => 'user_123']], - 'created_at' => '2023-01-01T00:00:00Z' - ] - ], - 'list_metadata' => ['after' => null] - ]) - ); - - $response = $events->getEventsByType(['user.created', 'user.updated'], ['limit' => 20]); - - $this->assertIsArray($response); - $this->assertArrayHasKey('data', $response); - $this->assertCount(1, $response['data']); - $this->assertEquals('user.created', $response['data'][0]['event']); - } - - public function testGetEventsByTypeWithInvalidTypes() - { - $events = new Events(); - - // Test with invalid event types - $this->expectException(\WorkOS\Exception\BadRequestException::class); - $events->getEventsByType(['invalid.event.type'], ['limit' => 20]); - } - - public function testGetEventsByTypeWithEmptyTypes() - { - $events = new Events(); - - // Test with empty event types - $this->expectException(\WorkOS\Exception\BadRequestException::class); - $events->getEventsByType([]); + $this->assertNull($before); + $this->assertEquals('cursor_next', $after); + $this->assertIsArray($eventsList); + $this->assertCount(0, $eventsList); } public function testEventResourceCreation() { $events = new Events(); - + $eventData = [ 'id' => 'event_123', 'event' => 'user.created', @@ -360,12 +168,12 @@ public function testEventResourceCreation() 'data' => ['user' => ['id' => 'user_123']], 'created_at' => '2023-01-01T00:00:00Z' ]; - + $this->mockRequest( Client::METHOD_GET, "events", null, - ['limit' => 1, 'events' => 'user.created'], + ['events' => 'user.created', 'limit' => 1], true, json_encode([ 'object' => 'list', @@ -373,23 +181,21 @@ public function testEventResourceCreation() 'list_metadata' => ['after' => null] ]) ); - - $response = $events->listEvents(['events' => ['user.created'], 'limit' => 1]); - - $this->assertNotEmpty($response['data']); - $eventData = $response['data'][0]; - - // Test creating Event resource - $event = \WorkOS\Resource\Event::constructFromResponse($eventData); - + + list($before, $after, $eventsList) = $events->listEvents(['events' => ['user.created'], 'limit' => 1]); + + $this->assertNotEmpty($eventsList); + $this->assertCount(1, $eventsList); + $event = $eventsList[0]; + $this->assertInstanceOf(\WorkOS\Resource\Event::class, $event); $this->assertEquals($eventData['id'], $event->getId()); $this->assertEquals($eventData['event'], $event->getEvent()); $this->assertEquals($eventData['data'], $event->getData()); - + // Test helper methods $this->assertTrue($event->isUserEvent()); - + // Test JSON formatting $jsonData = $event->getDataAsJson(); $this->assertIsString($jsonData); @@ -405,15 +211,15 @@ public function testEventResourceHelperMethods() 'data' => ['user' => ['id' => 'user_123']], 'created_at' => '2023-01-01T00:00:00Z' ]; - + $event = \WorkOS\Resource\Event::constructFromResponse($eventData); - + // Test event type checks $this->assertTrue($event->isAuthenticationEvent()); $this->assertFalse($event->isUserEvent()); $this->assertFalse($event->isOrganizationEvent()); $this->assertFalse($event->isDSyncEvent()); - + // Test specific event type $this->assertTrue($event->isEventType('authentication.sso_succeeded')); $this->assertFalse($event->isEventType('user.created')); @@ -433,19 +239,65 @@ public function testEventResourceDataAccess() ], 'created_at' => '2023-01-01T00:00:00Z' ]; - + $event = \WorkOS\Resource\Event::constructFromResponse($eventData); - + // Test data field access $userData = $event->getDataField('user'); $this->assertIsArray($userData); $this->assertEquals('user_123', $userData['id']); $this->assertEquals('test@example.com', $userData['email']); $this->assertNull($event->getDataField('nonexistent')); - + // Test formatted date $formattedDate = $event->getFormattedCreatedAt('Y-m-d H:i:s'); $this->assertIsString($formattedDate); $this->assertStringContainsString('2023-01-01', $formattedDate); } -} \ No newline at end of file + + public function testEventTypesConstants() + { + // Test that EventTypes constants are accessible and have correct values + $this->assertEquals('user.created', EventTypes::USER_CREATED); + $this->assertEquals('user.updated', EventTypes::USER_UPDATED); + $this->assertEquals('user.deleted', EventTypes::USER_DELETED); + $this->assertEquals('authentication.sso_succeeded', EventTypes::AUTHENTICATION_SSO_SUCCEEDED); + $this->assertEquals('organization.created', EventTypes::ORGANIZATION_CREATED); + } + + public function testListEventsWithEventTypesConstants() + { + $events = new Events(); + + $this->mockRequest( + Client::METHOD_GET, + "events", + null, + ['events' => EventTypes::USER_CREATED . ',' . EventTypes::USER_UPDATED], + true, + json_encode([ + 'object' => 'list', + 'data' => [ + [ + 'id' => 'event_123', + 'event' => 'user.created', + 'object' => 'event', + 'data' => ['user' => ['id' => 'user_123']], + 'created_at' => '2023-01-01T00:00:00Z' + ] + ], + 'list_metadata' => ['after' => null] + ]) + ); + + list($before, $after, $eventsList) = $events->listEvents([ + 'events' => [EventTypes::USER_CREATED, EventTypes::USER_UPDATED] + ]); + + $this->assertNull($before); + $this->assertNull($after); + $this->assertIsArray($eventsList); + $this->assertCount(1, $eventsList); + $this->assertInstanceOf(\WorkOS\Resource\Event::class, $eventsList[0]); + } +}