mailersend-ruby 3.0.1 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +2 -0
  3. data/.github/workflows/main.yml +1 -1
  4. data/.github/workflows/publish_gem.yml +1 -1
  5. data/.gitignore +2 -1
  6. data/CHANGELOG.md +12 -0
  7. data/README.md +534 -0
  8. data/fixtures/dmarc_monitoring/add_favorite.yml +31 -0
  9. data/fixtures/dmarc_monitoring/create.yml +34 -0
  10. data/fixtures/dmarc_monitoring/delete.yml +31 -0
  11. data/fixtures/dmarc_monitoring/list.yml +32 -0
  12. data/fixtures/dmarc_monitoring/remove_favorite.yml +31 -0
  13. data/fixtures/dmarc_monitoring/report.yml +31 -0
  14. data/fixtures/dmarc_monitoring/report_by_ip.yml +31 -0
  15. data/fixtures/dmarc_monitoring/report_sources.yml +31 -0
  16. data/fixtures/dmarc_monitoring/update.yml +34 -0
  17. data/fixtures/email/email_send_template_language.yml +56 -0
  18. data/fixtures/emails/emails_list_emails.yml +35 -0
  19. data/fixtures/emails/emails_list_emails_empty_page.yml +35 -0
  20. data/fixtures/emails/emails_single_email.yml +35 -0
  21. data/fixtures/whatsapp/whatsapp_send.yml +47 -0
  22. data/fixtures/whatsapp/whatsapp_send_multiple_recipients.yml +47 -0
  23. data/fixtures/whatsapp/whatsapp_send_personalization.yml +47 -0
  24. data/fixtures/whatsapp_activities/whatsapp_activities_list.yml +43 -0
  25. data/fixtures/whatsapp_inbound_messages/whatsapp_inbound_messages_get.yml +43 -0
  26. data/fixtures/whatsapp_inbound_messages/whatsapp_inbound_messages_list.yml +43 -0
  27. data/fixtures/whatsapp_items/whatsapp_items_get.yml +43 -0
  28. data/fixtures/whatsapp_items/whatsapp_items_list.yml +43 -0
  29. data/fixtures/whatsapp_messages/whatsapp_messages_get.yml +43 -0
  30. data/fixtures/whatsapp_messages/whatsapp_messages_list.yml +43 -0
  31. data/fixtures/whatsapp_recipient/whatsapp_recipient_get.yml +43 -0
  32. data/fixtures/whatsapp_recipient/whatsapp_recipient_list.yml +43 -0
  33. data/fixtures/whatsapp_templates/whatsapp_templates_get.yml +43 -0
  34. data/fixtures/whatsapp_templates/whatsapp_templates_list.yml +43 -0
  35. data/flake.lock +27 -0
  36. data/flake.nix +25 -0
  37. data/lefthook.yml +14 -0
  38. data/lib/mailersend/dmarc_monitoring/dmarc_monitoring.rb +59 -0
  39. data/lib/mailersend/email/email.rb +10 -0
  40. data/lib/mailersend/emails/emails.rb +44 -0
  41. data/lib/mailersend/version.rb +1 -1
  42. data/lib/mailersend/whatsapp/whatsapp.rb +47 -0
  43. data/lib/mailersend/whatsapp_activities/whatsapp_activities.rb +45 -0
  44. data/lib/mailersend/whatsapp_inbound_messages/whatsapp_inbound_messages.rb +47 -0
  45. data/lib/mailersend/whatsapp_items/whatsapp_items.rb +57 -0
  46. data/lib/mailersend/whatsapp_messages/whatsapp_messages.rb +47 -0
  47. data/lib/mailersend/whatsapp_recipient/whatsapp_recipient.rb +38 -0
  48. data/lib/mailersend/whatsapp_templates/whatsapp_templates.rb +44 -0
  49. data/lib/mailersend.rb +9 -0
  50. data/spec/dmarc_monitoring_rspec.rb +100 -0
  51. data/spec/email_rspec.rb +15 -0
  52. data/spec/emails_rspec.rb +175 -0
  53. data/spec/whatsapp_activities_rspec.rb +38 -0
  54. data/spec/whatsapp_inbound_messages_rspec.rb +49 -0
  55. data/spec/whatsapp_items_rspec.rb +52 -0
  56. data/spec/whatsapp_messages_rspec.rb +50 -0
  57. data/spec/whatsapp_recipient_rspec.rb +40 -0
  58. data/spec/whatsapp_rspec.rb +65 -0
  59. data/spec/whatsapp_templates_rspec.rb +48 -0
  60. metadata +52 -3
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailersend
4
+ # WhatsApp Activities endpoint from MailerSend API.
5
+ class WhatsAppActivities
6
+ attr_accessor :client,
7
+ :whatsapp_item_id,
8
+ :whatsapp_message_id,
9
+ :recipient,
10
+ :type,
11
+ :date_from,
12
+ :date_to,
13
+ :page,
14
+ :limit
15
+
16
+ def initialize(client = Mailersend::Client.new)
17
+ @client = client
18
+ @whatsapp_item_id = whatsapp_item_id
19
+ @whatsapp_message_id = whatsapp_message_id
20
+ @recipient = recipient
21
+ @type = type
22
+ @date_from = date_from
23
+ @date_to = date_to
24
+ @page = page
25
+ @limit = limit
26
+ end
27
+
28
+ def list(whatsapp_item_id: nil, whatsapp_message_id: nil, recipient: nil, type: nil, date_from: nil, date_to: nil,
29
+ page: nil, limit: nil)
30
+ hash = {
31
+ 'whatsapp_item_id' => whatsapp_item_id,
32
+ 'whatsapp_message_id' => whatsapp_message_id,
33
+ 'recipient' => recipient,
34
+ 'type[]' => type,
35
+ 'date_from' => date_from,
36
+ 'date_to' => date_to,
37
+ 'page' => page,
38
+ 'limit' => limit
39
+ }
40
+
41
+ client.http.get(URI::HTTPS.build(host: MAILERSEND_API_BASE_HOST, path: '/v1/whatsapp/activities',
42
+ query: URI.encode_www_form(hash.compact)))
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailersend
4
+ # WhatsApp Inbound Messages endpoint from MailerSend API.
5
+ class WhatsAppInboundMessages
6
+ attr_accessor :client,
7
+ :whatsapp_account_id,
8
+ :from,
9
+ :type,
10
+ :date_from,
11
+ :date_to,
12
+ :page,
13
+ :limit,
14
+ :whatsapp_inbound_message_id
15
+
16
+ def initialize(client = Mailersend::Client.new)
17
+ @client = client
18
+ @whatsapp_account_id = whatsapp_account_id
19
+ @from = from
20
+ @type = type
21
+ @date_from = date_from
22
+ @date_to = date_to
23
+ @page = page
24
+ @limit = limit
25
+ @whatsapp_inbound_message_id = whatsapp_inbound_message_id
26
+ end
27
+
28
+ def list(whatsapp_account_id: nil, from: nil, type: nil, date_from: nil, date_to: nil, page: nil, limit: nil)
29
+ hash = {
30
+ 'whatsapp_account_id' => whatsapp_account_id,
31
+ 'from' => from,
32
+ 'type[]' => type,
33
+ 'date_from' => date_from,
34
+ 'date_to' => date_to,
35
+ 'page' => page,
36
+ 'limit' => limit
37
+ }
38
+
39
+ client.http.get(URI::HTTPS.build(host: MAILERSEND_API_BASE_HOST, path: '/v1/whatsapp/inbound-messages',
40
+ query: URI.encode_www_form(hash.compact)))
41
+ end
42
+
43
+ def get(whatsapp_inbound_message_id:)
44
+ client.http.get("#{MAILERSEND_API_URL}/whatsapp/inbound-messages/#{whatsapp_inbound_message_id}")
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailersend
4
+ # WhatsApp Items endpoint from MailerSend API.
5
+ class WhatsAppItems
6
+ attr_accessor :client,
7
+ :recipient,
8
+ :whatsapp_message_id,
9
+ :status,
10
+ :whatsapp_account_id,
11
+ :from,
12
+ :template_id,
13
+ :date_from,
14
+ :date_to,
15
+ :page,
16
+ :limit,
17
+ :whatsapp_item_id
18
+
19
+ def initialize(client = Mailersend::Client.new)
20
+ @client = client
21
+ @recipient = recipient
22
+ @whatsapp_message_id = whatsapp_message_id
23
+ @status = status
24
+ @whatsapp_account_id = whatsapp_account_id
25
+ @from = from
26
+ @template_id = template_id
27
+ @date_from = date_from
28
+ @date_to = date_to
29
+ @page = page
30
+ @limit = limit
31
+ @whatsapp_item_id = whatsapp_item_id
32
+ end
33
+
34
+ def list(recipient: nil, whatsapp_message_id: nil, status: nil, whatsapp_account_id: nil, from: nil,
35
+ template_id: nil, date_from: nil, date_to: nil, page: nil, limit: nil)
36
+ hash = {
37
+ 'recipient' => recipient,
38
+ 'whatsapp_message_id' => whatsapp_message_id,
39
+ 'status[]' => status,
40
+ 'whatsapp_account_id' => whatsapp_account_id,
41
+ 'from' => from,
42
+ 'template_id' => template_id,
43
+ 'date_from' => date_from,
44
+ 'date_to' => date_to,
45
+ 'page' => page,
46
+ 'limit' => limit
47
+ }
48
+
49
+ client.http.get(URI::HTTPS.build(host: MAILERSEND_API_BASE_HOST, path: '/v1/whatsapp/items',
50
+ query: URI.encode_www_form(hash.compact)))
51
+ end
52
+
53
+ def get(whatsapp_item_id:)
54
+ client.http.get("#{MAILERSEND_API_URL}/whatsapp/items/#{whatsapp_item_id}")
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailersend
4
+ # WhatsApp Messages endpoint from MailerSend API.
5
+ class WhatsAppMessages
6
+ attr_accessor :client,
7
+ :whatsapp_account_id,
8
+ :from,
9
+ :template_id,
10
+ :date_from,
11
+ :date_to,
12
+ :page,
13
+ :limit,
14
+ :whatsapp_message_id
15
+
16
+ def initialize(client = Mailersend::Client.new)
17
+ @client = client
18
+ @whatsapp_account_id = whatsapp_account_id
19
+ @from = from
20
+ @template_id = template_id
21
+ @date_from = date_from
22
+ @date_to = date_to
23
+ @page = page
24
+ @limit = limit
25
+ @whatsapp_message_id = whatsapp_message_id
26
+ end
27
+
28
+ def list(whatsapp_account_id: nil, from: nil, template_id: nil, date_from: nil, date_to: nil, page: nil, limit: nil)
29
+ hash = {
30
+ 'whatsapp_account_id' => whatsapp_account_id,
31
+ 'from' => from,
32
+ 'template_id' => template_id,
33
+ 'date_from' => date_from,
34
+ 'date_to' => date_to,
35
+ 'page' => page,
36
+ 'limit' => limit
37
+ }
38
+
39
+ client.http.get(URI::HTTPS.build(host: MAILERSEND_API_BASE_HOST, path: '/v1/whatsapp/messages',
40
+ query: URI.encode_www_form(hash.compact)))
41
+ end
42
+
43
+ def get(whatsapp_message_id:)
44
+ client.http.get("#{MAILERSEND_API_URL}/whatsapp/messages/#{whatsapp_message_id}")
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailersend
4
+ # WhatsApp Recipient endpoint from MailerSend API.
5
+ class WhatsAppRecipient
6
+ attr_accessor :client,
7
+ :status,
8
+ :phone_number,
9
+ :page,
10
+ :limit,
11
+ :whatsapp_recipient_id
12
+
13
+ def initialize(client = Mailersend::Client.new)
14
+ @client = client
15
+ @status = status
16
+ @phone_number = phone_number
17
+ @page = page
18
+ @limit = limit
19
+ @whatsapp_recipient_id = whatsapp_recipient_id
20
+ end
21
+
22
+ def list(status: nil, phone_number: nil, page: nil, limit: nil)
23
+ hash = {
24
+ 'status' => status,
25
+ 'phone_number' => phone_number,
26
+ 'page' => page,
27
+ 'limit' => limit
28
+ }
29
+
30
+ client.http.get(URI::HTTPS.build(host: MAILERSEND_API_BASE_HOST, path: '/v1/whatsapp/recipients',
31
+ query: URI.encode_www_form(hash.compact)))
32
+ end
33
+
34
+ def get(whatsapp_recipient_id:)
35
+ client.http.get("#{MAILERSEND_API_URL}/whatsapp/recipients/#{whatsapp_recipient_id}")
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailersend
4
+ # WhatsApp Templates endpoint from MailerSend API.
5
+ class WhatsAppTemplates
6
+ attr_accessor :client,
7
+ :whatsapp_account_id,
8
+ :status,
9
+ :category,
10
+ :language,
11
+ :page,
12
+ :limit,
13
+ :whatsapp_template_id
14
+
15
+ def initialize(client = Mailersend::Client.new)
16
+ @client = client
17
+ @whatsapp_account_id = whatsapp_account_id
18
+ @status = status
19
+ @category = category
20
+ @language = language
21
+ @page = page
22
+ @limit = limit
23
+ @whatsapp_template_id = whatsapp_template_id
24
+ end
25
+
26
+ def list(whatsapp_account_id: nil, status: nil, category: nil, language: nil, page: nil, limit: nil)
27
+ hash = {
28
+ 'whatsapp_account_id' => whatsapp_account_id,
29
+ 'status' => status,
30
+ 'category' => category,
31
+ 'language' => language,
32
+ 'page' => page,
33
+ 'limit' => limit
34
+ }
35
+
36
+ client.http.get(URI::HTTPS.build(host: MAILERSEND_API_BASE_HOST, path: '/v1/whatsapp/templates',
37
+ query: URI.encode_www_form(hash.compact)))
38
+ end
39
+
40
+ def get(whatsapp_template_id:)
41
+ client.http.get("#{MAILERSEND_API_URL}/whatsapp/templates/#{whatsapp_template_id}")
42
+ end
43
+ end
44
+ end
data/lib/mailersend.rb CHANGED
@@ -6,15 +6,24 @@ require_relative 'mailersend/activity/activity'
6
6
  require_relative 'mailersend/api_quota/api_quota'
