From d60b2e397afba5510e9846fa001a0ac404191df6 Mon Sep 17 00:00:00 2001 From: Igor Dobryn Date: Tue, 5 May 2026 09:42:19 +0300 Subject: [PATCH] Add webhook endpoints --- README.md | 1 + examples/webhooks_api.rb | 42 +++++ lib/mailtrap.rb | 1 + lib/mailtrap/webhook.rb | 30 ++++ lib/mailtrap/webhooks_api.rb | 87 ++++++++++ .../maps_response_data_to_Webhook_object.yml | 73 ++++++++ .../raises_a_Mailtrap_Error.yml | 72 ++++++++ .../_delete/returns_deleted_Webhook.yml | 73 ++++++++ .../raises_not_found_error.yml | 71 ++++++++ .../maps_response_data_to_Webhook_object.yml | 73 ++++++++ .../raises_not_found_error.yml | 71 ++++++++ .../maps_response_data_to_Webhook_objects.yml | 74 ++++++++ .../raises_authorization_error.yml | 73 ++++++++ .../maps_response_data_to_Webhook_object.yml | 73 ++++++++ .../raises_not_found_error.yml | 71 ++++++++ spec/mailtrap/webhook_spec.rb | 35 ++++ spec/mailtrap/webhooks_api_spec.rb | 163 ++++++++++++++++++ spec/spec_helper.rb | 1 + 18 files changed, 1084 insertions(+) create mode 100644 examples/webhooks_api.rb create mode 100644 lib/mailtrap/webhook.rb create mode 100644 lib/mailtrap/webhooks_api.rb create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_create/maps_response_data_to_Webhook_object.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_create/when_API_returns_an_error/raises_a_Mailtrap_Error.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_delete/returns_deleted_Webhook.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_delete/when_webhook_does_not_exist/raises_not_found_error.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_get/maps_response_data_to_Webhook_object.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_get/when_webhook_does_not_exist/raises_not_found_error.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_list/maps_response_data_to_Webhook_objects.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_list/when_api_key_is_incorrect/raises_authorization_error.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_update/maps_response_data_to_Webhook_object.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_update/when_webhook_does_not_exist/raises_not_found_error.yml create mode 100644 spec/mailtrap/webhook_spec.rb create mode 100644 spec/mailtrap/webhooks_api_spec.rb diff --git a/README.md b/README.md index 383f1da..0a01349 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,7 @@ Email API: - Sending Domains API – [`sending_domains_api.rb`](examples/sending_domains_api.rb) - Sending Stats API – [`stats_api.rb`](examples/stats_api.rb) - Email Logs API – [`email_logs_api.rb`](examples/email_logs_api.rb) +- Webhooks API – [`webhooks_api.rb`](examples/webhooks_api.rb) Email Sandbox (Testing): diff --git a/examples/webhooks_api.rb b/examples/webhooks_api.rb new file mode 100644 index 0000000..88956f8 --- /dev/null +++ b/examples/webhooks_api.rb @@ -0,0 +1,42 @@ +require 'mailtrap' + +account_id = 3229 +client = Mailtrap::Client.new(api_key: 'your-api-key') +webhooks_api = Mailtrap::WebhooksAPI.new(account_id, client) + +# Create an `email_sending` webhook +webhook = webhooks_api.create( + url: 'https://example.com/mailtrap/webhooks', + webhook_type: 'email_sending', + payload_format: 'json', + sending_stream: 'transactional', + event_types: %w[delivery bounce], + domain_id: 435 +) +# => # + +# Create an `audit_log` webhook +webhooks_api.create( + url: 'https://example.com/mailtrap/audit-log', + webhook_type: 'audit_log' +) + +# List all webhooks +webhooks_api.list +# => [#, #] + +# Get a single webhook +webhooks_api.get(webhook.id) +# => # + +# Update webhook +webhooks_api.update( + webhook.id, + active: false, + event_types: %w[delivery bounce unsubscribe] +) +# => # + +# Delete webhook +webhooks_api.delete(webhook.id) +# => # diff --git a/lib/mailtrap.rb b/lib/mailtrap.rb index f5e52a8..7a0f680 100644 --- a/lib/mailtrap.rb +++ b/lib/mailtrap.rb @@ -26,6 +26,7 @@ require_relative 'mailtrap/sandbox_messages_api' require_relative 'mailtrap/sandbox_attachments_api' require_relative 'mailtrap/stats_api' +require_relative 'mailtrap/webhooks_api' module Mailtrap # @!macro api_errors diff --git a/lib/mailtrap/webhook.rb b/lib/mailtrap/webhook.rb new file mode 100644 index 0000000..66e2589 --- /dev/null +++ b/lib/mailtrap/webhook.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Mailtrap + # Data Transfer Object for Webhook + # @see https://api-docs.mailtrap.io/docs/mailtrap-api-docs/8d553c46c2d33-webhooks + # @attr_reader id [Integer] The webhook ID + # @attr_reader url [String] The URL that will receive webhook payloads + # @attr_reader active [Boolean] Whether the webhook is active + # @attr_reader webhook_type [String] The type of webhook (`email_sending` or `audit_log`) + # @attr_reader payload_format [String] The webhook payload format (`json` or `jsonlines`) + # @attr_reader sending_stream [String, nil] The sending stream (`transactional` or `bulk`). + # Applicable only for `email_sending` webhooks. + # @attr_reader domain_id [Integer, nil] The sending domain ID the webhook is scoped to, + # or nil for all domains. Applicable only for `email_sending` webhooks. + # @attr_reader event_types [Array] The event types the webhook is subscribed to. + # Applicable only for `email_sending` webhooks. + # @attr_reader signing_secret [String, nil] HMAC SHA-256 signing secret. Returned only on creation. + Webhook = Struct.new( + :id, + :url, + :active, + :webhook_type, + :payload_format, + :sending_stream, + :domain_id, + :event_types, + :signing_secret, + keyword_init: true + ) +end diff --git a/lib/mailtrap/webhooks_api.rb b/lib/mailtrap/webhooks_api.rb new file mode 100644 index 0000000..30dd320 --- /dev/null +++ b/lib/mailtrap/webhooks_api.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require_relative 'base_api' +require_relative 'webhook' + +module Mailtrap + class WebhooksAPI + include BaseAPI + + self.supported_options = %i[url webhook_type active payload_format sending_stream event_types domain_id] + + self.response_class = Webhook + + # Lists all webhooks for the account + # @return [Array] Array of webhooks + # @!macro api_errors + def list + response = client.get(base_path) + response[:data].map { |item| build_entity(item, response_class) } + end + + # Retrieves a specific webhook + # @param webhook_id [Integer] The webhook ID + # @return [Webhook] Webhook object + # @!macro api_errors + def get(webhook_id) + base_get(webhook_id) + end + + # Creates a new webhook + # @param [Hash] options The parameters to create + # @option options [String] :url The URL that will receive webhook payloads + # @option options [String] :webhook_type The type of webhook (`email_sending` or `audit_log`) + # @option options [Boolean] :active Whether the webhook is active. Defaults to true. + # @option options [String] :payload_format Payload format (`json` or `jsonlines`). Defaults to `json`. + # @option options [String] :sending_stream Sending stream (`transactional` or `bulk`). + # Required for `email_sending` webhook type. + # @option options [Array] :event_types Event types to subscribe to. + # Required for `email_sending` webhook type. + # @option options [Integer] :domain_id Sending domain ID to scope the webhook to. + # Applicable only for `email_sending` webhooks. + # @return [Webhook] Created webhook (includes `signing_secret`) + # @!macro api_errors + # @raise [ArgumentError] If invalid options are provided + def create(options) + base_create(options) + end + + # Updates an existing webhook + # @param webhook_id [Integer] The webhook ID + # @param [Hash] options The parameters to update + # @option options [String] :url The URL that will receive webhook payloads + # @option options [Boolean] :active Whether the webhook is active + # @option options [String] :payload_format Payload format (`json` or `jsonlines`) + # @option options [Array] :event_types Event types to subscribe to. + # Applicable only for `email_sending` webhooks. + # @return [Webhook] Updated webhook + # @!macro api_errors + # @raise [ArgumentError] If invalid options are provided + def update(webhook_id, options) + base_update(webhook_id, options, %i[url active payload_format event_types]) + end + + # Deletes a webhook + # @param webhook_id [Integer] The webhook ID + # @return [Webhook] Deleted webhook + # @!macro api_errors + def delete(webhook_id) + response = client.delete("#{base_path}/#{webhook_id}") + handle_response(response) if response + end + + private + + def base_path + "/api/accounts/#{account_id}/webhooks" + end + + def wrap_request(options) + { webhook: options } + end + + def handle_response(response) + build_entity(response[:data], response_class) + end + end +end diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_create/maps_response_data_to_Webhook_object.yml b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_create/maps_response_data_to_Webhook_object.yml new file mode 100644 index 0000000..72d851b --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_create/maps_response_data_to_Webhook_object.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: post + uri: https://mailtrap.io/api/accounts/1111111/webhooks + body: + encoding: UTF-8 + string: '{"webhook":{"url":"https://example.com/mailtrap/webhooks","webhook_type":"email_sending","payload_format":"json","sending_stream":"transactional","event_types":["delivery","bounce"]}}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 05 May 2026 06:28:51 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + Etag: + - W/"64f2974d0941249a1cff76a9a4195937" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.047439' + Strict-Transport-Security: + - max-age=2592000; includeSubDomains; preload + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"data":{"signing_secret":"my-signing-secret","id":3080,"url":"https://example.com/mailtrap/webhooks","active":true,"webhook_type":"email_sending","payload_format":"json","sending_stream":"transactional","domain_id":null,"event_types":["delivery","bounce"]}}' + recorded_at: Tue, 05 May 2026 06:28:51 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_create/when_API_returns_an_error/raises_a_Mailtrap_Error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_create/when_API_returns_an_error/raises_a_Mailtrap_Error.yml new file mode 100644 index 0000000..e0981f7 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_create/when_API_returns_an_error/raises_a_Mailtrap_Error.yml @@ -0,0 +1,72 @@ +--- +http_interactions: +- request: + method: post + uri: https://mailtrap.io/api/accounts/1111111/webhooks + body: + encoding: UTF-8 + string: '{"webhook":{"url":"","webhook_type":"email_sending"}}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 422 + message: Unprocessable Entity + headers: + Date: + - Tue, 05 May 2026 06:28:52 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '119' + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '148' + Cache-Control: + - no-cache + X-Runtime: + - '0.017551' + Strict-Transport-Security: + - max-age=2592000; includeSubDomains; preload + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"errors":{"url":["can''t be blank","is invalid"],"event_types":["can''t + be blank"],"sending_stream":["can''t be blank"]}}' + recorded_at: Tue, 05 May 2026 06:28:52 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_delete/returns_deleted_Webhook.yml b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_delete/returns_deleted_Webhook.yml new file mode 100644 index 0000000..c01e6c7 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_delete/returns_deleted_Webhook.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: delete + uri: https://mailtrap.io/api/accounts/1111111/webhooks/3080 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 05 May 2026 06:38:53 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + Etag: + - W/"e051559f5622f3f16fe3fb0861693ff5" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.031690' + Strict-Transport-Security: + - max-age=2592000; includeSubDomains; preload + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"data":{"id":3080,"url":"https://example.com/mailtrap/webhooks","active":false,"webhook_type":"email_sending","payload_format":"json","sending_stream":"transactional","domain_id":null,"event_types":["delivery","bounce","unsubscribe"]}}' + recorded_at: Tue, 05 May 2026 06:38:53 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_delete/when_webhook_does_not_exist/raises_not_found_error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_delete/when_webhook_does_not_exist/raises_not_found_error.yml new file mode 100644 index 0000000..dbf20f6 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_delete/when_webhook_does_not_exist/raises_not_found_error.yml @@ -0,0 +1,71 @@ +--- +http_interactions: +- request: + method: delete + uri: https://mailtrap.io/api/accounts/1111111/webhooks/-1 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 404 + message: Not Found + headers: + Date: + - Tue, 05 May 2026 06:38:53 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '21' + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '148' + Cache-Control: + - no-cache + X-Runtime: + - '0.015498' + Strict-Transport-Security: + - max-age=2592000; includeSubDomains; preload + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"error":"Not Found"}' + recorded_at: Tue, 05 May 2026 06:38:53 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_get/maps_response_data_to_Webhook_object.yml b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_get/maps_response_data_to_Webhook_object.yml new file mode 100644 index 0000000..e7ef102 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_get/maps_response_data_to_Webhook_object.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/webhooks/3080 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 05 May 2026 06:35:34 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + Etag: + - W/"e051559f5622f3f16fe3fb0861693ff5" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.017956' + Strict-Transport-Security: + - max-age=2592000; includeSubDomains; preload + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"data":{"id":3080,"url":"https://example.com/mailtrap/webhooks","active":false,"webhook_type":"email_sending","payload_format":"json","sending_stream":"transactional","domain_id":null,"event_types":["delivery","bounce","unsubscribe"]}}' + recorded_at: Tue, 05 May 2026 06:35:34 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_get/when_webhook_does_not_exist/raises_not_found_error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_get/when_webhook_does_not_exist/raises_not_found_error.yml new file mode 100644 index 0000000..5ffc182 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_get/when_webhook_does_not_exist/raises_not_found_error.yml @@ -0,0 +1,71 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/webhooks/-1 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 404 + message: Not Found + headers: + Date: + - Tue, 05 May 2026 06:35:34 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '21' + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '148' + Cache-Control: + - no-cache + X-Runtime: + - '0.017357' + Strict-Transport-Security: + - max-age=2592000; includeSubDomains; preload + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"error":"Not Found"}' + recorded_at: Tue, 05 May 2026 06:35:34 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_list/maps_response_data_to_Webhook_objects.yml b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_list/maps_response_data_to_Webhook_objects.yml new file mode 100644 index 0000000..f338ade --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_list/maps_response_data_to_Webhook_objects.yml @@ -0,0 +1,74 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/webhooks + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 05 May 2026 06:36:50 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Server: + - cloudflare + Vary: + - Accept + - Accept-Encoding + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + Etag: + - W/"59f972c44785ea45f43dedb61f1065e4" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.033635' + Strict-Transport-Security: + - max-age=2592000; includeSubDomains; preload + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"data":[{"id":3080,"url":"https://example.com/mailtrap/webhooks","active":false,"webhook_type":"email_sending","payload_format":"json","sending_stream":"transactional","domain_id":null,"event_types":["delivery","bounce","unsubscribe"]}]}' + recorded_at: Tue, 05 May 2026 06:36:50 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_list/when_api_key_is_incorrect/raises_authorization_error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_list/when_api_key_is_incorrect/raises_authorization_error.yml new file mode 100644 index 0000000..b8ca5c4 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_list/when_api_key_is_incorrect/raises_authorization_error.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/webhooks + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 401 + message: Unauthorized + headers: + Date: + - Tue, 05 May 2026 06:36:50 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '31' + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Www-Authenticate: + - Token realm="Application" + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + Cache-Control: + - no-cache + X-Runtime: + - '0.009574' + Strict-Transport-Security: + - max-age=2592000; includeSubDomains; preload + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"error":"Incorrect API token"}' + recorded_at: Tue, 05 May 2026 06:36:50 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_update/maps_response_data_to_Webhook_object.yml b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_update/maps_response_data_to_Webhook_object.yml new file mode 100644 index 0000000..610dde5 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_update/maps_response_data_to_Webhook_object.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: patch + uri: https://mailtrap.io/api/accounts/1111111/webhooks/3080 + body: + encoding: UTF-8 + string: '{"webhook":{"active":false,"event_types":["delivery","bounce","unsubscribe"]}}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 05 May 2026 06:30:57 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + Etag: + - W/"e051559f5622f3f16fe3fb0861693ff5" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.046662' + Strict-Transport-Security: + - max-age=2592000; includeSubDomains; preload + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"data":{"id":3080,"url":"https://example.com/mailtrap/webhooks","active":false,"webhook_type":"email_sending","payload_format":"json","sending_stream":"transactional","domain_id":null,"event_types":["delivery","bounce","unsubscribe"]}}' + recorded_at: Tue, 05 May 2026 06:30:57 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_update/when_webhook_does_not_exist/raises_not_found_error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_update/when_webhook_does_not_exist/raises_not_found_error.yml new file mode 100644 index 0000000..81779d6 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_WebhooksAPI/_update/when_webhook_does_not_exist/raises_not_found_error.yml @@ -0,0 +1,71 @@ +--- +http_interactions: +- request: + method: patch + uri: https://mailtrap.io/api/accounts/1111111/webhooks/-1 + body: + encoding: UTF-8 + string: '{"webhook":{"active":false,"event_types":["delivery","bounce","unsubscribe"]}}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 404 + message: Not Found + headers: + Date: + - Tue, 05 May 2026 06:30:57 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '21' + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '148' + Cache-Control: + - no-cache + X-Runtime: + - '0.012566' + Strict-Transport-Security: + - max-age=2592000; includeSubDomains; preload + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"error":"Not Found"}' + recorded_at: Tue, 05 May 2026 06:30:57 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/mailtrap/webhook_spec.rb b/spec/mailtrap/webhook_spec.rb new file mode 100644 index 0000000..1ea709a --- /dev/null +++ b/spec/mailtrap/webhook_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::Webhook do + describe '#initialize' do + subject(:webhook) { described_class.new(attributes) } + + let(:attributes) do + { + id: 1, + url: 'https://example.com/mailtrap/webhooks', + active: true, + webhook_type: 'email_sending', + payload_format: 'json', + sending_stream: 'transactional', + domain_id: 435, + event_types: %w[delivery bounce], + signing_secret: 'a1b2c3d4e5f6' + } + end + + it 'creates a webhook with all attributes' do + expect(webhook).to have_attributes( + id: 1, + url: 'https://example.com/mailtrap/webhooks', + active: true, + webhook_type: 'email_sending', + payload_format: 'json', + sending_stream: 'transactional', + domain_id: 435, + event_types: %w[delivery bounce], + signing_secret: 'a1b2c3d4e5f6' + ) + end + end +end diff --git a/spec/mailtrap/webhooks_api_spec.rb b/spec/mailtrap/webhooks_api_spec.rb new file mode 100644 index 0000000..aa067b3 --- /dev/null +++ b/spec/mailtrap/webhooks_api_spec.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::WebhooksAPI, :vcr do + subject(:webhooks_api) { described_class.new(account_id, client) } + + let(:account_id) { ENV.fetch('MAILTRAP_ACCOUNT_ID', 1_111_111) } + let(:client) { Mailtrap::Client.new(api_key: ENV.fetch('MAILTRAP_API_KEY', 'local-api-key')) } + + describe '#list' do + subject(:list) { webhooks_api.list } + + it 'maps response data to Webhook objects' do + expect(list).to all(be_a(Mailtrap::Webhook)) + end + + context 'when api key is incorrect' do + let(:client) { Mailtrap::Client.new(api_key: 'incorrect-api-key') } + + it 'raises authorization error' do + expect { list }.to raise_error do |error| + expect(error).to be_a(Mailtrap::AuthorizationError) + expect(error.message).to include('Incorrect API token') + end + end + end + end + + describe '#get' do + subject(:get) { webhooks_api.get(webhook_id) } + + let(:webhook_id) { 3080 } + + it 'maps response data to Webhook object' do + expect(get).to be_a(Mailtrap::Webhook) + expect(get).to have_attributes( + id: webhook_id, + webhook_type: 'email_sending' + ) + end + + context 'when webhook does not exist' do + let(:webhook_id) { -1 } + + it 'raises not found error' do + expect { get }.to raise_error do |error| + expect(error).to be_a(Mailtrap::Error) + expect(error.message).to include('Not Found') + end + end + end + end + + describe '#create' do + subject(:create) { webhooks_api.create(**request) } + + let(:request) do + { + url: 'https://example.com/mailtrap/webhooks', + webhook_type: 'email_sending', + payload_format: 'json', + sending_stream: 'transactional', + event_types: %w[delivery bounce] + } + end + + it 'maps response data to Webhook object' do + expect(create).to be_a(Mailtrap::Webhook) + expect(create).to have_attributes( + url: 'https://example.com/mailtrap/webhooks', + webhook_type: 'email_sending', + payload_format: 'json', + sending_stream: 'transactional', + event_types: %w[delivery bounce] + ) + expect(create.signing_secret).not_to be_nil + end + + context 'when invalid options are provided' do + let(:request) { { unknown_option: true } } + + it 'raises ArgumentError' do + expect { create }.to raise_error(ArgumentError, /invalid options are given/) + end + end + + context 'when API returns an error' do + let(:request) do + { + url: '', + webhook_type: 'email_sending' + } + end + + it 'raises a Mailtrap::Error' do + expect { create }.to raise_error do |error| + expect(error).to be_a(Mailtrap::Error) + end + end + end + end + + describe '#update' do + subject(:update) { webhooks_api.update(webhook_id, **request) } + + let(:webhook_id) { 3080 } + let(:request) do + { + active: false, + event_types: %w[delivery bounce unsubscribe] + } + end + + it 'maps response data to Webhook object' do + expect(update).to be_a(Mailtrap::Webhook) + expect(update).to have_attributes( + id: webhook_id, + active: false, + event_types: %w[delivery bounce unsubscribe] + ) + end + + context 'when invalid options are provided' do + let(:request) { { webhook_type: 'audit_log' } } + + it 'raises ArgumentError' do + expect { update }.to raise_error(ArgumentError, /invalid options are given/) + end + end + + context 'when webhook does not exist' do + let(:webhook_id) { -1 } + + it 'raises not found error' do + expect { update }.to raise_error do |error| + expect(error).to be_a(Mailtrap::Error) + expect(error.message).to include('Not Found') + end + end + end + end + + describe '#delete' do + subject(:delete) { webhooks_api.delete(webhook_id) } + + let(:webhook_id) { 3080 } + + it 'returns deleted Webhook' do + expect(delete).to be_a(Mailtrap::Webhook) + expect(delete.id).to eq(webhook_id) + end + + context 'when webhook does not exist' do + let(:webhook_id) { -1 } + + it 'raises not found error' do + expect { delete }.to raise_error do |error| + expect(error).to be_a(Mailtrap::Error) + expect(error.message).to include('Not Found') + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 678327d..abc42c9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -25,6 +25,7 @@ interaction.request.uri.gsub!(%r{/organizations/\d+/}, '/organizations/2222222/') interaction.response.body.gsub!(/"account_id":\d+/, '"account_id": 1111111') interaction.response.body.gsub!(/"organization_id":\d+/, '"account_id": 2222222') + interaction.response.body.gsub!(/"signing_secret":"[^"]*"/, '"signing_secret":"my-signing-secret"') interaction.response.body.gsub!(/"username":"[^"]*"/, '"username": "railsware"') interaction.response.body.gsub!(/"password":"[^"]*"/, '"password": "xxxxxxxx"') interaction.response.body.gsub!(/"email":"[^"]*"/, '"email": "welcome@rw.com"')