diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a13c6f..9805772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,40 @@ +## [Unreleased] + +### Added + +- Webhook handling spec helpers (CHA-2961): `UnknownEvent` class for forward-compat; + `gunzip_payload`, `decode_sqs_payload`, `decode_sns_payload` primitives; + `parse_event` (returns typed event or `UnknownEvent` for unrecognized discriminators); + `verify_and_parse_webhook` HTTP composite; `parse_sqs` / `parse_sns` + queue composites (no signature; backend emits no HMAC for queue messages today). + Security for queue-delivered payloads is enforced via AWS IAM on the SQS/SNS + subscription, not in-SDK. +- New `Stream::Webhook` module alias (preferred). `StreamChat::Webhook` retained as + backward-compat alias for one minor-version cycle. +- New unified error class: `StreamChat::Webhook::InvalidWebhookError` covering signature + mismatch, invalid JSON, missing/non-string `type` field, gzip decompression failure, + invalid base64 in a queue body, and malformed SNS envelopes. Distinguish failure modes + via the message substring or `cause` chain rather than the class. +- New instance methods on `GetStreamRuby::Client`: `verify_signature(body, signature)` and + `verify_and_parse_webhook(body, signature)` — drop the `api_secret` parameter in favor + of the client's stored secret. Dual API: module-level methods remain available. +- New instance methods on `GetStreamRuby::Client`: `parse_sqs(message_body)` and + `parse_sns(notification_body)` (no signature; AWS IAM). +- Conformance fixture suite under `test/fixtures/webhooks/` (14 event-type buckets plus + `_invalid/` negative cases). + +### Changed + +- No breaking changes. + +### Fixed + +- `event_class_for_type` now references `GetStream::Generated::Models::*Event` + (was `StreamChat::*Event`, which raised `NameError` at runtime). `parse_event` + resolves known event types correctly. + +[Spec](https://www.notion.so/stream-wiki/Server-Side-SDK-Webhook-Handling-Spec-34b6a5d7f9f681e78003c443f227493c) + ## [6.0.0] - 2026-04-17 ### major^2 changes diff --git a/generate.sh b/generate.sh index 30e61a6..a5fc073 100755 --- a/generate.sh +++ b/generate.sh @@ -21,6 +21,9 @@ set -ex # cd in API repo, generate new spec and then generate code from it ( cd $SOURCE_PATH ; make openapi ; go run ./cmd/chat-manager openapi generate-client --language ruby --spec ./releases/v2/serverside-api.yaml --output $DST_PATH ) +# Generate webhook conformance fixtures (CHA-2961) +( cd $SOURCE_PATH ; go run ./cmd/chat-manager openapi generate-webhook-fixtures --output $DST_PATH/test/fixtures/webhooks ) + # Fix any potential issues in generated code echo "Applying Ruby-specific fixes..." diff --git a/lib/getstream_ruby/client.rb b/lib/getstream_ruby/client.rb index 7604225..68a7398 100644 --- a/lib/getstream_ruby/client.rb +++ b/lib/getstream_ruby/client.rb @@ -13,6 +13,7 @@ require_relative 'generated/video_client' require_relative 'extensions/moderation_extensions' require_relative 'generated/feed' +require_relative 'generated/webhook' require_relative 'stream_response' module GetStreamRuby @@ -76,6 +77,52 @@ def feed(feed_group_id, feed_id) GetStream::Generated::Feed.new(self, feed_group_id, feed_id) end + # Verify a webhook signature using this client's API secret (CHA-2961). + # + # Convenience wrapper around StreamChat::Webhook.verify_signature that + # supplies the secret automatically. The module-level method is still + # available for callers that need to verify with an arbitrary secret. + # + # @param body [String] The raw request body (already-decompressed) + # @param signature [String] The signature from the X-Signature header + # @return [Boolean] true if the signature is valid, false otherwise + def verify_signature(body, signature) + StreamChat::Webhook.verify_signature(body, signature, @configuration.api_secret) + end + + # Verify and parse a webhook payload in one call, using this client's API + # secret (CHA-2961). + # + # Handles gzip-compressed bodies transparently. Raises + # StreamChat::Webhook::InvalidWebhookError on signature mismatch or parse + # failures; distinguish failure modes via the message substring. + # + # @param body [String] raw request body (possibly gzip-compressed) + # @param signature [String] X-Signature header value + # @return [Object] the typed event class instance or + # StreamChat::Webhook::UnknownEvent + # @raise [StreamChat::Webhook::InvalidWebhookError] + def verify_and_parse_webhook(body, signature) + StreamChat::Webhook.verify_and_parse_webhook(body, signature, @configuration.api_secret) + end + + # Decode + parse a Stream-delivered SQS message body. + # + # Convenience wrapper around StreamChat::Webhook.parse_sqs. No signature is + # required; SQS deliveries are authenticated via AWS IAM. + def parse_sqs(message_body) + StreamChat::Webhook.parse_sqs(message_body) + end + + # Decode + parse a Stream-delivered SNS notification body. + # + # Accepts either the raw SNS HTTP envelope JSON or the pre-extracted Message + # string. Convenience wrapper around StreamChat::Webhook.parse_sns. No signature + # is required; SNS deliveries are authenticated via AWS IAM. + def parse_sns(notification_body) + StreamChat::Webhook.parse_sns(notification_body) + end + # @param path [String] The API path # @param body [Hash] The request body # @return [GetStreamRuby::StreamResponse] The API response diff --git a/lib/getstream_ruby/generated/feed.rb b/lib/getstream_ruby/generated/feed.rb index 82ea989..dc39cb6 100644 --- a/lib/getstream_ruby/generated/feed.rb +++ b/lib/getstream_ruby/generated/feed.rb @@ -17,11 +17,13 @@ def initialize(client, feed_group_id, feed_id) # Delete a single feed by its ID # # @param hard_delete [Boolean] + # @param purge_user_activities [Boolean] # @return [Models::DeleteFeedResponse] - def delete_feed(hard_delete = nil) + def delete_feed(hard_delete = nil, purge_user_activities = nil) # Build query parameters query_params = {} query_params['hard_delete'] = hard_delete unless hard_delete.nil? + query_params['purge_user_activities'] = purge_user_activities unless purge_user_activities.nil? # Delegate to the FeedsClient @client.feeds.delete_feed(@feed_group_id, @feed_id, query_params) diff --git a/lib/getstream_ruby/generated/feeds_client.rb b/lib/getstream_ruby/generated/feeds_client.rb index 5649841..fd461b9 100644 --- a/lib/getstream_ruby/generated/feeds_client.rb +++ b/lib/getstream_ruby/generated/feeds_client.rb @@ -992,8 +992,9 @@ def create_feed_group(create_feed_group_request) # @param feed_group_id [String] # @param feed_id [String] # @param hard_delete [Boolean] + # @param purge_user_activities [Boolean] # @return [Models::DeleteFeedResponse] - def delete_feed(feed_group_id, feed_id, hard_delete = nil) + def delete_feed(feed_group_id, feed_id, hard_delete = nil, purge_user_activities = nil) path = '/api/v2/feeds/feed_groups/{feed_group_id}/feeds/{feed_id}' # Replace path parameters path = path.gsub('{feed_group_id}', feed_group_id.to_s) @@ -1001,6 +1002,7 @@ def delete_feed(feed_group_id, feed_id, hard_delete = nil) # Build query parameters query_params = {} query_params['hard_delete'] = hard_delete unless hard_delete.nil? + query_params['purge_user_activities'] = purge_user_activities unless purge_user_activities.nil? # Make the API request @client.make_request( diff --git a/lib/getstream_ruby/generated/models/async_export_error_event.rb b/lib/getstream_ruby/generated/models/async_export_error_event.rb index 1e922a8..f785b8c 100644 --- a/lib/getstream_ruby/generated/models/async_export_error_event.rb +++ b/lib/getstream_ruby/generated/models/async_export_error_event.rb @@ -43,7 +43,7 @@ def initialize(attributes = {}) @started_at = attributes[:started_at] || attributes['started_at'] @task_id = attributes[:task_id] || attributes['task_id'] @custom = attributes[:custom] || attributes['custom'] - @type = attributes[:type] || attributes['type'] || "export.users.error" + @type = attributes[:type] || attributes['type'] || "export.bulk_image_moderation.error" @received_at = attributes[:received_at] || attributes['received_at'] || nil end diff --git a/lib/getstream_ruby/generated/models/delete_feeds_batch_request.rb b/lib/getstream_ruby/generated/models/delete_feeds_batch_request.rb index 15461f8..7c97343 100644 --- a/lib/getstream_ruby/generated/models/delete_feeds_batch_request.rb +++ b/lib/getstream_ruby/generated/models/delete_feeds_batch_request.rb @@ -15,19 +15,24 @@ class DeleteFeedsBatchRequest < GetStream::BaseModel # @!attribute hard_delete # @return [Boolean] Whether to permanently delete the feeds instead of soft delete attr_accessor :hard_delete + # @!attribute purge_user_activities + # @return [Boolean] When hard-deleting, also fully delete activities authored by each feed's owner from every other feed those activities were fanned out to. Default false preserves existing fan-out. Requires 'hard_delete' to be true; the request is rejected otherwise. Feeds with no recorded owner (created_by_id is empty) are silently skipped for the purge step — owner-matching against an empty string is a safety guard, not a wildcard. + attr_accessor :purge_user_activities # Initialize with attributes def initialize(attributes = {}) super(attributes) @feeds = attributes[:feeds] || attributes['feeds'] @hard_delete = attributes[:hard_delete] || attributes['hard_delete'] || nil + @purge_user_activities = attributes[:purge_user_activities] || attributes['purge_user_activities'] || nil end # Override field mappings for JSON serialization def self.json_field_mappings { feeds: 'feeds', - hard_delete: 'hard_delete' + hard_delete: 'hard_delete', + purge_user_activities: 'purge_user_activities' } end end diff --git a/lib/getstream_ruby/generated/models/labels_request.rb b/lib/getstream_ruby/generated/models/labels_request.rb index 710c73c..25a403f 100644 --- a/lib/getstream_ruby/generated/models/labels_request.rb +++ b/lib/getstream_ruby/generated/models/labels_request.rb @@ -21,6 +21,9 @@ class LabelsRequest < GetStream::BaseModel # @!attribute content_type # @return [String] Type of content: 'text' (default), 'message', or 'username'. Stored as-sent; only 'username' routes to the username moderation API. attr_accessor :content_type + # @!attribute dry_run + # @return [Boolean] When true, run moderation and return labels without persisting the result. Useful for one-off checks (e.g. UI testers) that should not be recorded in the stored history. + attr_accessor :dry_run # @!attribute policy # @return [String] Optional moderation policy key (max 128 chars) attr_accessor :policy @@ -35,6 +38,7 @@ def initialize(attributes = {}) @category = attributes[:category] || attributes['category'] || nil @content_id = attributes[:content_id] || attributes['content_id'] || nil @content_type = attributes[:content_type] || attributes['content_type'] || nil + @dry_run = attributes[:dry_run] || attributes['dry_run'] || nil @policy = attributes[:policy] || attributes['policy'] || nil @user_id = attributes[:user_id] || attributes['user_id'] || nil end @@ -46,6 +50,7 @@ def self.json_field_mappings category: 'category', content_id: 'content_id', content_type: 'content_type', + dry_run: 'dry_run', policy: 'policy', user_id: 'user_id' } diff --git a/lib/getstream_ruby/generated/models/query_bookmarks_request.rb b/lib/getstream_ruby/generated/models/query_bookmarks_request.rb index 01f0086..f70d356 100644 --- a/lib/getstream_ruby/generated/models/query_bookmarks_request.rb +++ b/lib/getstream_ruby/generated/models/query_bookmarks_request.rb @@ -21,12 +21,18 @@ class QueryBookmarksRequest < GetStream::BaseModel # @!attribute prev # @return [String] attr_accessor :prev + # @!attribute user_id + # @return [String] + attr_accessor :user_id # @!attribute sort # @return [Array] Sorting parameters for the query attr_accessor :sort # @!attribute filter # @return [Object] Filters to apply to the query attr_accessor :filter + # @!attribute user + # @return [UserRequest] + attr_accessor :user # Initialize with attributes def initialize(attributes = {}) @@ -35,8 +41,10 @@ def initialize(attributes = {}) @limit = attributes[:limit] || attributes['limit'] || nil @next = attributes[:next] || attributes['next'] || nil @prev = attributes[:prev] || attributes['prev'] || nil + @user_id = attributes[:user_id] || attributes['user_id'] || nil @sort = attributes[:sort] || attributes['sort'] || nil @filter = attributes[:filter] || attributes['filter'] || nil + @user = attributes[:user] || attributes['user'] || nil end # Override field mappings for JSON serialization @@ -46,8 +54,10 @@ def self.json_field_mappings limit: 'limit', next: 'next', prev: 'prev', + user_id: 'user_id', sort: 'sort', - filter: 'filter' + filter: 'filter', + user: 'user' } end end diff --git a/lib/getstream_ruby/generated/webhook.rb b/lib/getstream_ruby/generated/webhook.rb index 9d51101..f3cd965 100644 --- a/lib/getstream_ruby/generated/webhook.rb +++ b/lib/getstream_ruby/generated/webhook.rb @@ -1,7 +1,10 @@ # Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. -require 'openssl' +require 'base64' require 'json' +require 'openssl' +require 'time' +require 'zlib' require_relative 'models/activity_added_event' require_relative 'models/activity_deleted_event' require_relative 'models/activity_feedback_event' @@ -169,6 +172,47 @@ module StreamChat module Webhook + # Raised for every webhook handling failure: signature mismatch, invalid + # JSON, missing/non-string +type+ field, gzip-prefixed body that fails to + # decompress, invalid base64 in a queue body, or a malformed SNS envelope. + # + # Rescue this single class for any webhook problem; filter on the message + # text or against the failure-mode constants below to differentiate. + class InvalidWebhookError < StandardError + SIGNATURE_MISMATCH = 'signature mismatch' + INVALID_BASE64 = 'invalid base64 encoding' + GZIP_FAILED = 'gzip decompression failed' + INVALID_JSON = 'invalid JSON payload' + end + + # Returned by parse_event when the type discriminator is well-formed but + # unknown to this SDK version. + # + # Stable forward-compat surface: backend may add new event types before this + # SDK is regenerated. Switch on the returned object's class and include + # UnknownEvent as a fallback case. + # + # @!attribute [r] type + # @return [String] the unrecognized discriminator value + # @!attribute [r] created_at + # @return [Time, nil] parsed timestamp from the envelope, or nil if missing/unparseable + # @!attribute [r] raw + # @return [Hash] the full parsed JSON hash for inspection + class UnknownEvent + attr_reader :type, :created_at, :raw + + def initialize(type:, created_at: nil, raw: {}) + @type = type + @created_at = created_at + @raw = raw + end + end + + # gzip magic prefix (RFC 1952 §2.3.1). JSON cannot start with these bytes, + # so this gives unambiguous detection for Stream's always-JSON payloads. + GZIP_MAGIC = "\x1F\x8B".b.freeze + private_constant :GZIP_MAGIC + # Webhook event type constants EVENT_TYPE_WILDCARD = '*' EVENT_TYPE_APPEAL_ACCEPTED = 'appeal.accepted' @@ -349,6 +393,9 @@ def self.get_event_type(raw_event) # Deserialize a raw webhook payload into a typed event object. # + # Throws on unknown event types. For forward-compatible parsing that returns + # an {UnknownEvent} on unrecognized discriminators, use {parse_event}. + # # @param raw_event [String, Hash] The raw webhook payload # @return [Object] A typed event object corresponding to the event type # @raise [ArgumentError] if the event type is unknown or deserialization fails @@ -383,339 +430,339 @@ def self.parse_webhook_event(raw_event) private_class_method def self.event_class_for_type(event_type) case event_type when '*' - StreamChat::CustomEvent + GetStream::Generated::Models::CustomEvent when 'appeal.accepted' - StreamChat::AppealAcceptedEvent + GetStream::Generated::Models::AppealAcceptedEvent when 'appeal.created' - StreamChat::AppealCreatedEvent + GetStream::Generated::Models::AppealCreatedEvent when 'appeal.rejected' - StreamChat::AppealRejectedEvent + GetStream::Generated::Models::AppealRejectedEvent when 'call.accepted' - StreamChat::CallAcceptedEvent + GetStream::Generated::Models::CallAcceptedEvent when 'call.blocked_user' - StreamChat::BlockedUserEvent + GetStream::Generated::Models::BlockedUserEvent when 'call.closed_caption' - StreamChat::ClosedCaptionEvent + GetStream::Generated::Models::ClosedCaptionEvent when 'call.closed_captions_failed' - StreamChat::CallClosedCaptionsFailedEvent + GetStream::Generated::Models::CallClosedCaptionsFailedEvent when 'call.closed_captions_started' - StreamChat::CallClosedCaptionsStartedEvent + GetStream::Generated::Models::CallClosedCaptionsStartedEvent when 'call.closed_captions_stopped' - StreamChat::CallClosedCaptionsStoppedEvent + GetStream::Generated::Models::CallClosedCaptionsStoppedEvent when 'call.created' - StreamChat::CallCreatedEvent + GetStream::Generated::Models::CallCreatedEvent when 'call.deleted' - StreamChat::CallDeletedEvent + GetStream::Generated::Models::CallDeletedEvent when 'call.dtmf' - StreamChat::CallDTMFEvent + GetStream::Generated::Models::CallDTMFEvent when 'call.ended' - StreamChat::CallEndedEvent + GetStream::Generated::Models::CallEndedEvent when 'call.frame_recording_failed' - StreamChat::CallFrameRecordingFailedEvent + GetStream::Generated::Models::CallFrameRecordingFailedEvent when 'call.frame_recording_ready' - StreamChat::CallFrameRecordingFrameReadyEvent + GetStream::Generated::Models::CallFrameRecordingFrameReadyEvent when 'call.frame_recording_started' - StreamChat::CallFrameRecordingStartedEvent + GetStream::Generated::Models::CallFrameRecordingStartedEvent when 'call.frame_recording_stopped' - StreamChat::CallFrameRecordingStoppedEvent + GetStream::Generated::Models::CallFrameRecordingStoppedEvent when 'call.hls_broadcasting_failed' - StreamChat::CallHLSBroadcastingFailedEvent + GetStream::Generated::Models::CallHLSBroadcastingFailedEvent when 'call.hls_broadcasting_started' - StreamChat::CallHLSBroadcastingStartedEvent + GetStream::Generated::Models::CallHLSBroadcastingStartedEvent when 'call.hls_broadcasting_stopped' - StreamChat::CallHLSBroadcastingStoppedEvent + GetStream::Generated::Models::CallHLSBroadcastingStoppedEvent when 'call.kicked_user' - StreamChat::KickedUserEvent + GetStream::Generated::Models::KickedUserEvent when 'call.live_started' - StreamChat::CallLiveStartedEvent + GetStream::Generated::Models::CallLiveStartedEvent when 'call.member_added' - StreamChat::CallMemberAddedEvent + GetStream::Generated::Models::CallMemberAddedEvent when 'call.member_removed' - StreamChat::CallMemberRemovedEvent + GetStream::Generated::Models::CallMemberRemovedEvent when 'call.member_updated' - StreamChat::CallMemberUpdatedEvent + GetStream::Generated::Models::CallMemberUpdatedEvent when 'call.member_updated_permission' - StreamChat::CallMemberUpdatedPermissionEvent + GetStream::Generated::Models::CallMemberUpdatedPermissionEvent when 'call.missed' - StreamChat::CallMissedEvent + GetStream::Generated::Models::CallMissedEvent when 'call.moderation_blur' - StreamChat::CallModerationBlurEvent + GetStream::Generated::Models::CallModerationBlurEvent when 'call.moderation_warning' - StreamChat::CallModerationWarningEvent + GetStream::Generated::Models::CallModerationWarningEvent when 'call.notification' - StreamChat::CallNotificationEvent + GetStream::Generated::Models::CallNotificationEvent when 'call.permission_request' - StreamChat::PermissionRequestEvent + GetStream::Generated::Models::PermissionRequestEvent when 'call.permissions_updated' - StreamChat::UpdatedCallPermissionsEvent + GetStream::Generated::Models::UpdatedCallPermissionsEvent when 'call.reaction_new' - StreamChat::CallReactionEvent + GetStream::Generated::Models::CallReactionEvent when 'call.recording_failed' - StreamChat::CallRecordingFailedEvent + GetStream::Generated::Models::CallRecordingFailedEvent when 'call.recording_ready' - StreamChat::CallRecordingReadyEvent + GetStream::Generated::Models::CallRecordingReadyEvent when 'call.recording_started' - StreamChat::CallRecordingStartedEvent + GetStream::Generated::Models::CallRecordingStartedEvent when 'call.recording_stopped' - StreamChat::CallRecordingStoppedEvent + GetStream::Generated::Models::CallRecordingStoppedEvent when 'call.rejected' - StreamChat::CallRejectedEvent + GetStream::Generated::Models::CallRejectedEvent when 'call.ring' - StreamChat::CallRingEvent + GetStream::Generated::Models::CallRingEvent when 'call.rtmp_broadcast_failed' - StreamChat::CallRtmpBroadcastFailedEvent + GetStream::Generated::Models::CallRtmpBroadcastFailedEvent when 'call.rtmp_broadcast_started' - StreamChat::CallRtmpBroadcastStartedEvent + GetStream::Generated::Models::CallRtmpBroadcastStartedEvent when 'call.rtmp_broadcast_stopped' - StreamChat::CallRtmpBroadcastStoppedEvent + GetStream::Generated::Models::CallRtmpBroadcastStoppedEvent when 'call.session_ended' - StreamChat::CallSessionEndedEvent + GetStream::Generated::Models::CallSessionEndedEvent when 'call.session_participant_count_updated' - StreamChat::CallSessionParticipantCountsUpdatedEvent + GetStream::Generated::Models::CallSessionParticipantCountsUpdatedEvent when 'call.session_participant_joined' - StreamChat::CallSessionParticipantJoinedEvent + GetStream::Generated::Models::CallSessionParticipantJoinedEvent when 'call.session_participant_left' - StreamChat::CallSessionParticipantLeftEvent + GetStream::Generated::Models::CallSessionParticipantLeftEvent when 'call.session_started' - StreamChat::CallSessionStartedEvent + GetStream::Generated::Models::CallSessionStartedEvent when 'call.stats_report_ready' - StreamChat::CallStatsReportReadyEvent + GetStream::Generated::Models::CallStatsReportReadyEvent when 'call.transcription_failed' - StreamChat::CallTranscriptionFailedEvent + GetStream::Generated::Models::CallTranscriptionFailedEvent when 'call.transcription_ready' - StreamChat::CallTranscriptionReadyEvent + GetStream::Generated::Models::CallTranscriptionReadyEvent when 'call.transcription_started' - StreamChat::CallTranscriptionStartedEvent + GetStream::Generated::Models::CallTranscriptionStartedEvent when 'call.transcription_stopped' - StreamChat::CallTranscriptionStoppedEvent + GetStream::Generated::Models::CallTranscriptionStoppedEvent when 'call.unblocked_user' - StreamChat::UnblockedUserEvent + GetStream::Generated::Models::UnblockedUserEvent when 'call.updated' - StreamChat::CallUpdatedEvent + GetStream::Generated::Models::CallUpdatedEvent when 'call.user_feedback_submitted' - StreamChat::CallUserFeedbackSubmittedEvent + GetStream::Generated::Models::CallUserFeedbackSubmittedEvent when 'call.user_muted' - StreamChat::CallUserMutedEvent + GetStream::Generated::Models::CallUserMutedEvent when 'campaign.completed' - StreamChat::CampaignCompletedEvent + GetStream::Generated::Models::CampaignCompletedEvent when 'campaign.started' - StreamChat::CampaignStartedEvent + GetStream::Generated::Models::CampaignStartedEvent when 'channel.created' - StreamChat::ChannelCreatedEvent + GetStream::Generated::Models::ChannelCreatedEvent when 'channel.deleted' - StreamChat::ChannelDeletedEvent + GetStream::Generated::Models::ChannelDeletedEvent when 'channel.frozen' - StreamChat::ChannelFrozenEvent + GetStream::Generated::Models::ChannelFrozenEvent when 'channel.hidden' - StreamChat::ChannelHiddenEvent + GetStream::Generated::Models::ChannelHiddenEvent when 'channel.max_streak_changed' - StreamChat::MaxStreakChangedEvent + GetStream::Generated::Models::MaxStreakChangedEvent when 'channel.muted' - StreamChat::ChannelMutedEvent + GetStream::Generated::Models::ChannelMutedEvent when 'channel.truncated' - StreamChat::ChannelTruncatedEvent + GetStream::Generated::Models::ChannelTruncatedEvent when 'channel.unfrozen' - StreamChat::ChannelUnFrozenEvent + GetStream::Generated::Models::ChannelUnFrozenEvent when 'channel.unmuted' - StreamChat::ChannelUnmutedEvent + GetStream::Generated::Models::ChannelUnmutedEvent when 'channel.updated' - StreamChat::ChannelUpdatedEvent + GetStream::Generated::Models::ChannelUpdatedEvent when 'channel.visible' - StreamChat::ChannelVisibleEvent + GetStream::Generated::Models::ChannelVisibleEvent when 'channel_batch_update.completed' - StreamChat::ChannelBatchCompletedEvent + GetStream::Generated::Models::ChannelBatchCompletedEvent when 'channel_batch_update.started' - StreamChat::ChannelBatchStartedEvent + GetStream::Generated::Models::ChannelBatchStartedEvent when 'custom' - StreamChat::CustomVideoEvent + GetStream::Generated::Models::CustomVideoEvent when 'export.bulk_image_moderation.error' - StreamChat::AsyncExportErrorEvent + GetStream::Generated::Models::AsyncExportErrorEvent when 'export.bulk_image_moderation.success' - StreamChat::AsyncBulkImageModerationEvent + GetStream::Generated::Models::AsyncBulkImageModerationEvent when 'export.channels.error' - StreamChat::AsyncExportErrorEvent + GetStream::Generated::Models::AsyncExportErrorEvent when 'export.channels.success' - StreamChat::AsyncExportChannelsEvent + GetStream::Generated::Models::AsyncExportChannelsEvent when 'export.moderation_logs.error' - StreamChat::AsyncExportErrorEvent + GetStream::Generated::Models::AsyncExportErrorEvent when 'export.moderation_logs.success' - StreamChat::AsyncExportModerationLogsEvent + GetStream::Generated::Models::AsyncExportModerationLogsEvent when 'export.users.error' - StreamChat::AsyncExportErrorEvent + GetStream::Generated::Models::AsyncExportErrorEvent when 'export.users.success' - StreamChat::AsyncExportUsersEvent + GetStream::Generated::Models::AsyncExportUsersEvent when 'feeds.activity.added' - StreamChat::ActivityAddedEvent + GetStream::Generated::Models::ActivityAddedEvent when 'feeds.activity.deleted' - StreamChat::ActivityDeletedEvent + GetStream::Generated::Models::ActivityDeletedEvent when 'feeds.activity.feedback' - StreamChat::ActivityFeedbackEvent + GetStream::Generated::Models::ActivityFeedbackEvent when 'feeds.activity.marked' - StreamChat::ActivityMarkEvent + GetStream::Generated::Models::ActivityMarkEvent when 'feeds.activity.pinned' - StreamChat::ActivityPinnedEvent + GetStream::Generated::Models::ActivityPinnedEvent when 'feeds.activity.reaction.added' - StreamChat::ActivityReactionAddedEvent + GetStream::Generated::Models::ActivityReactionAddedEvent when 'feeds.activity.reaction.deleted' - StreamChat::ActivityReactionDeletedEvent + GetStream::Generated::Models::ActivityReactionDeletedEvent when 'feeds.activity.reaction.updated' - StreamChat::ActivityReactionUpdatedEvent + GetStream::Generated::Models::ActivityReactionUpdatedEvent when 'feeds.activity.removed_from_feed' - StreamChat::ActivityRemovedFromFeedEvent + GetStream::Generated::Models::ActivityRemovedFromFeedEvent when 'feeds.activity.restored' - StreamChat::ActivityRestoredEvent + GetStream::Generated::Models::ActivityRestoredEvent when 'feeds.activity.unpinned' - StreamChat::ActivityUnpinnedEvent + GetStream::Generated::Models::ActivityUnpinnedEvent when 'feeds.activity.updated' - StreamChat::ActivityUpdatedEvent + GetStream::Generated::Models::ActivityUpdatedEvent when 'feeds.bookmark.added' - StreamChat::BookmarkAddedEvent + GetStream::Generated::Models::BookmarkAddedEvent when 'feeds.bookmark.deleted' - StreamChat::BookmarkDeletedEvent + GetStream::Generated::Models::BookmarkDeletedEvent when 'feeds.bookmark.updated' - StreamChat::BookmarkUpdatedEvent + GetStream::Generated::Models::BookmarkUpdatedEvent when 'feeds.bookmark_folder.deleted' - StreamChat::BookmarkFolderDeletedEvent + GetStream::Generated::Models::BookmarkFolderDeletedEvent when 'feeds.bookmark_folder.updated' - StreamChat::BookmarkFolderUpdatedEvent + GetStream::Generated::Models::BookmarkFolderUpdatedEvent when 'feeds.comment.added' - StreamChat::CommentAddedEvent + GetStream::Generated::Models::CommentAddedEvent when 'feeds.comment.deleted' - StreamChat::CommentDeletedEvent + GetStream::Generated::Models::CommentDeletedEvent when 'feeds.comment.reaction.added' - StreamChat::CommentReactionAddedEvent + GetStream::Generated::Models::CommentReactionAddedEvent when 'feeds.comment.reaction.deleted' - StreamChat::CommentReactionDeletedEvent + GetStream::Generated::Models::CommentReactionDeletedEvent when 'feeds.comment.reaction.updated' - StreamChat::CommentReactionUpdatedEvent + GetStream::Generated::Models::CommentReactionUpdatedEvent when 'feeds.comment.restored' - StreamChat::CommentRestoredEvent + GetStream::Generated::Models::CommentRestoredEvent when 'feeds.comment.updated' - StreamChat::CommentUpdatedEvent + GetStream::Generated::Models::CommentUpdatedEvent when 'feeds.feed.created' - StreamChat::FeedCreatedEvent + GetStream::Generated::Models::FeedCreatedEvent when 'feeds.feed.deleted' - StreamChat::FeedDeletedEvent + GetStream::Generated::Models::FeedDeletedEvent when 'feeds.feed.updated' - StreamChat::FeedUpdatedEvent + GetStream::Generated::Models::FeedUpdatedEvent when 'feeds.feed_group.changed' - StreamChat::FeedGroupChangedEvent + GetStream::Generated::Models::FeedGroupChangedEvent when 'feeds.feed_group.deleted' - StreamChat::FeedGroupDeletedEvent + GetStream::Generated::Models::FeedGroupDeletedEvent when 'feeds.feed_group.restored' - StreamChat::FeedGroupRestoredEvent + GetStream::Generated::Models::FeedGroupRestoredEvent when 'feeds.feed_member.added' - StreamChat::FeedMemberAddedEvent + GetStream::Generated::Models::FeedMemberAddedEvent when 'feeds.feed_member.removed' - StreamChat::FeedMemberRemovedEvent + GetStream::Generated::Models::FeedMemberRemovedEvent when 'feeds.feed_member.updated' - StreamChat::FeedMemberUpdatedEvent + GetStream::Generated::Models::FeedMemberUpdatedEvent when 'feeds.follow.created' - StreamChat::FollowCreatedEvent + GetStream::Generated::Models::FollowCreatedEvent when 'feeds.follow.deleted' - StreamChat::FollowDeletedEvent + GetStream::Generated::Models::FollowDeletedEvent when 'feeds.follow.updated' - StreamChat::FollowUpdatedEvent + GetStream::Generated::Models::FollowUpdatedEvent when 'feeds.notification_feed.updated' - StreamChat::NotificationFeedUpdatedEvent + GetStream::Generated::Models::NotificationFeedUpdatedEvent when 'feeds.stories_feed.updated' - StreamChat::StoriesFeedUpdatedEvent + GetStream::Generated::Models::StoriesFeedUpdatedEvent when 'flag.updated' - StreamChat::FlagUpdatedEvent + GetStream::Generated::Models::FlagUpdatedEvent when 'ingress.error' - StreamChat::IngressErrorEvent + GetStream::Generated::Models::IngressErrorEvent when 'ingress.started' - StreamChat::IngressStartedEvent + GetStream::Generated::Models::IngressStartedEvent when 'ingress.stopped' - StreamChat::IngressStoppedEvent + GetStream::Generated::Models::IngressStoppedEvent when 'member.added' - StreamChat::MemberAddedEvent + GetStream::Generated::Models::MemberAddedEvent when 'member.removed' - StreamChat::MemberRemovedEvent + GetStream::Generated::Models::MemberRemovedEvent when 'member.updated' - StreamChat::MemberUpdatedEvent + GetStream::Generated::Models::MemberUpdatedEvent when 'message.deleted' - StreamChat::MessageDeletedEvent + GetStream::Generated::Models::MessageDeletedEvent when 'message.flagged' - StreamChat::MessageFlaggedEvent + GetStream::Generated::Models::MessageFlaggedEvent when 'message.new' - StreamChat::MessageNewEvent + GetStream::Generated::Models::MessageNewEvent when 'message.pending' - StreamChat::PendingMessageEvent + GetStream::Generated::Models::PendingMessageEvent when 'message.read' - StreamChat::MessageReadEvent + GetStream::Generated::Models::MessageReadEvent when 'message.unblocked' - StreamChat::MessageUnblockedEvent + GetStream::Generated::Models::MessageUnblockedEvent when 'message.undeleted' - StreamChat::MessageUndeletedEvent + GetStream::Generated::Models::MessageUndeletedEvent when 'message.updated' - StreamChat::MessageUpdatedEvent + GetStream::Generated::Models::MessageUpdatedEvent when 'moderation.custom_action' - StreamChat::ModerationCustomActionEvent + GetStream::Generated::Models::ModerationCustomActionEvent when 'moderation.flagged' - StreamChat::ModerationFlaggedEvent + GetStream::Generated::Models::ModerationFlaggedEvent when 'moderation.mark_reviewed' - StreamChat::ModerationMarkReviewedEvent + GetStream::Generated::Models::ModerationMarkReviewedEvent when 'moderation_check.completed' - StreamChat::ModerationCheckCompletedEvent + GetStream::Generated::Models::ModerationCheckCompletedEvent when 'moderation_rule.triggered' - StreamChat::ModerationRulesTriggeredEvent + GetStream::Generated::Models::ModerationRulesTriggeredEvent when 'notification.mark_unread' - StreamChat::NotificationMarkUnreadEvent + GetStream::Generated::Models::NotificationMarkUnreadEvent when 'notification.reminder_due' - StreamChat::ReminderNotificationEvent + GetStream::Generated::Models::ReminderNotificationEvent when 'notification.thread_message_new' - StreamChat::NotificationThreadMessageNewEvent + GetStream::Generated::Models::NotificationThreadMessageNewEvent when 'reaction.deleted' - StreamChat::ReactionDeletedEvent + GetStream::Generated::Models::ReactionDeletedEvent when 'reaction.new' - StreamChat::ReactionNewEvent + GetStream::Generated::Models::ReactionNewEvent when 'reaction.updated' - StreamChat::ReactionUpdatedEvent + GetStream::Generated::Models::ReactionUpdatedEvent when 'reminder.created' - StreamChat::ReminderCreatedEvent + GetStream::Generated::Models::ReminderCreatedEvent when 'reminder.deleted' - StreamChat::ReminderDeletedEvent + GetStream::Generated::Models::ReminderDeletedEvent when 'reminder.updated' - StreamChat::ReminderUpdatedEvent + GetStream::Generated::Models::ReminderUpdatedEvent when 'review_queue_item.new' - StreamChat::ReviewQueueItemNewEvent + GetStream::Generated::Models::ReviewQueueItemNewEvent when 'review_queue_item.updated' - StreamChat::ReviewQueueItemUpdatedEvent + GetStream::Generated::Models::ReviewQueueItemUpdatedEvent when 'thread.updated' - StreamChat::ThreadUpdatedEvent + GetStream::Generated::Models::ThreadUpdatedEvent when 'user.banned' - StreamChat::UserBannedEvent + GetStream::Generated::Models::UserBannedEvent when 'user.deactivated' - StreamChat::UserDeactivatedEvent + GetStream::Generated::Models::UserDeactivatedEvent when 'user.deleted' - StreamChat::UserDeletedEvent + GetStream::Generated::Models::UserDeletedEvent when 'user.flagged' - StreamChat::UserFlaggedEvent + GetStream::Generated::Models::UserFlaggedEvent when 'user.messages.deleted' - StreamChat::UserMessagesDeletedEvent + GetStream::Generated::Models::UserMessagesDeletedEvent when 'user.muted' - StreamChat::UserMutedEvent + GetStream::Generated::Models::UserMutedEvent when 'user.reactivated' - StreamChat::UserReactivatedEvent + GetStream::Generated::Models::UserReactivatedEvent when 'user.unbanned' - StreamChat::UserUnbannedEvent + GetStream::Generated::Models::UserUnbannedEvent when 'user.unmuted' - StreamChat::UserUnmutedEvent + GetStream::Generated::Models::UserUnmutedEvent when 'user.unread_message_reminder' - StreamChat::UserUnreadReminderEvent + GetStream::Generated::Models::UserUnreadReminderEvent when 'user.updated' - StreamChat::UserUpdatedEvent + GetStream::Generated::Models::UserUpdatedEvent when 'user_group.created' - StreamChat::UserGroupCreatedEvent + GetStream::Generated::Models::UserGroupCreatedEvent when 'user_group.deleted' - StreamChat::UserGroupDeletedEvent + GetStream::Generated::Models::UserGroupDeletedEvent when 'user_group.member_added' - StreamChat::UserGroupMemberAddedEvent + GetStream::Generated::Models::UserGroupMemberAddedEvent when 'user_group.member_removed' - StreamChat::UserGroupMemberRemovedEvent + GetStream::Generated::Models::UserGroupMemberRemovedEvent when 'user_group.updated' - StreamChat::UserGroupUpdatedEvent + GetStream::Generated::Models::UserGroupUpdatedEvent else nil end @@ -725,17 +772,201 @@ def self.parse_webhook_event(raw_event) # Verify the HMAC-SHA256 signature of a webhook payload. # - # @param body [String] The raw request body + # @param body [String] The raw request body (already-decompressed) # @param signature [String] The signature from the X-Signature header # @param secret [String] Your webhook secret (found in the Stream Dashboard) # @return [Boolean] true if the signature is valid, false otherwise def self.verify_signature(body, signature, secret) return false if signature.nil? || signature.bytesize != 64 - + expected = OpenSSL::HMAC.hexdigest('SHA256', secret, body) OpenSSL.fixed_length_secure_compare(signature, expected) rescue ArgumentError false end + + # Decompress the body if it is gzip-prefixed (first two bytes 0x1F 0x8B), + # else return the body bytes unchanged. + # + # Magic-byte detection is reliable for Stream payloads because Stream webhook + # bodies are always JSON, and JSON cannot start with 0x1F. + # + # @param body [String] raw body (binary-safe) + # @return [String] decompressed body, or the original body if not gzip-prefixed + # @raise [InvalidWebhookError] if body has the gzip magic prefix but + # isn't a valid gzip stream + def self.gunzip_payload(body) + raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: body must be a String" unless body.is_a?(String) + + bytes = body.b + return bytes if bytes.bytesize < 2 || bytes.byteslice(0, 2) != GZIP_MAGIC + + Zlib.gunzip(bytes) + rescue Zlib::Error => e + raise InvalidWebhookError, "#{InvalidWebhookError::GZIP_FAILED}: #{e.message}" + end + + # Decode an SQS Message Body: try base64 first, fall back to raw bytes if + # base64 fails, then gunzip if gzip-prefixed. + # + # Wire format (per CHA-3071): SQS bodies are raw JSON when + # enable_hook_payload_compression is off (today's default for all existing + # apps), and base64(gzip(json)) when it's on. This helper handles both: + # raw JSON starts with '{' which is not valid base64, so the base64 decode + # fails and we fall through to raw bytes, then {gunzip_payload}'s magic-byte + # detection decides whether to decompress. + # + # {parse_sqs} sits on top of this and works transparently for both wire + # formats — no caller code change, no flag, no header. + # + # @param message_body [String] + # @return [String] + # @raise [InvalidWebhookError] only if gzip decompression fails (input had gzip magic prefix) + def self.decode_sqs_payload(message_body) + raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: message_body must be a String" unless message_body.is_a?(String) + + decoded = + begin + Base64.strict_decode64(message_body) + rescue ArgumentError + # Not base64 — treat input as raw bytes (uncompressed wire format). + message_body.dup.force_encoding(Encoding::ASCII_8BIT) + end + gunzip_payload(decoded) + end + + # Return the inner +Message+ field when +body+ is a standard SNS + # notification envelope JSON; otherwise return +body+ unchanged so a + # pre-extracted Message string flows through. + # + # Heuristic: try to JSON-parse the input. If it yields a Hash with a + # String +Message+ field, that's the envelope shape — return the Message. + # Otherwise the input is presumed to BE the pre-extracted Message + # (base64-encoded bytes are not valid JSON, so this falls through cleanly). + def self.unwrap_sns_notification_body(body) + env = JSON.parse(body) + return env['Message'] if env.is_a?(Hash) && env['Message'].is_a?(String) + + body + rescue JSON::ParserError + body + end + private_class_method :unwrap_sns_notification_body + + # Decode an SNS notification body. Accepts either: + # * a full SNS HTTP notification envelope JSON + # ({"Type":"Notification","Message":"",...}), or + # * a pre-extracted Message string (forwarded-through-SQS path). + # The inner payload is then base64-decoded and gunzipped via + # {decode_sqs_payload}. + # + # @param notification_body [String] + # @return [String] + # @raise [InvalidWebhookError] + def self.decode_sns_payload(notification_body) + raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: notification_body must be a String" unless notification_body.is_a?(String) + + decode_sqs_payload(unwrap_sns_notification_body(notification_body)) + end + + # Parse a webhook payload and return the typed event for known discriminators + # or {UnknownEvent} for well-formed-but-unknown ones. + # + # Distinct from {parse_webhook_event}: parse_event returns an UnknownEvent on + # unrecognized discriminators (forward-compat); parse_webhook_event throws. + # + # @param payload [String] + # @return [Object] the typed event class instance or {UnknownEvent} + # @raise [InvalidWebhookError] for invalid JSON, missing/non-string type field, + # or known-type deserialization failure + def self.parse_event(payload) + raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: payload must be a String" unless payload.is_a?(String) + raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: payload must not be empty" if payload.empty? + + data = JSON.parse(payload) + raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: webhook payload must be a JSON object" unless data.is_a?(Hash) + + event_type = data['type'] + unless event_type.is_a?(String) && !event_type.empty? + raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: webhook payload missing 'type' string field" + end + + event_class = event_class_for_type(event_type) + return build_unknown_event(event_type, data) if event_class.nil? + + begin + event_class.new(data) + rescue StandardError => e + raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: failed to deserialize event: #{e.message}" + end + rescue JSON::ParserError => e + raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: failed to parse webhook payload: #{e.message}" + end + + private_class_method def self.build_unknown_event(event_type, data) + created_raw = data['created_at'] + created_at = nil + if created_raw.is_a?(String) + begin + created_at = Time.iso8601(created_raw) + rescue ArgumentError + created_at = nil + end + end + UnknownEvent.new(type: event_type, created_at: created_at, raw: data) + end + + public + + # HTTP composite: gunzip (if gzip-prefixed) -> verify HMAC-SHA256 -> parse. + # + # The signature header is X-Signature. The signature is HMAC-SHA256 of the + # *uncompressed* JSON body, hex-encoded. Magic-byte detection means callers + # can pass either the raw HTTP body or already-decompressed bytes; both work. + # + # @param body [String] raw request body (possibly gzip-compressed) + # @param signature [String] X-Signature header value + # @param secret [String] webhook secret + # @return [Object] the typed event class instance or {UnknownEvent} + # @raise [InvalidWebhookError] for every failure mode: signature mismatch, + # gunzip failure, JSON parse, type dispatch, or event deserialization + # failure. Filter on the message text (or the failure-mode constants + # on InvalidWebhookError) to differentiate. + def self.verify_and_parse_webhook(body, signature, secret) + payload = gunzip_payload(body) + raise InvalidWebhookError, InvalidWebhookError::SIGNATURE_MISMATCH unless verify_signature(payload, signature, secret) + + parse_event(payload) + end + + # SQS composite: base64-decode -> gunzip (if gzip-prefixed) -> parse. + # + # The backend emits no signature attribute on SQS messages today; this helper + # therefore performs no signature verification. If a signed variant is added + # later, it will be a separate function rather than retrofitting this signature. + # + # @param message_body [String] + # @return [Object] the typed event class instance or {UnknownEvent} + # @raise [InvalidWebhookError] + def self.parse_sqs(message_body) + parse_event(decode_sqs_payload(message_body)) + end + + # SNS composite: parse SNS envelope -> base64-decode -> gunzip -> parse. + # Same no-signature posture as {parse_sqs}. + # + # @param notification_body [String] + # @return [Object] the typed event class instance or {UnknownEvent} + # @raise [InvalidWebhookError] + def self.parse_sns(notification_body) + parse_event(decode_sns_payload(notification_body)) + end end end + +# Spec §7 alias: canonical name +Stream::Webhook+ aliases the existing +# +StreamChat::Webhook+ for one minor-version cycle. New callers should use +# the canonical name; existing callers continue to work unchanged. +module Stream + Webhook = StreamChat::Webhook +end diff --git a/spec/webhook_spec.rb b/spec/webhook_spec.rb new file mode 100644 index 0000000..096be57 --- /dev/null +++ b/spec/webhook_spec.rb @@ -0,0 +1,1636 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +require 'spec_helper' + +require 'base64' +require 'json' +require 'openssl' +require 'zlib' + +require_relative '../lib/getstream_ruby/generated/webhook' + +# Top-level constants — pulled out of the RSpec.describe blocks below to avoid +# Lint/ConstantDefinitionInBlock. WEBHOOK_FIXTURE_ROOT is evaluated at file-load +# time so the `Dir.children` iteration below can build the per-fixture contexts. +WEBHOOK_TEST_SECRET = 'test-webhook-secret' +WEBHOOK_TEST_BODY = '{"type":"test.event"}' +WEBHOOK_CONFORMANCE_SECRET = 'test_secret_do_not_use_in_production' +WEBHOOK_FIXTURE_ROOT = File.expand_path('../test/fixtures/webhooks', __dir__) + +def compute_signature(body, secret) + OpenSSL::HMAC.hexdigest('SHA256', secret, body) +end + +RSpec.describe 'Webhook' do + + describe 'verify_signature' do + + it 'accepts a valid signature' do + + signature = compute_signature(WEBHOOK_TEST_BODY, WEBHOOK_TEST_SECRET) + expect(StreamChat::Webhook.verify_signature(WEBHOOK_TEST_BODY, signature, WEBHOOK_TEST_SECRET)).to be true + + end + + it 'rejects a wrong signature' do + + result = StreamChat::Webhook.verify_signature(WEBHOOK_TEST_BODY, 'invalidsignature', WEBHOOK_TEST_SECRET) + expect(result).to be false + + end + + it 'rejects a tampered body' do + + signature = compute_signature(WEBHOOK_TEST_BODY, WEBHOOK_TEST_SECRET) + expect(StreamChat::Webhook.verify_signature('{"type":"tampered"}', signature, WEBHOOK_TEST_SECRET)).to be false + + end + + it 'rejects a wrong secret' do + + signature = compute_signature(WEBHOOK_TEST_BODY, WEBHOOK_TEST_SECRET) + expect(StreamChat::Webhook.verify_signature(WEBHOOK_TEST_BODY, signature, 'wrong-secret')).to be false + + end + + it 'rejects an empty signature' do + + expect(StreamChat::Webhook.verify_signature(WEBHOOK_TEST_BODY, '', WEBHOOK_TEST_SECRET)).to be false + + end + + end + + describe 'get_event_type' do + + it 'reads the type from a string payload' do + + expect(StreamChat::Webhook.get_event_type('{"type":"message.new"}')).to eq('message.new') + + end + + it 'reads the type from a hash payload' do + + expect(StreamChat::Webhook.get_event_type({ 'type' => 'message.new' })).to eq('message.new') + + end + + it 'returns nil when the type field is missing' do + + expect(StreamChat::Webhook.get_event_type('{"foo":"bar"}')).to be_nil + + end + + it 'returns nil for an empty object' do + + expect(StreamChat::Webhook.get_event_type('{}')).to be_nil + + end + + end + + describe 'parse_webhook_event' do + + it 'parses *' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"*"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CustomEvent') + + end + + it 'parses appeal.accepted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"appeal.accepted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::AppealAcceptedEvent') + + end + + it 'parses appeal.created' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"appeal.created"}') + expect(event.class.name).to eq('GetStream::Generated::Models::AppealCreatedEvent') + + end + + it 'parses appeal.rejected' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"appeal.rejected"}') + expect(event.class.name).to eq('GetStream::Generated::Models::AppealRejectedEvent') + + end + + it 'parses call.accepted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.accepted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallAcceptedEvent') + + end + + it 'parses call.blocked_user' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.blocked_user"}') + expect(event.class.name).to eq('GetStream::Generated::Models::BlockedUserEvent') + + end + + it 'parses call.closed_caption' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.closed_caption"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ClosedCaptionEvent') + + end + + it 'parses call.closed_captions_failed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.closed_captions_failed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallClosedCaptionsFailedEvent') + + end + + it 'parses call.closed_captions_started' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.closed_captions_started"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallClosedCaptionsStartedEvent') + + end + + it 'parses call.closed_captions_stopped' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.closed_captions_stopped"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallClosedCaptionsStoppedEvent') + + end + + it 'parses call.created' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.created"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallCreatedEvent') + + end + + it 'parses call.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallDeletedEvent') + + end + + it 'parses call.dtmf' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.dtmf"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallDTMFEvent') + + end + + it 'parses call.ended' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.ended"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallEndedEvent') + + end + + it 'parses call.frame_recording_failed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.frame_recording_failed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallFrameRecordingFailedEvent') + + end + + it 'parses call.frame_recording_ready' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.frame_recording_ready"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallFrameRecordingFrameReadyEvent') + + end + + it 'parses call.frame_recording_started' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.frame_recording_started"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallFrameRecordingStartedEvent') + + end + + it 'parses call.frame_recording_stopped' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.frame_recording_stopped"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallFrameRecordingStoppedEvent') + + end + + it 'parses call.hls_broadcasting_failed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.hls_broadcasting_failed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallHLSBroadcastingFailedEvent') + + end + + it 'parses call.hls_broadcasting_started' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.hls_broadcasting_started"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallHLSBroadcastingStartedEvent') + + end + + it 'parses call.hls_broadcasting_stopped' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.hls_broadcasting_stopped"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallHLSBroadcastingStoppedEvent') + + end + + it 'parses call.kicked_user' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.kicked_user"}') + expect(event.class.name).to eq('GetStream::Generated::Models::KickedUserEvent') + + end + + it 'parses call.live_started' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.live_started"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallLiveStartedEvent') + + end + + it 'parses call.member_added' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.member_added"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallMemberAddedEvent') + + end + + it 'parses call.member_removed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.member_removed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallMemberRemovedEvent') + + end + + it 'parses call.member_updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.member_updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallMemberUpdatedEvent') + + end + + it 'parses call.member_updated_permission' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.member_updated_permission"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallMemberUpdatedPermissionEvent') + + end + + it 'parses call.missed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.missed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallMissedEvent') + + end + + it 'parses call.moderation_blur' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.moderation_blur"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallModerationBlurEvent') + + end + + it 'parses call.moderation_warning' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.moderation_warning"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallModerationWarningEvent') + + end + + it 'parses call.notification' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.notification"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallNotificationEvent') + + end + + it 'parses call.permission_request' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.permission_request"}') + expect(event.class.name).to eq('GetStream::Generated::Models::PermissionRequestEvent') + + end + + it 'parses call.permissions_updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.permissions_updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UpdatedCallPermissionsEvent') + + end + + it 'parses call.reaction_new' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.reaction_new"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallReactionEvent') + + end + + it 'parses call.recording_failed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.recording_failed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallRecordingFailedEvent') + + end + + it 'parses call.recording_ready' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.recording_ready"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallRecordingReadyEvent') + + end + + it 'parses call.recording_started' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.recording_started"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallRecordingStartedEvent') + + end + + it 'parses call.recording_stopped' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.recording_stopped"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallRecordingStoppedEvent') + + end + + it 'parses call.rejected' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.rejected"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallRejectedEvent') + + end + + it 'parses call.ring' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.ring"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallRingEvent') + + end + + it 'parses call.rtmp_broadcast_failed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.rtmp_broadcast_failed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallRtmpBroadcastFailedEvent') + + end + + it 'parses call.rtmp_broadcast_started' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.rtmp_broadcast_started"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallRtmpBroadcastStartedEvent') + + end + + it 'parses call.rtmp_broadcast_stopped' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.rtmp_broadcast_stopped"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallRtmpBroadcastStoppedEvent') + + end + + it 'parses call.session_ended' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.session_ended"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallSessionEndedEvent') + + end + + it 'parses call.session_participant_count_updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.session_participant_count_updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallSessionParticipantCountsUpdatedEvent') + + end + + it 'parses call.session_participant_joined' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.session_participant_joined"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallSessionParticipantJoinedEvent') + + end + + it 'parses call.session_participant_left' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.session_participant_left"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallSessionParticipantLeftEvent') + + end + + it 'parses call.session_started' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.session_started"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallSessionStartedEvent') + + end + + it 'parses call.stats_report_ready' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.stats_report_ready"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallStatsReportReadyEvent') + + end + + it 'parses call.transcription_failed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.transcription_failed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallTranscriptionFailedEvent') + + end + + it 'parses call.transcription_ready' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.transcription_ready"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallTranscriptionReadyEvent') + + end + + it 'parses call.transcription_started' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.transcription_started"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallTranscriptionStartedEvent') + + end + + it 'parses call.transcription_stopped' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.transcription_stopped"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallTranscriptionStoppedEvent') + + end + + it 'parses call.unblocked_user' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.unblocked_user"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UnblockedUserEvent') + + end + + it 'parses call.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallUpdatedEvent') + + end + + it 'parses call.user_feedback_submitted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.user_feedback_submitted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallUserFeedbackSubmittedEvent') + + end + + it 'parses call.user_muted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"call.user_muted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CallUserMutedEvent') + + end + + it 'parses campaign.completed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"campaign.completed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CampaignCompletedEvent') + + end + + it 'parses campaign.started' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"campaign.started"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CampaignStartedEvent') + + end + + it 'parses channel.created' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.created"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ChannelCreatedEvent') + + end + + it 'parses channel.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ChannelDeletedEvent') + + end + + it 'parses channel.frozen' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.frozen"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ChannelFrozenEvent') + + end + + it 'parses channel.hidden' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.hidden"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ChannelHiddenEvent') + + end + + it 'parses channel.max_streak_changed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.max_streak_changed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::MaxStreakChangedEvent') + + end + + it 'parses channel.muted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.muted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ChannelMutedEvent') + + end + + it 'parses channel.truncated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.truncated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ChannelTruncatedEvent') + + end + + it 'parses channel.unfrozen' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.unfrozen"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ChannelUnFrozenEvent') + + end + + it 'parses channel.unmuted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.unmuted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ChannelUnmutedEvent') + + end + + it 'parses channel.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ChannelUpdatedEvent') + + end + + it 'parses channel.visible' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.visible"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ChannelVisibleEvent') + + end + + it 'parses channel_batch_update.completed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"channel_batch_update.completed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ChannelBatchCompletedEvent') + + end + + it 'parses channel_batch_update.started' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"channel_batch_update.started"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ChannelBatchStartedEvent') + + end + + it 'parses custom' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"custom"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CustomVideoEvent') + + end + + it 'parses export.bulk_image_moderation.error' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"export.bulk_image_moderation.error"}') + expect(event.class.name).to eq('GetStream::Generated::Models::AsyncExportErrorEvent') + + end + + it 'parses export.bulk_image_moderation.success' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"export.bulk_image_moderation.success"}') + expect(event.class.name).to eq('GetStream::Generated::Models::AsyncBulkImageModerationEvent') + + end + + it 'parses export.channels.error' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"export.channels.error"}') + expect(event.class.name).to eq('GetStream::Generated::Models::AsyncExportErrorEvent') + + end + + it 'parses export.channels.success' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"export.channels.success"}') + expect(event.class.name).to eq('GetStream::Generated::Models::AsyncExportChannelsEvent') + + end + + it 'parses export.moderation_logs.error' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"export.moderation_logs.error"}') + expect(event.class.name).to eq('GetStream::Generated::Models::AsyncExportErrorEvent') + + end + + it 'parses export.moderation_logs.success' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"export.moderation_logs.success"}') + expect(event.class.name).to eq('GetStream::Generated::Models::AsyncExportModerationLogsEvent') + + end + + it 'parses export.users.error' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"export.users.error"}') + expect(event.class.name).to eq('GetStream::Generated::Models::AsyncExportErrorEvent') + + end + + it 'parses export.users.success' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"export.users.success"}') + expect(event.class.name).to eq('GetStream::Generated::Models::AsyncExportUsersEvent') + + end + + it 'parses feeds.activity.added' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.added"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ActivityAddedEvent') + + end + + it 'parses feeds.activity.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ActivityDeletedEvent') + + end + + it 'parses feeds.activity.feedback' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.feedback"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ActivityFeedbackEvent') + + end + + it 'parses feeds.activity.marked' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.marked"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ActivityMarkEvent') + + end + + it 'parses feeds.activity.pinned' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.pinned"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ActivityPinnedEvent') + + end + + it 'parses feeds.activity.reaction.added' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.reaction.added"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ActivityReactionAddedEvent') + + end + + it 'parses feeds.activity.reaction.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.reaction.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ActivityReactionDeletedEvent') + + end + + it 'parses feeds.activity.reaction.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.reaction.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ActivityReactionUpdatedEvent') + + end + + it 'parses feeds.activity.removed_from_feed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.removed_from_feed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ActivityRemovedFromFeedEvent') + + end + + it 'parses feeds.activity.restored' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.restored"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ActivityRestoredEvent') + + end + + it 'parses feeds.activity.unpinned' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.unpinned"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ActivityUnpinnedEvent') + + end + + it 'parses feeds.activity.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ActivityUpdatedEvent') + + end + + it 'parses feeds.bookmark.added' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.bookmark.added"}') + expect(event.class.name).to eq('GetStream::Generated::Models::BookmarkAddedEvent') + + end + + it 'parses feeds.bookmark.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.bookmark.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::BookmarkDeletedEvent') + + end + + it 'parses feeds.bookmark.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.bookmark.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::BookmarkUpdatedEvent') + + end + + it 'parses feeds.bookmark_folder.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.bookmark_folder.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::BookmarkFolderDeletedEvent') + + end + + it 'parses feeds.bookmark_folder.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.bookmark_folder.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::BookmarkFolderUpdatedEvent') + + end + + it 'parses feeds.comment.added' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.comment.added"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CommentAddedEvent') + + end + + it 'parses feeds.comment.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.comment.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CommentDeletedEvent') + + end + + it 'parses feeds.comment.reaction.added' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.comment.reaction.added"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CommentReactionAddedEvent') + + end + + it 'parses feeds.comment.reaction.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.comment.reaction.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CommentReactionDeletedEvent') + + end + + it 'parses feeds.comment.reaction.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.comment.reaction.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CommentReactionUpdatedEvent') + + end + + it 'parses feeds.comment.restored' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.comment.restored"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CommentRestoredEvent') + + end + + it 'parses feeds.comment.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.comment.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::CommentUpdatedEvent') + + end + + it 'parses feeds.feed.created' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed.created"}') + expect(event.class.name).to eq('GetStream::Generated::Models::FeedCreatedEvent') + + end + + it 'parses feeds.feed.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::FeedDeletedEvent') + + end + + it 'parses feeds.feed.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::FeedUpdatedEvent') + + end + + it 'parses feeds.feed_group.changed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed_group.changed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::FeedGroupChangedEvent') + + end + + it 'parses feeds.feed_group.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed_group.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::FeedGroupDeletedEvent') + + end + + it 'parses feeds.feed_group.restored' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed_group.restored"}') + expect(event.class.name).to eq('GetStream::Generated::Models::FeedGroupRestoredEvent') + + end + + it 'parses feeds.feed_member.added' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed_member.added"}') + expect(event.class.name).to eq('GetStream::Generated::Models::FeedMemberAddedEvent') + + end + + it 'parses feeds.feed_member.removed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed_member.removed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::FeedMemberRemovedEvent') + + end + + it 'parses feeds.feed_member.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed_member.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::FeedMemberUpdatedEvent') + + end + + it 'parses feeds.follow.created' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.follow.created"}') + expect(event.class.name).to eq('GetStream::Generated::Models::FollowCreatedEvent') + + end + + it 'parses feeds.follow.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.follow.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::FollowDeletedEvent') + + end + + it 'parses feeds.follow.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.follow.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::FollowUpdatedEvent') + + end + + it 'parses feeds.notification_feed.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.notification_feed.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::NotificationFeedUpdatedEvent') + + end + + it 'parses feeds.stories_feed.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.stories_feed.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::StoriesFeedUpdatedEvent') + + end + + it 'parses flag.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"flag.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::FlagUpdatedEvent') + + end + + it 'parses ingress.error' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"ingress.error"}') + expect(event.class.name).to eq('GetStream::Generated::Models::IngressErrorEvent') + + end + + it 'parses ingress.started' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"ingress.started"}') + expect(event.class.name).to eq('GetStream::Generated::Models::IngressStartedEvent') + + end + + it 'parses ingress.stopped' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"ingress.stopped"}') + expect(event.class.name).to eq('GetStream::Generated::Models::IngressStoppedEvent') + + end + + it 'parses member.added' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"member.added"}') + expect(event.class.name).to eq('GetStream::Generated::Models::MemberAddedEvent') + + end + + it 'parses member.removed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"member.removed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::MemberRemovedEvent') + + end + + it 'parses member.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"member.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::MemberUpdatedEvent') + + end + + it 'parses message.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"message.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::MessageDeletedEvent') + + end + + it 'parses message.flagged' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"message.flagged"}') + expect(event.class.name).to eq('GetStream::Generated::Models::MessageFlaggedEvent') + + end + + it 'parses message.new' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"message.new"}') + expect(event.class.name).to eq('GetStream::Generated::Models::MessageNewEvent') + + end + + it 'parses message.pending' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"message.pending"}') + expect(event.class.name).to eq('GetStream::Generated::Models::PendingMessageEvent') + + end + + it 'parses message.read' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"message.read"}') + expect(event.class.name).to eq('GetStream::Generated::Models::MessageReadEvent') + + end + + it 'parses message.unblocked' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"message.unblocked"}') + expect(event.class.name).to eq('GetStream::Generated::Models::MessageUnblockedEvent') + + end + + it 'parses message.undeleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"message.undeleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::MessageUndeletedEvent') + + end + + it 'parses message.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"message.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::MessageUpdatedEvent') + + end + + it 'parses moderation.custom_action' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"moderation.custom_action"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ModerationCustomActionEvent') + + end + + it 'parses moderation.flagged' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"moderation.flagged"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ModerationFlaggedEvent') + + end + + it 'parses moderation.mark_reviewed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"moderation.mark_reviewed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ModerationMarkReviewedEvent') + + end + + it 'parses moderation_check.completed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"moderation_check.completed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ModerationCheckCompletedEvent') + + end + + it 'parses moderation_rule.triggered' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"moderation_rule.triggered"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ModerationRulesTriggeredEvent') + + end + + it 'parses notification.mark_unread' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"notification.mark_unread"}') + expect(event.class.name).to eq('GetStream::Generated::Models::NotificationMarkUnreadEvent') + + end + + it 'parses notification.reminder_due' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"notification.reminder_due"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ReminderNotificationEvent') + + end + + it 'parses notification.thread_message_new' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"notification.thread_message_new"}') + expect(event.class.name).to eq('GetStream::Generated::Models::NotificationThreadMessageNewEvent') + + end + + it 'parses reaction.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"reaction.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ReactionDeletedEvent') + + end + + it 'parses reaction.new' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"reaction.new"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ReactionNewEvent') + + end + + it 'parses reaction.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"reaction.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ReactionUpdatedEvent') + + end + + it 'parses reminder.created' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"reminder.created"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ReminderCreatedEvent') + + end + + it 'parses reminder.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"reminder.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ReminderDeletedEvent') + + end + + it 'parses reminder.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"reminder.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ReminderUpdatedEvent') + + end + + it 'parses review_queue_item.new' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"review_queue_item.new"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ReviewQueueItemNewEvent') + + end + + it 'parses review_queue_item.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"review_queue_item.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ReviewQueueItemUpdatedEvent') + + end + + it 'parses thread.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"thread.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::ThreadUpdatedEvent') + + end + + it 'parses user.banned' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"user.banned"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UserBannedEvent') + + end + + it 'parses user.deactivated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"user.deactivated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UserDeactivatedEvent') + + end + + it 'parses user.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"user.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UserDeletedEvent') + + end + + it 'parses user.flagged' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"user.flagged"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UserFlaggedEvent') + + end + + it 'parses user.messages.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"user.messages.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UserMessagesDeletedEvent') + + end + + it 'parses user.muted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"user.muted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UserMutedEvent') + + end + + it 'parses user.reactivated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"user.reactivated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UserReactivatedEvent') + + end + + it 'parses user.unbanned' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"user.unbanned"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UserUnbannedEvent') + + end + + it 'parses user.unmuted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"user.unmuted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UserUnmutedEvent') + + end + + it 'parses user.unread_message_reminder' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"user.unread_message_reminder"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UserUnreadReminderEvent') + + end + + it 'parses user.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"user.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UserUpdatedEvent') + + end + + it 'parses user_group.created' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.created"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UserGroupCreatedEvent') + + end + + it 'parses user_group.deleted' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.deleted"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UserGroupDeletedEvent') + + end + + it 'parses user_group.member_added' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.member_added"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UserGroupMemberAddedEvent') + + end + + it 'parses user_group.member_removed' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.member_removed"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UserGroupMemberRemovedEvent') + + end + + it 'parses user_group.updated' do + + event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.updated"}') + expect(event.class.name).to eq('GetStream::Generated::Models::UserGroupUpdatedEvent') + + end + + it 'raises on unknown event type' do + + expect { StreamChat::Webhook.parse_webhook_event('{"type":"unknown.event"}') }.to raise_error(ArgumentError) + + end + + it 'raises when type field is missing' do + + expect { StreamChat::Webhook.parse_webhook_event('{"foo":"bar"}') }.to raise_error(ArgumentError) + + end + + it 'raises on invalid JSON' do + + expect { StreamChat::Webhook.parse_webhook_event('not json') }.to raise_error(ArgumentError) + + end + + end + + # --------------------------------------------------------------------------- + # Spec §6 helpers + composites: parse_event, gunzip_payload, decode_sqs_payload, + # decode_sns_payload, verify_and_parse_webhook, parse_sqs, parse_sns. + # --------------------------------------------------------------------------- + + describe 'canonical alias' do + + it 'resolves Stream::Webhook to StreamChat::Webhook' do + + expect(Stream::Webhook).to eq(StreamChat::Webhook) + + end + + end + + describe 'parse_event' do + + it 'returns an UnknownEvent for unknown discriminator' do + + body = '{"type":"totally.made.up","created_at":"2026-05-08T00:00:00Z"}' + event = StreamChat::Webhook.parse_event(body) + expect(event).to be_a(StreamChat::Webhook::UnknownEvent) + expect(event.type).to eq('totally.made.up') + expect(event.created_at).not_to be_nil + expect(event.raw['type']).to eq('totally.made.up') + + end + + it 'raises on empty body' do + + expect { StreamChat::Webhook.parse_event('') }.to raise_error(StreamChat::Webhook::InvalidWebhookError) + + end + + it 'raises on invalid JSON' do + + expect { StreamChat::Webhook.parse_event('not json') }.to raise_error(StreamChat::Webhook::InvalidWebhookError) + + end + + it 'raises on non-object JSON' do + + expect { StreamChat::Webhook.parse_event('[1,2,3]') }.to raise_error(StreamChat::Webhook::InvalidWebhookError) + + end + + it 'raises when type field is missing' do + + err_class = StreamChat::Webhook::InvalidWebhookError + expect { StreamChat::Webhook.parse_event('{"foo":"bar"}') }.to raise_error(err_class) + + end + + end + + describe 'gunzip_payload' do + + it 'passes through a plain body' do + + plain = '{"type":"message.new"}' + expect(StreamChat::Webhook.gunzip_payload(plain)).to eq(plain.b) + + end + + it 'decompresses a gzip body' do + + plain = '{"type":"message.new"}' + gz = Zlib.gzip(plain) + expect(StreamChat::Webhook.gunzip_payload(gz)).to eq(plain) + + end + + it 'raises on a corrupt gzip stream' do + + corrupt = "\x1F\x8Bnot-actually-a-gzip-stream".b + expect { StreamChat::Webhook.gunzip_payload(corrupt) }.to raise_error(StreamChat::Webhook::InvalidWebhookError) + + end + + end + + describe 'decode_sqs_payload' do + + it 'decodes plain base64' do + + plain = '{"type":"message.new"}' + encoded = Base64.strict_encode64(plain) + expect(StreamChat::Webhook.decode_sqs_payload(encoded)).to eq(plain.b) + + end + + it 'decodes base64-then-gzip payload' do + + plain = '{"type":"message.new"}' + gz = Zlib.gzip(plain) + encoded = Base64.strict_encode64(gz) + expect(StreamChat::Webhook.decode_sqs_payload(encoded)).to eq(plain) + + end + + it 'passes through non-base64 raw JSON' do + + # Per chat#13392 wire format: SQS bodies are raw JSON when + # hook_payload_compression is off. decode_sqs_payload must fall back to + # raw bytes on non-base64 input rather than raise. + plain = '{"type":"message.new"}' + expect(StreamChat::Webhook.decode_sqs_payload(plain)).to eq(plain) + + end + + end + + describe 'decode_sns_payload' do + + it 'extracts and decodes the inner Message' do + + plain = '{"type":"message.new"}' + envelope = JSON.generate( + 'Type' => 'Notification', + 'Message' => Base64.strict_encode64(plain), + 'MessageId' => 'abc-123', + 'Timestamp' => '2026-05-08T00:00:00Z', + 'TopicArn' => 'arn:aws:sns:us-east-1:123:test', + ) + expect(StreamChat::Webhook.decode_sns_payload(envelope)).to eq(plain.b) + + end + + it 'treats non-envelope input as a raw Message string' do + + # decode_sns_payload accepts either a full SNS envelope or a + # pre-extracted Message string. Non-envelope input flows through as bytes. + expect(StreamChat::Webhook.decode_sns_payload('not json')).not_to be_nil + + end + + it 'treats envelope-shape without Message as raw' do + + # Envelope-shaped JSON without a string Message field is treated + # as a pre-extracted Message string and flows through. + expect(StreamChat::Webhook.decode_sns_payload('{"Type":"Notification"}')).not_to be_nil + + end + + end + + describe 'verify_and_parse_webhook' do + + it 'parses a valid happy-path body' do + + body = '{"type":"message.new"}' + sig = compute_signature(body, WEBHOOK_TEST_SECRET) + expect(StreamChat::Webhook.verify_and_parse_webhook(body, sig, WEBHOOK_TEST_SECRET)).not_to be_nil + + end + + it 'parses a valid gzip body' do + + body = '{"type":"message.new"}' + sig = compute_signature(body, WEBHOOK_TEST_SECRET) + gz = Zlib.gzip(body) + expect(StreamChat::Webhook.verify_and_parse_webhook(gz, sig, WEBHOOK_TEST_SECRET)).not_to be_nil + + end + + it 'raises on tampered body' do + + body = '{"type":"message.new"}' + sig = compute_signature(body, WEBHOOK_TEST_SECRET) + tampered = '{"type":"message.deleted"}' + err_class = StreamChat::Webhook::InvalidWebhookError + # Single-arm rescue on the unified InvalidWebhookError; message identifies the mode. + expect { StreamChat::Webhook.verify_and_parse_webhook(tampered, sig, WEBHOOK_TEST_SECRET) } + .to raise_error(err_class, /#{err_class::SIGNATURE_MISMATCH}/) + + end + + end + + describe 'parse_sqs' do + + it 'parses a happy-path SQS body' do + + plain = '{"type":"message.new"}' + expect(StreamChat::Webhook.parse_sqs(Base64.strict_encode64(plain))).not_to be_nil + + end + + end + + describe 'parse_sns' do + + it 'parses a happy-path SNS envelope' do + + plain = '{"type":"message.new"}' + envelope = JSON.generate( + 'Type' => 'Notification', + 'Message' => Base64.strict_encode64(plain), + ) + expect(StreamChat::Webhook.parse_sns(envelope)).not_to be_nil + + end + + end + +end + +# --------------------------------------------------------------------------- +# Fixture-driven conformance tests. Fixtures live at test/fixtures/webhooks/ +# (one subdirectory per case for happy path; _invalid/ for negative cases). +# --------------------------------------------------------------------------- +RSpec.describe 'Webhook conformance', type: :integration do + + it 'has conformance fixtures present' do + + expect(File).to be_directory(WEBHOOK_FIXTURE_ROOT) + + end + + if File.directory?(WEBHOOK_FIXTURE_ROOT) + + Dir.children(WEBHOOK_FIXTURE_ROOT).sort.each do |name| + + next if name == '_invalid' + + dir = File.join(WEBHOOK_FIXTURE_ROOT, name) + next unless File.directory?(dir) + + context "happy fixture #{name}" do + + it 'verifies signature, parses event, and round-trips all transports' do + + body = File.binread(File.join(dir, 'body.json')) + body_gz = File.binread(File.join(dir, 'body.gz')) + sqs_compressed = File.read(File.join(dir, 'sqs_body.txt')).strip + sqs_raw = File.read(File.join(dir, 'sqs_body_uncompressed.txt')).strip + sns = File.read(File.join(dir, 'sns_notification.txt')).strip + sig = File.read(File.join(dir, 'signature.txt')).strip + + expect(StreamChat::Webhook.verify_signature(body, sig, WEBHOOK_CONFORMANCE_SECRET)).to be true + expect(StreamChat::Webhook.parse_event(body)).not_to be_nil + expect(StreamChat::Webhook.verify_and_parse_webhook(body, sig, WEBHOOK_CONFORMANCE_SECRET)).not_to be_nil + expect(StreamChat::Webhook.verify_and_parse_webhook(body_gz, sig, WEBHOOK_CONFORMANCE_SECRET)).not_to be_nil + expect(StreamChat::Webhook.parse_sqs(sqs_compressed)).not_to be_nil + expect(StreamChat::Webhook.parse_sqs(sqs_raw)).not_to be_nil + expect(StreamChat::Webhook.parse_sns(sns)).not_to be_nil + + end + + end + + end + + end + + context 'negative fixtures' do + + def neg_dir(name) + File.join(WEBHOOK_FIXTURE_ROOT, '_invalid', name) + end + + it 'rejects tampered body' do + + skip 'fixtures not present' unless File.directory?(WEBHOOK_FIXTURE_ROOT) + + body = File.binread(File.join(neg_dir('tampered_body'), 'body.json')) + sig = File.read(File.join(neg_dir('tampered_body'), 'signature.txt')).strip + err_class = StreamChat::Webhook::InvalidWebhookError + expect { StreamChat::Webhook.verify_and_parse_webhook(body, sig, WEBHOOK_CONFORMANCE_SECRET) } + .to raise_error(err_class, /#{err_class::SIGNATURE_MISMATCH}/) + + end + + it 'returns UnknownEvent for unknown type' do + + skip 'fixtures not present' unless File.directory?(WEBHOOK_FIXTURE_ROOT) + + body = File.binread(File.join(neg_dir('unknown_type'), 'body.json')) + result = StreamChat::Webhook.parse_event(body) + expect(result).to be_a(StreamChat::Webhook::UnknownEvent) + expect(result.type).to eq('totally.made.up') + + end + + it 'raises when type field is missing' do + + skip 'fixtures not present' unless File.directory?(WEBHOOK_FIXTURE_ROOT) + + body = File.binread(File.join(neg_dir('missing_type'), 'body.json')) + err_class = StreamChat::Webhook::InvalidWebhookError + expect { StreamChat::Webhook.parse_event(body) }.to raise_error(err_class, /missing 'type'/) + + end + + it 'raises on malformed JSON' do + + skip 'fixtures not present' unless File.directory?(WEBHOOK_FIXTURE_ROOT) + + body = File.binread(File.join(neg_dir('malformed_json'), 'body.json')) + err_class = StreamChat::Webhook::InvalidWebhookError + expect { StreamChat::Webhook.parse_event(body) }.to raise_error(err_class, /failed to parse webhook payload/) + + end + + it 'raises on empty body' do + + skip 'fixtures not present' unless File.directory?(WEBHOOK_FIXTURE_ROOT) + + body = File.binread(File.join(neg_dir('empty_body'), 'body.json')) + err_class = StreamChat::Webhook::InvalidWebhookError + expect { StreamChat::Webhook.parse_event(body) }.to raise_error(err_class, /must not be empty/) + + end + + it 'raises on bad gzip compression' do + + skip 'fixtures not present' unless File.directory?(WEBHOOK_FIXTURE_ROOT) + + body = File.binread(File.join(neg_dir('bad_compression'), 'body.gz')) + err_class = StreamChat::Webhook::InvalidWebhookError + expect { StreamChat::Webhook.gunzip_payload(body) }.to raise_error(err_class, /gzip decompression failed/) + + end + + it 'raises on bad base64 (downstream parse failure)' do + + # Per CHA-3071 wire format: decode_sqs_payload falls back to raw bytes + # when base64 decoding fails (uncompressed wire format). For input that + # is neither valid base64 nor valid JSON nor gzip-prefixed, parse_sqs + # still raises InvalidWebhookError — just down the chain at JSON parsing. + skip 'fixtures not present' unless File.directory?(WEBHOOK_FIXTURE_ROOT) + + msg = File.read(File.join(neg_dir('bad_base64'), 'sqs_body.txt')).strip + expect { StreamChat::Webhook.parse_sqs(msg) }.to raise_error(StreamChat::Webhook::InvalidWebhookError) + + end + + it 'raises on bad SNS envelope (downstream parse failure)' do + + # bad_sns_envelope (non-envelope JSON) is now treated as a pre-extracted + # Message string and flows through the SQS path, surfacing as a downstream + # parse failure rather than SNS-specific. Still InvalidWebhookError. + skip 'fixtures not present' unless File.directory?(WEBHOOK_FIXTURE_ROOT) + + notif = File.read(File.join(neg_dir('bad_sns_envelope'), 'sns_notification.txt')).strip + expect { StreamChat::Webhook.parse_sns(notif) }.to raise_error(StreamChat::Webhook::InvalidWebhookError) + + end + + end + +end diff --git a/test/fixtures/webhooks/_invalid/bad_base64/body.gz b/test/fixtures/webhooks/_invalid/bad_base64/body.gz new file mode 100644 index 0000000..3c472d2 Binary files /dev/null and b/test/fixtures/webhooks/_invalid/bad_base64/body.gz differ diff --git a/test/fixtures/webhooks/_invalid/bad_base64/body.json b/test/fixtures/webhooks/_invalid/bad_base64/body.json new file mode 100644 index 0000000..3c5e004 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_base64/body.json @@ -0,0 +1 @@ +{"type":"message.new","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_base64/expected.json b/test/fixtures/webhooks/_invalid/bad_base64/expected.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_base64/expected.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_base64/signature.txt b/test/fixtures/webhooks/_invalid/bad_base64/signature.txt new file mode 100644 index 0000000..f463836 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_base64/signature.txt @@ -0,0 +1 @@ +9e43985889d9d37c51c4bb92f6b814331edf8aa1b86ed7d3a8189acc93baabc8 \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_base64/sns_notification.txt b/test/fixtures/webhooks/_invalid/bad_base64/sns_notification.txt new file mode 100644 index 0000000..d73f5e1 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_base64/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"!!!not base64!!!","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_base64/sqs_body.txt b/test/fixtures/webhooks/_invalid/bad_base64/sqs_body.txt new file mode 100644 index 0000000..36fbc6d --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_base64/sqs_body.txt @@ -0,0 +1 @@ +!!!not base64!!! \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_base64/sqs_body_uncompressed.txt b/test/fixtures/webhooks/_invalid/bad_base64/sqs_body_uncompressed.txt new file mode 100644 index 0000000..66f5ff0 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_base64/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoibWVzc2FnZS5uZXciLCJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wOFQwMDowMDowMFoifQ== \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_compression/body.gz b/test/fixtures/webhooks/_invalid/bad_compression/body.gz new file mode 100644 index 0000000..ff79505 Binary files /dev/null and b/test/fixtures/webhooks/_invalid/bad_compression/body.gz differ diff --git a/test/fixtures/webhooks/_invalid/bad_compression/body.json b/test/fixtures/webhooks/_invalid/bad_compression/body.json new file mode 100644 index 0000000..3c5e004 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_compression/body.json @@ -0,0 +1 @@ +{"type":"message.new","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_compression/expected.json b/test/fixtures/webhooks/_invalid/bad_compression/expected.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_compression/expected.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_compression/signature.txt b/test/fixtures/webhooks/_invalid/bad_compression/signature.txt new file mode 100644 index 0000000..bc2cba8 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_compression/signature.txt @@ -0,0 +1 @@ +fdd94f434124de48ef9f6237234ce28fd965fcb0f20b3f739f47d42aa17eb7b2 \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_compression/sns_notification.txt b/test/fixtures/webhooks/_invalid/bad_compression/sns_notification.txt new file mode 100644 index 0000000..7d10e3a --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_compression/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAAAFhYWFhYWA==","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_compression/sqs_body.txt b/test/fixtures/webhooks/_invalid/bad_compression/sqs_body.txt new file mode 100644 index 0000000..1ac8964 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_compression/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAAAFhYWFhYWA== \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_compression/sqs_body_uncompressed.txt b/test/fixtures/webhooks/_invalid/bad_compression/sqs_body_uncompressed.txt new file mode 100644 index 0000000..66f5ff0 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_compression/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoibWVzc2FnZS5uZXciLCJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wOFQwMDowMDowMFoifQ== \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_sns_envelope/body.gz b/test/fixtures/webhooks/_invalid/bad_sns_envelope/body.gz new file mode 100644 index 0000000..3c472d2 Binary files /dev/null and b/test/fixtures/webhooks/_invalid/bad_sns_envelope/body.gz differ diff --git a/test/fixtures/webhooks/_invalid/bad_sns_envelope/body.json b/test/fixtures/webhooks/_invalid/bad_sns_envelope/body.json new file mode 100644 index 0000000..3c5e004 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_sns_envelope/body.json @@ -0,0 +1 @@ +{"type":"message.new","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_sns_envelope/expected.json b/test/fixtures/webhooks/_invalid/bad_sns_envelope/expected.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_sns_envelope/expected.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_sns_envelope/signature.txt b/test/fixtures/webhooks/_invalid/bad_sns_envelope/signature.txt new file mode 100644 index 0000000..f463836 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_sns_envelope/signature.txt @@ -0,0 +1 @@ +9e43985889d9d37c51c4bb92f6b814331edf8aa1b86ed7d3a8189acc93baabc8 \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_sns_envelope/sns_notification.txt b/test/fixtures/webhooks/_invalid/bad_sns_envelope/sns_notification.txt new file mode 100644 index 0000000..d98c2de --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_sns_envelope/sns_notification.txt @@ -0,0 +1 @@ +{"Type":"Notification","Message":"@@@not-valid-anything@@@"} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_sns_envelope/sqs_body.txt b/test/fixtures/webhooks/_invalid/bad_sns_envelope/sqs_body.txt new file mode 100644 index 0000000..9f3042b --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_sns_envelope/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWKqksSFWyUspNLS5OTE/Vy0stV9JRSi5KTSxJTYlPLFGyUjIyMDLTNTDVNbAIMTCwAqMopVpAAAAA//+PXB06OgAAAA== \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/bad_sns_envelope/sqs_body_uncompressed.txt b/test/fixtures/webhooks/_invalid/bad_sns_envelope/sqs_body_uncompressed.txt new file mode 100644 index 0000000..66f5ff0 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/bad_sns_envelope/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoibWVzc2FnZS5uZXciLCJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wOFQwMDowMDowMFoifQ== \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/empty_body/body.gz b/test/fixtures/webhooks/_invalid/empty_body/body.gz new file mode 100644 index 0000000..1f0aab1 Binary files /dev/null and b/test/fixtures/webhooks/_invalid/empty_body/body.gz differ diff --git a/test/fixtures/webhooks/_invalid/empty_body/body.json b/test/fixtures/webhooks/_invalid/empty_body/body.json new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/webhooks/_invalid/empty_body/expected.json b/test/fixtures/webhooks/_invalid/empty_body/expected.json new file mode 100644 index 0000000..e6b1496 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/empty_body/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/empty_body/signature.txt b/test/fixtures/webhooks/_invalid/empty_body/signature.txt new file mode 100644 index 0000000..dc24308 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/empty_body/signature.txt @@ -0,0 +1 @@ +ff0affd4e69c01904d58894b1d0ffac80fb093b36fa32ee2879ded7801a29578 \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/empty_body/sns_notification.txt b/test/fixtures/webhooks/_invalid/empty_body/sns_notification.txt new file mode 100644 index 0000000..39da5fd --- /dev/null +++ b/test/fixtures/webhooks/_invalid/empty_body/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/wEAAP//AAAAAAAAAAA=","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/empty_body/sqs_body.txt b/test/fixtures/webhooks/_invalid/empty_body/sqs_body.txt new file mode 100644 index 0000000..cc33db9 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/empty_body/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/wEAAP//AAAAAAAAAAA= \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/empty_body/sqs_body_uncompressed.txt b/test/fixtures/webhooks/_invalid/empty_body/sqs_body_uncompressed.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/webhooks/_invalid/malformed_json/body.gz b/test/fixtures/webhooks/_invalid/malformed_json/body.gz new file mode 100644 index 0000000..a2798f7 Binary files /dev/null and b/test/fixtures/webhooks/_invalid/malformed_json/body.gz differ diff --git a/test/fixtures/webhooks/_invalid/malformed_json/body.json b/test/fixtures/webhooks/_invalid/malformed_json/body.json new file mode 100644 index 0000000..8dc5858 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/malformed_json/body.json @@ -0,0 +1 @@ +{not valid json \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/malformed_json/expected.json b/test/fixtures/webhooks/_invalid/malformed_json/expected.json new file mode 100644 index 0000000..e6b1496 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/malformed_json/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/malformed_json/signature.txt b/test/fixtures/webhooks/_invalid/malformed_json/signature.txt new file mode 100644 index 0000000..da3ad90 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/malformed_json/signature.txt @@ -0,0 +1 @@ +ab763ae1d822a7035b0c97d61cebd33ed83ad6011cc77bcc8b9873d32df86854 \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/malformed_json/sns_notification.txt b/test/fixtures/webhooks/_invalid/malformed_json/sns_notification.txt new file mode 100644 index 0000000..72cee88 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/malformed_json/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6rOyy9RKEvMyUxRyCrOzwMEAAD//8DbrPEPAAAA","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/malformed_json/sqs_body.txt b/test/fixtures/webhooks/_invalid/malformed_json/sqs_body.txt new file mode 100644 index 0000000..99bad10 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/malformed_json/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6rOyy9RKEvMyUxRyCrOzwMEAAD//8DbrPEPAAAA \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/malformed_json/sqs_body_uncompressed.txt b/test/fixtures/webhooks/_invalid/malformed_json/sqs_body_uncompressed.txt new file mode 100644 index 0000000..80c0d30 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/malformed_json/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +e25vdCB2YWxpZCBqc29u \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/missing_type/body.gz b/test/fixtures/webhooks/_invalid/missing_type/body.gz new file mode 100644 index 0000000..accd9f1 Binary files /dev/null and b/test/fixtures/webhooks/_invalid/missing_type/body.gz differ diff --git a/test/fixtures/webhooks/_invalid/missing_type/body.json b/test/fixtures/webhooks/_invalid/missing_type/body.json new file mode 100644 index 0000000..6dd72c7 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/missing_type/body.json @@ -0,0 +1 @@ +{"created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/missing_type/expected.json b/test/fixtures/webhooks/_invalid/missing_type/expected.json new file mode 100644 index 0000000..e6b1496 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/missing_type/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/missing_type/signature.txt b/test/fixtures/webhooks/_invalid/missing_type/signature.txt new file mode 100644 index 0000000..c899669 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/missing_type/signature.txt @@ -0,0 +1 @@ +0af44ad112e7a090278f4eacbf342504c13f3bf7368544f3c029b6a2e82cf7ac \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/missing_type/sns_notification.txt b/test/fixtures/webhooks/_invalid/missing_type/sns_notification.txt new file mode 100644 index 0000000..6cba666 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/missing_type/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6pWSi5KTSxJTYlPLFGyUjIyMDLTNTDVNbAIMTCwAqMopVpAAAAA//+C/s2rJQAAAA==","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/missing_type/sqs_body.txt b/test/fixtures/webhooks/_invalid/missing_type/sqs_body.txt new file mode 100644 index 0000000..2a7491b --- /dev/null +++ b/test/fixtures/webhooks/_invalid/missing_type/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWSi5KTSxJTYlPLFGyUjIyMDLTNTDVNbAIMTCwAqMopVpAAAAA//+C/s2rJQAAAA== \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/missing_type/sqs_body_uncompressed.txt b/test/fixtures/webhooks/_invalid/missing_type/sqs_body_uncompressed.txt new file mode 100644 index 0000000..414ed50 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/missing_type/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wOFQwMDowMDowMFoifQ== \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/tampered_body/body.gz b/test/fixtures/webhooks/_invalid/tampered_body/body.gz new file mode 100644 index 0000000..aa3a900 Binary files /dev/null and b/test/fixtures/webhooks/_invalid/tampered_body/body.gz differ diff --git a/test/fixtures/webhooks/_invalid/tampered_body/body.json b/test/fixtures/webhooks/_invalid/tampered_body/body.json new file mode 100644 index 0000000..8944a30 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/tampered_body/body.json @@ -0,0 +1 @@ +{"type":"message.new","created_at":"2026-05-08T00:00:00Z","tampered":true} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/tampered_body/expected.json b/test/fixtures/webhooks/_invalid/tampered_body/expected.json new file mode 100644 index 0000000..c9ac9a9 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/tampered_body/expected.json @@ -0,0 +1,7 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z", + "tampered": true + }, + "type": "message.new" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/tampered_body/signature.txt b/test/fixtures/webhooks/_invalid/tampered_body/signature.txt new file mode 100644 index 0000000..f463836 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/tampered_body/signature.txt @@ -0,0 +1 @@ +9e43985889d9d37c51c4bb92f6b814331edf8aa1b86ed7d3a8189acc93baabc8 \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/tampered_body/sns_notification.txt b/test/fixtures/webhooks/_invalid/tampered_body/sns_notification.txt new file mode 100644 index 0000000..b689d2b --- /dev/null +++ b/test/fixtures/webhooks/_invalid/tampered_body/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6pWKqksSFWyUspNLS5OTE/Vy0stV9JRSi5KTSxJTYlPLFGyUjIyMDLTNTDVNbAIMTCwAqMoJR2lksTcgtSi1BQlq5Ki0tRaQAAAAP//KNcucUoAAAA=","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/tampered_body/sqs_body.txt b/test/fixtures/webhooks/_invalid/tampered_body/sqs_body.txt new file mode 100644 index 0000000..c5c165c --- /dev/null +++ b/test/fixtures/webhooks/_invalid/tampered_body/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWKqksSFWyUspNLS5OTE/Vy0stV9JRSi5KTSxJTYlPLFGyUjIyMDLTNTDVNbAIMTCwAqMoJR2lksTcgtSi1BQlq5Ki0tRaQAAAAP//KNcucUoAAAA= \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/tampered_body/sqs_body_uncompressed.txt b/test/fixtures/webhooks/_invalid/tampered_body/sqs_body_uncompressed.txt new file mode 100644 index 0000000..f3f6f82 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/tampered_body/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoibWVzc2FnZS5uZXciLCJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wOFQwMDowMDowMFoiLCJ0YW1wZXJlZCI6dHJ1ZX0= \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/unknown_type/body.gz b/test/fixtures/webhooks/_invalid/unknown_type/body.gz new file mode 100644 index 0000000..b044b81 Binary files /dev/null and b/test/fixtures/webhooks/_invalid/unknown_type/body.gz differ diff --git a/test/fixtures/webhooks/_invalid/unknown_type/body.json b/test/fixtures/webhooks/_invalid/unknown_type/body.json new file mode 100644 index 0000000..612a640 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/unknown_type/body.json @@ -0,0 +1 @@ +{"type":"totally.made.up","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/unknown_type/expected.json b/test/fixtures/webhooks/_invalid/unknown_type/expected.json new file mode 100644 index 0000000..0bdda8c --- /dev/null +++ b/test/fixtures/webhooks/_invalid/unknown_type/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "totally.made.up" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/unknown_type/signature.txt b/test/fixtures/webhooks/_invalid/unknown_type/signature.txt new file mode 100644 index 0000000..6c0d872 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/unknown_type/signature.txt @@ -0,0 +1 @@ +fec459361cdb0d6c0c19ebe323dc37bf0b47c23172e6840260fe66b9b6fdb876 \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/unknown_type/sns_notification.txt b/test/fixtures/webhooks/_invalid/unknown_type/sns_notification.txt new file mode 100644 index 0000000..e0a95ca --- /dev/null +++ b/test/fixtures/webhooks/_invalid/unknown_type/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6pWKqksSFWyUirJL0nMyanUy01MSdUrLVDSUUouSk0sSU2JTyxRslIyMjAy0zUw1TWwCDEwsAKjKKVaQAAAAP//rIh10D4AAAA=","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/unknown_type/sqs_body.txt b/test/fixtures/webhooks/_invalid/unknown_type/sqs_body.txt new file mode 100644 index 0000000..e06298c --- /dev/null +++ b/test/fixtures/webhooks/_invalid/unknown_type/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWKqksSFWyUirJL0nMyanUy01MSdUrLVDSUUouSk0sSU2JTyxRslIyMjAy0zUw1TWwCDEwsAKjKKVaQAAAAP//rIh10D4AAAA= \ No newline at end of file diff --git a/test/fixtures/webhooks/_invalid/unknown_type/sqs_body_uncompressed.txt b/test/fixtures/webhooks/_invalid/unknown_type/sqs_body_uncompressed.txt new file mode 100644 index 0000000..ba70b33 --- /dev/null +++ b/test/fixtures/webhooks/_invalid/unknown_type/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoidG90YWxseS5tYWRlLnVwIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDUtMDhUMDA6MDA6MDBaIn0= \ No newline at end of file diff --git a/test/fixtures/webhooks/call.session_ended/body.gz b/test/fixtures/webhooks/call.session_ended/body.gz new file mode 100644 index 0000000..416a2ca Binary files /dev/null and b/test/fixtures/webhooks/call.session_ended/body.gz differ diff --git a/test/fixtures/webhooks/call.session_ended/body.json b/test/fixtures/webhooks/call.session_ended/body.json new file mode 100644 index 0000000..571bc24 --- /dev/null +++ b/test/fixtures/webhooks/call.session_ended/body.json @@ -0,0 +1 @@ +{"type":"call.session_ended","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/call.session_ended/expected.json b/test/fixtures/webhooks/call.session_ended/expected.json new file mode 100644 index 0000000..e983f41 --- /dev/null +++ b/test/fixtures/webhooks/call.session_ended/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "call.session_ended" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/call.session_ended/signature.txt b/test/fixtures/webhooks/call.session_ended/signature.txt new file mode 100644 index 0000000..68b0389 --- /dev/null +++ b/test/fixtures/webhooks/call.session_ended/signature.txt @@ -0,0 +1 @@ +6f414fcd7a2a2d8e0e2a83b297ae2785862f84d8c69742efbbd425dc5da974b2 \ No newline at end of file diff --git a/test/fixtures/webhooks/call.session_ended/sns_notification.txt b/test/fixtures/webhooks/call.session_ended/sns_notification.txt new file mode 100644 index 0000000..522d667 --- /dev/null +++ b/test/fixtures/webhooks/call.session_ended/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6pWKqksSFWyUkpOzMnRK04tLs7Mz4tPzUtJTVHSUUouSk0sSU2JTyxRslIyMjAy0zUw1TWwCDEwsAKjKKVaQAAAAP//wLjdnkEAAAA=","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/call.session_ended/sqs_body.txt b/test/fixtures/webhooks/call.session_ended/sqs_body.txt new file mode 100644 index 0000000..0d88d33 --- /dev/null +++ b/test/fixtures/webhooks/call.session_ended/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWKqksSFWyUkpOzMnRK04tLs7Mz4tPzUtJTVHSUUouSk0sSU2JTyxRslIyMjAy0zUw1TWwCDEwsAKjKKVaQAAAAP//wLjdnkEAAAA= \ No newline at end of file diff --git a/test/fixtures/webhooks/call.session_ended/sqs_body_uncompressed.txt b/test/fixtures/webhooks/call.session_ended/sqs_body_uncompressed.txt new file mode 100644 index 0000000..4cd0b86 --- /dev/null +++ b/test/fixtures/webhooks/call.session_ended/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoiY2FsbC5zZXNzaW9uX2VuZGVkIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDUtMDhUMDA6MDA6MDBaIn0= \ No newline at end of file diff --git a/test/fixtures/webhooks/call.session_started/body.gz b/test/fixtures/webhooks/call.session_started/body.gz new file mode 100644 index 0000000..96f426d Binary files /dev/null and b/test/fixtures/webhooks/call.session_started/body.gz differ diff --git a/test/fixtures/webhooks/call.session_started/body.json b/test/fixtures/webhooks/call.session_started/body.json new file mode 100644 index 0000000..890ce83 --- /dev/null +++ b/test/fixtures/webhooks/call.session_started/body.json @@ -0,0 +1 @@ +{"type":"call.session_started","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/call.session_started/expected.json b/test/fixtures/webhooks/call.session_started/expected.json new file mode 100644 index 0000000..19ab9a9 --- /dev/null +++ b/test/fixtures/webhooks/call.session_started/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "call.session_started" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/call.session_started/signature.txt b/test/fixtures/webhooks/call.session_started/signature.txt new file mode 100644 index 0000000..78bf403 --- /dev/null +++ b/test/fixtures/webhooks/call.session_started/signature.txt @@ -0,0 +1 @@ +fb227f610f1c65f9e8da88a29215dc88c20c8b8906ff9efe0dac2dd018fc3288 \ No newline at end of file diff --git a/test/fixtures/webhooks/call.session_started/sns_notification.txt b/test/fixtures/webhooks/call.session_started/sns_notification.txt new file mode 100644 index 0000000..646af21 --- /dev/null +++ b/test/fixtures/webhooks/call.session_started/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6pWKqksSFWyUkpOzMnRK04tLs7Mz4svLkksKklNUdJRSi5KTSxJTYlPLFGyUjIyMDLTNTDVNbAIMTCwAqMopVpAAAAA///rR2lwQwAAAA==","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/call.session_started/sqs_body.txt b/test/fixtures/webhooks/call.session_started/sqs_body.txt new file mode 100644 index 0000000..289e271 --- /dev/null +++ b/test/fixtures/webhooks/call.session_started/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWKqksSFWyUkpOzMnRK04tLs7Mz4svLkksKklNUdJRSi5KTSxJTYlPLFGyUjIyMDLTNTDVNbAIMTCwAqMopVpAAAAA///rR2lwQwAAAA== \ No newline at end of file diff --git a/test/fixtures/webhooks/call.session_started/sqs_body_uncompressed.txt b/test/fixtures/webhooks/call.session_started/sqs_body_uncompressed.txt new file mode 100644 index 0000000..0f4bfcb --- /dev/null +++ b/test/fixtures/webhooks/call.session_started/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoiY2FsbC5zZXNzaW9uX3N0YXJ0ZWQiLCJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wOFQwMDowMDowMFoifQ== \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.created/body.gz b/test/fixtures/webhooks/channel.created/body.gz new file mode 100644 index 0000000..17b4367 Binary files /dev/null and b/test/fixtures/webhooks/channel.created/body.gz differ diff --git a/test/fixtures/webhooks/channel.created/body.json b/test/fixtures/webhooks/channel.created/body.json new file mode 100644 index 0000000..29f7752 --- /dev/null +++ b/test/fixtures/webhooks/channel.created/body.json @@ -0,0 +1 @@ +{"type":"channel.created","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.created/expected.json b/test/fixtures/webhooks/channel.created/expected.json new file mode 100644 index 0000000..bd277f5 --- /dev/null +++ b/test/fixtures/webhooks/channel.created/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "channel.created" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.created/signature.txt b/test/fixtures/webhooks/channel.created/signature.txt new file mode 100644 index 0000000..fd0348d --- /dev/null +++ b/test/fixtures/webhooks/channel.created/signature.txt @@ -0,0 +1 @@ +55aba7ebf3429a1353b7280a217e8e8572502551affe8e5e4eca64071f6a45f3 \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.created/sns_notification.txt b/test/fixtures/webhooks/channel.created/sns_notification.txt new file mode 100644 index 0000000..0f7173b --- /dev/null +++ b/test/fixtures/webhooks/channel.created/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6pWKqksSFWyUkrOSMzLS83RSy5KTSxJTVHSUYKy4hNLlKyUjAyMzHQNTHUNLEIMDKzAKEqpFhAAAP//beAAyj4AAAA=","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.created/sqs_body.txt b/test/fixtures/webhooks/channel.created/sqs_body.txt new file mode 100644 index 0000000..c8edaff --- /dev/null +++ b/test/fixtures/webhooks/channel.created/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWKqksSFWyUkrOSMzLS83RSy5KTSxJTVHSUYKy4hNLlKyUjAyMzHQNTHUNLEIMDKzAKEqpFhAAAP//beAAyj4AAAA= \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.created/sqs_body_uncompressed.txt b/test/fixtures/webhooks/channel.created/sqs_body_uncompressed.txt new file mode 100644 index 0000000..e37ba62 --- /dev/null +++ b/test/fixtures/webhooks/channel.created/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoiY2hhbm5lbC5jcmVhdGVkIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDUtMDhUMDA6MDA6MDBaIn0= \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.deleted/body.gz b/test/fixtures/webhooks/channel.deleted/body.gz new file mode 100644 index 0000000..3626f52 Binary files /dev/null and b/test/fixtures/webhooks/channel.deleted/body.gz differ diff --git a/test/fixtures/webhooks/channel.deleted/body.json b/test/fixtures/webhooks/channel.deleted/body.json new file mode 100644 index 0000000..c828830 --- /dev/null +++ b/test/fixtures/webhooks/channel.deleted/body.json @@ -0,0 +1 @@ +{"type":"channel.deleted","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.deleted/expected.json b/test/fixtures/webhooks/channel.deleted/expected.json new file mode 100644 index 0000000..4bbe53d --- /dev/null +++ b/test/fixtures/webhooks/channel.deleted/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "channel.deleted" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.deleted/signature.txt b/test/fixtures/webhooks/channel.deleted/signature.txt new file mode 100644 index 0000000..ecfb466 --- /dev/null +++ b/test/fixtures/webhooks/channel.deleted/signature.txt @@ -0,0 +1 @@ +392ccc998b554a812c0a88888842dae42a788e59af1aaef8f13a9a62f1b05f6e \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.deleted/sns_notification.txt b/test/fixtures/webhooks/channel.deleted/sns_notification.txt new file mode 100644 index 0000000..4991f20 --- /dev/null +++ b/test/fixtures/webhooks/channel.deleted/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6pWKqksSFWyUkrOSMzLS83RS0nNSS1JTVHSUUouSk0sSU2JTyxRslIyMjAy0zUw1TWwCDEwsAKjKKVaQAAAAP//XUKi9D4AAAA=","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.deleted/sqs_body.txt b/test/fixtures/webhooks/channel.deleted/sqs_body.txt new file mode 100644 index 0000000..df8a408 --- /dev/null +++ b/test/fixtures/webhooks/channel.deleted/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWKqksSFWyUkrOSMzLS83RS0nNSS1JTVHSUUouSk0sSU2JTyxRslIyMjAy0zUw1TWwCDEwsAKjKKVaQAAAAP//XUKi9D4AAAA= \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.deleted/sqs_body_uncompressed.txt b/test/fixtures/webhooks/channel.deleted/sqs_body_uncompressed.txt new file mode 100644 index 0000000..523d431 --- /dev/null +++ b/test/fixtures/webhooks/channel.deleted/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoiY2hhbm5lbC5kZWxldGVkIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDUtMDhUMDA6MDA6MDBaIn0= \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.updated/body.gz b/test/fixtures/webhooks/channel.updated/body.gz new file mode 100644 index 0000000..602cddc Binary files /dev/null and b/test/fixtures/webhooks/channel.updated/body.gz differ diff --git a/test/fixtures/webhooks/channel.updated/body.json b/test/fixtures/webhooks/channel.updated/body.json new file mode 100644 index 0000000..dbe1764 --- /dev/null +++ b/test/fixtures/webhooks/channel.updated/body.json @@ -0,0 +1 @@ +{"type":"channel.updated","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.updated/expected.json b/test/fixtures/webhooks/channel.updated/expected.json new file mode 100644 index 0000000..d864720 --- /dev/null +++ b/test/fixtures/webhooks/channel.updated/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "channel.updated" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.updated/signature.txt b/test/fixtures/webhooks/channel.updated/signature.txt new file mode 100644 index 0000000..f03fc7a --- /dev/null +++ b/test/fixtures/webhooks/channel.updated/signature.txt @@ -0,0 +1 @@ +6203925bfdbe21df850d9b2cd3ffde3962cfbbfaa8812d5f81cdfafdf4a53afd \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.updated/sns_notification.txt b/test/fixtures/webhooks/channel.updated/sns_notification.txt new file mode 100644 index 0000000..3a8a15d --- /dev/null +++ b/test/fixtures/webhooks/channel.updated/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6pWKqksSFWyUkrOSMzLS83RKy1ISSxJTVHSUUouSgWx4hNLlKyUjAyMzHQNTHUNLEIMDKzAKEqpFhAAAP//NjgZnz4AAAA=","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.updated/sqs_body.txt b/test/fixtures/webhooks/channel.updated/sqs_body.txt new file mode 100644 index 0000000..34d9b4c --- /dev/null +++ b/test/fixtures/webhooks/channel.updated/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWKqksSFWyUkrOSMzLS83RKy1ISSxJTVHSUUouSgWx4hNLlKyUjAyMzHQNTHUNLEIMDKzAKEqpFhAAAP//NjgZnz4AAAA= \ No newline at end of file diff --git a/test/fixtures/webhooks/channel.updated/sqs_body_uncompressed.txt b/test/fixtures/webhooks/channel.updated/sqs_body_uncompressed.txt new file mode 100644 index 0000000..6d53b80 --- /dev/null +++ b/test/fixtures/webhooks/channel.updated/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoiY2hhbm5lbC51cGRhdGVkIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDUtMDhUMDA6MDA6MDBaIn0= \ No newline at end of file diff --git a/test/fixtures/webhooks/feeds.activity.added/body.gz b/test/fixtures/webhooks/feeds.activity.added/body.gz new file mode 100644 index 0000000..79db364 Binary files /dev/null and b/test/fixtures/webhooks/feeds.activity.added/body.gz differ diff --git a/test/fixtures/webhooks/feeds.activity.added/body.json b/test/fixtures/webhooks/feeds.activity.added/body.json new file mode 100644 index 0000000..653ca14 --- /dev/null +++ b/test/fixtures/webhooks/feeds.activity.added/body.json @@ -0,0 +1 @@ +{"type":"feeds.activity.added","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/feeds.activity.added/expected.json b/test/fixtures/webhooks/feeds.activity.added/expected.json new file mode 100644 index 0000000..f5ffece --- /dev/null +++ b/test/fixtures/webhooks/feeds.activity.added/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "feeds.activity.added" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/feeds.activity.added/signature.txt b/test/fixtures/webhooks/feeds.activity.added/signature.txt new file mode 100644 index 0000000..81ea6f4 --- /dev/null +++ b/test/fixtures/webhooks/feeds.activity.added/signature.txt @@ -0,0 +1 @@ +36ac7583d9d0bada766e9831912b848345bb2f38eaa718a82b48604733b88f7e \ No newline at end of file diff --git a/test/fixtures/webhooks/feeds.activity.added/sns_notification.txt b/test/fixtures/webhooks/feeds.activity.added/sns_notification.txt new file mode 100644 index 0000000..96fc9d9 --- /dev/null +++ b/test/fixtures/webhooks/feeds.activity.added/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6pWKqksSFWyUkpLTU0p1ktMLsksyyyp1EtMSUlNUdJRSi5KTSxJTYlPLFGyUjIyMDLTNTDVNbAIMTCwAqMopVpAAAAA///ytZDqQwAAAA==","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/feeds.activity.added/sqs_body.txt b/test/fixtures/webhooks/feeds.activity.added/sqs_body.txt new file mode 100644 index 0000000..967c43a --- /dev/null +++ b/test/fixtures/webhooks/feeds.activity.added/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWKqksSFWyUkpLTU0p1ktMLsksyyyp1EtMSUlNUdJRSi5KTSxJTYlPLFGyUjIyMDLTNTDVNbAIMTCwAqMopVpAAAAA///ytZDqQwAAAA== \ No newline at end of file diff --git a/test/fixtures/webhooks/feeds.activity.added/sqs_body_uncompressed.txt b/test/fixtures/webhooks/feeds.activity.added/sqs_body_uncompressed.txt new file mode 100644 index 0000000..ddec009 --- /dev/null +++ b/test/fixtures/webhooks/feeds.activity.added/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoiZmVlZHMuYWN0aXZpdHkuYWRkZWQiLCJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wOFQwMDowMDowMFoifQ== \ No newline at end of file diff --git a/test/fixtures/webhooks/message.deleted/body.gz b/test/fixtures/webhooks/message.deleted/body.gz new file mode 100644 index 0000000..d0e4077 Binary files /dev/null and b/test/fixtures/webhooks/message.deleted/body.gz differ diff --git a/test/fixtures/webhooks/message.deleted/body.json b/test/fixtures/webhooks/message.deleted/body.json new file mode 100644 index 0000000..b6bebc1 --- /dev/null +++ b/test/fixtures/webhooks/message.deleted/body.json @@ -0,0 +1 @@ +{"type":"message.deleted","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/message.deleted/expected.json b/test/fixtures/webhooks/message.deleted/expected.json new file mode 100644 index 0000000..b7e398f --- /dev/null +++ b/test/fixtures/webhooks/message.deleted/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "message.deleted" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/message.deleted/signature.txt b/test/fixtures/webhooks/message.deleted/signature.txt new file mode 100644 index 0000000..39fbecb --- /dev/null +++ b/test/fixtures/webhooks/message.deleted/signature.txt @@ -0,0 +1 @@ +dc9780ba23cb0741265b1f417e03b18899386ec48cfd4f9692a11ce105ad28c2 \ No newline at end of file diff --git a/test/fixtures/webhooks/message.deleted/sns_notification.txt b/test/fixtures/webhooks/message.deleted/sns_notification.txt new file mode 100644 index 0000000..15159ef --- /dev/null +++ b/test/fixtures/webhooks/message.deleted/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6pWKqksSFWyUspNLS5OTE/VS0nNSS1JTVHSUUouSk0sSU2JTyxRslIyMjAy0zUw1TWwCDEwsAKjKKVaQAAAAP//KViNpz4AAAA=","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/message.deleted/sqs_body.txt b/test/fixtures/webhooks/message.deleted/sqs_body.txt new file mode 100644 index 0000000..0f74777 --- /dev/null +++ b/test/fixtures/webhooks/message.deleted/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWKqksSFWyUspNLS5OTE/VS0nNSS1JTVHSUUouSk0sSU2JTyxRslIyMjAy0zUw1TWwCDEwsAKjKKVaQAAAAP//KViNpz4AAAA= \ No newline at end of file diff --git a/test/fixtures/webhooks/message.deleted/sqs_body_uncompressed.txt b/test/fixtures/webhooks/message.deleted/sqs_body_uncompressed.txt new file mode 100644 index 0000000..45b277e --- /dev/null +++ b/test/fixtures/webhooks/message.deleted/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoibWVzc2FnZS5kZWxldGVkIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDUtMDhUMDA6MDA6MDBaIn0= \ No newline at end of file diff --git a/test/fixtures/webhooks/message.new/body.gz b/test/fixtures/webhooks/message.new/body.gz new file mode 100644 index 0000000..3c472d2 Binary files /dev/null and b/test/fixtures/webhooks/message.new/body.gz differ diff --git a/test/fixtures/webhooks/message.new/body.json b/test/fixtures/webhooks/message.new/body.json new file mode 100644 index 0000000..3c5e004 --- /dev/null +++ b/test/fixtures/webhooks/message.new/body.json @@ -0,0 +1 @@ +{"type":"message.new","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/message.new/expected.json b/test/fixtures/webhooks/message.new/expected.json new file mode 100644 index 0000000..9ecb641 --- /dev/null +++ b/test/fixtures/webhooks/message.new/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "message.new" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/message.new/signature.txt b/test/fixtures/webhooks/message.new/signature.txt new file mode 100644 index 0000000..f463836 --- /dev/null +++ b/test/fixtures/webhooks/message.new/signature.txt @@ -0,0 +1 @@ +9e43985889d9d37c51c4bb92f6b814331edf8aa1b86ed7d3a8189acc93baabc8 \ No newline at end of file diff --git a/test/fixtures/webhooks/message.new/sns_notification.txt b/test/fixtures/webhooks/message.new/sns_notification.txt new file mode 100644 index 0000000..d33ba7a --- /dev/null +++ b/test/fixtures/webhooks/message.new/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6pWKqksSFWyUspNLS5OTE/Vy0stV9JRSi5KTSxJTYlPLFGyUjIyMDLTNTDVNbAIMTCwAqMopVpAAAAA//+PXB06OgAAAA==","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/message.new/sqs_body.txt b/test/fixtures/webhooks/message.new/sqs_body.txt new file mode 100644 index 0000000..9f3042b --- /dev/null +++ b/test/fixtures/webhooks/message.new/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWKqksSFWyUspNLS5OTE/Vy0stV9JRSi5KTSxJTYlPLFGyUjIyMDLTNTDVNbAIMTCwAqMopVpAAAAA//+PXB06OgAAAA== \ No newline at end of file diff --git a/test/fixtures/webhooks/message.new/sqs_body_uncompressed.txt b/test/fixtures/webhooks/message.new/sqs_body_uncompressed.txt new file mode 100644 index 0000000..66f5ff0 --- /dev/null +++ b/test/fixtures/webhooks/message.new/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoibWVzc2FnZS5uZXciLCJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wOFQwMDowMDowMFoifQ== \ No newline at end of file diff --git a/test/fixtures/webhooks/message.updated/body.gz b/test/fixtures/webhooks/message.updated/body.gz new file mode 100644 index 0000000..37adb0a Binary files /dev/null and b/test/fixtures/webhooks/message.updated/body.gz differ diff --git a/test/fixtures/webhooks/message.updated/body.json b/test/fixtures/webhooks/message.updated/body.json new file mode 100644 index 0000000..e48d1fc --- /dev/null +++ b/test/fixtures/webhooks/message.updated/body.json @@ -0,0 +1 @@ +{"type":"message.updated","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/message.updated/expected.json b/test/fixtures/webhooks/message.updated/expected.json new file mode 100644 index 0000000..aaff32c --- /dev/null +++ b/test/fixtures/webhooks/message.updated/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "message.updated" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/message.updated/signature.txt b/test/fixtures/webhooks/message.updated/signature.txt new file mode 100644 index 0000000..123558b --- /dev/null +++ b/test/fixtures/webhooks/message.updated/signature.txt @@ -0,0 +1 @@ +c50590eafba7f62f5bb3c9e1a69c6894527573f8a76111a154d2e647e1f6dc7b \ No newline at end of file diff --git a/test/fixtures/webhooks/message.updated/sns_notification.txt b/test/fixtures/webhooks/message.updated/sns_notification.txt new file mode 100644 index 0000000..29f7821 --- /dev/null +++ b/test/fixtures/webhooks/message.updated/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6pWKqksSFWyUspNLS5OTE/VKy1ISSxJTVHSUUouSgWx4hNLlKyUjAyMzHQNTHUNLEIMDKzAKEqpFhAAAP//QiI2zD4AAAA=","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/message.updated/sqs_body.txt b/test/fixtures/webhooks/message.updated/sqs_body.txt new file mode 100644 index 0000000..0f74f39 --- /dev/null +++ b/test/fixtures/webhooks/message.updated/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWKqksSFWyUspNLS5OTE/VKy1ISSxJTVHSUUouSgWx4hNLlKyUjAyMzHQNTHUNLEIMDKzAKEqpFhAAAP//QiI2zD4AAAA= \ No newline at end of file diff --git a/test/fixtures/webhooks/message.updated/sqs_body_uncompressed.txt b/test/fixtures/webhooks/message.updated/sqs_body_uncompressed.txt new file mode 100644 index 0000000..5b37a3b --- /dev/null +++ b/test/fixtures/webhooks/message.updated/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoibWVzc2FnZS51cGRhdGVkIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDUtMDhUMDA6MDA6MDBaIn0= \ No newline at end of file diff --git a/test/fixtures/webhooks/moderation.flagged/body.gz b/test/fixtures/webhooks/moderation.flagged/body.gz new file mode 100644 index 0000000..7ee9846 Binary files /dev/null and b/test/fixtures/webhooks/moderation.flagged/body.gz differ diff --git a/test/fixtures/webhooks/moderation.flagged/body.json b/test/fixtures/webhooks/moderation.flagged/body.json new file mode 100644 index 0000000..ad79f15 --- /dev/null +++ b/test/fixtures/webhooks/moderation.flagged/body.json @@ -0,0 +1 @@ +{"type":"moderation.flagged","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/moderation.flagged/expected.json b/test/fixtures/webhooks/moderation.flagged/expected.json new file mode 100644 index 0000000..ebbc54f --- /dev/null +++ b/test/fixtures/webhooks/moderation.flagged/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "moderation.flagged" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/moderation.flagged/signature.txt b/test/fixtures/webhooks/moderation.flagged/signature.txt new file mode 100644 index 0000000..1c41c5b --- /dev/null +++ b/test/fixtures/webhooks/moderation.flagged/signature.txt @@ -0,0 +1 @@ +7851b65fe601aba1e37f3bbc6f0c3f115376475301e0d62ac561d941fc009490 \ No newline at end of file diff --git a/test/fixtures/webhooks/moderation.flagged/sns_notification.txt b/test/fixtures/webhooks/moderation.flagged/sns_notification.txt new file mode 100644 index 0000000..587ecde --- /dev/null +++ b/test/fixtures/webhooks/moderation.flagged/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6pWKqksSFWyUsrNT0ktSizJzM/TS8tJTE9PTVHSUUouSk0sSU2JTyxRslIyMjAy0zUw1TWwCDEwsAKjKKVaQAAAAP//YHVqW0EAAAA=","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/moderation.flagged/sqs_body.txt b/test/fixtures/webhooks/moderation.flagged/sqs_body.txt new file mode 100644 index 0000000..6eae82f --- /dev/null +++ b/test/fixtures/webhooks/moderation.flagged/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWKqksSFWyUsrNT0ktSizJzM/TS8tJTE9PTVHSUUouSk0sSU2JTyxRslIyMjAy0zUw1TWwCDEwsAKjKKVaQAAAAP//YHVqW0EAAAA= \ No newline at end of file diff --git a/test/fixtures/webhooks/moderation.flagged/sqs_body_uncompressed.txt b/test/fixtures/webhooks/moderation.flagged/sqs_body_uncompressed.txt new file mode 100644 index 0000000..3b0ffa1 --- /dev/null +++ b/test/fixtures/webhooks/moderation.flagged/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoibW9kZXJhdGlvbi5mbGFnZ2VkIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDUtMDhUMDA6MDA6MDBaIn0= \ No newline at end of file diff --git a/test/fixtures/webhooks/reaction.new/body.gz b/test/fixtures/webhooks/reaction.new/body.gz new file mode 100644 index 0000000..faf0eca Binary files /dev/null and b/test/fixtures/webhooks/reaction.new/body.gz differ diff --git a/test/fixtures/webhooks/reaction.new/body.json b/test/fixtures/webhooks/reaction.new/body.json new file mode 100644 index 0000000..58f8d84 --- /dev/null +++ b/test/fixtures/webhooks/reaction.new/body.json @@ -0,0 +1 @@ +{"type":"reaction.new","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/reaction.new/expected.json b/test/fixtures/webhooks/reaction.new/expected.json new file mode 100644 index 0000000..2eaeb7b --- /dev/null +++ b/test/fixtures/webhooks/reaction.new/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "reaction.new" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/reaction.new/signature.txt b/test/fixtures/webhooks/reaction.new/signature.txt new file mode 100644 index 0000000..0ba7a98 --- /dev/null +++ b/test/fixtures/webhooks/reaction.new/signature.txt @@ -0,0 +1 @@ +a998b37083d4293d28164f49aa2331464517650713814e601f78bd658a5fde7d \ No newline at end of file diff --git a/test/fixtures/webhooks/reaction.new/sns_notification.txt b/test/fixtures/webhooks/reaction.new/sns_notification.txt new file mode 100644 index 0000000..5e0bb65 --- /dev/null +++ b/test/fixtures/webhooks/reaction.new/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6pWKqksSFWyUipKTUwuyczP08tLLVfSUUouSk0sSU2JTyxRslIyMjAy0zUw1TWwCDEwsAKjKKVaQAAAAP//0Iyi3jsAAAA=","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/reaction.new/sqs_body.txt b/test/fixtures/webhooks/reaction.new/sqs_body.txt new file mode 100644 index 0000000..5aedf9c --- /dev/null +++ b/test/fixtures/webhooks/reaction.new/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWKqksSFWyUipKTUwuyczP08tLLVfSUUouSk0sSU2JTyxRslIyMjAy0zUw1TWwCDEwsAKjKKVaQAAAAP//0Iyi3jsAAAA= \ No newline at end of file diff --git a/test/fixtures/webhooks/reaction.new/sqs_body_uncompressed.txt b/test/fixtures/webhooks/reaction.new/sqs_body_uncompressed.txt new file mode 100644 index 0000000..521552b --- /dev/null +++ b/test/fixtures/webhooks/reaction.new/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoicmVhY3Rpb24ubmV3IiwiY3JlYXRlZF9hdCI6IjIwMjYtMDUtMDhUMDA6MDA6MDBaIn0= \ No newline at end of file diff --git a/test/fixtures/webhooks/user.banned/body.gz b/test/fixtures/webhooks/user.banned/body.gz new file mode 100644 index 0000000..7f236bb Binary files /dev/null and b/test/fixtures/webhooks/user.banned/body.gz differ diff --git a/test/fixtures/webhooks/user.banned/body.json b/test/fixtures/webhooks/user.banned/body.json new file mode 100644 index 0000000..0fb8ab5 --- /dev/null +++ b/test/fixtures/webhooks/user.banned/body.json @@ -0,0 +1 @@ +{"type":"user.banned","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/user.banned/expected.json b/test/fixtures/webhooks/user.banned/expected.json new file mode 100644 index 0000000..42aa4c9 --- /dev/null +++ b/test/fixtures/webhooks/user.banned/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "user.banned" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/user.banned/signature.txt b/test/fixtures/webhooks/user.banned/signature.txt new file mode 100644 index 0000000..a88f140 --- /dev/null +++ b/test/fixtures/webhooks/user.banned/signature.txt @@ -0,0 +1 @@ +35504e4551320c86f8166f96df7d193677fd779599e01760ba5653a275970be7 \ No newline at end of file diff --git a/test/fixtures/webhooks/user.banned/sns_notification.txt b/test/fixtures/webhooks/user.banned/sns_notification.txt new file mode 100644 index 0000000..6cecfb6 --- /dev/null +++ b/test/fixtures/webhooks/user.banned/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6pWKqksSFWyUiotTi3SS0rMy0tNUdJRSi5KTSxJTYlPLFGyUjIyMDLTNTDVNbAIMTCwAqMopVpAAAAA//8UUgDfOgAAAA==","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/user.banned/sqs_body.txt b/test/fixtures/webhooks/user.banned/sqs_body.txt new file mode 100644 index 0000000..8d1959c --- /dev/null +++ b/test/fixtures/webhooks/user.banned/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWKqksSFWyUiotTi3SS0rMy0tNUdJRSi5KTSxJTYlPLFGyUjIyMDLTNTDVNbAIMTCwAqMopVpAAAAA//8UUgDfOgAAAA== \ No newline at end of file diff --git a/test/fixtures/webhooks/user.banned/sqs_body_uncompressed.txt b/test/fixtures/webhooks/user.banned/sqs_body_uncompressed.txt new file mode 100644 index 0000000..956dbec --- /dev/null +++ b/test/fixtures/webhooks/user.banned/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoidXNlci5iYW5uZWQiLCJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wOFQwMDowMDowMFoifQ== \ No newline at end of file diff --git a/test/fixtures/webhooks/user.unbanned/body.gz b/test/fixtures/webhooks/user.unbanned/body.gz new file mode 100644 index 0000000..dd65c12 Binary files /dev/null and b/test/fixtures/webhooks/user.unbanned/body.gz differ diff --git a/test/fixtures/webhooks/user.unbanned/body.json b/test/fixtures/webhooks/user.unbanned/body.json new file mode 100644 index 0000000..fcb0281 --- /dev/null +++ b/test/fixtures/webhooks/user.unbanned/body.json @@ -0,0 +1 @@ +{"type":"user.unbanned","created_at":"2026-05-08T00:00:00Z"} \ No newline at end of file diff --git a/test/fixtures/webhooks/user.unbanned/expected.json b/test/fixtures/webhooks/user.unbanned/expected.json new file mode 100644 index 0000000..f60a330 --- /dev/null +++ b/test/fixtures/webhooks/user.unbanned/expected.json @@ -0,0 +1,6 @@ +{ + "fields": { + "created_at": "2026-05-08T00:00:00Z" + }, + "type": "user.unbanned" +} \ No newline at end of file diff --git a/test/fixtures/webhooks/user.unbanned/signature.txt b/test/fixtures/webhooks/user.unbanned/signature.txt new file mode 100644 index 0000000..ec75779 --- /dev/null +++ b/test/fixtures/webhooks/user.unbanned/signature.txt @@ -0,0 +1 @@ +04b33a93f8ea848fbca682c487d871120b726493d3c25083d694ee7a59fb1cfa \ No newline at end of file diff --git a/test/fixtures/webhooks/user.unbanned/sns_notification.txt b/test/fixtures/webhooks/user.unbanned/sns_notification.txt new file mode 100644 index 0000000..7797255 --- /dev/null +++ b/test/fixtures/webhooks/user.unbanned/sns_notification.txt @@ -0,0 +1 @@ +{"Message":"H4sIAAAAAAAA/6pWKqksSFWyUiotTi3SK81LSszLS01R0lFKLkpNLElNiU8sUbJSMjIwMtM1MNU1sAgxMLACoyilWkAAAAD//5U5ygE8AAAA","MessageId":"00000000-0000-0000-0000-000000000000","Timestamp":"2026-05-08T00:00:00.000Z","TopicArn":"arn:aws:sns:us-east-1:000000000000:fixture-topic","Type":"Notification"} \ No newline at end of file diff --git a/test/fixtures/webhooks/user.unbanned/sqs_body.txt b/test/fixtures/webhooks/user.unbanned/sqs_body.txt new file mode 100644 index 0000000..d099a17 --- /dev/null +++ b/test/fixtures/webhooks/user.unbanned/sqs_body.txt @@ -0,0 +1 @@ +H4sIAAAAAAAA/6pWKqksSFWyUiotTi3SK81LSszLS01R0lFKLkpNLElNiU8sUbJSMjIwMtM1MNU1sAgxMLACoyilWkAAAAD//5U5ygE8AAAA \ No newline at end of file diff --git a/test/fixtures/webhooks/user.unbanned/sqs_body_uncompressed.txt b/test/fixtures/webhooks/user.unbanned/sqs_body_uncompressed.txt new file mode 100644 index 0000000..284de09 --- /dev/null +++ b/test/fixtures/webhooks/user.unbanned/sqs_body_uncompressed.txt @@ -0,0 +1 @@ +eyJ0eXBlIjoidXNlci51bmJhbm5lZCIsImNyZWF0ZWRfYXQiOiIyMDI2LTA1LTA4VDAwOjAwOjAwWiJ9 \ No newline at end of file diff --git a/test/webhook_test.rb b/test/webhook_test.rb deleted file mode 100644 index ddd851b..0000000 --- a/test/webhook_test.rb +++ /dev/null @@ -1,906 +0,0 @@ -# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. - -require 'minitest/autorun' -require 'openssl' -require 'json' -require_relative '../lib/getstream_ruby/generated/webhook' - -class WebhookTest < Minitest::Test - SECRET = 'test-webhook-secret' - BODY = '{"type":"test.event"}' - - def compute_signature(body, secret) - OpenSSL::HMAC.hexdigest('SHA256', secret, body) - end - - def test_verify_signature_valid - signature = compute_signature(BODY, SECRET) - assert StreamChat::Webhook.verify_signature(BODY, signature, SECRET) - end - - def test_verify_signature_wrong_signature - refute StreamChat::Webhook.verify_signature(BODY, 'invalidsignature', SECRET) - end - - def test_verify_signature_tampered_body - signature = compute_signature(BODY, SECRET) - refute StreamChat::Webhook.verify_signature('{"type":"tampered"}', signature, SECRET) - end - - def test_verify_signature_wrong_secret - signature = compute_signature(BODY, SECRET) - refute StreamChat::Webhook.verify_signature(BODY, signature, 'wrong-secret') - end - - def test_verify_signature_empty_signature - refute StreamChat::Webhook.verify_signature(BODY, '', SECRET) - end - - def test_get_event_type_from_string - assert_equal 'message.new', StreamChat::Webhook.get_event_type('{"type":"message.new"}') - end - - def test_get_event_type_from_hash - assert_equal 'message.new', StreamChat::Webhook.get_event_type({ 'type' => 'message.new' }) - end - - def test_get_event_type_missing_field - assert_nil StreamChat::Webhook.get_event_type('{"foo":"bar"}') - end - - def test_get_event_type_empty_object - assert_nil StreamChat::Webhook.get_event_type('{}') - end - def test_parse_ - event = StreamChat::Webhook.parse_webhook_event('{"type":"*"}') - assert_equal 'StreamChat::CustomEvent', event.class.name - end - - def test_parse_appeal_accepted - event = StreamChat::Webhook.parse_webhook_event('{"type":"appeal.accepted"}') - assert_equal 'StreamChat::AppealAcceptedEvent', event.class.name - end - - def test_parse_appeal_created - event = StreamChat::Webhook.parse_webhook_event('{"type":"appeal.created"}') - assert_equal 'StreamChat::AppealCreatedEvent', event.class.name - end - - def test_parse_appeal_rejected - event = StreamChat::Webhook.parse_webhook_event('{"type":"appeal.rejected"}') - assert_equal 'StreamChat::AppealRejectedEvent', event.class.name - end - - def test_parse_call_accepted - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.accepted"}') - assert_equal 'StreamChat::CallAcceptedEvent', event.class.name - end - - def test_parse_call_blocked_user - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.blocked_user"}') - assert_equal 'StreamChat::BlockedUserEvent', event.class.name - end - - def test_parse_call_closed_caption - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.closed_caption"}') - assert_equal 'StreamChat::ClosedCaptionEvent', event.class.name - end - - def test_parse_call_closed_captions_failed - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.closed_captions_failed"}') - assert_equal 'StreamChat::CallClosedCaptionsFailedEvent', event.class.name - end - - def test_parse_call_closed_captions_started - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.closed_captions_started"}') - assert_equal 'StreamChat::CallClosedCaptionsStartedEvent', event.class.name - end - - def test_parse_call_closed_captions_stopped - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.closed_captions_stopped"}') - assert_equal 'StreamChat::CallClosedCaptionsStoppedEvent', event.class.name - end - - def test_parse_call_created - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.created"}') - assert_equal 'StreamChat::CallCreatedEvent', event.class.name - end - - def test_parse_call_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.deleted"}') - assert_equal 'StreamChat::CallDeletedEvent', event.class.name - end - - def test_parse_call_dtmf - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.dtmf"}') - assert_equal 'StreamChat::CallDTMFEvent', event.class.name - end - - def test_parse_call_ended - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.ended"}') - assert_equal 'StreamChat::CallEndedEvent', event.class.name - end - - def test_parse_call_frame_recording_failed - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.frame_recording_failed"}') - assert_equal 'StreamChat::CallFrameRecordingFailedEvent', event.class.name - end - - def test_parse_call_frame_recording_ready - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.frame_recording_ready"}') - assert_equal 'StreamChat::CallFrameRecordingFrameReadyEvent', event.class.name - end - - def test_parse_call_frame_recording_started - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.frame_recording_started"}') - assert_equal 'StreamChat::CallFrameRecordingStartedEvent', event.class.name - end - - def test_parse_call_frame_recording_stopped - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.frame_recording_stopped"}') - assert_equal 'StreamChat::CallFrameRecordingStoppedEvent', event.class.name - end - - def test_parse_call_hls_broadcasting_failed - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.hls_broadcasting_failed"}') - assert_equal 'StreamChat::CallHLSBroadcastingFailedEvent', event.class.name - end - - def test_parse_call_hls_broadcasting_started - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.hls_broadcasting_started"}') - assert_equal 'StreamChat::CallHLSBroadcastingStartedEvent', event.class.name - end - - def test_parse_call_hls_broadcasting_stopped - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.hls_broadcasting_stopped"}') - assert_equal 'StreamChat::CallHLSBroadcastingStoppedEvent', event.class.name - end - - def test_parse_call_kicked_user - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.kicked_user"}') - assert_equal 'StreamChat::KickedUserEvent', event.class.name - end - - def test_parse_call_live_started - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.live_started"}') - assert_equal 'StreamChat::CallLiveStartedEvent', event.class.name - end - - def test_parse_call_member_added - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.member_added"}') - assert_equal 'StreamChat::CallMemberAddedEvent', event.class.name - end - - def test_parse_call_member_removed - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.member_removed"}') - assert_equal 'StreamChat::CallMemberRemovedEvent', event.class.name - end - - def test_parse_call_member_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.member_updated"}') - assert_equal 'StreamChat::CallMemberUpdatedEvent', event.class.name - end - - def test_parse_call_member_updated_permission - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.member_updated_permission"}') - assert_equal 'StreamChat::CallMemberUpdatedPermissionEvent', event.class.name - end - - def test_parse_call_missed - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.missed"}') - assert_equal 'StreamChat::CallMissedEvent', event.class.name - end - - def test_parse_call_moderation_blur - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.moderation_blur"}') - assert_equal 'StreamChat::CallModerationBlurEvent', event.class.name - end - - def test_parse_call_moderation_warning - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.moderation_warning"}') - assert_equal 'StreamChat::CallModerationWarningEvent', event.class.name - end - - def test_parse_call_notification - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.notification"}') - assert_equal 'StreamChat::CallNotificationEvent', event.class.name - end - - def test_parse_call_permission_request - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.permission_request"}') - assert_equal 'StreamChat::PermissionRequestEvent', event.class.name - end - - def test_parse_call_permissions_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.permissions_updated"}') - assert_equal 'StreamChat::UpdatedCallPermissionsEvent', event.class.name - end - - def test_parse_call_reaction_new - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.reaction_new"}') - assert_equal 'StreamChat::CallReactionEvent', event.class.name - end - - def test_parse_call_recording_failed - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.recording_failed"}') - assert_equal 'StreamChat::CallRecordingFailedEvent', event.class.name - end - - def test_parse_call_recording_ready - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.recording_ready"}') - assert_equal 'StreamChat::CallRecordingReadyEvent', event.class.name - end - - def test_parse_call_recording_started - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.recording_started"}') - assert_equal 'StreamChat::CallRecordingStartedEvent', event.class.name - end - - def test_parse_call_recording_stopped - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.recording_stopped"}') - assert_equal 'StreamChat::CallRecordingStoppedEvent', event.class.name - end - - def test_parse_call_rejected - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.rejected"}') - assert_equal 'StreamChat::CallRejectedEvent', event.class.name - end - - def test_parse_call_ring - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.ring"}') - assert_equal 'StreamChat::CallRingEvent', event.class.name - end - - def test_parse_call_rtmp_broadcast_failed - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.rtmp_broadcast_failed"}') - assert_equal 'StreamChat::CallRtmpBroadcastFailedEvent', event.class.name - end - - def test_parse_call_rtmp_broadcast_started - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.rtmp_broadcast_started"}') - assert_equal 'StreamChat::CallRtmpBroadcastStartedEvent', event.class.name - end - - def test_parse_call_rtmp_broadcast_stopped - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.rtmp_broadcast_stopped"}') - assert_equal 'StreamChat::CallRtmpBroadcastStoppedEvent', event.class.name - end - - def test_parse_call_session_ended - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.session_ended"}') - assert_equal 'StreamChat::CallSessionEndedEvent', event.class.name - end - - def test_parse_call_session_participant_count_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.session_participant_count_updated"}') - assert_equal 'StreamChat::CallSessionParticipantCountsUpdatedEvent', event.class.name - end - - def test_parse_call_session_participant_joined - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.session_participant_joined"}') - assert_equal 'StreamChat::CallSessionParticipantJoinedEvent', event.class.name - end - - def test_parse_call_session_participant_left - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.session_participant_left"}') - assert_equal 'StreamChat::CallSessionParticipantLeftEvent', event.class.name - end - - def test_parse_call_session_started - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.session_started"}') - assert_equal 'StreamChat::CallSessionStartedEvent', event.class.name - end - - def test_parse_call_stats_report_ready - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.stats_report_ready"}') - assert_equal 'StreamChat::CallStatsReportReadyEvent', event.class.name - end - - def test_parse_call_transcription_failed - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.transcription_failed"}') - assert_equal 'StreamChat::CallTranscriptionFailedEvent', event.class.name - end - - def test_parse_call_transcription_ready - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.transcription_ready"}') - assert_equal 'StreamChat::CallTranscriptionReadyEvent', event.class.name - end - - def test_parse_call_transcription_started - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.transcription_started"}') - assert_equal 'StreamChat::CallTranscriptionStartedEvent', event.class.name - end - - def test_parse_call_transcription_stopped - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.transcription_stopped"}') - assert_equal 'StreamChat::CallTranscriptionStoppedEvent', event.class.name - end - - def test_parse_call_unblocked_user - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.unblocked_user"}') - assert_equal 'StreamChat::UnblockedUserEvent', event.class.name - end - - def test_parse_call_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.updated"}') - assert_equal 'StreamChat::CallUpdatedEvent', event.class.name - end - - def test_parse_call_user_feedback_submitted - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.user_feedback_submitted"}') - assert_equal 'StreamChat::CallUserFeedbackSubmittedEvent', event.class.name - end - - def test_parse_call_user_muted - event = StreamChat::Webhook.parse_webhook_event('{"type":"call.user_muted"}') - assert_equal 'StreamChat::CallUserMutedEvent', event.class.name - end - - def test_parse_campaign_completed - event = StreamChat::Webhook.parse_webhook_event('{"type":"campaign.completed"}') - assert_equal 'StreamChat::CampaignCompletedEvent', event.class.name - end - - def test_parse_campaign_started - event = StreamChat::Webhook.parse_webhook_event('{"type":"campaign.started"}') - assert_equal 'StreamChat::CampaignStartedEvent', event.class.name - end - - def test_parse_channel_created - event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.created"}') - assert_equal 'StreamChat::ChannelCreatedEvent', event.class.name - end - - def test_parse_channel_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.deleted"}') - assert_equal 'StreamChat::ChannelDeletedEvent', event.class.name - end - - def test_parse_channel_frozen - event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.frozen"}') - assert_equal 'StreamChat::ChannelFrozenEvent', event.class.name - end - - def test_parse_channel_hidden - event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.hidden"}') - assert_equal 'StreamChat::ChannelHiddenEvent', event.class.name - end - - def test_parse_channel_max_streak_changed - event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.max_streak_changed"}') - assert_equal 'StreamChat::MaxStreakChangedEvent', event.class.name - end - - def test_parse_channel_muted - event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.muted"}') - assert_equal 'StreamChat::ChannelMutedEvent', event.class.name - end - - def test_parse_channel_truncated - event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.truncated"}') - assert_equal 'StreamChat::ChannelTruncatedEvent', event.class.name - end - - def test_parse_channel_unfrozen - event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.unfrozen"}') - assert_equal 'StreamChat::ChannelUnFrozenEvent', event.class.name - end - - def test_parse_channel_unmuted - event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.unmuted"}') - assert_equal 'StreamChat::ChannelUnmutedEvent', event.class.name - end - - def test_parse_channel_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.updated"}') - assert_equal 'StreamChat::ChannelUpdatedEvent', event.class.name - end - - def test_parse_channel_visible - event = StreamChat::Webhook.parse_webhook_event('{"type":"channel.visible"}') - assert_equal 'StreamChat::ChannelVisibleEvent', event.class.name - end - - def test_parse_channel_batch_update_completed - event = StreamChat::Webhook.parse_webhook_event('{"type":"channel_batch_update.completed"}') - assert_equal 'StreamChat::ChannelBatchCompletedEvent', event.class.name - end - - def test_parse_channel_batch_update_started - event = StreamChat::Webhook.parse_webhook_event('{"type":"channel_batch_update.started"}') - assert_equal 'StreamChat::ChannelBatchStartedEvent', event.class.name - end - - def test_parse_custom - event = StreamChat::Webhook.parse_webhook_event('{"type":"custom"}') - assert_equal 'StreamChat::CustomVideoEvent', event.class.name - end - - def test_parse_export_bulk_image_moderation_error - event = StreamChat::Webhook.parse_webhook_event('{"type":"export.bulk_image_moderation.error"}') - assert_equal 'StreamChat::AsyncExportErrorEvent', event.class.name - end - - def test_parse_export_bulk_image_moderation_success - event = StreamChat::Webhook.parse_webhook_event('{"type":"export.bulk_image_moderation.success"}') - assert_equal 'StreamChat::AsyncBulkImageModerationEvent', event.class.name - end - - def test_parse_export_channels_error - event = StreamChat::Webhook.parse_webhook_event('{"type":"export.channels.error"}') - assert_equal 'StreamChat::AsyncExportErrorEvent', event.class.name - end - - def test_parse_export_channels_success - event = StreamChat::Webhook.parse_webhook_event('{"type":"export.channels.success"}') - assert_equal 'StreamChat::AsyncExportChannelsEvent', event.class.name - end - - def test_parse_export_moderation_logs_error - event = StreamChat::Webhook.parse_webhook_event('{"type":"export.moderation_logs.error"}') - assert_equal 'StreamChat::AsyncExportErrorEvent', event.class.name - end - - def test_parse_export_moderation_logs_success - event = StreamChat::Webhook.parse_webhook_event('{"type":"export.moderation_logs.success"}') - assert_equal 'StreamChat::AsyncExportModerationLogsEvent', event.class.name - end - - def test_parse_export_users_error - event = StreamChat::Webhook.parse_webhook_event('{"type":"export.users.error"}') - assert_equal 'StreamChat::AsyncExportErrorEvent', event.class.name - end - - def test_parse_export_users_success - event = StreamChat::Webhook.parse_webhook_event('{"type":"export.users.success"}') - assert_equal 'StreamChat::AsyncExportUsersEvent', event.class.name - end - - def test_parse_feeds_activity_added - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.added"}') - assert_equal 'StreamChat::ActivityAddedEvent', event.class.name - end - - def test_parse_feeds_activity_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.deleted"}') - assert_equal 'StreamChat::ActivityDeletedEvent', event.class.name - end - - def test_parse_feeds_activity_feedback - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.feedback"}') - assert_equal 'StreamChat::ActivityFeedbackEvent', event.class.name - end - - def test_parse_feeds_activity_marked - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.marked"}') - assert_equal 'StreamChat::ActivityMarkEvent', event.class.name - end - - def test_parse_feeds_activity_pinned - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.pinned"}') - assert_equal 'StreamChat::ActivityPinnedEvent', event.class.name - end - - def test_parse_feeds_activity_reaction_added - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.reaction.added"}') - assert_equal 'StreamChat::ActivityReactionAddedEvent', event.class.name - end - - def test_parse_feeds_activity_reaction_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.reaction.deleted"}') - assert_equal 'StreamChat::ActivityReactionDeletedEvent', event.class.name - end - - def test_parse_feeds_activity_reaction_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.reaction.updated"}') - assert_equal 'StreamChat::ActivityReactionUpdatedEvent', event.class.name - end - - def test_parse_feeds_activity_removed_from_feed - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.removed_from_feed"}') - assert_equal 'StreamChat::ActivityRemovedFromFeedEvent', event.class.name - end - - def test_parse_feeds_activity_restored - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.restored"}') - assert_equal 'StreamChat::ActivityRestoredEvent', event.class.name - end - - def test_parse_feeds_activity_unpinned - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.unpinned"}') - assert_equal 'StreamChat::ActivityUnpinnedEvent', event.class.name - end - - def test_parse_feeds_activity_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.activity.updated"}') - assert_equal 'StreamChat::ActivityUpdatedEvent', event.class.name - end - - def test_parse_feeds_bookmark_added - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.bookmark.added"}') - assert_equal 'StreamChat::BookmarkAddedEvent', event.class.name - end - - def test_parse_feeds_bookmark_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.bookmark.deleted"}') - assert_equal 'StreamChat::BookmarkDeletedEvent', event.class.name - end - - def test_parse_feeds_bookmark_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.bookmark.updated"}') - assert_equal 'StreamChat::BookmarkUpdatedEvent', event.class.name - end - - def test_parse_feeds_bookmark_folder_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.bookmark_folder.deleted"}') - assert_equal 'StreamChat::BookmarkFolderDeletedEvent', event.class.name - end - - def test_parse_feeds_bookmark_folder_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.bookmark_folder.updated"}') - assert_equal 'StreamChat::BookmarkFolderUpdatedEvent', event.class.name - end - - def test_parse_feeds_comment_added - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.comment.added"}') - assert_equal 'StreamChat::CommentAddedEvent', event.class.name - end - - def test_parse_feeds_comment_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.comment.deleted"}') - assert_equal 'StreamChat::CommentDeletedEvent', event.class.name - end - - def test_parse_feeds_comment_reaction_added - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.comment.reaction.added"}') - assert_equal 'StreamChat::CommentReactionAddedEvent', event.class.name - end - - def test_parse_feeds_comment_reaction_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.comment.reaction.deleted"}') - assert_equal 'StreamChat::CommentReactionDeletedEvent', event.class.name - end - - def test_parse_feeds_comment_reaction_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.comment.reaction.updated"}') - assert_equal 'StreamChat::CommentReactionUpdatedEvent', event.class.name - end - - def test_parse_feeds_comment_restored - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.comment.restored"}') - assert_equal 'StreamChat::CommentRestoredEvent', event.class.name - end - - def test_parse_feeds_comment_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.comment.updated"}') - assert_equal 'StreamChat::CommentUpdatedEvent', event.class.name - end - - def test_parse_feeds_feed_created - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed.created"}') - assert_equal 'StreamChat::FeedCreatedEvent', event.class.name - end - - def test_parse_feeds_feed_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed.deleted"}') - assert_equal 'StreamChat::FeedDeletedEvent', event.class.name - end - - def test_parse_feeds_feed_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed.updated"}') - assert_equal 'StreamChat::FeedUpdatedEvent', event.class.name - end - - def test_parse_feeds_feed_group_changed - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed_group.changed"}') - assert_equal 'StreamChat::FeedGroupChangedEvent', event.class.name - end - - def test_parse_feeds_feed_group_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed_group.deleted"}') - assert_equal 'StreamChat::FeedGroupDeletedEvent', event.class.name - end - - def test_parse_feeds_feed_group_restored - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed_group.restored"}') - assert_equal 'StreamChat::FeedGroupRestoredEvent', event.class.name - end - - def test_parse_feeds_feed_member_added - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed_member.added"}') - assert_equal 'StreamChat::FeedMemberAddedEvent', event.class.name - end - - def test_parse_feeds_feed_member_removed - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed_member.removed"}') - assert_equal 'StreamChat::FeedMemberRemovedEvent', event.class.name - end - - def test_parse_feeds_feed_member_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed_member.updated"}') - assert_equal 'StreamChat::FeedMemberUpdatedEvent', event.class.name - end - - def test_parse_feeds_follow_created - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.follow.created"}') - assert_equal 'StreamChat::FollowCreatedEvent', event.class.name - end - - def test_parse_feeds_follow_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.follow.deleted"}') - assert_equal 'StreamChat::FollowDeletedEvent', event.class.name - end - - def test_parse_feeds_follow_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.follow.updated"}') - assert_equal 'StreamChat::FollowUpdatedEvent', event.class.name - end - - def test_parse_feeds_notification_feed_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.notification_feed.updated"}') - assert_equal 'StreamChat::NotificationFeedUpdatedEvent', event.class.name - end - - def test_parse_feeds_stories_feed_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.stories_feed.updated"}') - assert_equal 'StreamChat::StoriesFeedUpdatedEvent', event.class.name - end - - def test_parse_flag_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"flag.updated"}') - assert_equal 'StreamChat::FlagUpdatedEvent', event.class.name - end - - def test_parse_ingress_error - event = StreamChat::Webhook.parse_webhook_event('{"type":"ingress.error"}') - assert_equal 'StreamChat::IngressErrorEvent', event.class.name - end - - def test_parse_ingress_started - event = StreamChat::Webhook.parse_webhook_event('{"type":"ingress.started"}') - assert_equal 'StreamChat::IngressStartedEvent', event.class.name - end - - def test_parse_ingress_stopped - event = StreamChat::Webhook.parse_webhook_event('{"type":"ingress.stopped"}') - assert_equal 'StreamChat::IngressStoppedEvent', event.class.name - end - - def test_parse_member_added - event = StreamChat::Webhook.parse_webhook_event('{"type":"member.added"}') - assert_equal 'StreamChat::MemberAddedEvent', event.class.name - end - - def test_parse_member_removed - event = StreamChat::Webhook.parse_webhook_event('{"type":"member.removed"}') - assert_equal 'StreamChat::MemberRemovedEvent', event.class.name - end - - def test_parse_member_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"member.updated"}') - assert_equal 'StreamChat::MemberUpdatedEvent', event.class.name - end - - def test_parse_message_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"message.deleted"}') - assert_equal 'StreamChat::MessageDeletedEvent', event.class.name - end - - def test_parse_message_flagged - event = StreamChat::Webhook.parse_webhook_event('{"type":"message.flagged"}') - assert_equal 'StreamChat::MessageFlaggedEvent', event.class.name - end - - def test_parse_message_new - event = StreamChat::Webhook.parse_webhook_event('{"type":"message.new"}') - assert_equal 'StreamChat::MessageNewEvent', event.class.name - end - - def test_parse_message_pending - event = StreamChat::Webhook.parse_webhook_event('{"type":"message.pending"}') - assert_equal 'StreamChat::PendingMessageEvent', event.class.name - end - - def test_parse_message_read - event = StreamChat::Webhook.parse_webhook_event('{"type":"message.read"}') - assert_equal 'StreamChat::MessageReadEvent', event.class.name - end - - def test_parse_message_unblocked - event = StreamChat::Webhook.parse_webhook_event('{"type":"message.unblocked"}') - assert_equal 'StreamChat::MessageUnblockedEvent', event.class.name - end - - def test_parse_message_undeleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"message.undeleted"}') - assert_equal 'StreamChat::MessageUndeletedEvent', event.class.name - end - - def test_parse_message_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"message.updated"}') - assert_equal 'StreamChat::MessageUpdatedEvent', event.class.name - end - - def test_parse_moderation_custom_action - event = StreamChat::Webhook.parse_webhook_event('{"type":"moderation.custom_action"}') - assert_equal 'StreamChat::ModerationCustomActionEvent', event.class.name - end - - def test_parse_moderation_flagged - event = StreamChat::Webhook.parse_webhook_event('{"type":"moderation.flagged"}') - assert_equal 'StreamChat::ModerationFlaggedEvent', event.class.name - end - - def test_parse_moderation_mark_reviewed - event = StreamChat::Webhook.parse_webhook_event('{"type":"moderation.mark_reviewed"}') - assert_equal 'StreamChat::ModerationMarkReviewedEvent', event.class.name - end - - def test_parse_moderation_check_completed - event = StreamChat::Webhook.parse_webhook_event('{"type":"moderation_check.completed"}') - assert_equal 'StreamChat::ModerationCheckCompletedEvent', event.class.name - end - - def test_parse_moderation_rule_triggered - event = StreamChat::Webhook.parse_webhook_event('{"type":"moderation_rule.triggered"}') - assert_equal 'StreamChat::ModerationRulesTriggeredEvent', event.class.name - end - - def test_parse_notification_mark_unread - event = StreamChat::Webhook.parse_webhook_event('{"type":"notification.mark_unread"}') - assert_equal 'StreamChat::NotificationMarkUnreadEvent', event.class.name - end - - def test_parse_notification_reminder_due - event = StreamChat::Webhook.parse_webhook_event('{"type":"notification.reminder_due"}') - assert_equal 'StreamChat::ReminderNotificationEvent', event.class.name - end - - def test_parse_notification_thread_message_new - event = StreamChat::Webhook.parse_webhook_event('{"type":"notification.thread_message_new"}') - assert_equal 'StreamChat::NotificationThreadMessageNewEvent', event.class.name - end - - def test_parse_reaction_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"reaction.deleted"}') - assert_equal 'StreamChat::ReactionDeletedEvent', event.class.name - end - - def test_parse_reaction_new - event = StreamChat::Webhook.parse_webhook_event('{"type":"reaction.new"}') - assert_equal 'StreamChat::ReactionNewEvent', event.class.name - end - - def test_parse_reaction_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"reaction.updated"}') - assert_equal 'StreamChat::ReactionUpdatedEvent', event.class.name - end - - def test_parse_reminder_created - event = StreamChat::Webhook.parse_webhook_event('{"type":"reminder.created"}') - assert_equal 'StreamChat::ReminderCreatedEvent', event.class.name - end - - def test_parse_reminder_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"reminder.deleted"}') - assert_equal 'StreamChat::ReminderDeletedEvent', event.class.name - end - - def test_parse_reminder_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"reminder.updated"}') - assert_equal 'StreamChat::ReminderUpdatedEvent', event.class.name - end - - def test_parse_review_queue_item_new - event = StreamChat::Webhook.parse_webhook_event('{"type":"review_queue_item.new"}') - assert_equal 'StreamChat::ReviewQueueItemNewEvent', event.class.name - end - - def test_parse_review_queue_item_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"review_queue_item.updated"}') - assert_equal 'StreamChat::ReviewQueueItemUpdatedEvent', event.class.name - end - - def test_parse_thread_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"thread.updated"}') - assert_equal 'StreamChat::ThreadUpdatedEvent', event.class.name - end - - def test_parse_user_banned - event = StreamChat::Webhook.parse_webhook_event('{"type":"user.banned"}') - assert_equal 'StreamChat::UserBannedEvent', event.class.name - end - - def test_parse_user_deactivated - event = StreamChat::Webhook.parse_webhook_event('{"type":"user.deactivated"}') - assert_equal 'StreamChat::UserDeactivatedEvent', event.class.name - end - - def test_parse_user_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"user.deleted"}') - assert_equal 'StreamChat::UserDeletedEvent', event.class.name - end - - def test_parse_user_flagged - event = StreamChat::Webhook.parse_webhook_event('{"type":"user.flagged"}') - assert_equal 'StreamChat::UserFlaggedEvent', event.class.name - end - - def test_parse_user_messages_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"user.messages.deleted"}') - assert_equal 'StreamChat::UserMessagesDeletedEvent', event.class.name - end - - def test_parse_user_muted - event = StreamChat::Webhook.parse_webhook_event('{"type":"user.muted"}') - assert_equal 'StreamChat::UserMutedEvent', event.class.name - end - - def test_parse_user_reactivated - event = StreamChat::Webhook.parse_webhook_event('{"type":"user.reactivated"}') - assert_equal 'StreamChat::UserReactivatedEvent', event.class.name - end - - def test_parse_user_unbanned - event = StreamChat::Webhook.parse_webhook_event('{"type":"user.unbanned"}') - assert_equal 'StreamChat::UserUnbannedEvent', event.class.name - end - - def test_parse_user_unmuted - event = StreamChat::Webhook.parse_webhook_event('{"type":"user.unmuted"}') - assert_equal 'StreamChat::UserUnmutedEvent', event.class.name - end - - def test_parse_user_unread_message_reminder - event = StreamChat::Webhook.parse_webhook_event('{"type":"user.unread_message_reminder"}') - assert_equal 'StreamChat::UserUnreadReminderEvent', event.class.name - end - - def test_parse_user_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"user.updated"}') - assert_equal 'StreamChat::UserUpdatedEvent', event.class.name - end - - def test_parse_user_group_created - event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.created"}') - assert_equal 'StreamChat::UserGroupCreatedEvent', event.class.name - end - - def test_parse_user_group_deleted - event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.deleted"}') - assert_equal 'StreamChat::UserGroupDeletedEvent', event.class.name - end - - def test_parse_user_group_member_added - event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.member_added"}') - assert_equal 'StreamChat::UserGroupMemberAddedEvent', event.class.name - end - - def test_parse_user_group_member_removed - event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.member_removed"}') - assert_equal 'StreamChat::UserGroupMemberRemovedEvent', event.class.name - end - - def test_parse_user_group_updated - event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.updated"}') - assert_equal 'StreamChat::UserGroupUpdatedEvent', event.class.name - end - - def test_parse_webhook_event_unknown_type - assert_raises(ArgumentError) do - StreamChat::Webhook.parse_webhook_event('{"type":"unknown.event"}') - end - end - - def test_parse_webhook_event_missing_type - assert_raises(ArgumentError) do - StreamChat::Webhook.parse_webhook_event('{"foo":"bar"}') - end - end - - def test_parse_webhook_event_invalid_json - assert_raises(ArgumentError) do - StreamChat::Webhook.parse_webhook_event('not json') - end - end -end