7
7
  require_relative 'mailersend/analytics/analytics'
8
8
  require_relative 'mailersend/bulk_email/bulk_email'
9
+ require_relative 'mailersend/dmarc_monitoring/dmarc_monitoring'
9
10
  require_relative 'mailersend/domains/domains'
10
11
  require_relative 'mailersend/email/email'
11
12
  require_relative 'mailersend/email_verification/email_verification'
13
+ require_relative 'mailersend/emails/emails'
12
14
  require_relative 'mailersend/inbound_routing/inbound_routing'
13
15
  require_relative 'mailersend/messages/messages'
14
16
  require_relative 'mailersend/recipients/recipients'
15
17
  require_relative 'mailersend/scheduled_messages/scheduled_messages'
16
18
  require_relative 'mailersend/sender_identity/sender_identity'
17
19
  require_relative 'mailersend/sms/sms'
20
+ require_relative 'mailersend/whatsapp/whatsapp'
21
+ require_relative 'mailersend/whatsapp_messages/whatsapp_messages'
22
+ require_relative 'mailersend/whatsapp_items/whatsapp_items'
23
+ require_relative 'mailersend/whatsapp_activities/whatsapp_activities'
24
+ require_relative 'mailersend/whatsapp_inbound_messages/whatsapp_inbound_messages'
25
+ require_relative 'mailersend/whatsapp_recipient/whatsapp_recipient'
26
+ require_relative 'mailersend/whatsapp_templates/whatsapp_templates'
18
27
  require_relative 'mailersend/sms_activity/sms_activity'
