From 4a2ea211bf8cd4d6e791a489ad9abdd08f1d35f4 Mon Sep 17 00:00:00 2001 From: Maciej Walusiak Date: Tue, 13 Oct 2026 10:31:32 +0200 Subject: [PATCH 1/3] Add MarketingStatsAPI for GET /api/marketing/stats Decisions: - token-scoped like EmailCampaignsAPI: the endpoint has no account id in its path - one get with keyword filters mirrors StatsAPI#get; five keywords stay within ParameterLists - separate MarketingStats struct: same fields as EmailCampaignStats but different rate denominators - date normalization and key[] query building moved into StatsQueryParams so both stats APIs share them - VCR cassette is hand-written until the endpoint is live; re-record it with bin/record-vcr then Co-Authored-By: Claude Opus 5.5 --- README.md | 1 + examples/marketing_stats_api.rb | 28 +++++ lib/mailtrap.rb | 1 + lib/mailtrap/marketing_stats.rb | 39 ++++++ lib/mailtrap/marketing_stats_api.rb | 43 +++++++ lib/mailtrap/stats_api.rb | 36 +----- lib/mailtrap/stats_query_params.rb | 40 ++++++ ...sponse_data_to_a_MarketingStats_object.yml | 40 ++++++ spec/mailtrap/marketing_stats_api_spec.rb | 116 ++++++++++++++++++ spec/mailtrap/marketing_stats_spec.rb | 29 +++++ 10 files changed, 339 insertions(+), 34 deletions(-) create mode 100644 examples/marketing_stats_api.rb create mode 100644 lib/mailtrap/marketing_stats.rb create mode 100644 lib/mailtrap/marketing_stats_api.rb create mode 100644 lib/mailtrap/stats_query_params.rb create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_MarketingStatsAPI/vcr_get/maps_response_data_to_a_MarketingStats_object.yml create mode 100644 spec/mailtrap/marketing_stats_api_spec.rb create mode 100644 spec/mailtrap/marketing_stats_spec.rb diff --git a/README.md b/README.md index afa65f4c..562275ef 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,7 @@ Contact management: Email marketing: - Email Campaigns CRUD, Lifecycle Actions & Stats – [`email_campaigns_api.rb`](examples/email_campaigns_api.rb) +- Email Marketing Stats API – [`marketing_stats_api.rb`](examples/marketing_stats_api.rb) General: diff --git a/examples/marketing_stats_api.rb b/examples/marketing_stats_api.rb new file mode 100644 index 00000000..0dd72021 --- /dev/null +++ b/examples/marketing_stats_api.rb @@ -0,0 +1,28 @@ +require 'mailtrap' +require 'date' + +client = Mailtrap::Client.new(api_key: 'your-api-key') +marketing_stats = Mailtrap::MarketingStatsAPI.new(client) + +today = Date.today + +# Get aggregated Email Marketing stats (campaigns and automations) for the last 30 days +marketing_stats.get(start_date: today - 30, end_date: today) +# => # + +# Narrow to campaigns only (or automations only), for some sending domains and email service providers +marketing_stats.get( + start_date: today - 30, + end_date: today, + sending_domain_ids: [3938, 3939], + sources: ['campaigns'], + email_service_providers: %w[Google Yahoo] +) +# => # diff --git a/lib/mailtrap.rb b/lib/mailtrap.rb index aa2fafae..6f739f34 100644 --- a/lib/mailtrap.rb +++ b/lib/mailtrap.rb @@ -21,6 +21,7 @@ require_relative 'mailtrap/suppressions_api' require_relative 'mailtrap/tracking_opt_outs_api' require_relative 'mailtrap/email_campaigns_api' +require_relative 'mailtrap/marketing_stats_api' require_relative 'mailtrap/sending_domains_api' require_relative 'mailtrap/company_info_api' require_relative 'mailtrap/email_logs_api' diff --git a/lib/mailtrap/marketing_stats.rb b/lib/mailtrap/marketing_stats.rb new file mode 100644 index 00000000..930613e5 --- /dev/null +++ b/lib/mailtrap/marketing_stats.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Mailtrap + # Data Transfer Object for Email Marketing Stats + # + # Aggregated metrics of the account's Email Marketing traffic (campaigns and automations) for + # the requested period and filters. Rates are fractions from 0 to 1. Every count and rate is +0+ + # when nothing matched. + # @see https://docs.mailtrap.io/developers/email-marketing/marketing-stats + # @attr_reader delivery_count [Integer] Number of delivered messages + # @attr_reader delivery_rate [Float] Share of sent messages that were delivered (0–1) + # @attr_reader bounce_count [Integer] Number of bounced messages + # @attr_reader bounce_rate [Float] Share of sent messages that bounced (0–1) + # @attr_reader open_count [Integer] Number of opened messages + # @attr_reader open_rate [Float] Share of delivered messages that were opened (0–1) + # @attr_reader click_count [Integer] Number of clicked messages + # @attr_reader click_rate [Float] Share of delivered messages that were clicked (0–1) + # @attr_reader spam_count [Integer] Number of spam complaints + # @attr_reader spam_rate [Float] Share of delivered messages marked as spam (0–1) + # @attr_reader unsubscription_count [Integer] Number of unsubscriptions + # @attr_reader unsubscription_rate [Float] Share of delivered messages that unsubscribed (0–1) + # @attr_reader sent_count [Integer] Number of sent messages (delivered plus bounced) + MarketingStats = Struct.new( + :delivery_count, + :delivery_rate, + :bounce_count, + :bounce_rate, + :open_count, + :open_rate, + :click_count, + :click_rate, + :spam_count, + :spam_rate, + :unsubscription_count, + :unsubscription_rate, + :sent_count, + keyword_init: true + ) +end diff --git a/lib/mailtrap/marketing_stats_api.rb b/lib/mailtrap/marketing_stats_api.rb new file mode 100644 index 00000000..60120a39 --- /dev/null +++ b/lib/mailtrap/marketing_stats_api.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require_relative 'base_api' +require_relative 'stats_query_params' +require_relative 'marketing_stats' + +module Mailtrap + class MarketingStatsAPI + include BaseAPI + include StatsQueryParams + + # @param client [Mailtrap::Client] The client instance + def initialize(client = Mailtrap::Client.new) + @client = client + end + + # Get aggregated Email Marketing stats: campaigns and automations together, the same numbers + # as Email Marketing > Stats Overview in the app. Email API/SMTP traffic is reported by {StatsAPI}. + # @param start_date [String, Date, Time] Start of the period, inclusive (required) + # @param end_date [String, Date, Time] End of the period, inclusive (required) + # @param sending_domain_ids [Array] Filter by sending domain IDs + # @param sources [Array] Filter by source: +campaigns+ or +automations+; + # omit it, or pass both, to include campaigns and automations together + # @param email_service_providers [Array] Filter by recipient email service providers + # @return [MarketingStats] Aggregated Email Marketing stats + # @!macro api_errors + # @raise [ArgumentError] If a date is not a Date, Time, or String in +YYYY-MM-DD+ format + def get(start_date:, end_date:, sending_domain_ids: nil, sources: nil, email_service_providers: nil) + query_params = build_query_params( + start_date, end_date, + { sending_domain_ids:, sources:, email_service_providers: } + ) + response = client.get(base_path, query_params) + build_entity(response[:data], MarketingStats) + end + + private + + def base_path + '/api/marketing/stats' + end + end +end diff --git a/lib/mailtrap/stats_api.rb b/lib/mailtrap/stats_api.rb index dfdd8ef8..d4c867ca 100644 --- a/lib/mailtrap/stats_api.rb +++ b/lib/mailtrap/stats_api.rb @@ -1,15 +1,15 @@ # frozen_string_literal: true -require 'date' require_relative 'base_api' +require_relative 'stats_query_params' require_relative 'sending_stats' require_relative 'sending_stat_group' module Mailtrap class StatsAPI include BaseAPI + include StatsQueryParams - ARRAY_FILTERS = %i[sending_domain_ids sending_streams categories email_service_providers].freeze GROUP_KEYS = { 'domains' => :sending_domain_id, 'categories' => :category, @@ -112,38 +112,6 @@ def grouped_stats(group, start_date, end_date, filters) end end - def build_query_params(start_date, end_date, filters) - params = { start_date: normalize_date(start_date), end_date: normalize_date(end_date) } - - ARRAY_FILTERS.each do |filter_key| - values = filters[filter_key] - params["#{filter_key}[]"] = values if values - end - - params - end - - def normalize_date(value) - case value - when Date - value.iso8601 - when Time - value.strftime('%F') - when String - unless /\A\d{4}-\d{2}-\d{2}\z/.match?(value) - raise ArgumentError, - "Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format." - end - - Date.iso8601(value).iso8601 - else - raise ArgumentError, - "Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format." - end - rescue Date::Error - raise ArgumentError, "Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format." - end - def base_path "/api/accounts/#{account_id}/stats" end diff --git a/lib/mailtrap/stats_query_params.rb b/lib/mailtrap/stats_query_params.rb new file mode 100644 index 00000000..fdc37b2a --- /dev/null +++ b/lib/mailtrap/stats_query_params.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'date' + +module Mailtrap + # Query parameters of the sending stats and Email Marketing stats endpoints: the period as + # +YYYY-MM-DD+ dates and every array filter as a repeated +key[]+ parameter. + module StatsQueryParams + private + + def build_query_params(start_date, end_date, filters) + params = { start_date: normalize_date(start_date), end_date: normalize_date(end_date) } + + filters.compact.each { |filter_key, values| params["#{filter_key}[]"] = values } + + params + end + + def normalize_date(value) + case value + when Date + value.iso8601 + when Time + value.strftime('%F') + when String + unless /\A\d{4}-\d{2}-\d{2}\z/.match?(value) + raise ArgumentError, + "Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format." + end + + Date.iso8601(value).iso8601 + else + raise ArgumentError, + "Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format." + end + rescue Date::Error + raise ArgumentError, "Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format." + end + end +end diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_MarketingStatsAPI/vcr_get/maps_response_data_to_a_MarketingStats_object.yml b/spec/fixtures/vcr_cassettes/Mailtrap_MarketingStatsAPI/vcr_get/maps_response_data_to_a_MarketingStats_object.yml new file mode 100644 index 00000000..af516cb2 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_MarketingStatsAPI/vcr_get/maps_response_data_to_a_MarketingStats_object.yml @@ -0,0 +1,40 @@ +# Hand-written from the documented response until GET /api/marketing/stats is live. +# Re-record it then: delete this file and run +# bin/record-vcr spec/mailtrap/marketing_stats_api_spec.rb +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/marketing/stats?end_date=2026-09-30&sources%5B%5D=campaigns&start_date=2026-09-01 + 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, 13 Oct 2026 10:00:00 GMT + Content-Type: + - application/json; charset=utf-8 + X-Mailtrap-Version: + - v2 + body: + encoding: ASCII-8BIT + string: '{"data":{"delivery_count":1200,"delivery_rate":0.96,"bounce_count":50,"bounce_rate":0.04,"open_count":600,"open_rate":0.5,"click_count":240,"click_rate":0.2,"spam_count":3,"spam_rate":0.0025,"unsubscription_count":12,"unsubscription_rate":0.01,"sent_count":1250}}' + recorded_at: Tue, 13 Oct 2026 10:00:00 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/mailtrap/marketing_stats_api_spec.rb b/spec/mailtrap/marketing_stats_api_spec.rb new file mode 100644 index 00000000..034230dc --- /dev/null +++ b/spec/mailtrap/marketing_stats_api_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::MarketingStatsAPI do + subject(:marketing_stats_api) { described_class.new(Mailtrap::Client.new(api_key: 'correct-api-key')) } + + let(:base_url) { 'https://mailtrap.io/api/marketing/stats' } + let(:stats_attributes) do + { + 'delivery_count' => 1200, + 'delivery_rate' => 0.96, + 'bounce_count' => 50, + 'bounce_rate' => 0.04, + 'open_count' => 600, + 'open_rate' => 0.5, + 'click_count' => 240, + 'click_rate' => 0.2, + 'spam_count' => 3, + 'spam_rate' => 0.0025, + 'unsubscription_count' => 12, + 'unsubscription_rate' => 0.01, + 'sent_count' => 1250 + } + end + + describe '#get' do + subject(:stats) { marketing_stats_api.get(start_date: '2026-05-01', end_date: '2026-05-31') } + + it 'returns a MarketingStats object' do + stub = stub_request(:get, "#{base_url}?start_date=2026-05-01&end_date=2026-05-31") + .to_return( + status: 200, + body: { 'data' => stats_attributes }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + expect(stats).to be_a(Mailtrap::MarketingStats) + expect(stats).to match_struct(stats_attributes.transform_keys(&:to_sym)) + expect(stub).to have_been_requested + end + + context 'with optional filters' do + subject(:stats) do + marketing_stats_api.get( + start_date: '2026-05-01', + end_date: '2026-05-31', + sending_domain_ids: [3938, 3939], + sources: ['campaigns'], + email_service_providers: %w[Google Yahoo] + ) + end + + it 'sends every filter as an array query parameter' do + stub = stub_request( + :get, + "#{base_url}?start_date=2026-05-01&end_date=2026-05-31" \ + '&sending_domain_ids[]=3938&sending_domain_ids[]=3939&sources[]=campaigns' \ + '&email_service_providers[]=Google&email_service_providers[]=Yahoo' + ).to_return( + status: 200, + body: { 'data' => stats_attributes }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + expect(stats).to have_attributes(delivery_count: 1200, sent_count: 1250) + expect(stub).to have_been_requested + end + end + + it 'raises ArgumentError for an invalid date' do + expect { marketing_stats_api.get(start_date: '2026-05-01', end_date: '2026-13-01') } + .to raise_error(ArgumentError, /Invalid date: "2026-13-01"/) + end + + it 'raises error when a filter is rejected' do + stub_request(:get, "#{base_url}?start_date=2026-05-01&end_date=2026-05-31&sources[]=newsletters") + .to_return( + status: 400, + body: { 'errors' => "Invalid source: 'newsletters'" }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + expect { marketing_stats_api.get(start_date: '2026-05-01', end_date: '2026-05-31', sources: ['newsletters']) } + .to raise_error(Mailtrap::Error, "Invalid source: 'newsletters'") + end + + it 'raises authorization error when the API key is incorrect' do + stub_request(:get, "#{base_url}?start_date=2026-05-01&end_date=2026-05-31") + .to_return( + status: 401, + body: { 'error' => 'Incorrect API token' }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + expect { stats }.to raise_error(Mailtrap::AuthorizationError, 'Incorrect API token') + end + end + + describe 'vcr#get', :vcr do + subject(:stats) do + described_class.new(client).get(start_date: '2026-09-01', end_date: '2026-09-30', sources: ['campaigns']) + end + + let(:client) { Mailtrap::Client.new(api_key: ENV.fetch('MAILTRAP_API_KEY', 'local-api-key')) } + + it 'maps response data to a MarketingStats object' do + expect(stats).to be_a(Mailtrap::MarketingStats) + expect(stats).to have_attributes( + delivery_count: be_a(Integer), + delivery_rate: be_a(Numeric), + open_count: be_a(Integer), + open_rate: be_a(Numeric), + sent_count: be_a(Integer) + ) + end + end +end diff --git a/spec/mailtrap/marketing_stats_spec.rb b/spec/mailtrap/marketing_stats_spec.rb new file mode 100644 index 00000000..71968531 --- /dev/null +++ b/spec/mailtrap/marketing_stats_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::MarketingStats do + describe '#initialize' do + subject(:marketing_stats) { described_class.new(attributes) } + + let(:attributes) do + { + delivery_count: 1200, + delivery_rate: 0.96, + bounce_count: 50, + bounce_rate: 0.04, + open_count: 600, + open_rate: 0.5, + click_count: 240, + click_rate: 0.2, + spam_count: 3, + spam_rate: 0.0025, + unsubscription_count: 12, + unsubscription_rate: 0.01, + sent_count: 1250 + } + end + + it 'creates marketing stats with all attributes' do + expect(marketing_stats).to match_struct(attributes) + end + end +end From e56424d96ea50ebfd93b82f229265af4b90ab4a1 Mon Sep 17 00:00:00 2001 From: Maciej Walusiak Date: Tue, 13 Oct 2026 11:54:34 +0200 Subject: [PATCH 2/3] Send DateTime stats dates as plain dates Decisions: - DateTime is a Date whose #iso8601 carries the time of day, which the stats endpoints reject - check YYYY-MM-DD strings with Date.valid_date? and raise one ArgumentError instead of three copies and a rescue - build the query hash in one expression; spec attributes use symbol keys to match the struct directly Co-Authored-By: Claude Opus 5.5 --- lib/mailtrap/stats_query_params.rb | 28 +++++----------- spec/mailtrap/marketing_stats_api_spec.rb | 41 +++++++++++++++-------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/lib/mailtrap/stats_query_params.rb b/lib/mailtrap/stats_query_params.rb index fdc37b2a..cf1f5fee 100644 --- a/lib/mailtrap/stats_query_params.rb +++ b/lib/mailtrap/stats_query_params.rb @@ -9,31 +9,21 @@ module StatsQueryParams private def build_query_params(start_date, end_date, filters) - params = { start_date: normalize_date(start_date), end_date: normalize_date(end_date) } - - filters.compact.each { |filter_key, values| params["#{filter_key}[]"] = values } - - params + { + start_date: normalize_date(start_date), + end_date: normalize_date(end_date), + **filters.compact.transform_keys { |filter_key| "#{filter_key}[]" } + } end def normalize_date(value) case value - when Date - value.iso8601 - when Time - value.strftime('%F') + when Date, Time # DateTime is a Date, but its #iso8601 includes the time of day + return value.strftime('%F') when String - unless /\A\d{4}-\d{2}-\d{2}\z/.match?(value) - raise ArgumentError, - "Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format." - end - - Date.iso8601(value).iso8601 - else - raise ArgumentError, - "Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format." + return value if /\A\d{4}-\d{2}-\d{2}\z/.match?(value) && Date.valid_date?(*value.split('-').map(&:to_i)) end - rescue Date::Error + raise ArgumentError, "Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format." end end diff --git a/spec/mailtrap/marketing_stats_api_spec.rb b/spec/mailtrap/marketing_stats_api_spec.rb index 034230dc..ce5ab8c2 100644 --- a/spec/mailtrap/marketing_stats_api_spec.rb +++ b/spec/mailtrap/marketing_stats_api_spec.rb @@ -6,19 +6,19 @@ let(:base_url) { 'https://mailtrap.io/api/marketing/stats' } let(:stats_attributes) do { - 'delivery_count' => 1200, - 'delivery_rate' => 0.96, - 'bounce_count' => 50, - 'bounce_rate' => 0.04, - 'open_count' => 600, - 'open_rate' => 0.5, - 'click_count' => 240, - 'click_rate' => 0.2, - 'spam_count' => 3, - 'spam_rate' => 0.0025, - 'unsubscription_count' => 12, - 'unsubscription_rate' => 0.01, - 'sent_count' => 1250 + delivery_count: 1200, + delivery_rate: 0.96, + bounce_count: 50, + bounce_rate: 0.04, + open_count: 600, + open_rate: 0.5, + click_count: 240, + click_rate: 0.2, + spam_count: 3, + spam_rate: 0.0025, + unsubscription_count: 12, + unsubscription_rate: 0.01, + sent_count: 1250 } end @@ -34,7 +34,7 @@ ) expect(stats).to be_a(Mailtrap::MarketingStats) - expect(stats).to match_struct(stats_attributes.transform_keys(&:to_sym)) + expect(stats).to match_struct(stats_attributes) expect(stub).to have_been_requested end @@ -66,6 +66,19 @@ end end + it 'sends DateTime and Time arguments as plain dates' do + stub = stub_request(:get, "#{base_url}?start_date=2026-05-01&end_date=2026-05-31") + .to_return( + status: 200, + body: { 'data' => stats_attributes }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + marketing_stats_api.get(start_date: DateTime.new(2026, 5, 1, 13, 45), end_date: Time.new(2026, 5, 31, 23, 59)) + + expect(stub).to have_been_requested + end + it 'raises ArgumentError for an invalid date' do expect { marketing_stats_api.get(start_date: '2026-05-01', end_date: '2026-13-01') } .to raise_error(ArgumentError, /Invalid date: "2026-13-01"/) From 7ace1f7d797065f2528129169f30b70b554b7436 Mon Sep 17 00:00:00 2001 From: Maciej Walusiak Date: Tue, 13 Oct 2026 12:50:51 +0200 Subject: [PATCH 3/3] Leave empty stats filters out of the query Decisions: - an empty filter array made URI.encode_www_form emit a bare "&&"; nil and [] are now both skipped, "" still passes through - StatsAPI gets its own DateTime spec, since the plain-date fix changes what it sends too - rate attributes are documented as Float or Integer: the API returns 0 for every rate when nothing matched - example asks for today - 29..today, as both ends of the range are inclusive - build_query_params is a plain loop again, and the spec shares one success response Co-Authored-By: Claude Opus 5.5 --- examples/marketing_stats_api.rb | 4 +-- lib/mailtrap/marketing_stats.rb | 12 ++++----- lib/mailtrap/stats_query_params.rb | 11 ++++---- spec/mailtrap/marketing_stats_api_spec.rb | 33 ++++++++++++----------- spec/mailtrap/stats_api_spec.rb | 6 +++++ 5 files changed, 37 insertions(+), 29 deletions(-) diff --git a/examples/marketing_stats_api.rb b/examples/marketing_stats_api.rb index 0dd72021..dddafaad 100644 --- a/examples/marketing_stats_api.rb +++ b/examples/marketing_stats_api.rb @@ -7,7 +7,7 @@ today = Date.today # Get aggregated Email Marketing stats (campaigns and automations) for the last 30 days -marketing_stats.get(start_date: today - 30, end_date: today) +marketing_stats.get(start_date: today - 29, end_date: today) # => # stats_attributes }.to_json, headers: { 'Content-Type' => 'application/json' } } + end describe '#get' do subject(:stats) { marketing_stats_api.get(start_date: '2026-05-01', end_date: '2026-05-31') } it 'returns a MarketingStats object' do stub = stub_request(:get, "#{base_url}?start_date=2026-05-01&end_date=2026-05-31") - .to_return( - status: 200, - body: { 'data' => stats_attributes }.to_json, - headers: { 'Content-Type' => 'application/json' } - ) + .to_return(success_response) expect(stats).to be_a(Mailtrap::MarketingStats) expect(stats).to match_struct(stats_attributes) @@ -55,11 +54,7 @@ "#{base_url}?start_date=2026-05-01&end_date=2026-05-31" \ '&sending_domain_ids[]=3938&sending_domain_ids[]=3939&sources[]=campaigns' \ '&email_service_providers[]=Google&email_service_providers[]=Yahoo' - ).to_return( - status: 200, - body: { 'data' => stats_attributes }.to_json, - headers: { 'Content-Type' => 'application/json' } - ) + ).to_return(success_response) expect(stats).to have_attributes(delivery_count: 1200, sent_count: 1250) expect(stub).to have_been_requested @@ -68,17 +63,25 @@ it 'sends DateTime and Time arguments as plain dates' do stub = stub_request(:get, "#{base_url}?start_date=2026-05-01&end_date=2026-05-31") - .to_return( - status: 200, - body: { 'data' => stats_attributes }.to_json, - headers: { 'Content-Type' => 'application/json' } - ) + .to_return(success_response) marketing_stats_api.get(start_date: DateTime.new(2026, 5, 1, 13, 45), end_date: Time.new(2026, 5, 31, 23, 59)) expect(stub).to have_been_requested end + it 'leaves out empty filters' do + allow(marketing_stats_api.client).to receive(:get).and_return(data: stats_attributes) + + marketing_stats_api.get(start_date: '2026-05-01', end_date: '2026-05-31', sending_domain_ids: [], + sources: ['campaigns']) + + expect(marketing_stats_api.client).to have_received(:get).with( + '/api/marketing/stats', + { start_date: '2026-05-01', end_date: '2026-05-31', 'sources[]' => ['campaigns'] } + ) + end + it 'raises ArgumentError for an invalid date' do expect { marketing_stats_api.get(start_date: '2026-05-01', end_date: '2026-13-01') } .to raise_error(ArgumentError, /Invalid date: "2026-13-01"/) diff --git a/spec/mailtrap/stats_api_spec.rb b/spec/mailtrap/stats_api_spec.rb index 5400539b..378a1959 100644 --- a/spec/mailtrap/stats_api_spec.rb +++ b/spec/mailtrap/stats_api_spec.rb @@ -199,6 +199,12 @@ expect(stats).to be_a(Mailtrap::SendingStats) end + it 'sends DateTime objects as plain dates', + vcr: { cassette_name: 'Mailtrap_StatsAPI/_get/returns_aggregated_sending_stats' } do + stats = stats_api.get(start_date: DateTime.new(2026, 1, 1, 13, 45), end_date: DateTime.new(2026, 1, 31, 23, 59)) + expect(stats).to be_a(Mailtrap::SendingStats) + end + it 'raises ArgumentError for invalid start_date' do expect { stats_api.get(start_date: 'not-a-date', end_date: end_date) } .to raise_error(ArgumentError, /Invalid date: "not-a-date"/)