From e2464eeb3c31ae068fa4bd7222ccdc0345021fe6 Mon Sep 17 00:00:00 2001 From: Marcin Klocek Date: Wed, 26 Aug 2026 15:57:57 +0200 Subject: [PATCH 1/4] Drop redundant comments and attr_readers BaseAPI already declares attr_reader :account_id, :client, so five API classes were redeclaring what they inherit. The inbox_id, sandbox_message_id and organization_id readers stay -- those are not in BaseAPI. The inbound classes also carried a comment restating that their initializer takes no account_id, which the signature already shows. --- lib/mailtrap/account_accesses_api.rb | 2 -- lib/mailtrap/accounts_api.rb | 2 -- lib/mailtrap/email_campaigns_api.rb | 2 -- lib/mailtrap/inbound_folders_api.rb | 1 - lib/mailtrap/inbound_inboxes_api.rb | 1 - lib/mailtrap/inbound_messages_api.rb | 1 - lib/mailtrap/inbound_threads_api.rb | 1 - lib/mailtrap/sandbox_attachments_api.rb | 2 +- lib/mailtrap/sandbox_messages_api.rb | 2 +- 9 files changed, 2 insertions(+), 12 deletions(-) diff --git a/lib/mailtrap/account_accesses_api.rb b/lib/mailtrap/account_accesses_api.rb index f30aba16..b6c596ae 100644 --- a/lib/mailtrap/account_accesses_api.rb +++ b/lib/mailtrap/account_accesses_api.rb @@ -7,8 +7,6 @@ module Mailtrap class AccountAccessesAPI include BaseAPI - attr_reader :account_id - self.response_class = AccountAccess # Retrieves a list of account accesses with optional filtering by domain, inbox, or project IDs diff --git a/lib/mailtrap/accounts_api.rb b/lib/mailtrap/accounts_api.rb index 07a48d59..883dac03 100644 --- a/lib/mailtrap/accounts_api.rb +++ b/lib/mailtrap/accounts_api.rb @@ -9,8 +9,6 @@ class AccountsAPI self.response_class = Account - attr_reader :client - # @param client [Mailtrap::Client] The client instance # @raise [ArgumentError] If account_id is nil def initialize(client = Mailtrap::Client.new) diff --git a/lib/mailtrap/email_campaigns_api.rb b/lib/mailtrap/email_campaigns_api.rb index c299d355..fd1d6e0c 100644 --- a/lib/mailtrap/email_campaigns_api.rb +++ b/lib/mailtrap/email_campaigns_api.rb @@ -22,8 +22,6 @@ class EmailCampaignsAPI self.response_class = EmailCampaign - attr_reader :client - # @param client [Mailtrap::Client] The client instance def initialize(client = Mailtrap::Client.new) @client = client diff --git a/lib/mailtrap/inbound_folders_api.rb b/lib/mailtrap/inbound_folders_api.rb index 2889ccb5..d4f76bc8 100644 --- a/lib/mailtrap/inbound_folders_api.rb +++ b/lib/mailtrap/inbound_folders_api.rb @@ -10,7 +10,6 @@ class InboundFoldersAPI self.supported_options = %i[name] self.response_class = InboundFolder - # Inbound is scoped to the token's account, so no account_id is required. # @param client [Mailtrap::Client] The client instance def initialize(client = Mailtrap::Client.new) @client = client diff --git a/lib/mailtrap/inbound_inboxes_api.rb b/lib/mailtrap/inbound_inboxes_api.rb index a76ef9c3..414040a5 100644 --- a/lib/mailtrap/inbound_inboxes_api.rb +++ b/lib/mailtrap/inbound_inboxes_api.rb @@ -12,7 +12,6 @@ class InboundInboxesAPI CREATE_OPTIONS = %i[name domain_id].freeze UPDATE_OPTIONS = %i[name].freeze - # Inbound is scoped to the token's account, so no account_id is required. # @param client [Mailtrap::Client] The client instance def initialize(client = Mailtrap::Client.new) @client = client diff --git a/lib/mailtrap/inbound_messages_api.rb b/lib/mailtrap/inbound_messages_api.rb index 7c3774f3..7fb93283 100644 --- a/lib/mailtrap/inbound_messages_api.rb +++ b/lib/mailtrap/inbound_messages_api.rb @@ -14,7 +14,6 @@ class InboundMessagesAPI SEND_OPTIONS = %i[from to cc bcc reply_to text html category attachments headers custom_variables].freeze - # Inbound is scoped to the token's account, so no account_id is required. # @param client [Mailtrap::Client] The client instance def initialize(client = Mailtrap::Client.new) @client = client diff --git a/lib/mailtrap/inbound_threads_api.rb b/lib/mailtrap/inbound_threads_api.rb index 92ab8909..25aa25eb 100644 --- a/lib/mailtrap/inbound_threads_api.rb +++ b/lib/mailtrap/inbound_threads_api.rb @@ -12,7 +12,6 @@ class InboundThreadsAPI self.response_class = InboundThread - # Inbound is scoped to the token's account, so no account_id is required. # @param client [Mailtrap::Client] The client instance def initialize(client = Mailtrap::Client.new) @client = client diff --git a/lib/mailtrap/sandbox_attachments_api.rb b/lib/mailtrap/sandbox_attachments_api.rb index 1e8301c1..befe3ed8 100644 --- a/lib/mailtrap/sandbox_attachments_api.rb +++ b/lib/mailtrap/sandbox_attachments_api.rb @@ -7,7 +7,7 @@ module Mailtrap class SandboxAttachmentsAPI include BaseAPI - attr_reader :account_id, :inbox_id, :sandbox_message_id, :client + attr_reader :inbox_id, :sandbox_message_id self.response_class = SandboxAttachment diff --git a/lib/mailtrap/sandbox_messages_api.rb b/lib/mailtrap/sandbox_messages_api.rb index 210619f5..d0ba3740 100644 --- a/lib/mailtrap/sandbox_messages_api.rb +++ b/lib/mailtrap/sandbox_messages_api.rb @@ -7,7 +7,7 @@ module Mailtrap class SandboxMessagesAPI include BaseAPI - attr_reader :account_id, :inbox_id, :client + attr_reader :inbox_id self.supported_options = %i[is_read] From 32f57d0ec4729086abccbf013787217cbf4baf29 Mon Sep 17 00:00:00 2001 From: Marcin Klocek Date: Wed, 26 Aug 2026 17:04:42 +0200 Subject: [PATCH 2/4] Add tracking opt-outs and create-suppression endpoints Fills the gap against the OpenAPI spec: POST on suppressions plus the tracking opt-outs list/create/delete. Both endpoints take a flat body rather than a wrapped one, and both create responses come back inside a data envelope, so neither can go through base_create. Tracking opt-outs get their own class following EmailCampaignsAPI, since the spec path is /api/tracking_opt_outs and the account comes from the API token. Its list returns a TrackingOptOutsListResponse rather than a bare array, because the endpoint is cursor-paginated and wraps the page in data with a last_id -- unlike the suppressions list, which is a flat array and still uses base_list. --- README.md | 2 + examples/suppressions_api.rb | 13 ++- examples/tracking_opt_outs_api.rb | 46 ++++++++ lib/mailtrap.rb | 1 + lib/mailtrap/suppressions_api.rb | 17 +++ lib/mailtrap/tracking_opt_out.rb | 17 +++ lib/mailtrap/tracking_opt_outs_api.rb | 70 ++++++++++++ .../tracking_opt_outs_list_response.rb | 13 +++ ...response_data_to_TrackingOptOut_object.yml | 73 +++++++++++++ .../raises_a_Mailtrap_Error.yml | 71 ++++++++++++ .../returns_the_deleted_tracking_opt-out.yml | 73 +++++++++++++ .../raises_a_Mailtrap_Error.yml | 71 ++++++++++++ ...esponse_data_to_TrackingOptOut_objects.yml | 73 +++++++++++++ .../raises_authorization_error.yml | 73 +++++++++++++ ...returns_the_matching_tracking_opt-outs.yml | 73 +++++++++++++ spec/mailtrap/suppressions_api_spec.rb | 66 ++++++++++++ spec/mailtrap/tracking_opt_outs_api_spec.rb | 101 ++++++++++++++++++ 17 files changed, 851 insertions(+), 2 deletions(-) create mode 100644 examples/tracking_opt_outs_api.rb create mode 100644 lib/mailtrap/tracking_opt_out.rb create mode 100644 lib/mailtrap/tracking_opt_outs_api.rb create mode 100644 lib/mailtrap/tracking_opt_outs_list_response.rb create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_create/maps_response_data_to_TrackingOptOut_object.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_create/when_email_is_invalid/raises_a_Mailtrap_Error.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_delete/returns_the_deleted_tracking_opt-out.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_delete/when_tracking_opt-out_does_not_exist/raises_a_Mailtrap_Error.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_list/maps_response_data_to_TrackingOptOut_objects.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_list/when_api_key_is_incorrect/raises_authorization_error.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_list/with_filters/returns_the_matching_tracking_opt-outs.yml create mode 100644 spec/mailtrap/tracking_opt_outs_api_spec.rb diff --git a/README.md b/README.md index 85d016fa..fef56467 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,8 @@ Email API: - 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) +- Suppressions API – [`suppressions_api.rb`](examples/suppressions_api.rb) +- Tracking Opt-outs API – [`tracking_opt_outs_api.rb`](examples/tracking_opt_outs_api.rb) Email Sandbox (Testing): diff --git a/examples/suppressions_api.rb b/examples/suppressions_api.rb index a09dffbf..e15e4762 100644 --- a/examples/suppressions_api.rb +++ b/examples/suppressions_api.rb @@ -34,7 +34,16 @@ # ] # Get suppressions for the email -list = suppressions.list(email: 'recipient@example.com') +suppressions.list(email: 'recipient@example.com') + +# Add an email to the suppression list. +# `type` is optional and defaults to 'manual import'. +suppression = suppressions.create( + email: 'recipient@example.com', + domain_id: 12_345, + sending_stream: 'transactional' +) +# => # # Delete a suppression -suppressions.delete(list.first.id) +suppressions.delete(suppression.id) diff --git a/examples/tracking_opt_outs_api.rb b/examples/tracking_opt_outs_api.rb new file mode 100644 index 00000000..71166847 --- /dev/null +++ b/examples/tracking_opt_outs_api.rb @@ -0,0 +1,46 @@ +require 'mailtrap' + +client = Mailtrap::Client.new(api_key: 'your-api-key') +tracking_opt_outs = Mailtrap::TrackingOptOutsAPI.new(client) + +# Set your API credentials as an environment variable +# export MAILTRAP_API_KEY='your-api-key' +# +# tracking_opt_outs = Mailtrap::TrackingOptOutsAPI.new + +# Get tracking opt-outs +response = tracking_opt_outs.list +# => +# # +# ], +# last_id=nil> + +response.data.each { |opt_out| puts opt_out.email } + +# Filter by email and creation time +tracking_opt_outs.list( + email: 'tracked@example.com', + start_time: '2025-01-01T00:00:00Z', + end_time: '2025-12-31T23:59:59Z' +) + +# Page through large lists — pass the previous response's `last_id` until it is nil +page = tracking_opt_outs.list +while page.last_id + page = tracking_opt_outs.list(last_id: page.last_id) + puts page.data.size +end + +# Opt an email out of open and click tracking for a sending domain +opt_out = tracking_opt_outs.create(email: 'tracked@example.com', domain_id: 12_345) +# => # + +# Remove an email from the tracking opt-out list. Returns the deleted record. +tracking_opt_outs.delete(opt_out.id) +# => # diff --git a/lib/mailtrap.rb b/lib/mailtrap.rb index 01aa2ba2..44dca60c 100644 --- a/lib/mailtrap.rb +++ b/lib/mailtrap.rb @@ -18,6 +18,7 @@ require_relative 'mailtrap/contact_exports_api' require_relative 'mailtrap/contact_events_api' require_relative 'mailtrap/suppressions_api' +require_relative 'mailtrap/tracking_opt_outs_api' require_relative 'mailtrap/email_campaigns_api' require_relative 'mailtrap/sending_domains_api' require_relative 'mailtrap/company_info_api' diff --git a/lib/mailtrap/suppressions_api.rb b/lib/mailtrap/suppressions_api.rb index 93f60f8c..02db044e 100644 --- a/lib/mailtrap/suppressions_api.rb +++ b/lib/mailtrap/suppressions_api.rb @@ -7,6 +7,8 @@ module Mailtrap class SuppressionsAPI include BaseAPI + self.supported_options = %i[email domain_id sending_stream type].freeze + self.response_class = Suppression # Lists all suppressions for the account @@ -20,6 +22,21 @@ def list(email: nil) base_list(query_params) end + # Adds an email address to the account's suppression list + # @param [Hash] options The suppression attributes + # @option options [String] :email Email address to suppress + # @option options [Integer] :domain_id ID of the domain to suppress this email for + # @option options [String] :sending_stream The sending stream to suppress for: "transactional" or "bulk" + # @option options [String] :type Reason for the suppression, defaults to "manual import" when omitted + # @return [Suppression] Created suppression + # @!macro api_errors + # @raise [ArgumentError] If invalid options are provided + def create(options) + validate_options!(options, supported_options) + response = client.post(base_path, options) + build_entity(response[:data], Suppression) + end + # Deletes a suppression # @param suppression_id [String] The suppression UUID # @return nil diff --git a/lib/mailtrap/tracking_opt_out.rb b/lib/mailtrap/tracking_opt_out.rb new file mode 100644 index 00000000..c3710155 --- /dev/null +++ b/lib/mailtrap/tracking_opt_out.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Mailtrap + # Data Transfer Object for Tracking Opt-out + # @see https://docs.mailtrap.io/developers/management/tracking-opt-outs + # @attr_reader id [String] The tracking opt-out UUID + # @attr_reader email [String] The email address opted out of tracking + # @attr_reader created_at [String] The creation timestamp + # @attr_reader domain_name [String, nil] Sending domain the tracking opt-out applies to + TrackingOptOut = Struct.new( + :id, + :email, + :created_at, + :domain_name, + keyword_init: true + ) +end diff --git a/lib/mailtrap/tracking_opt_outs_api.rb b/lib/mailtrap/tracking_opt_outs_api.rb new file mode 100644 index 00000000..4f3238ba --- /dev/null +++ b/lib/mailtrap/tracking_opt_outs_api.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require_relative 'base_api' +require_relative 'tracking_opt_out' +require_relative 'tracking_opt_outs_list_response' + +module Mailtrap + class TrackingOptOutsAPI + include BaseAPI + + self.supported_options = %i[email domain_id].freeze + + self.response_class = TrackingOptOut + + # @param client [Mailtrap::Client] The client instance + def initialize(client = Mailtrap::Client.new) + @client = client + end + + # Lists email addresses that have opted out of open and click tracking + # @param email [String, nil] Filter by exact email address (case-insensitive) + # @param start_time [String, nil] Only opt-outs created at or after this ISO 8601 timestamp + # @param end_time [String, nil] Only opt-outs created at or before this ISO 8601 timestamp + # @param last_id [String, nil] Cursor from the previous response's +last_id+ for the next page + # @return [TrackingOptOutsListResponse] The page of tracking opt-outs and the next cursor + # @!macro api_errors + def list(email: nil, start_time: nil, end_time: nil, last_id: nil) + query_params = {} + query_params[:email] = email unless email.nil? + query_params[:start_time] = start_time unless start_time.nil? + query_params[:end_time] = end_time unless end_time.nil? + query_params[:last_id] = last_id unless last_id.nil? + + response = client.get(base_path, query_params) + + TrackingOptOutsListResponse.new( + data: Array(response[:data]).map { |item| build_entity(item, TrackingOptOut) }, + last_id: response[:last_id] + ) + end + + # Adds an email address to the tracking opt-out list for a sending domain + # @param [Hash] options The tracking opt-out attributes + # @option options [String] :email Email address to opt out of tracking + # @option options [Integer] :domain_id ID of the sending domain the opt-out applies to + # @return [TrackingOptOut] Created tracking opt-out + # @!macro api_errors + # @raise [ArgumentError] If invalid options are provided + def create(options) + validate_options!(options, supported_options) + response = client.post(base_path, options) + build_entity(response[:data], TrackingOptOut) + end + + # Removes an email address from the tracking opt-out list + # @param tracking_opt_out_id [String] The tracking opt-out UUID + # @return [TrackingOptOut] The deleted tracking opt-out + # @!macro api_errors + def delete(tracking_opt_out_id) + response = client.delete("#{base_path}/#{tracking_opt_out_id}") + build_entity(response, TrackingOptOut) + end + + private + + def base_path + '/api/tracking_opt_outs' + end + end +end diff --git a/lib/mailtrap/tracking_opt_outs_list_response.rb b/lib/mailtrap/tracking_opt_outs_list_response.rb new file mode 100644 index 00000000..6d884792 --- /dev/null +++ b/lib/mailtrap/tracking_opt_outs_list_response.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Mailtrap + # Data Transfer Object for a paginated list of tracking opt-outs + # @see https://docs.mailtrap.io/developers/management/tracking-opt-outs + # @attr_reader data [Array] The tracking opt-outs on this page + # @attr_reader last_id [String, nil] Cursor for the next page, or nil if this is the last page + TrackingOptOutsListResponse = Struct.new( + :data, + :last_id, + keyword_init: true + ) +end diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_create/maps_response_data_to_TrackingOptOut_object.yml b/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_create/maps_response_data_to_TrackingOptOut_object.yml new file mode 100644 index 00000000..60471b19 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_create/maps_response_data_to_TrackingOptOut_object.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: post + uri: https://mailtrap.io/api/tracking_opt_outs + body: + encoding: UTF-8 + string: '{"email":"tracked@example.com","domain_id":12345}' + 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: 201 + message: Created + headers: + Date: + - Wed, 26 Aug 2026 14:52:25 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '164' + 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/"5d738475c54060f0e8cc53e25e896af2" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.049461' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"data":{"id":"e8425b32-7368-42d3-b297-2c2bb5441634","email":"welcome@rw.com","created_at":"2026-08-26T14:52:25.909Z","domain_name":"example.com"}}' + recorded_at: Wed, 26 Aug 2026 14:52:25 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_create/when_email_is_invalid/raises_a_Mailtrap_Error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_create/when_email_is_invalid/raises_a_Mailtrap_Error.yml new file mode 100644 index 00000000..1bbf9212 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_create/when_email_is_invalid/raises_a_Mailtrap_Error.yml @@ -0,0 +1,71 @@ +--- +http_interactions: +- request: + method: post + uri: https://mailtrap.io/api/tracking_opt_outs + body: + encoding: UTF-8 + string: '{"email":"not-an-email","domain_id":12345}' + 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: + - Wed, 26 Aug 2026 14:53:07 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '42' + 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' + Cache-Control: + - no-cache + X-Runtime: + - '0.017892' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"errors":{"email":["format is invalid"]}}' + recorded_at: Wed, 26 Aug 2026 14:53:07 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_delete/returns_the_deleted_tracking_opt-out.yml b/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_delete/returns_the_deleted_tracking_opt-out.yml new file mode 100644 index 00000000..f63ef42f --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_delete/returns_the_deleted_tracking_opt-out.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: delete + uri: https://mailtrap.io/api/tracking_opt_outs/4c3c5ece-436e-4596-af69-647f99059380 + 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: + - Wed, 26 Aug 2026 14:53:39 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/"1995de4b428bde06e794a8aa06f19ad6" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.044938' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"id":"4c3c5ece-436e-4596-af69-647f99059380","email":"welcome@rw.com","created_at":"2026-08-26T14:39:13.000Z","domain_name":"example.com"}' + recorded_at: Wed, 26 Aug 2026 14:53:39 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_delete/when_tracking_opt-out_does_not_exist/raises_a_Mailtrap_Error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_delete/when_tracking_opt-out_does_not_exist/raises_a_Mailtrap_Error.yml new file mode 100644 index 00000000..5539d921 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_delete/when_tracking_opt-out_does_not_exist/raises_a_Mailtrap_Error.yml @@ -0,0 +1,71 @@ +--- +http_interactions: +- request: + method: delete + uri: https://mailtrap.io/api/tracking_opt_outs/missing + 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: + - Wed, 26 Aug 2026 14:53:40 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' + Cache-Control: + - no-cache + X-Runtime: + - '0.032373' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"errors":{"base":["Tracking opt-out not found"]}}' + recorded_at: Wed, 26 Aug 2026 14:53:40 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_list/maps_response_data_to_TrackingOptOut_objects.yml b/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_list/maps_response_data_to_TrackingOptOut_objects.yml new file mode 100644 index 00000000..746c18fc --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_list/maps_response_data_to_TrackingOptOut_objects.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/tracking_opt_outs + 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: + - Wed, 26 Aug 2026 14:51:14 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/"8a059d7eaa983ff49c1b91f37ec7a5b1" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.040923' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"data":[{"id":"4c3c5ece-436e-4596-af69-647f99059380","email":"welcome@rw.com","created_at":"2026-08-26T14:39:13.000Z","domain_name":"example.com"}],"last_id":null}' + recorded_at: Wed, 26 Aug 2026 14:51:14 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_list/when_api_key_is_incorrect/raises_authorization_error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_list/when_api_key_is_incorrect/raises_authorization_error.yml new file mode 100644 index 00000000..9576f24d --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_list/when_api_key_is_incorrect/raises_authorization_error.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/tracking_opt_outs + 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: + - Wed, 26 Aug 2026 14:51:15 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.006445' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"error":"Incorrect API token"}' + recorded_at: Wed, 26 Aug 2026 14:51:15 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_list/with_filters/returns_the_matching_tracking_opt-outs.yml b/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_list/with_filters/returns_the_matching_tracking_opt-outs.yml new file mode 100644 index 00000000..6bac0891 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_TrackingOptOutsAPI/_list/with_filters/returns_the_matching_tracking_opt-outs.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/tracking_opt_outs?email=tracked@example.com&end_time=2026-08-31T23:59:59Z&start_time=2026-08-01T00:00:00Z + 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: + - Wed, 26 Aug 2026 14:51:15 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: + - '148' + Etag: + - W/"8a059d7eaa983ff49c1b91f37ec7a5b1" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.034170' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"data":[{"id":"4c3c5ece-436e-4596-af69-647f99059380","email":"welcome@rw.com","created_at":"2026-08-26T14:39:13.000Z","domain_name":"example.com"}],"last_id":null}' + recorded_at: Wed, 26 Aug 2026 14:51:14 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/mailtrap/suppressions_api_spec.rb b/spec/mailtrap/suppressions_api_spec.rb index fe2c2616..022f4a89 100644 --- a/spec/mailtrap/suppressions_api_spec.rb +++ b/spec/mailtrap/suppressions_api_spec.rb @@ -94,6 +94,72 @@ end end + describe '#create' do + let(:request) { { email: 'user@example.com', domain_id: 12_345, sending_stream: 'transactional' } } + let(:created_attributes) do + { + 'id' => '123e4567-e89b-12d3-a456-426614174000', + 'type' => 'manual import', + 'created_at' => '2024-06-01T12:00:00Z', + 'email' => 'user@example.com', + 'sending_stream' => 'transactional', + 'domain_name' => 'example.com' + } + end + + it 'sends a flat body and maps the wrapped response' do + stub_request(:post, "#{base_url}/suppressions") + .with(body: request.to_json) + .to_return( + status: 201, + body: { 'data' => created_attributes }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + suppression = suppressions.create(request) + + expect(suppression).to be_a(Mailtrap::Suppression) + expect(suppression).to have_attributes( + id: '123e4567-e89b-12d3-a456-426614174000', + email: 'user@example.com', + sending_stream: 'transactional', + type: 'manual import' + ) + end + + it 'sends the optional type when provided' do + stub_request(:post, "#{base_url}/suppressions") + .with(body: request.merge(type: 'spam complaint').to_json) + .to_return( + status: 201, + body: { 'data' => created_attributes.merge('type' => 'spam complaint') }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + suppression = suppressions.create(request.merge(type: 'spam complaint')) + + expect(suppression.type).to eq('spam complaint') + end + + context 'when invalid options are provided' do + it 'raises ArgumentError' do + expect { suppressions.create(request.merge(unknown_option: true)) } + .to raise_error(ArgumentError, /invalid options are given/) + end + end + + it 'raises error when the request is rejected' do + stub_request(:post, "#{base_url}/suppressions") + .to_return( + status: 422, + body: { 'errors' => 'Email is invalid' }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + expect { suppressions.create(request) }.to raise_error(Mailtrap::Error) + end + end + describe '#delete' do let(:suppression_id) { 1 } diff --git a/spec/mailtrap/tracking_opt_outs_api_spec.rb b/spec/mailtrap/tracking_opt_outs_api_spec.rb new file mode 100644 index 00000000..f8c3fa5d --- /dev/null +++ b/spec/mailtrap/tracking_opt_outs_api_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::TrackingOptOutsAPI, :vcr do + subject(:tracking_opt_outs) { described_class.new(client) } + + let(:client) { Mailtrap::Client.new(api_key: ENV.fetch('MAILTRAP_API_KEY', 'local-api-key')) } + let(:domain_id) { 12_345 } + let(:tracking_opt_out_id) { '4c3c5ece-436e-4596-af69-647f99059380' } + + describe '#list' do + subject(:list) { tracking_opt_outs.list } + + it 'maps response data to TrackingOptOut objects' do + expect(list).to be_a(Mailtrap::TrackingOptOutsListResponse) + expect(list.data).to all(be_a(Mailtrap::TrackingOptOut)) + expect(list.data.first).to have_attributes( + id: be_a(String), + email: be_a(String), + created_at: be_a(String), + domain_name: be_a(String) + ) + end + + context 'with filters' do + subject(:list) do + tracking_opt_outs.list( + email: 'tracked@example.com', + start_time: '2026-08-01T00:00:00Z', + end_time: '2026-08-31T23:59:59Z' + ) + end + + it 'returns the matching tracking opt-outs' do + expect(list.data).to all(be_a(Mailtrap::TrackingOptOut)) + end + 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 '#create' do + subject(:create) { tracking_opt_outs.create(request) } + + let(:request) { { email: 'tracked@example.com', domain_id: } } + + it 'maps response data to TrackingOptOut object' do + expect(create).to be_a(Mailtrap::TrackingOptOut) + expect(create).to have_attributes( + id: be_a(String), + email: be_a(String), + created_at: be_a(String), + domain_name: be_a(String) + ) + end + + context 'when invalid options are provided' do + let(:request) { super().merge(unknown_option: true) } + + it 'raises ArgumentError' do + expect { create }.to raise_error(ArgumentError, /invalid options are given/) + end + end + + context 'when email is invalid' do + let(:request) { { email: 'not-an-email', domain_id: } } + + it 'raises a Mailtrap::Error' do + expect { create }.to raise_error(Mailtrap::Error) + end + end + end + + describe '#delete' do + subject(:delete) { tracking_opt_outs.delete(tracking_opt_out_id) } + + it 'returns the deleted tracking opt-out' do + expect(delete).to be_a(Mailtrap::TrackingOptOut) + expect(delete).to have_attributes( + id: be_a(String), + email: be_a(String) + ) + end + + context 'when tracking opt-out does not exist' do + let(:tracking_opt_out_id) { 'missing' } + + it 'raises a Mailtrap::Error' do + expect { delete }.to raise_error(Mailtrap::Error) + end + end + end +end From a449ef4160911f2116df852e8f90c32954d41801 Mon Sep 17 00:00:00 2001 From: Marcin Klocek Date: Wed, 26 Aug 2026 17:04:54 +0200 Subject: [PATCH 3/4] Return the deleted suppression from SuppressionsAPI#delete The endpoint responds 200 with the deleted record -- the controller renders the show view and the spec types the response as a bare Suppression -- but #delete discarded the body and returned nil, so the Ruby usage documented in the spec (`result.email`) could not work. Callers relying on the nil return get a Suppression instead. --- lib/mailtrap/suppressions_api.rb | 5 +++-- spec/mailtrap/suppressions_api_spec.rb | 27 ++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/lib/mailtrap/suppressions_api.rb b/lib/mailtrap/suppressions_api.rb index 02db044e..33f0111a 100644 --- a/lib/mailtrap/suppressions_api.rb +++ b/lib/mailtrap/suppressions_api.rb @@ -39,10 +39,11 @@ def create(options) # Deletes a suppression # @param suppression_id [String] The suppression UUID - # @return nil + # @return [Suppression] The deleted suppression # @!macro api_errors def delete(suppression_id) - client.delete("#{base_path}/#{suppression_id}") + response = client.delete("#{base_path}/#{suppression_id}") + build_entity(response, Suppression) end private diff --git a/spec/mailtrap/suppressions_api_spec.rb b/spec/mailtrap/suppressions_api_spec.rb index 022f4a89..63f949c4 100644 --- a/spec/mailtrap/suppressions_api_spec.rb +++ b/spec/mailtrap/suppressions_api_spec.rb @@ -162,13 +162,32 @@ describe '#delete' do let(:suppression_id) { 1 } + let(:deleted_attributes) do + { + 'id' => '123e4567-e89b-12d3-a456-426614174000', + 'type' => 'hard bounce', + 'created_at' => '2024-06-01T12:00:00Z', + 'email' => 'user1@example.com', + 'sending_stream' => 'transactional', + 'domain_name' => 'example.com' + } + end - it 'deletes a suppression' do + it 'returns the deleted suppression' do stub_request(:delete, "#{base_url}/suppressions/#{suppression_id}") - .to_return(status: 204) + .to_return( + status: 200, + body: deleted_attributes.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + suppression = suppressions.delete(suppression_id) - response = suppressions.delete(suppression_id) - expect(response).to be_nil + expect(suppression).to be_a(Mailtrap::Suppression) + expect(suppression).to have_attributes( + id: '123e4567-e89b-12d3-a456-426614174000', + email: 'user1@example.com' + ) end it 'raises error when suppression not found' do From 610f7ea3653e87b9d3454de0748902bc1b24af85 Mon Sep 17 00:00:00 2001 From: Marcin Klocek Date: Wed, 26 Aug 2026 17:05:19 +0200 Subject: [PATCH 4/4] Record suppressions specs against VCR cassettes The suppressions spec drove every example through WebMock stubs, so it asserted hand-written payload shapes rather than what the API returns. It now uses recorded cassettes like the rest of the API specs, and the two pre-existing list cassettes move from vcr_list/ to _list/ to match the renamed describe block. Assertions check types rather than values wherever the payload comes from live data -- spec_helper rewrites response emails to a placeholder, so asserting one would only ever test the sanitiser. The recordings are scrubbed of account data: sender addresses, domain names, recipient and outgoing IPs, MX hostnames, subjects, and the addresses embedded in ESP bounce strings. The list cassette is trimmed to two records; the endpoint returns up to 1000 and the spec only reads the first. --- ...ps_response_data_to_Suppression_object.yml | 74 +++++ .../raises_a_Mailtrap_Error.yml | 71 +++++ ...returns_the_suppression_with_that_type.yml | 73 +++++ .../returns_the_deleted_suppression.yml} | 20 +- .../raises_a_Mailtrap_Error.yml | 71 +++++ ...s_response_data_to_Suppression_objects.yml | 75 +++++ .../raises_authorization_error.yml | 0 .../returns_the_matching_suppressions.yml | 77 +++++ spec/mailtrap/suppressions_api_spec.rb | 265 +++++------------- 9 files changed, 521 insertions(+), 205 deletions(-) create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_create/maps_response_data_to_Suppression_object.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_create/when_email_is_invalid/raises_a_Mailtrap_Error.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_create/with_an_explicit_type/returns_the_suppression_with_that_type.yml rename spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/{vcr_list/maps_response_data_to_Suppression_objects.yml => _delete/returns_the_deleted_suppression.yml} (60%) create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_delete/when_suppression_does_not_exist/raises_a_Mailtrap_Error.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_list/maps_response_data_to_Suppression_objects.yml rename spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/{vcr_list => _list}/when_api_key_is_incorrect/raises_authorization_error.yml (100%) create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_list/with_email_filter/returns_the_matching_suppressions.yml diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_create/maps_response_data_to_Suppression_object.yml b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_create/maps_response_data_to_Suppression_object.yml new file mode 100644 index 00000000..a73d736f --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_create/maps_response_data_to_Suppression_object.yml @@ -0,0 +1,74 @@ +--- +http_interactions: +- request: + method: post + uri: https://mailtrap.io/api/accounts/1111111/suppressions + body: + encoding: UTF-8 + string: '{"email":"recipient.new@example.com","domain_id":12345,"sending_stream":"transactional"}' + 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: 201 + message: Created + headers: + Date: + - Wed, 26 Aug 2026 14:49:33 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '502' + 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/"11a00fb8e5e9bbd475144ff2fe15324b" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.075345' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"data":{"id":"c5dd1c56-64bb-4f91-89eb-050450322f49","type":"manual + import","created_at":"2026-08-26T14:49:33.307Z","email":"welcome@rw.com","sending_stream":"transactional","domain_name":"example.com","message_bounce_category":null,"message_category":null,"message_client_ip":null,"message_esp_response":null,"message_esp_server_type":null,"message_outgoing_ip":null,"message_recipient_mx_name":null,"message_sender_email":null,"message_subject":null,"message_created_at":null}}' + recorded_at: Wed, 26 Aug 2026 14:49:33 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_create/when_email_is_invalid/raises_a_Mailtrap_Error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_create/when_email_is_invalid/raises_a_Mailtrap_Error.yml new file mode 100644 index 00000000..baccaea7 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_create/when_email_is_invalid/raises_a_Mailtrap_Error.yml @@ -0,0 +1,71 @@ +--- +http_interactions: +- request: + method: post + uri: https://mailtrap.io/api/accounts/1111111/suppressions + body: + encoding: UTF-8 + string: '{"email":"not-an-email","domain_id":12345,"sending_stream":"transactional"}' + 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: + - Wed, 26 Aug 2026 14:41:42 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '42' + 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: + - '146' + Cache-Control: + - no-cache + X-Runtime: + - '0.020582' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"errors":{"email":["format is invalid"]}}' + recorded_at: Wed, 26 Aug 2026 14:41:42 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_create/with_an_explicit_type/returns_the_suppression_with_that_type.yml b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_create/with_an_explicit_type/returns_the_suppression_with_that_type.yml new file mode 100644 index 00000000..c65a1896 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_create/with_an_explicit_type/returns_the_suppression_with_that_type.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: post + uri: https://mailtrap.io/api/accounts/1111111/suppressions + body: + encoding: UTF-8 + string: '{"email":"recipient.unsubscribed@example.com","domain_id":12345,"sending_stream":"transactional","type":"unsubscription"}' + 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: 201 + message: Created + headers: + Date: + - Wed, 26 Aug 2026 14:49:58 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '512' + 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/"15b56b947f79171a40b0535045c875f0" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.044768' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"data":{"id":"65a839f6-babc-4b11-be04-d46d58067263","type":"unsubscription","created_at":"2026-08-26T14:49:58.740Z","email":"welcome@rw.com","sending_stream":"transactional","domain_name":"example.com","message_bounce_category":null,"message_category":null,"message_client_ip":null,"message_esp_response":null,"message_esp_server_type":null,"message_outgoing_ip":null,"message_recipient_mx_name":null,"message_sender_email":null,"message_subject":null,"message_created_at":null}}' + recorded_at: Wed, 26 Aug 2026 14:49:58 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/maps_response_data_to_Suppression_objects.yml b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_delete/returns_the_deleted_suppression.yml similarity index 60% rename from spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/maps_response_data_to_Suppression_objects.yml rename to spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_delete/returns_the_deleted_suppression.yml index 983d1d1f..8e899558 100644 --- a/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/maps_response_data_to_Suppression_objects.yml +++ b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_delete/returns_the_deleted_suppression.yml @@ -1,8 +1,8 @@ --- http_interactions: - request: - method: get - uri: https://mailtrap.io/api/accounts/1111111/suppressions + method: delete + uri: https://mailtrap.io/api/accounts/1111111/suppressions/fef0c580-1086-4ede-9fea-b8f5e256ba89 body: encoding: US-ASCII string: '' @@ -13,6 +13,8 @@ http_interactions: - "*/*" User-Agent: - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io Authorization: - Bearer Content-Type: @@ -23,7 +25,7 @@ http_interactions: message: OK headers: Date: - - Fri, 27 Jun 2025 07:56:23 GMT + - Wed, 26 Aug 2026 14:41:43 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -51,13 +53,13 @@ http_interactions: X-Ratelimit-Limit: - '150' X-Ratelimit-Remaining: - - '149' + - '145' Etag: - - W/"f549768628f00ba29406ba914e08f1e4" + - W/"3230761a1067a7871abd18cc577d7799" Cache-Control: - max-age=0, private, must-revalidate X-Runtime: - - '0.065844' + - '0.063299' Strict-Transport-Security: - max-age=0 Cf-Cache-Status: @@ -66,6 +68,6 @@ http_interactions: - h3=":443"; ma=86400 body: encoding: ASCII-8BIT - string: '[{"id":"af4485b5-5181-4fb5-9d41-287c78f708c1","type":"manual import","created_at":"2025-06-27T07:56:17Z","email":"asd@asd.com","sending_stream":"transactional","domain_name":"","message_bounce_category":"","message_category":"","message_client_ip":"","message_created_at":"","message_esp_response":"","message_esp_server_type":"","message_outgoing_ip":"","message_recipient_mx_name":"","message_sender_email":"","message_subject":""}]' - recorded_at: Fri, 27 Jun 2025 07:56:23 GMT -recorded_with: VCR 6.2.0 + string: '{"id":"fef0c580-1086-4ede-9fea-b8f5e256ba89","type":"unsubscription","created_at":"2026-08-20T06:49:13.000Z","email":"welcome@rw.com","sending_stream":"transactional","domain_name":"example.com","message_bounce_category":null,"message_category":"Testing","message_client_ip":"","message_esp_response":"","message_esp_server_type":"","message_outgoing_ip":"","message_recipient_mx_name":"","message_sender_email":"sender@example.com","message_subject":"","message_created_at":"2026-05-07T16:34:22Z"}' + recorded_at: Wed, 26 Aug 2026 14:41:43 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_delete/when_suppression_does_not_exist/raises_a_Mailtrap_Error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_delete/when_suppression_does_not_exist/raises_a_Mailtrap_Error.yml new file mode 100644 index 00000000..9c7a83f4 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_delete/when_suppression_does_not_exist/raises_a_Mailtrap_Error.yml @@ -0,0 +1,71 @@ +--- +http_interactions: +- request: + method: delete + uri: https://mailtrap.io/api/accounts/1111111/suppressions/missing + 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: + - Wed, 26 Aug 2026 14:41:43 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '45' + 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: + - '144' + Cache-Control: + - no-cache + X-Runtime: + - '0.018136' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"errors":{"base":["Suppression not found"]}}' + recorded_at: Wed, 26 Aug 2026 14:41:43 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_list/maps_response_data_to_Suppression_objects.yml b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_list/maps_response_data_to_Suppression_objects.yml new file mode 100644 index 00000000..a6dfd23a --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_list/maps_response_data_to_Suppression_objects.yml @@ -0,0 +1,75 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/suppressions + 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: + - Wed, 26 Aug 2026 14:44:51 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/"f58b7f57bd0a5d82558e48f07c2d2080" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.519590' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '[{"id":"c25f9252-abd0-4e61-b665-e2b26831cd88","type":"manual import","created_at":"2026-08-26T14:41:40.000Z","email":"welcome@rw.com","sending_stream":"transactional","domain_name":"example.com","message_bounce_category":null,"message_category":null,"message_client_ip":null,"message_esp_response":null,"message_esp_server_type":null,"message_outgoing_ip":null,"message_recipient_mx_name":null,"message_sender_email":null,"message_subject":null,"message_created_at":null},{"id":"1f7e1ccd-59c7-4a87-8b42-dae3e11a479b","type":"manual + import","created_at":"2025-12-24T08:44:21.000Z","email":"welcome@rw.com","sending_stream":"any","domain_name":null,"message_bounce_category":null,"message_category":null,"message_client_ip":null,"message_esp_response":null,"message_esp_server_type":null,"message_outgoing_ip":null,"message_recipient_mx_name":null,"message_sender_email":null,"message_subject":null,"message_created_at":null}]' + recorded_at: Wed, 26 Aug 2026 14:44:51 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/when_api_key_is_incorrect/raises_authorization_error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_list/when_api_key_is_incorrect/raises_authorization_error.yml similarity index 100% rename from spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/when_api_key_is_incorrect/raises_authorization_error.yml rename to spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_list/when_api_key_is_incorrect/raises_authorization_error.yml diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_list/with_email_filter/returns_the_matching_suppressions.yml b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_list/with_email_filter/returns_the_matching_suppressions.yml new file mode 100644 index 00000000..bb355180 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/_list/with_email_filter/returns_the_matching_suppressions.yml @@ -0,0 +1,77 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/suppressions?email=recipient@example.com + 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: + - Wed, 26 Aug 2026 14:41:40 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/"2e5b6793c947117b934b5f3f58908444" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.041244' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '[{"id":"1efc252c-1304-4162-a312-1cfd8087568f","type":"hard bounce","created_at":"2025-03-24T13:17:20.000Z","email":"welcome@rw.com","sending_stream":"transactional","domain_name":null,"message_bounce_category":"Bad + recipient","message_category":"Mass Email","message_client_ip":"192.0.2.1","message_esp_response":"Requested + action not taken: mailbox unavailable For explanation visit https://postmaster.mail.com/en/case?c=r1601&i=ip&v=45.158.83.1&r=1MzCtZ-1tAd8d3NEh-00rxa3","message_esp_server_type":"Other + Providers","message_outgoing_ip":"198.51.100.1","message_recipient_mx_name":"mx.example.com","message_sender_email":"sender@example.com","message_subject":"Test + subject","message_created_at":"2025-03-24T13:17:20Z"}]' + recorded_at: Wed, 26 Aug 2026 14:41:40 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/mailtrap/suppressions_api_spec.rb b/spec/mailtrap/suppressions_api_spec.rb index 63f949c4..b98e001d 100644 --- a/spec/mailtrap/suppressions_api_spec.rb +++ b/spec/mailtrap/suppressions_api_spec.rb @@ -1,245 +1,118 @@ # frozen_string_literal: true -RSpec.describe Mailtrap::SuppressionsAPI do +RSpec.describe Mailtrap::SuppressionsAPI, :vcr do subject(:suppressions) { 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')) } - - let(:base_url) { "https://mailtrap.io/api/accounts/#{account_id}" } + let(:domain_id) { 12_345 } + let(:suppression_id) { 'fef0c580-1086-4ede-9fea-b8f5e256ba89' } describe '#list' do - let(:expected_attributes) do - { - 'id' => '123e4567-e89b-12d3-a456-426614174000', - 'type' => 'hard bounce', - 'created_at' => '2024-06-01T12:00:00Z', - 'email' => 'user1@example.com', - 'sending_stream' => 'transactional', - 'domain_name' => 'example.com', - 'message_bounce_category' => 'invalid recipient', - 'message_category' => 'transactional', - 'message_client_ip' => '192.0.2.1', - 'message_created_at' => '2024-06-01T11:59:00Z', - 'message_esp_response' => '550 5.1.1 User unknown', - 'message_esp_server_type' => 'smtp', - 'message_outgoing_ip' => '198.51.100.1', - 'message_recipient_mx_name' => 'mx.example.com', - 'message_sender_email' => 'sender@example.com', - 'message_subject' => 'Test subject' - } - end - let(:expected_response) do - [ - expected_attributes, - { - 'id' => '456e7890-e89b-12d3-a456-426614174001', - 'type' => 'spam complaint', - 'created_at' => '2024-06-01T13:00:00Z', - 'email' => 'user2@example.com', - 'sending_stream' => 'bulk', - 'domain_name' => 'example.org', - 'message_bounce_category' => nil, - 'message_category' => 'bulk', - 'message_client_ip' => '192.0.2.2', - 'message_created_at' => '2024-06-01T12:59:00Z', - 'message_esp_response' => nil, - 'message_esp_server_type' => nil, - 'message_outgoing_ip' => '198.51.100.2', - 'message_recipient_mx_name' => 'mx.example.org', - 'message_sender_email' => 'sender2@example.com', - 'message_subject' => 'Bulk email subject' - } - ] - end + subject(:list) { suppressions.list } - it 'returns all suppressions' do - stub_request(:get, "#{base_url}/suppressions") - .to_return( - status: 200, - body: expected_response.to_json, - headers: { 'Content-Type' => 'application/json' } - ) - - response = suppressions.list - expect(response).to all(be_a(Mailtrap::Suppression)) - expect(response.length).to eq(2) - expect(response.first).to have_attributes(expected_attributes) + it 'maps response data to Suppression objects' do + expect(list).to all(be_a(Mailtrap::Suppression)) + expect(list.first).to have_attributes( + id: be_a(String), + type: be_a(String), + created_at: be_a(String), + email: be_a(String), + sending_stream: be_a(String), + domain_name: be_a(String) + ) end - it 'returns suppressions filtered by email' do - email = 'user1@example.com' - stub_request(:get, "#{base_url}/suppressions?email=#{email}") - .to_return( - status: 200, - body: [expected_attributes].to_json, - headers: { 'Content-Type' => 'application/json' } - ) - - response = suppressions.list(email:) - expect(response).to all(be_a(Mailtrap::Suppression)) - expect(response.length).to eq(1) - expect(response.first).to have_attributes(expected_attributes) + context 'with email filter' do + subject(:list) { suppressions.list(email: 'recipient@example.com') } + + it 'returns the matching suppressions' do + expect(list).to all(be_a(Mailtrap::Suppression)) + end end - it 'raises error when unauthorized' do - stub_request(:get, "#{base_url}/suppressions") - .to_return( - status: 401, - body: { 'error' => 'Unauthorized' }.to_json, - headers: { 'Content-Type' => 'application/json' } - ) + context 'when api key is incorrect' do + let(:client) { Mailtrap::Client.new(api_key: 'incorrect-api-key') } - expect { suppressions.list }.to raise_error(Mailtrap::AuthorizationError) + 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') + expect(error.messages.any? { |msg| msg.include?('Incorrect API token') }).to be true + end + end end end describe '#create' do - let(:request) { { email: 'user@example.com', domain_id: 12_345, sending_stream: 'transactional' } } - let(:created_attributes) do + subject(:create) { suppressions.create(request) } + + let(:request) do { - 'id' => '123e4567-e89b-12d3-a456-426614174000', - 'type' => 'manual import', - 'created_at' => '2024-06-01T12:00:00Z', - 'email' => 'user@example.com', - 'sending_stream' => 'transactional', - 'domain_name' => 'example.com' + email: 'recipient.new@example.com', + domain_id:, + sending_stream: 'transactional' } end - it 'sends a flat body and maps the wrapped response' do - stub_request(:post, "#{base_url}/suppressions") - .with(body: request.to_json) - .to_return( - status: 201, - body: { 'data' => created_attributes }.to_json, - headers: { 'Content-Type' => 'application/json' } - ) - - suppression = suppressions.create(request) - - expect(suppression).to be_a(Mailtrap::Suppression) - expect(suppression).to have_attributes( - id: '123e4567-e89b-12d3-a456-426614174000', - email: 'user@example.com', + it 'maps response data to Suppression object' do + expect(create).to be_a(Mailtrap::Suppression) + expect(create).to have_attributes( + id: be_a(String), + email: be_a(String), sending_stream: 'transactional', - type: 'manual import' + type: be_a(String) ) end - it 'sends the optional type when provided' do - stub_request(:post, "#{base_url}/suppressions") - .with(body: request.merge(type: 'spam complaint').to_json) - .to_return( - status: 201, - body: { 'data' => created_attributes.merge('type' => 'spam complaint') }.to_json, - headers: { 'Content-Type' => 'application/json' } - ) - - suppression = suppressions.create(request.merge(type: 'spam complaint')) + context 'with an explicit type' do + let(:request) do + { + email: 'recipient.unsubscribed@example.com', + domain_id:, + sending_stream: 'transactional', + type: 'unsubscription' + } + end - expect(suppression.type).to eq('spam complaint') + it 'returns the suppression with that type' do + expect(create.type).to eq('unsubscription') + end end context 'when invalid options are provided' do + let(:request) { super().merge(unknown_option: true) } + it 'raises ArgumentError' do - expect { suppressions.create(request.merge(unknown_option: true)) } - .to raise_error(ArgumentError, /invalid options are given/) + expect { create }.to raise_error(ArgumentError, /invalid options are given/) end end - it 'raises error when the request is rejected' do - stub_request(:post, "#{base_url}/suppressions") - .to_return( - status: 422, - body: { 'errors' => 'Email is invalid' }.to_json, - headers: { 'Content-Type' => 'application/json' } - ) + context 'when email is invalid' do + let(:request) { super().merge(email: 'not-an-email') } - expect { suppressions.create(request) }.to raise_error(Mailtrap::Error) + it 'raises a Mailtrap::Error' do + expect { create }.to raise_error(Mailtrap::Error) + end end end describe '#delete' do - let(:suppression_id) { 1 } - let(:deleted_attributes) do - { - 'id' => '123e4567-e89b-12d3-a456-426614174000', - 'type' => 'hard bounce', - 'created_at' => '2024-06-01T12:00:00Z', - 'email' => 'user1@example.com', - 'sending_stream' => 'transactional', - 'domain_name' => 'example.com' - } - end + subject(:delete) { suppressions.delete(suppression_id) } it 'returns the deleted suppression' do - stub_request(:delete, "#{base_url}/suppressions/#{suppression_id}") - .to_return( - status: 200, - body: deleted_attributes.to_json, - headers: { 'Content-Type' => 'application/json' } - ) - - suppression = suppressions.delete(suppression_id) - - expect(suppression).to be_a(Mailtrap::Suppression) - expect(suppression).to have_attributes( - id: '123e4567-e89b-12d3-a456-426614174000', - email: 'user1@example.com' - ) - end - - it 'raises error when suppression not found' do - stub_request(:delete, "#{base_url}/suppressions/999") - .to_return( - status: 404, - body: { 'error' => 'Not Found' }.to_json, - headers: { 'Content-Type' => 'application/json' } - ) - - expect { suppressions.delete(999) }.to raise_error(Mailtrap::Error) - end - - it 'raises error when unauthorized' do - stub_request(:delete, "#{base_url}/suppressions/#{suppression_id}") - .to_return( - status: 401, - body: { 'error' => 'Unauthorized' }.to_json, - headers: { 'Content-Type' => 'application/json' } - ) - - expect { suppressions.delete(suppression_id) }.to raise_error(Mailtrap::AuthorizationError) - end - end - - describe 'vcr#list', :vcr do - subject(:list) { suppressions.list } - - it 'maps response data to Suppression objects' do - expect(list).to all(be_a(Mailtrap::Suppression)) - expect(list.first).to have_attributes( + expect(delete).to be_a(Mailtrap::Suppression) + expect(delete).to have_attributes( id: be_a(String), - type: be_a(String), - created_at: be_a(String), - email: be_a(String), - sending_stream: be_a(String), - domain_name: be_a(String), - message_bounce_category: be_a(String), - message_category: be_a(String), - message_client_ip: be_a(String) + email: be_a(String) ) end - context 'when api key is incorrect' do - let(:client) { Mailtrap::Client.new(api_key: 'incorrect-api-key') } + context 'when suppression does not exist' do + let(:suppression_id) { 'missing' } - 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') - expect(error.messages.any? { |msg| msg.include?('Incorrect API token') }).to be true - end + it 'raises a Mailtrap::Error' do + expect { delete }.to raise_error(Mailtrap::Error) end end end