19
28
  require_relative 'mailersend/sms_messages/sms_messages'
20
29
  require_relative 'mailersend/sms_number/sms_number'
@@ -0,0 +1,100 @@
1
+ require 'rspec'
2
+ require 'vcr'
3
+ require 'json'
4
+
5
+ VCR.configure do |config|
6
+ config.cassette_library_dir = './fixtures'
7
+ config.hook_into :webmock
8
+ config.filter_sensitive_data('<AUTH>') do |interaction|
9
+ interaction.request.headers['Authorization'][0]
10
+ end
11
+ end
12
+
13
+ RSpec.describe Mailersend::DmarcMonitoring do
14
+ let(:client) { Mailersend::Client.new(API_TOKEN) }
15
+ let(:dmarc) { Mailersend::DmarcMonitoring.new(client) }
16
+
17
+ it 'returns a list of monitors' do
18
+ VCR.use_cassette('dmarc_monitoring/list') do
19
+ response = dmarc.list
20
+ parsed_response = JSON.parse(response.body)
21
+
22
+ expect(response.status).to eq(200)
23
+ expect(parsed_response['data']).to be_an(Array)
24
+ end
25
+ end
26
+
27
+ it 'creates a monitor' do
28
+ VCR.use_cassette('dmarc_monitoring/create') do
29
+ response = dmarc.create(domain_id: 'example_domain_id')
30
+ parsed_response = JSON.parse(response.body)
31
+
32
+ expect(response.status).to eq(201)
33
+ expect(parsed_response['data']).to be_a(Hash)
34
+ end
35
+ end
36
+
37
+ it 'updates a monitor' do
38
+ VCR.use_cassette('dmarc_monitoring/update') do
39
+ response = dmarc.update(monitor_id: 'example_monitor_id', wanted_dmarc_record: 'v=DMARC1; p=reject;')
40
+ parsed_response = JSON.parse(response.body)
41
+
42
+ expect(response.status).to eq(200)
43
+ expect(parsed_response['data']).to be_a(Hash)
44
+ end
45
+ end
46
+
47
+ it 'deletes a monitor' do
48
+ VCR.use_cassette('dmarc_monitoring/delete') do
49
+ response = dmarc.delete(monitor_id: 'example_monitor_id')
50
+
51
+ expect(response.status).to eq(204)
52
+ end
53
+ end
54
+
55
+ it 'returns aggregated report' do
56
+ VCR.use_cassette('dmarc_monitoring/report') do
57
+ response = dmarc.report(monitor_id: 'example_monitor_id')
58
+ parsed_response = JSON.parse(response.body)
59
+
60
+ expect(response.status).to eq(200)
61
+ expect(parsed_response['data']).to be_an(Array)
62
+ end
63
+ end
64
+
65
+ it 'returns IP-specific report' do
66
+ VCR.use_cassette('dmarc_monitoring/report_by_ip') do
67
+ response = dmarc.report_by_ip(monitor_id: 'example_monitor_id', ip: '1.2.3.4')
68
+ parsed_response = JSON.parse(response.body)
69
+
70
+ expect(response.status).to eq(200)
71
+ expect(parsed_response['data']).to be_an(Array)
72
+ end
73
+ end
74
+
75
+ it 'returns report sources' do
76
+ VCR.use_cassette('dmarc_monitoring/report_sources') do
77
+ response = dmarc.report_sources(monitor_id: 'example_monitor_id')
78
+ parsed_response = JSON.parse(response.body)
79
+
80
+ expect(response.status).to eq(200)
81
+ expect(parsed_response['data']).to be_an(Array)
82
+ end
83
+ end
84
+
85
+ it 'marks an IP as favorite' do
86
+ VCR.use_cassette('dmarc_monitoring/add_favorite') do
87
+ response = dmarc.add_favorite(monitor_id: 'example_monitor_id', ip: '1.2.3.4')
88
+
89
+ expect(response.status).to eq(200)
90
+ end
91
+ end
92
+
93
+ it 'removes an IP from favorites' do
94
+ VCR.use_cassette('dmarc_monitoring/remove_favorite') do
95
+ response = dmarc.remove_favorite(monitor_id: 'example_monitor_id', ip: '1.2.3.4')
96
+
97
+ expect(response.status).to eq(200)
98
+ end
99
+ end
100
+ end
data/spec/email_rspec.rb CHANGED
@@ -26,4 +26,19 @@ RSpec.describe Mailersend::Email do
26
26
  expect(response.status).to eq(202)
27
27
  end
28
28
  end
29
+
30
+ it 'includes the language in the request body when set' do
31
+ # Cassette is matched on the request body, so this passes only when
32
+ # the serialized payload contains the language field.
33
+ VCR.use_cassette('email/email_send_template_language', match_requests_on: %i[method uri body]) do
34
+ email.from = { 'email' => 'sender@test-sdk.com', 'name' => 'Sender' }
35
+ email.recipients = [{ 'email' => 'test@mailerlite.com', 'name' => 'Test' }]
36
+ email.subject = 'Test Email'
37
+ email.add_template_id('123abc')
38
+ email.add_language('de')
39
+
40
+ response = email.send
41
+ expect(response.status).to eq(202)
42
+ end
43
+ end
29
44
  end
@@ -0,0 +1,175 @@
1
+ require 'rspec'
2
+ require 'vcr'
3
+ require 'json'
4
+
5
+ VCR.configure do |config|
6
+ config.cassette_library_dir = './fixtures'
7
+ config.hook_into :webmock
8
+ config.filter_sensitive_data('<AUTH>') do |interaction|
9
+ interaction.request.headers['Authorization'][0]
10
+ end
11
+ end
12
+
13
+ RSpec.describe Mailersend::Emails do
14
+ let(:client) { Mailersend::Client.new(API_TOKEN) }
15
+ let(:emails) { Mailersend::Emails.new(client) }
16
+
17
+ # The three required arguments, reused by every example.
18
+ let(:domain_id) { 'jpzkmgq7e5vl059v' }
19
+ let(:date_from) { 1_756_252_800 }
20
+ let(:date_to) { 1_756_339_200 }
21
+
22
+ describe '#list' do
23
+ # VCR matches on method + URI, so the `uri:` recorded in the cassette is itself the
24
+ # assertion that `list` hits `v1/emails` with domain_id, date_from, date_to, limit and page.
25
+ it 'returns a list of emails' do
26
+ VCR.use_cassette('emails/emails_list_emails', match_requests_on: %i[method uri]) do
27
+ response = emails.list(domain_id: domain_id, date_from: date_from, date_to: date_to, page: 1, limit: 10)
28
+ parsed_response = JSON.parse(response.body)
29
+
30
+ expect(response.status).to eq(200)
31
+ expect(parsed_response['data']).to be_an(Array)
32
+
33
+ email = parsed_response['data'].first
34
+
35
+ expect(email['id']).to eq('6a8fa9b1902fab56e0ce50dd')
36
+ expect(email['from']).to eq('sender@example.com')
37
+ expect(email['to']).to eq('rcpt@example.org')
38
+ expect(email['status']).to eq('sent')
39
+ expect(email['tags']).to eq(['newsletter'])
40
+ expect(email['interaction']).to eq(['opened'])
41
+ expect(email['suppression_reason']).to be_nil
42
+ expect(email['headers']).to eq([{ 'name' => 'X-Custom', 'value' => 'foo' }])
43
+
44
+ # List rows never carry content — it is only returned by `single`.
45
+ expect(email['text']).to be_nil
46
+ expect(email['html']).to be_nil
47
+
48
+ expect(parsed_response['links']['first']).to eq('https://api.mailersend.com/v1/emails?page=1')
49
+ expect(parsed_response['links']['last']).to be_nil
50
+ expect(parsed_response['meta']['current_page']).to eq(1)
51
+ expect(parsed_response['meta']['per_page']).to eq(10)
52
+ end
53
+ end
54
+
55
+ it 'returns an empty page past the end of the result set' do
56
+ VCR.use_cassette('emails/emails_list_emails_empty_page', match_requests_on: %i[method uri]) do
57
+ response = emails.list(domain_id: domain_id, date_from: date_from, date_to: date_to, page: 2, limit: 10)
58
+ parsed_response = JSON.parse(response.body)
59
+
60
+ expect(response.status).to eq(200)
61
+ expect(parsed_response['data']).to eq([])
62
+ expect(parsed_response['links']['prev']).to eq('https://api.mailersend.com/v1/emails?page=1')
63
+ expect(parsed_response['links']['next']).to be_nil
64
+ expect(parsed_response['meta']['current_page']).to eq(2)
65
+ expect(parsed_response['meta']['from']).to be_nil
66
+ expect(parsed_response['meta']['to']).to be_nil
67
+ end
68
+ end
69
+ end
70
+
71
+ describe '#single' do
72
+ # The path is singular (`v1/email/{id}`) while `list` is plural (`v1/emails`); the cassette
73
+ # URI pins that difference.
74
+ it 'returns a single email with its recipient and activity' do
75
+ VCR.use_cassette('emails/emails_single_email', match_requests_on: %i[method uri]) do
76
+ response = emails.single(email_id: '6a8fa9b1902fab56e0ce50dd')
77
+ parsed_response = JSON.parse(response.body)
78
+
79
+ expect(response.status).to eq(200)
80
+ expect(parsed_response['data']).to be_a(Hash)
81
+ expect(parsed_response['data']['id']).to eq('6a8fa9b1902fab56e0ce50dd')
82
+
83
+ # `single` adds content, the recipient object and the activity array on top of a list row.
84
+ expect(parsed_response['data']['text']).to eq('Welcome aboard.')
85
+ expect(parsed_response['data']['html']).to eq('<p>Welcome aboard.</p>')
86
+ expect(parsed_response['data']['recipient']).to be_a(Hash)
87
+ expect(parsed_response['data']['recipient']['email']).to eq('rcpt@example.org')
88
+
89
+ activity = parsed_response['data']['activity']
90
+
91
+ expect(activity).to be_an(Array)
92
+ expect(activity.length).to eq(3)
93
+ expect(activity.first).to include('id', 'type', 'created_at')
94
+ expect(activity.first['type']).to eq('opened')
95
+ expect(activity.map { |event| event['type'] }).to eq(%w[opened delivered sent])
96
+ end
97
+ end
98
+ end
99
+
100
+ # These examples assert the URI the SDK builds rather than replaying a cassette, because query
101
+ # serialization is the contract that matters most here: the API rejects a scalar `status` or
102
+ # `interaction` with 422, so both must go out as repeated `status[]` / `interaction[]` params and
103
+ # never as a single comma-joined value.
104
+ describe 'query serialization' do
105
+ let(:http) { double('http') }
106
+ let(:requested_urls) { [] }
107
+
108
+ before do
109
+ allow(client).to receive(:http).and_return(http)
110
+ allow(http).to receive(:get) { |url| requested_urls << url.to_s }
111
+ end
112
+
113
+ it 'sends status and interaction as repeated array params' do
114
+ emails.list(domain_id: domain_id, date_from: date_from, date_to: date_to, limit: 10, page: 1,
115
+ status: %w[sent delivered], interaction: %w[opened clicked])
116
+
117
+ expect(requested_urls.last).to eq(
118
+ 'https://api.mailersend.com/v1/emails' \
119
+ '?domain_id=jpzkmgq7e5vl059v&date_from=1756252800&date_to=1756339200&limit=10&page=1' \
120
+ '&status%5B%5D=sent&status%5B%5D=delivered' \
121
+ '&interaction%5B%5D=opened&interaction%5B%5D=clicked'
122
+ )
123
+
124
+ # One key per value, and no comma-joined value anywhere (%2C is an encoded comma).
125
+ expect(requested_urls.last.scan('status%5B%5D=').length).to eq(2)
126
+ expect(requested_urls.last.scan('interaction%5B%5D=').length).to eq(2)
127
+ expect(requested_urls.last).not_to include('%2C')
128
+ end
129
+
130
+ it 'sends a scalar status and interaction in array form too' do
131
+ emails.list(domain_id: domain_id, date_from: date_from, date_to: date_to,
132
+ status: 'sent', interaction: 'opened')
133
+
134
+ expect(requested_urls.last).to eq(
135
+ 'https://api.mailersend.com/v1/emails' \
136
+ '?domain_id=jpzkmgq7e5vl059v&date_from=1756252800&date_to=1756339200' \
137
+ '&status%5B%5D=sent&interaction%5B%5D=opened'
138
+ )
139
+ end
140
+
141
+ it 'includes every optional filter when set' do
142
+ emails.list(domain_id: domain_id, date_from: date_from, date_to: date_to, limit: 25, page: 2,
143
+ status: %w[sent], interaction: %w[opened], recipient_email: 'rcpt@example.org',
144
+ message_id: '6a8fa9b1902fab56e0ce50aa', template_id: '7nxe3yjmeq28vp0k',
145
+ subject: 'Welcome', tag: 'newsletter')
146
+
147
+ expect(requested_urls.last).to eq(
148
+ 'https://api.mailersend.com/v1/emails' \
149
+ '?domain_id=jpzkmgq7e5vl059v&date_from=1756252800&date_to=1756339200&limit=25&page=2' \
150
+ '&status%5B%5D=sent&interaction%5B%5D=opened' \
151
+ '&recipient_email=rcpt%40example.org&message_id=6a8fa9b1902fab56e0ce50aa' \
152
+ '&template_id=7nxe3yjmeq28vp0k&subject=Welcome&tag=newsletter'
153
+ )
154
+ end
155
+
156
+ it 'omits every optional filter that is nil' do
157
+ emails.list(domain_id: domain_id, date_from: date_from, date_to: date_to)
158
+
159
+ expect(requested_urls.last).to eq(
160
+ 'https://api.mailersend.com/v1/emails?domain_id=jpzkmgq7e5vl059v&date_from=1756252800&date_to=1756339200'
161
+ )
162
+
163
+ %w[limit page status interaction recipient_email message_id template_id subject tag].each do |param|
164
+ expect(requested_urls.last).not_to include(param)
165
+ end
166
+ end
167
+
168
+ it 'requests the singular v1/email/{id} path for a single email' do
169
+ emails.single(email_id: '6a8fa9b1902fab56e0ce50dd')
170
+
171
+ expect(requested_urls.last).to eq('https://api.mailersend.com/v1/email/6a8fa9b1902fab56e0ce50dd')
172
+ expect(requested_urls.last).not_to include('/v1/emails/')
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,38 @@
1
+ require 'rspec'
2
+ require 'vcr'
3
+ require 'json'
4
+
5
+ VCR.configure do |config|
6
+ config.cassette_library_dir = './fixtures'
7
+ config.hook_into :webmock
8
+ config.filter_sensitive_data('<AUTH>') do |interaction|
9
+ interaction.request.headers['Authorization'][0]
10
+ end
11
+ end
12
+
13
+ RSpec.describe Mailersend::WhatsAppActivities do
14
+ let(:client) { Mailersend::Client.new(API_TOKEN) }
15
+ let(:whatsapp_activities) { Mailersend::WhatsAppActivities.new(client) }
16
+
17
+ it 'returns a list of WhatsApp activities' do
18
+ VCR.use_cassette('whatsapp_activities/whatsapp_activities_list', match_requests_on: %i[method host path query]) do
19
+ response = whatsapp_activities.list(
20
+ whatsapp_item_id: '67f91abe2202f3705540234a',
21
+ whatsapp_message_id: '67f91abd69f79df391e9d78d',
22
+ recipient: '48600000001',
23
+ type: %w[delivered read],
24
+ date_from: 1_791_936_000,
25
+ date_to: 1_792_022_400,
26
+ page: 1,
27
+ limit: 25
28
+ )
29
+ parsed_response = JSON.parse(response.body)
30
+
31
+ expect(response.status).to eq(200)
32
+ expect(parsed_response['data']).to be_an(Array)
33
+ expect(parsed_response['data'].map { |activity| activity['type'] }).to eq(%w[read delivered])
34
+ expect(parsed_response['links']['last']).to be_nil
35
+ expect(parsed_response['meta']).not_to have_key('total')
36
+ end
37
+ end
38
+ end