From 0a21de3375f3d0df605de2e30d9bfd581b325205 Mon Sep 17 00:00:00 2001 From: sarco3t <14gainward88@gmail.com> Date: Mon, 14 Jul 2025 18:41:22 +0300 Subject: [PATCH 01/11] added ContactImportAPI --- examples/contacts_api.rb | 41 +++++++- lib/mailtrap.rb | 1 + lib/mailtrap/contact_import.rb | 23 +++++ lib/mailtrap/contact_imports_api.rb | 45 +++++++++ spec/mailtrap/contact_imports_api_spec.rb | 111 ++++++++++++++++++++++ 5 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 lib/mailtrap/contact_import.rb create mode 100644 lib/mailtrap/contact_imports_api.rb create mode 100644 spec/mailtrap/contact_imports_api_spec.rb diff --git a/examples/contacts_api.rb b/examples/contacts_api.rb index 6e48aba..3f2f534 100644 --- a/examples/contacts_api.rb +++ b/examples/contacts_api.rb @@ -4,6 +4,7 @@ contact_lists = Mailtrap::ContactListsAPI.new 3229, client contacts = Mailtrap::ContactsAPI.new 3229, client contact_fields = Mailtrap::ContactFieldsAPI.new 3229, client +contact_imports = Mailtrap::ContactImportsAPI.new 3229, client # Set your API credentials as environment variables # export MAILTRAP_API_KEY='your-api-key' @@ -12,6 +13,7 @@ # contact_lists = Mailtrap::ContactListsAPI.new # contacts = Mailtrap::ContactsAPI.new # contact_fields = Mailtrap::ContactFieldsAPI.new +# contact_imports = Mailtrap::ContactImportsAPI.new # Create new contact list list = contact_lists.create(name: 'Test List') @@ -135,8 +137,41 @@ # Delete contact contacts.delete(contact.id) -# Delete contact list -contact_lists.delete(list.id) - # Delete contact field contact_fields.delete(field.id) + +# Create a new contact import +contact_import = contact_imports.create( + [ + { + email: 'imported@example.com', + fields: { + first_name: 'Jane', + }, + list_ids_included: [list.id], + list_ids_excluded: [] + } + ] +) +# => ContactImport.new( +# id: 1, +# status: 'created', +# list_ids: [1], +# created_contacts_count: 1, +# updated_contacts_count: 0, +# contacts_over_limit_count: 0 +# ) + +# Get a contact import by ID +contact_imports.get(contact_import.id) +# => ContactImport.new( +# id: 1, +# status: 'started', +# list_ids: [1], +# created_contacts_count: 1, +# updated_contacts_count: 0, +# contacts_over_limit_count: 0 +# ) + +# Delete contact list +contact_lists.delete(list.id) diff --git a/lib/mailtrap.rb b/lib/mailtrap.rb index 33e4dad..9d3b856 100644 --- a/lib/mailtrap.rb +++ b/lib/mailtrap.rb @@ -8,6 +8,7 @@ require_relative 'mailtrap/contacts_api' require_relative 'mailtrap/contact_lists_api' require_relative 'mailtrap/contact_fields_api' +require_relative 'mailtrap/contact_imports_api' module Mailtrap # @!macro api_errors diff --git a/lib/mailtrap/contact_import.rb b/lib/mailtrap/contact_import.rb new file mode 100644 index 0000000..9194157 --- /dev/null +++ b/lib/mailtrap/contact_import.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Mailtrap + # Data Transfer Object for Contact Import + # @attr_reader id [String] The contact import ID + # @attr_reader status [String] The status of the import (created, started, finished, failed) + # @attr_reader created_contacts_count [Integer, nil] Number of contacts created in this import + # @attr_reader updated_contacts_count [Integer, nil] Number of contacts updated in this import + # @attr_reader contacts_over_limit_count [Integer, nil] Number of contacts over the allowed limit + ContactImport = Struct.new( + :id, + :status, + :created_contacts_count, + :updated_contacts_count, + :contacts_over_limit_count, + keyword_init: true + ) do + # @return [Hash] The contact attributes as a hash + def to_h + super.compact + end + end +end diff --git a/lib/mailtrap/contact_imports_api.rb b/lib/mailtrap/contact_imports_api.rb new file mode 100644 index 0000000..2445e39 --- /dev/null +++ b/lib/mailtrap/contact_imports_api.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require_relative 'contact_import' + +module Mailtrap + class ContactImportsAPI + include BaseAPI + + self.supported_options = %i[email fields list_ids_included list_ids_excluded] + + self.response_class = ContactImport + + # Retrieves a specific contact import + # @param import_id [String] The contact import identifier + # @return [ContactImport] Contact import object + # @!macro api_errors + def get(import_id) + base_get(import_id) + end + + # Create contacts import + # @param contacts [Array] Array of contact objects to import + # Each contact object should have the following keys: + # - email [String] The contact's email address + # - fields [Hash] Object of fields in the format: field_merge_tag => String, Integer, Float, Boolean, or ISO-8601 date string (yyyy-mm-dd) # rubocop:disable Layout/LineLength + # - list_ids_included [Array] List IDs to include the contact in + # - list_ids_excluded [Array] List IDs to exclude the contact from + # @return [ContactImport] Created contact list object + # @!macro api_errors + # @raise [ArgumentError] If invalid options are provided + def create(contacts) + contacts.each do |contact| + validate_options!(contact, supported_options) + end + response = client.post(base_path, contacts:) + handle_response(response) + end + + private + + def base_path + "/api/accounts/#{account_id}/contacts/imports" + end + end +end diff --git a/spec/mailtrap/contact_imports_api_spec.rb b/spec/mailtrap/contact_imports_api_spec.rb new file mode 100644 index 0000000..8694b61 --- /dev/null +++ b/spec/mailtrap/contact_imports_api_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::ContactImportsAPI do + let(:client) { described_class.new('1111111', Mailtrap::Client.new(api_key: 'correct-api-key')) } + let(:base_url) { 'https://mailtrap.io/api/accounts/1111111' } + + describe '#get' do + let(:import_id) { 'import-123' } + let(:expected_response) do + { + 'id' => 'import-123', + 'status' => 'finished', + 'created_contacts_count' => 10, + 'updated_contacts_count' => 2, + 'contacts_over_limit_count' => 0 + } + end + + it 'returns a specific contact import' do + stub_request(:get, "#{base_url}/contacts/imports/#{import_id}") + .to_return( + status: 200, + body: expected_response.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + response = client.get(import_id) + expect(response).to have_attributes( + id: 'import-123', + status: 'finished', + created_contacts_count: 10, + updated_contacts_count: 2, + contacts_over_limit_count: 0 + ) + end + + it 'raises error when contact import not found' do + stub_request(:get, "#{base_url}/contacts/imports/not-found") + .to_return( + status: 404, + body: { 'error' => 'Not Found' }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + expect { client.get('not-found') }.to raise_error(Mailtrap::Error) + end + end + + describe '#create' do + let(:contacts) do + [ + { + email: 'example@example.com', + fields: { + fname: 'John', + age: 30, + is_subscribed: true, + birthday: '1990-05-15' + }, + list_ids_included: [1, 2], + list_ids_excluded: [3] + } + ] + end + let(:expected_response) do + { + 'id' => 'import-456', + 'status' => 'created', + 'created_contacts_count' => 1, + 'updated_contacts_count' => 0, + 'contacts_over_limit_count' => 0 + } + end + + it 'creates a new contact import with hash' do + stub_request(:post, "#{base_url}/contacts/imports") + .with(body: { contacts: }.to_json) + .to_return( + status: 200, + body: expected_response.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + response = client.create(contacts) + expect(response).to have_attributes( + id: 'import-456', + status: 'created', + created_contacts_count: 1, + updated_contacts_count: 0, + contacts_over_limit_count: 0 + ) + end + + it 'raises error when invalid options are provided' do + invalid_contacts = [contacts.first.merge(foo: 'bar')] + expect { client.create(invalid_contacts) }.to raise_error(ArgumentError, /invalid options are given/) + end + + it 'raises error when API returns an error' do + stub_request(:post, "#{base_url}/contacts/imports") + .with(body: { contacts: }.to_json) + .to_return( + status: 422, + body: { 'errors' => { 'email' => ['is invalid'] } }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + expect { client.create(contacts) }.to raise_error(Mailtrap::Error) + end + end +end From 42839bc07b9d6c88b4d70567df09d828ee0bdd5b Mon Sep 17 00:00:00 2001 From: sarco3t <14gainward88@gmail.com> Date: Wed, 30 Jul 2025 11:26:37 +0300 Subject: [PATCH 02/11] added ContactsImportRequest --- examples/contacts_api.rb | 56 +++++++++++++++++ lib/mailtrap/contact_imports_api.rb | 12 ++-- lib/mailtrap/contacts_import_request.rb | 63 +++++++++++++++++++ spec/mailtrap/contact_imports_api_spec.rb | 77 +++++++++++++++++++++++ 4 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 lib/mailtrap/contacts_import_request.rb diff --git a/examples/contacts_api.rb b/examples/contacts_api.rb index 3f2f534..bebe9bd 100644 --- a/examples/contacts_api.rb +++ b/examples/contacts_api.rb @@ -173,5 +173,61 @@ # contacts_over_limit_count: 0 # ) +# Create a new contact import using ContactsImportRequest builder +import_request = Mailtrap::ContactsImportRequest.new + +# Add contacts using the builder pattern +import_request + .upsert( + email: 'john.doe@example.com', + fields: { first_name: 'John' } + ) + .add_to_lists(email: 'john.doe@example.com', list_ids: [list.id]) + .upsert( + email: 'jane.smith@example.com', + fields: { first_name: 'Jane' } + ) + .add_to_lists(email: 'jane.smith@example.com', list_ids: [list.id]) + .remove_from_lists(email: 'jane.smith@example.com', list_ids: []) + +# Execute the import +contact_imports.create(import_request) +# => ContactImport.new( +# id: 2, +# status: 'created', +# list_ids: [1], +# created_contacts_count: 2, +# updated_contacts_count: 0, +# contacts_over_limit_count: 0 +# ) + +# Alternative: Step-by-step building +builder = Mailtrap::ContactsImportRequest.new +builder.upsert(email: 'jane.doe@example.com', fields: { first_name: 'Jane' }) +builder.add_to_lists(email: 'jane.doe@example.com', list_ids: [list.id]) + +contact_import_2 = contact_imports.create(builder) +# => ContactImport.new( +# id: 3, +# status: 'created', +# list_ids: [1], +# created_contacts_count: 1, +# updated_contacts_count: 0, +# contacts_over_limit_count: 0 +# ) + +sleep 3 # Wait for the import to complete (if needed) + +# Get the import status +contact_imports.get(contact_import_2.id) +# => ContactImport.new( +# id: 3, +# status: 'completed', +# list_ids: [1], +# created_contacts_count: 1, +# updated_contacts_count: 0, +# contacts_over_limit_count: 0 +# ) + # Delete contact list contact_lists.delete(list.id) diff --git a/lib/mailtrap/contact_imports_api.rb b/lib/mailtrap/contact_imports_api.rb index 2445e39..dcf54f7 100644 --- a/lib/mailtrap/contact_imports_api.rb +++ b/lib/mailtrap/contact_imports_api.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative 'contact_import' +require_relative 'contacts_import_request' module Mailtrap class ContactImportsAPI @@ -19,8 +20,9 @@ def get(import_id) end # Create contacts import - # @param contacts [Array] Array of contact objects to import - # Each contact object should have the following keys: + # @param contacts [Array, ContactsImportRequest, #to_a] Any object that responds to #to_a and returns an array of contact hashes. # rubocop:disable Layout/LineLength + # Accepts Array, ContactsImportRequest, or any other object implementing #to_a + # When using Array, each contact object should have the following keys: # - email [String] The contact's email address # - fields [Hash] Object of fields in the format: field_merge_tag => String, Integer, Float, Boolean, or ISO-8601 date string (yyyy-mm-dd) # rubocop:disable Layout/LineLength # - list_ids_included [Array] List IDs to include the contact in @@ -29,12 +31,14 @@ def get(import_id) # @!macro api_errors # @raise [ArgumentError] If invalid options are provided def create(contacts) - contacts.each do |contact| + contact_data = contacts.to_a + contact_data.each do |contact| validate_options!(contact, supported_options) end - response = client.post(base_path, contacts:) + response = client.post(base_path, contacts: contact_data) handle_response(response) end + alias start create private diff --git a/lib/mailtrap/contacts_import_request.rb b/lib/mailtrap/contacts_import_request.rb new file mode 100644 index 0000000..2a280cf --- /dev/null +++ b/lib/mailtrap/contacts_import_request.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Mailtrap + # A builder class for creating contact import requests + # Allows you to build a collection of contacts with their associated fields and list memberships + class ContactsImportRequest + def initialize + @data = {} + end + + # Creates or updates a contact with the provided email and fields + # @param email [String] The contact's email address + # @param fields [Hash] Contact fields in the format: field_merge_tag => String, Integer, Float, Boolean, or ISO-8601 date string (yyyy-mm-dd) # rubocop:disable Layout/LineLength + # @return [ContactsImportRequest] Returns self for method chaining + def upsert(email:, fields: {}) + unless @data[email] + @data[email] = { email:, fields:, list_ids_included: [], list_ids_excluded: [] } + return self + end + + @data[email][:fields] = fields + + self + end + + # Adds a contact to the specified lists + # @param email [String] The contact's email address + # @param list_ids [Array] Array of list IDs to add the contact to + # @return [ContactsImportRequest] Returns self for method chaining + def add_to_lists(email:, list_ids:) + unless @data[email] + @data[email] = { email:, fields: {}, list_ids_included: list_ids, list_ids_excluded: [] } + return self + end + + @data[email][:list_ids_included].concat(list_ids) + + self + end + + # Removes a contact from the specified lists + # @param email [String] The contact's email address + # @param list_ids [Array] Array of list IDs to remove the contact from + # @return [ContactsImportRequest] Returns self for method chaining + def remove_from_lists(email:, list_ids:) + unless @data[email] + @data[email] = { email:, fields: {}, list_ids_included: [], list_ids_excluded: list_ids } + return self + end + + @data[email][:list_ids_excluded].concat(list_ids) + + self + end + + # Converts the import request to a JSON-serializable array + # @return [Array] Array of contact objects ready for import + def as_json + @data.values + end + alias to_a as_json + end +end diff --git a/spec/mailtrap/contact_imports_api_spec.rb b/spec/mailtrap/contact_imports_api_spec.rb index 8694b61..c7d937d 100644 --- a/spec/mailtrap/contact_imports_api_spec.rb +++ b/spec/mailtrap/contact_imports_api_spec.rb @@ -107,5 +107,82 @@ expect { client.create(contacts) }.to raise_error(Mailtrap::Error) end + + context 'when using ContactsImportRequest' do + let(:basic_contact_data) do + { + email: 'example@example.com', + fields: { + fname: 'John', + age: 30, + is_subscribed: true, + birthday: '1990-05-15' + } + } + end + + def expect_successful_import(request_body, response_data = expected_response) + stub_request(:post, "#{base_url}/contacts/imports") + .with(body: request_body.to_json) + .to_return( + status: 200, + body: response_data.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + end + + shared_examples 'successful contact import' do |expected_id, expected_count| + it 'returns the expected import response' do + expect(response).to have_attributes( + id: expected_id, + status: 'created', + created_contacts_count: expected_count, + updated_contacts_count: 0, + contacts_over_limit_count: 0 + ) + end + end + + describe 'step-by-step method calls' do + let(:request) do + req = Mailtrap::ContactsImportRequest.new + req.upsert(email: basic_contact_data[:email], fields: basic_contact_data[:fields]) + req.add_to_lists(email: basic_contact_data[:email], list_ids: [1, 2]) + req.remove_from_lists(email: basic_contact_data[:email], list_ids: [3]) + end + + let(:expected_request_body) do + { + contacts: [ + basic_contact_data.merge( + list_ids_included: [1, 2], + list_ids_excluded: [3] + ) + ] + } + end + + let(:response) do + expect_successful_import(expected_request_body) + client.create(request) + end + + include_examples 'successful contact import', 'import-456', 1 + end + + it 'validates request data and raises error when invalid options are provided' do + request = Mailtrap::ContactsImportRequest.new + # Manually add invalid data to simulate corrupted request + request.instance_variable_get(:@data)['test@example.com'] = { + email: 'test@example.com', + fields: {}, + list_ids_included: [], + list_ids_excluded: [], + invalid_field: 'should not be here' + } + + expect { client.create(request) }.to raise_error(ArgumentError, /invalid options are given/) + end + end end end From 3801aa6ae50c6207c4f0e56d6633e627c5fd56f9 Mon Sep 17 00:00:00 2001 From: sarco3t <14gainward88@gmail.com> Date: Wed, 30 Jul 2025 11:31:07 +0300 Subject: [PATCH 03/11] rubocop fix in specs --- spec/mailtrap/contact_imports_api_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/mailtrap/contact_imports_api_spec.rb b/spec/mailtrap/contact_imports_api_spec.rb index c7d937d..25b474d 100644 --- a/spec/mailtrap/contact_imports_api_spec.rb +++ b/spec/mailtrap/contact_imports_api_spec.rb @@ -143,7 +143,7 @@ def expect_successful_import(request_body, response_data = expected_response) end end - describe 'step-by-step method calls' do + describe 'step-by-step method calls' do # rubocop:disable RSpec/MultipleMemoizedHelpers let(:request) do req = Mailtrap::ContactsImportRequest.new req.upsert(email: basic_contact_data[:email], fields: basic_contact_data[:fields]) From 544023cd2e99c7fc0a2c1b326c68c08716dfe716 Mon Sep 17 00:00:00 2001 From: sarco3t <14gainward88@gmail.com> Date: Wed, 30 Jul 2025 12:14:45 +0300 Subject: [PATCH 04/11] added specs for `Mailtrap::ContactsImportRequest` --- lib/mailtrap/contacts_import_request.rb | 6 +- spec/mailtrap/contact_imports_api_spec.rb | 47 ++++ spec/mailtrap/contacts_import_request_spec.rb | 248 ++++++++++++++++++ 3 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 spec/mailtrap/contacts_import_request_spec.rb diff --git a/lib/mailtrap/contacts_import_request.rb b/lib/mailtrap/contacts_import_request.rb index 2a280cf..656d24c 100644 --- a/lib/mailtrap/contacts_import_request.rb +++ b/lib/mailtrap/contacts_import_request.rb @@ -18,7 +18,7 @@ def upsert(email:, fields: {}) return self end - @data[email][:fields] = fields + @data[email][:fields].merge!(fields) self end @@ -33,7 +33,7 @@ def add_to_lists(email:, list_ids:) return self end - @data[email][:list_ids_included].concat(list_ids) + @data[email][:list_ids_included] |= list_ids self end @@ -48,7 +48,7 @@ def remove_from_lists(email:, list_ids:) return self end - @data[email][:list_ids_excluded].concat(list_ids) + @data[email][:list_ids_excluded] |= list_ids self end diff --git a/spec/mailtrap/contact_imports_api_spec.rb b/spec/mailtrap/contact_imports_api_spec.rb index 25b474d..86f4752 100644 --- a/spec/mailtrap/contact_imports_api_spec.rb +++ b/spec/mailtrap/contact_imports_api_spec.rb @@ -185,4 +185,51 @@ def expect_successful_import(request_body, response_data = expected_response) end end end + + describe '#start' do + let(:contacts) do + [ + { + email: 'example@example.com', + fields: { + fname: 'John', + age: 30, + is_subscribed: true, + birthday: '1990-05-15' + }, + list_ids_included: [1, 2], + list_ids_excluded: [3] + } + ] + end + + let(:expected_response) do + { + 'id' => 'import-789', + 'status' => 'created', + 'created_contacts_count' => 1, + 'updated_contacts_count' => 0, + 'contacts_over_limit_count' => 0 + } + end + + it 'is an alias for #create and works identically' do + stub_request(:post, "#{base_url}/contacts/imports") + .with(body: { contacts: }.to_json) + .to_return( + status: 200, + body: expected_response.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + response = client.start(contacts) + expect(response).to have_attributes( + id: 'import-789', + status: 'created', + created_contacts_count: 1, + updated_contacts_count: 0, + contacts_over_limit_count: 0 + ) + end + end end diff --git a/spec/mailtrap/contacts_import_request_spec.rb b/spec/mailtrap/contacts_import_request_spec.rb new file mode 100644 index 0000000..03ec665 --- /dev/null +++ b/spec/mailtrap/contacts_import_request_spec.rb @@ -0,0 +1,248 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::ContactsImportRequest do + subject(:request) { described_class.new } + + # Test data + let(:test_email) { 'john.doe@example.com' } + let(:another_email) { 'jane.smith@example.com' } + let(:test_fields) { { first_name: 'John', last_name: 'Doe' } } + let(:additional_fields) { { age: 30, company: 'Example Corp' } } + let(:test_list_ids) { [1, 2, 3] } + + # Shared examples for method chaining + shared_examples 'supports method chaining' do |method_name, **args| + it 'returns self for method chaining' do + result = request.public_send(method_name, **args) + expect(result).to eq(request) + end + end + + # Shared examples for contact creation + shared_examples 'creates contact when not exists' do |method_name, expected_data, **method_args| + it "creates a new contact using #{method_name}" do + result = request.public_send(method_name, **method_args) + + expect(result).to eq(request) + expect(request.as_json).to contain_exactly(expected_data) + end + end + + describe '#upsert' do + include_examples 'supports method chaining', :upsert, email: 'test@example.com', fields: { name: 'Test' } + + context 'when contact does not exist' do + include_examples 'creates contact when not exists', :upsert, { + email: 'john.doe@example.com', + fields: { first_name: 'John', last_name: 'Doe' }, + list_ids_included: [], + list_ids_excluded: [] + }, email: 'john.doe@example.com', fields: { first_name: 'John', last_name: 'Doe' } + + it 'creates a contact with empty fields when none provided' do + result = request.upsert(email: test_email) + + expect(result).to eq(request) + expect(request.as_json).to contain_exactly( + email: test_email, + fields: {}, + list_ids_included: [], + list_ids_excluded: [] + ) + end + end + + context 'when contact already exists' do + before { request.upsert(email: test_email, fields: { first_name: 'John' }) } + + it 'merges new fields with existing fields' do + result = request.upsert(email: test_email, fields: additional_fields) + + expect(result).to eq(request) + expect(request.as_json).to contain_exactly( + email: test_email, + fields: { first_name: 'John', **additional_fields }, + list_ids_included: [], + list_ids_excluded: [] + ) + end + + it 'overwrites existing field values' do + result = request.upsert(email: test_email, fields: { first_name: 'Jane' }) + + expect(result).to eq(request) + expect(request.as_json).to contain_exactly( + email: test_email, + fields: { first_name: 'Jane' }, + list_ids_included: [], + list_ids_excluded: [] + ) + end + + it 'preserves existing list associations when upserting fields' do + request.add_to_lists(email: test_email, list_ids: [1, 2]) + request.remove_from_lists(email: test_email, list_ids: [3]) + + request.upsert(email: test_email, fields: { last_name: 'Doe' }) + + expect(request.as_json).to contain_exactly( + email: test_email, + fields: { first_name: 'John', last_name: 'Doe' }, + list_ids_included: [1, 2], + list_ids_excluded: [3] + ) + end + end + end + + describe '#add_to_lists' do + include_examples 'supports method chaining', :add_to_lists, email: 'test@example.com', list_ids: [1, 2] + + context 'when contact does not exist' do + include_examples 'creates contact when not exists', :add_to_lists, { + email: 'jane.doe@example.com', + fields: {}, + list_ids_included: [1, 2, 3], + list_ids_excluded: [] + }, email: 'jane.doe@example.com', list_ids: [1, 2, 3] + end + + context 'when contact already exists' do + before { request.upsert(email: another_email, fields: { name: 'Jane' }) } + + it 'adds new list IDs to existing inclusions' do + request.add_to_lists(email: another_email, list_ids: [1, 2]) + result = request.add_to_lists(email: another_email, list_ids: [3, 4]) + + expect(result).to eq(request) + expect(request.as_json).to contain_exactly( + email: another_email, + fields: { name: 'Jane' }, + list_ids_included: [1, 2, 3, 4], + list_ids_excluded: [] + ) + end + + it 'prevents duplicate list IDs in inclusions' do + request.add_to_lists(email: another_email, list_ids: [1, 2]) + result = request.add_to_lists(email: another_email, list_ids: [2, 3]) + + expect(result).to eq(request) + expect(request.as_json).to contain_exactly( + email: another_email, + fields: { name: 'Jane' }, + list_ids_included: [1, 2, 3], + list_ids_excluded: [] + ) + end + end + + it 'works with method chaining on same email' do + request + .add_to_lists(email: test_email, list_ids: [1, 2]) + .add_to_lists(email: test_email, list_ids: [3, 4]) + + expect(request.as_json.first[:list_ids_included]).to eq([1, 2, 3, 4]) + end + end + + describe '#remove_from_lists' do + include_examples 'supports method chaining', :remove_from_lists, email: 'test@example.com', list_ids: [5, 6] + + context 'when contact does not exist' do + include_examples 'creates contact when not exists', :remove_from_lists, { + email: 'bob.smith@example.com', + fields: {}, + list_ids_included: [], + list_ids_excluded: [5, 6, 7] + }, email: 'bob.smith@example.com', list_ids: [5, 6, 7] + end + + context 'when contact already exists' do # rubocop:disable RSpec/MultipleMemoizedHelpers + let(:bob_email) { 'bob.smith@example.com' } + + before { request.upsert(email: bob_email, fields: { name: 'Bob' }) } + + it 'adds new list IDs to existing exclusions' do + request.remove_from_lists(email: bob_email, list_ids: [5, 6]) + result = request.remove_from_lists(email: bob_email, list_ids: [7, 8]) + + expect(result).to eq(request) + expect(request.as_json).to contain_exactly( + email: bob_email, + fields: { name: 'Bob' }, + list_ids_included: [], + list_ids_excluded: [5, 6, 7, 8] + ) + end + + it 'prevents duplicate list IDs in exclusions' do + request.remove_from_lists(email: bob_email, list_ids: [5, 6]) + result = request.remove_from_lists(email: bob_email, list_ids: [6, 7]) + + expect(result).to eq(request) + expect(request.as_json).to contain_exactly( + email: bob_email, + fields: { name: 'Bob' }, + list_ids_included: [], + list_ids_excluded: [5, 6, 7] + ) + end + end + + it 'works with method chaining on same email' do + request + .remove_from_lists(email: test_email, list_ids: [5, 6]) + .remove_from_lists(email: test_email, list_ids: [7, 8]) + + expect(request.as_json.first[:list_ids_excluded]).to eq([5, 6, 7, 8]) + end + end + + describe 'serialization methods' do + describe '#as_json' do + context 'when no contacts added' do + it 'returns an empty array' do + expect(request.as_json).to eq([]) + end + end + + context 'when contacts are added' do + before do + request + .upsert(email: test_email, fields: { name: 'John' }) + .add_to_lists(email: test_email, list_ids: [1]) + .upsert(email: another_email, fields: { name: 'Jane' }) + .remove_from_lists(email: another_email, list_ids: [2]) + end + + it 'returns array of contact data hashes' do + result = request.as_json + + expect(result).to contain_exactly( + { + email: test_email, + fields: { name: 'John' }, + list_ids_included: [1], + list_ids_excluded: [] + }, + { + email: another_email, + fields: { name: 'Jane' }, + list_ids_included: [], + list_ids_excluded: [2] + } + ) + end + end + end + + describe '#to_a' do + it 'is an alias for #as_json' do + request.upsert(email: 'test@example.com', fields: { name: 'Test' }) + + expect(request.to_a).to eq(request.as_json) + end + end + end +end From 89c9a2abc3c35fd1b2098d9aa6944559a3fee95c Mon Sep 17 00:00:00 2001 From: Ivan Yurchanka Date: Thu, 18 Sep 2025 16:29:28 +0200 Subject: [PATCH 05/11] Improve contact import request implementation and tests --- lib/mailtrap/contacts_import_request.rb | 34 +-- spec/mailtrap/contacts_import_request_spec.rb | 281 ++++-------------- 2 files changed, 77 insertions(+), 238 deletions(-) diff --git a/lib/mailtrap/contacts_import_request.rb b/lib/mailtrap/contacts_import_request.rb index 656d24c..61da275 100644 --- a/lib/mailtrap/contacts_import_request.rb +++ b/lib/mailtrap/contacts_import_request.rb @@ -5,19 +5,17 @@ module Mailtrap # Allows you to build a collection of contacts with their associated fields and list memberships class ContactsImportRequest def initialize - @data = {} + @data = Hash.new do |h, k| + h[k] = { email: k, fields: {}, list_ids_included: [], list_ids_excluded: [] } + end end # Creates or updates a contact with the provided email and fields # @param email [String] The contact's email address - # @param fields [Hash] Contact fields in the format: field_merge_tag => String, Integer, Float, Boolean, or ISO-8601 date string (yyyy-mm-dd) # rubocop:disable Layout/LineLength + # @param fields [Hash] Contact fields in the format: field_merge_tag => String, Integer, Float, Boolean, or + # ISO-8601 date string (yyyy-mm-dd) # @return [ContactsImportRequest] Returns self for method chaining def upsert(email:, fields: {}) - unless @data[email] - @data[email] = { email:, fields:, list_ids_included: [], list_ids_excluded: [] } - return self - end - @data[email][:fields].merge!(fields) self @@ -28,12 +26,7 @@ def upsert(email:, fields: {}) # @param list_ids [Array] Array of list IDs to add the contact to # @return [ContactsImportRequest] Returns self for method chaining def add_to_lists(email:, list_ids:) - unless @data[email] - @data[email] = { email:, fields: {}, list_ids_included: list_ids, list_ids_excluded: [] } - return self - end - - @data[email][:list_ids_included] |= list_ids + append_list_ids email:, list_ids:, key: :list_ids_included self end @@ -43,12 +36,7 @@ def add_to_lists(email:, list_ids:) # @param list_ids [Array] Array of list IDs to remove the contact from # @return [ContactsImportRequest] Returns self for method chaining def remove_from_lists(email:, list_ids:) - unless @data[email] - @data[email] = { email:, fields: {}, list_ids_included: [], list_ids_excluded: list_ids } - return self - end - - @data[email][:list_ids_excluded] |= list_ids + append_list_ids email:, list_ids:, key: :list_ids_excluded self end @@ -59,5 +47,13 @@ def as_json @data.values end alias to_a as_json + + private + + def append_list_ids(email:, list_ids:, key:) + raise ArgumentError, 'list_ids must not be empty' if list_ids.empty? + + @data[email][key] |= list_ids + end end end diff --git a/spec/mailtrap/contacts_import_request_spec.rb b/spec/mailtrap/contacts_import_request_spec.rb index 03ec665..c39373a 100644 --- a/spec/mailtrap/contacts_import_request_spec.rb +++ b/spec/mailtrap/contacts_import_request_spec.rb @@ -1,248 +1,91 @@ # frozen_string_literal: true RSpec.describe Mailtrap::ContactsImportRequest do - subject(:request) { described_class.new } - - # Test data - let(:test_email) { 'john.doe@example.com' } - let(:another_email) { 'jane.smith@example.com' } - let(:test_fields) { { first_name: 'John', last_name: 'Doe' } } - let(:additional_fields) { { age: 30, company: 'Example Corp' } } - let(:test_list_ids) { [1, 2, 3] } - - # Shared examples for method chaining - shared_examples 'supports method chaining' do |method_name, **args| - it 'returns self for method chaining' do - result = request.public_send(method_name, **args) - expect(result).to eq(request) - end - end - - # Shared examples for contact creation - shared_examples 'creates contact when not exists' do |method_name, expected_data, **method_args| - it "creates a new contact using #{method_name}" do - result = request.public_send(method_name, **method_args) - - expect(result).to eq(request) - expect(request.as_json).to contain_exactly(expected_data) - end - end - describe '#upsert' do - include_examples 'supports method chaining', :upsert, email: 'test@example.com', fields: { name: 'Test' } - - context 'when contact does not exist' do - include_examples 'creates contact when not exists', :upsert, { - email: 'john.doe@example.com', - fields: { first_name: 'John', last_name: 'Doe' }, - list_ids_included: [], - list_ids_excluded: [] - }, email: 'john.doe@example.com', fields: { first_name: 'John', last_name: 'Doe' } - - it 'creates a contact with empty fields when none provided' do - result = request.upsert(email: test_email) - - expect(result).to eq(request) - expect(request.as_json).to contain_exactly( - email: test_email, - fields: {}, - list_ids_included: [], - list_ids_excluded: [] - ) - end - end - - context 'when contact already exists' do - before { request.upsert(email: test_email, fields: { first_name: 'John' }) } - - it 'merges new fields with existing fields' do - result = request.upsert(email: test_email, fields: additional_fields) - - expect(result).to eq(request) - expect(request.as_json).to contain_exactly( - email: test_email, - fields: { first_name: 'John', **additional_fields }, - list_ids_included: [], - list_ids_excluded: [] - ) - end - - it 'overwrites existing field values' do - result = request.upsert(email: test_email, fields: { first_name: 'Jane' }) - - expect(result).to eq(request) - expect(request.as_json).to contain_exactly( - email: test_email, - fields: { first_name: 'Jane' }, - list_ids_included: [], - list_ids_excluded: [] - ) - end - - it 'preserves existing list associations when upserting fields' do - request.add_to_lists(email: test_email, list_ids: [1, 2]) - request.remove_from_lists(email: test_email, list_ids: [3]) - - request.upsert(email: test_email, fields: { last_name: 'Doe' }) - - expect(request.as_json).to contain_exactly( - email: test_email, - fields: { first_name: 'John', last_name: 'Doe' }, - list_ids_included: [1, 2], - list_ids_excluded: [3] - ) + it 'adds contact to the request' do + request = described_class.new.tap do |req| + req.upsert(email: 'one@example.com') + .upsert(email: 'two@example.com', fields: { first_name: 'John', last_name: 'Doe' }) + .upsert(email: 'trhee@example.com', fields: { first_name: 'Jack' }) + .upsert(email: 'trhee@example.com', fields: { first_name: 'Joe', last_name: 'Blow', age: 33 }) end + expect(request.to_a).to contain_exactly( + hash_including(email: 'one@example.com', fields: {}), + hash_including(email: 'two@example.com', fields: { first_name: 'John', last_name: 'Doe' }), + hash_including(email: 'trhee@example.com', fields: { first_name: 'Joe', last_name: 'Blow', age: 33 }) + ) end end describe '#add_to_lists' do - include_examples 'supports method chaining', :add_to_lists, email: 'test@example.com', list_ids: [1, 2] - - context 'when contact does not exist' do - include_examples 'creates contact when not exists', :add_to_lists, { - email: 'jane.doe@example.com', - fields: {}, - list_ids_included: [1, 2, 3], - list_ids_excluded: [] - }, email: 'jane.doe@example.com', list_ids: [1, 2, 3] + it 'does not allow empty list' do + expect { described_class.new.add_to_lists(email: 'one@example.com', list_ids: []) }.to raise_error(ArgumentError) end - context 'when contact already exists' do - before { request.upsert(email: another_email, fields: { name: 'Jane' }) } - - it 'adds new list IDs to existing inclusions' do - request.add_to_lists(email: another_email, list_ids: [1, 2]) - result = request.add_to_lists(email: another_email, list_ids: [3, 4]) - - expect(result).to eq(request) - expect(request.as_json).to contain_exactly( - email: another_email, - fields: { name: 'Jane' }, - list_ids_included: [1, 2, 3, 4], - list_ids_excluded: [] - ) - end - - it 'prevents duplicate list IDs in inclusions' do - request.add_to_lists(email: another_email, list_ids: [1, 2]) - result = request.add_to_lists(email: another_email, list_ids: [2, 3]) - - expect(result).to eq(request) - expect(request.as_json).to contain_exactly( - email: another_email, - fields: { name: 'Jane' }, - list_ids_included: [1, 2, 3], - list_ids_excluded: [] - ) + it 'adds contact to the lists' do + request = described_class.new.tap do |req| + req.add_to_lists(email: 'one@example.com', list_ids: [1]) + .add_to_lists(email: 'two@example.com', list_ids: [1]) + .add_to_lists(email: 'two@example.com', list_ids: [2]) + .add_to_lists(email: 'trhee@example.com', list_ids: [1]) + .add_to_lists(email: 'trhee@example.com', list_ids: [1, 2]) end - end - - it 'works with method chaining on same email' do - request - .add_to_lists(email: test_email, list_ids: [1, 2]) - .add_to_lists(email: test_email, list_ids: [3, 4]) - - expect(request.as_json.first[:list_ids_included]).to eq([1, 2, 3, 4]) + expect(request.to_a).to contain_exactly( + hash_including(email: 'one@example.com', list_ids_included: [1]), + hash_including(email: 'two@example.com', list_ids_included: [1, 2]), + hash_including(email: 'trhee@example.com', list_ids_included: [1, 2]) + ) end end describe '#remove_from_lists' do - include_examples 'supports method chaining', :remove_from_lists, email: 'test@example.com', list_ids: [5, 6] - - context 'when contact does not exist' do - include_examples 'creates contact when not exists', :remove_from_lists, { - email: 'bob.smith@example.com', - fields: {}, - list_ids_included: [], - list_ids_excluded: [5, 6, 7] - }, email: 'bob.smith@example.com', list_ids: [5, 6, 7] + it 'does not allow empty list' do + expect do + described_class.new.remove_from_lists(email: 'one@example.com', list_ids: []) + end.to raise_error(ArgumentError) end - context 'when contact already exists' do # rubocop:disable RSpec/MultipleMemoizedHelpers - let(:bob_email) { 'bob.smith@example.com' } - - before { request.upsert(email: bob_email, fields: { name: 'Bob' }) } - - it 'adds new list IDs to existing exclusions' do - request.remove_from_lists(email: bob_email, list_ids: [5, 6]) - result = request.remove_from_lists(email: bob_email, list_ids: [7, 8]) - - expect(result).to eq(request) - expect(request.as_json).to contain_exactly( - email: bob_email, - fields: { name: 'Bob' }, - list_ids_included: [], - list_ids_excluded: [5, 6, 7, 8] - ) + it 'adds contact to the lists' do + request = described_class.new.tap do |req| + req.remove_from_lists(email: 'one@example.com', list_ids: [1]) + .remove_from_lists(email: 'two@example.com', list_ids: [1]) + .remove_from_lists(email: 'two@example.com', list_ids: [2]) + .remove_from_lists(email: 'trhee@example.com', list_ids: [1]) + .remove_from_lists(email: 'trhee@example.com', list_ids: [1, 2]) end - - it 'prevents duplicate list IDs in exclusions' do - request.remove_from_lists(email: bob_email, list_ids: [5, 6]) - result = request.remove_from_lists(email: bob_email, list_ids: [6, 7]) - - expect(result).to eq(request) - expect(request.as_json).to contain_exactly( - email: bob_email, - fields: { name: 'Bob' }, - list_ids_included: [], - list_ids_excluded: [5, 6, 7] - ) - end - end - - it 'works with method chaining on same email' do - request - .remove_from_lists(email: test_email, list_ids: [5, 6]) - .remove_from_lists(email: test_email, list_ids: [7, 8]) - - expect(request.as_json.first[:list_ids_excluded]).to eq([5, 6, 7, 8]) + expect(request.to_a).to contain_exactly( + hash_including(email: 'one@example.com', list_ids_excluded: [1]), + hash_including(email: 'two@example.com', list_ids_excluded: [1, 2]), + hash_including(email: 'trhee@example.com', list_ids_excluded: [1, 2]) + ) end end - describe 'serialization methods' do - describe '#as_json' do - context 'when no contacts added' do - it 'returns an empty array' do - expect(request.as_json).to eq([]) - end - end - - context 'when contacts are added' do - before do - request - .upsert(email: test_email, fields: { name: 'John' }) - .add_to_lists(email: test_email, list_ids: [1]) - .upsert(email: another_email, fields: { name: 'Jane' }) - .remove_from_lists(email: another_email, list_ids: [2]) - end - - it 'returns array of contact data hashes' do - result = request.as_json + describe '#as_json' do + it 'returns json array' do + request = described_class.new + expect(request.as_json).to eq([]) - expect(result).to contain_exactly( - { - email: test_email, - fields: { name: 'John' }, - list_ids_included: [1], - list_ids_excluded: [] - }, - { - email: another_email, - fields: { name: 'Jane' }, - list_ids_included: [], - list_ids_excluded: [2] - } - ) - end - end + request.upsert(email: 'one@example.com', fields: { first_name: 'John' }) + expect(request.as_json).to contain_exactly( + { email: 'one@example.com', fields: { first_name: 'John' }, list_ids_included: [], list_ids_excluded: [] } + ) end + end - describe '#to_a' do - it 'is an alias for #as_json' do - request.upsert(email: 'test@example.com', fields: { name: 'Test' }) + it 'supports multiple operations on one contact' do + request = described_class.new.tap do |req| + req.upsert(email: 'one@example.com', fields: { first_name: 'John' }) + .add_to_lists(email: 'one@example.com', list_ids: [1]) + .remove_from_lists(email: 'one@example.com', list_ids: [2]) - expect(request.to_a).to eq(request.as_json) - end + req.remove_from_lists(email: 'two@example.com', list_ids: [12]) + .add_to_lists(email: 'two@example.com', list_ids: [11]) + .upsert(email: 'two@example.com', fields: { first_name: 'Jack' }) end + expect(request.to_a).to contain_exactly( + { email: 'one@example.com', fields: { first_name: 'John' }, list_ids_included: [1], list_ids_excluded: [2] }, + { email: 'two@example.com', fields: { first_name: 'Jack' }, list_ids_included: [11], list_ids_excluded: [12] } + ) end end From d753fb48c6ce9990babf27ee3e96a66476788390 Mon Sep 17 00:00:00 2001 From: Ivan Yurchanka Date: Thu, 18 Sep 2025 17:55:25 +0200 Subject: [PATCH 06/11] Update ContactImportsAPI docs --- lib/mailtrap/contact_imports_api.rb | 30 +++++++++++++------ lib/mailtrap/contacts_import_request.rb | 4 +-- spec/mailtrap/contacts_import_request_spec.rb | 6 ++-- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/lib/mailtrap/contact_imports_api.rb b/lib/mailtrap/contact_imports_api.rb index dcf54f7..abd019f 100644 --- a/lib/mailtrap/contact_imports_api.rb +++ b/lib/mailtrap/contact_imports_api.rb @@ -20,19 +20,31 @@ def get(import_id) end # Create contacts import - # @param contacts [Array, ContactsImportRequest, #to_a] Any object that responds to #to_a and returns an array of contact hashes. # rubocop:disable Layout/LineLength - # Accepts Array, ContactsImportRequest, or any other object implementing #to_a - # When using Array, each contact object should have the following keys: - # - email [String] The contact's email address - # - fields [Hash] Object of fields in the format: field_merge_tag => String, Integer, Float, Boolean, or ISO-8601 date string (yyyy-mm-dd) # rubocop:disable Layout/LineLength - # - list_ids_included [Array] List IDs to include the contact in - # - list_ids_excluded [Array] List IDs to exclude the contact from + # + # @example Using Mailtrap::ContactsImportRequest + # import_request = Mailtrap::ContactsImportRequest.new.tap do |req| + # req.upsert(email: 'jane@example.com', fields: { first_name: 'Jane' }) + # .add_to_lists(email: 'jane@example.com', list_ids: [1]) + # .remove_from_lists(email: 'jane@example.com', list_ids: [2]) + # req.upsert(email: 'john@example.com', fields: { first_name: 'John' }) + # .add_to_lists(email: 'john@example.com', list_ids: [1]) + # .remove_from_lists(email: 'john@example.com', list_ids: [2]) + # end + # contact_imports.create(import_request) + # + # @example Using plain hash + # contact_imports.create([ + # {email: "john@example.com", fields: { first_name: 'John' }, list_ids_included: [1], list_ids_excluded: [2]}, + # {email: "jane@example.com", fields: { first_name: 'Jane' }, list_ids_included: [1], list_ids_excluded: [2]} + # ]) + # + # @param contacts [Mailtrap::ContactsImportRequest, Array] The contacts import request + # # @return [ContactImport] Created contact list object # @!macro api_errors # @raise [ArgumentError] If invalid options are provided def create(contacts) - contact_data = contacts.to_a - contact_data.each do |contact| + contact_data = contacts.to_a.each do |contact| validate_options!(contact, supported_options) end response = client.post(base_path, contacts: contact_data) diff --git a/lib/mailtrap/contacts_import_request.rb b/lib/mailtrap/contacts_import_request.rb index 61da275..a924754 100644 --- a/lib/mailtrap/contacts_import_request.rb +++ b/lib/mailtrap/contacts_import_request.rb @@ -41,12 +41,10 @@ def remove_from_lists(email:, list_ids:) self end - # Converts the import request to a JSON-serializable array # @return [Array] Array of contact objects ready for import - def as_json + def to_a @data.values end - alias to_a as_json private diff --git a/spec/mailtrap/contacts_import_request_spec.rb b/spec/mailtrap/contacts_import_request_spec.rb index c39373a..9600d6a 100644 --- a/spec/mailtrap/contacts_import_request_spec.rb +++ b/spec/mailtrap/contacts_import_request_spec.rb @@ -61,13 +61,13 @@ end end - describe '#as_json' do + describe '#to_a' do it 'returns json array' do request = described_class.new - expect(request.as_json).to eq([]) + expect(request.to_a).to eq([]) request.upsert(email: 'one@example.com', fields: { first_name: 'John' }) - expect(request.as_json).to contain_exactly( + expect(request.to_a).to contain_exactly( { email: 'one@example.com', fields: { first_name: 'John' }, list_ids_included: [], list_ids_excluded: [] } ) end From 5c815504b3e38d793fe452b7e7ad2ef7c247bab9 Mon Sep 17 00:00:00 2001 From: Ivan Yurchanka Date: Thu, 18 Sep 2025 18:02:57 +0200 Subject: [PATCH 07/11] Update contacts api examples --- examples/contacts_api.rb | 229 +++++++++++----------------- lib/mailtrap/contact_imports_api.rb | 4 +- 2 files changed, 89 insertions(+), 144 deletions(-) diff --git a/examples/contacts_api.rb b/examples/contacts_api.rb index bebe9bd..8838e19 100644 --- a/examples/contacts_api.rb +++ b/examples/contacts_api.rb @@ -17,35 +17,38 @@ # Create new contact list list = contact_lists.create(name: 'Test List') -# => ContactList.new(id: 1, name: 'Test List') +# => # # Get all contact lists contact_lists.list -# => [ContactList.new(id: 1, name: 'Test List')] +# => [#] # Update contact list contact_lists.update(list.id, name: 'Test List Updated') -# => ContactList.new(id: 1, name: 'Test List Updated') +# => # # Get contact list list = contact_lists.get(list.id) -# => ContactList.new(id: 1, name: 'Test List Updated') +# => # + +# Delete contact list +contact_lists.delete(list.id) # Create new contact field field = contact_fields.create(name: 'Nickname', data_type: 'text', merge_tag: 'nickname') -# => ContactField.new(id: 1, name: 'Nickname', data_type: 'text', merge_tag: 'nickname') +# => # # Get all contact fields contact_fields.list -# => [ContactField.new(id: 1, name: 'Nickname', data_type: 'text', merge_tag: 'nickname')] +# => [#] # Update contact field contact_fields.update(field.id, name: 'Nickname 2', merge_tag: 'nickname') -# => ContactField.new(id: 1, name: 'Nickname 2', data_type: 'text', merge_tag: 'nickname') +# => # # Get contact field field = contact_fields.get(field.id) -# => ContactField.new(id: 1, name: 'Nickname 2', data_type: 'text', merge_tag: 'nickname') +# => # # Create new contact with all possible fields contact = contacts.create( @@ -53,28 +56,26 @@ fields: { field.merge_tag => 'John Doe' }, list_ids: [list.id] ) -# => Contact.new( -# id: 1, -# email: 'test@example.com', -# fields: { 'nickname' => 'John Doe' }, -# list_ids: [1], -# status: 'subscribed', -# created_at: 1721212345, -# updated_at: 1721212345 -# ) +# => # "John Doe" }, +# list_ids=[1], +# status="subscribed", +# created_at=1721212345, +# updated_at=1721212345> contact.newly_created? # => true # Get contact contact = contacts.get(contact.id) -# => Contact.new( -# id: 1, -# email: 'test@example.com', -# fields: { 'nickname' => 'John Doe' }, -# list_ids: [1], -# status: 'subscribed', -# created_at: 1721212345, -# updated_at: 1721212345 -# ) +# => # "John Doe" }, +# list_ids=[1], +# status="subscribed", +# created_at=1721212345, +# updated_at=1721212345> # Update contact using id updated_contact = contacts.upsert( @@ -82,15 +83,14 @@ email: 'test2@example.com', fields: { field.merge_tag => 'Jane Doe' } ) -# => Contact.new( -# id: 1, -# email: 'test2@example.com', -# fields: { 'nickname' => 'Jane Doe' }, -# list_ids: [1], -# status: 'subscribed', -# created_at: 1721212345, -# updated_at: 1721212350 -# ) +# => # "Jane Doe" }, +# list_ids=[1], +# status="subscribed", +# created_at=1721212345, +# updated_at=1721212350> updated_contact.newly_created? # => false # Update contact using email @@ -99,40 +99,37 @@ email: 'test3@example.com', fields: { field.merge_tag => 'Jane Doe' } ) -# => Contact.new( -# id: 1, -# email: 'test3@example.com', -# fields: { 'nickname' => 'Jane Doe' }, -# list_ids: [1], -# status: 'subscribed', -# created_at: 1721212345, -# updated_at: 1721212355 -# ) +# => # "Jane Doe" }, +# list_ids=[1], +# status="subscribed", +# created_at=1721212345, +# updated_at=1721212355> updated_contact.newly_created? # => false # Remove contact from lists contacts.remove_from_lists(contact.id, [list.id]) -# => Contact.new( -# id: 1, -# email: 'test3@example.com', -# fields: { 'nickname' => 'Jane Doe' }, -# list_ids: [], -# status: 'subscribed', -# created_at: 1721212345, -# updated_at: 1721212360 -# ) +# => # "Jane Doe" }, +# list_ids=[], +# status="subscribed", +# created_at=1721212345, +# updated_at=1721212360> # Add contact to lists contacts.add_to_lists(contact.id, [list.id]) -# => Contact.new( -# id: 1, -# email: 'test3@example.com', -# fields: { 'nickname' => 'Jane Doe' }, -# list_ids: [1], -# status: 'subscribed', -# created_at: 1721212345, -# updated_at: 1721212365 -# ) +# => # "Jane Doe" }, +# list_ids=[1], +# status="subscribed", +# created_at=1721212345, +# updated_at=1721212365> # Delete contact contacts.delete(contact.id) @@ -140,94 +137,42 @@ # Delete contact field contact_fields.delete(field.id) -# Create a new contact import -contact_import = contact_imports.create( - [ - { - email: 'imported@example.com', - fields: { - first_name: 'Jane', - }, - list_ids_included: [list.id], - list_ids_excluded: [] - } - ] -) -# => ContactImport.new( -# id: 1, -# status: 'created', -# list_ids: [1], -# created_contacts_count: 1, -# updated_contacts_count: 0, -# contacts_over_limit_count: 0 -# ) - -# Get a contact import by ID -contact_imports.get(contact_import.id) -# => ContactImport.new( -# id: 1, -# status: 'started', -# list_ids: [1], -# created_contacts_count: 1, -# updated_contacts_count: 0, -# contacts_over_limit_count: 0 -# ) - # Create a new contact import using ContactsImportRequest builder -import_request = Mailtrap::ContactsImportRequest.new - -# Add contacts using the builder pattern -import_request - .upsert( - email: 'john.doe@example.com', - fields: { first_name: 'John' } - ) - .add_to_lists(email: 'john.doe@example.com', list_ids: [list.id]) - .upsert( - email: 'jane.smith@example.com', - fields: { first_name: 'Jane' } - ) - .add_to_lists(email: 'jane.smith@example.com', list_ids: [list.id]) - .remove_from_lists(email: 'jane.smith@example.com', list_ids: []) +import_request = Mailtrap::ContactsImportRequest.new.tap do |req| + req.upsert(email: 'john.doe@example.com', fields: { first_name: 'John' }) + .add_to_lists(email: 'john.doe@example.com', list_ids: [1]) + .remove_from_lists(email: 'jane.smith@example.com', list_ids: [2]) -# Execute the import -contact_imports.create(import_request) -# => ContactImport.new( -# id: 2, -# status: 'created', -# list_ids: [1], -# created_contacts_count: 2, -# updated_contacts_count: 0, -# contacts_over_limit_count: 0 -# ) - -# Alternative: Step-by-step building -builder = Mailtrap::ContactsImportRequest.new -builder.upsert(email: 'jane.doe@example.com', fields: { first_name: 'Jane' }) -builder.add_to_lists(email: 'jane.doe@example.com', list_ids: [list.id]) + req.upsert(email: 'jane.smith@example.com', fields: { first_name: 'Jane' }) + .add_to_lists(email: 'jane.smith@example.com', list_ids: [1]) + .remove_from_lists(email: 'jane.smith@example.com', list_ids: [2]) +end -contact_import_2 = contact_imports.create(builder) -# => ContactImport.new( -# id: 3, -# status: 'created', -# list_ids: [1], -# created_contacts_count: 1, -# updated_contacts_count: 0, -# contacts_over_limit_count: 0 -# ) +# Execute the import +contact_import = contact_imports.create(import_request) +# => # -sleep 3 # Wait for the import to complete (if needed) +# Wait for the import to complete (if needed) # Get the import status -contact_imports.get(contact_import_2.id) +contact_imports.get(contact_import.id) # => ContactImport.new( -# id: 3, -# status: 'completed', -# list_ids: [1], -# created_contacts_count: 1, +# id: 1, +# status: 'finished', +# created_contacts_count: 2, # updated_contacts_count: 0, # contacts_over_limit_count: 0 # ) -# Delete contact list -contact_lists.delete(list.id) +# Import using plain hash +contact_imports.create( + [ + { email: 'john@example.com', fields: { first_name: 'John' }, list_ids_included: [1], list_ids_excluded: [2] }, + { email: 'jane@example.com', fields: { first_name: 'Jane' }, list_ids_included: [1], list_ids_excluded: [2] } + ] +) diff --git a/lib/mailtrap/contact_imports_api.rb b/lib/mailtrap/contact_imports_api.rb index abd019f..019693b 100644 --- a/lib/mailtrap/contact_imports_api.rb +++ b/lib/mailtrap/contact_imports_api.rb @@ -34,8 +34,8 @@ def get(import_id) # # @example Using plain hash # contact_imports.create([ - # {email: "john@example.com", fields: { first_name: 'John' }, list_ids_included: [1], list_ids_excluded: [2]}, - # {email: "jane@example.com", fields: { first_name: 'Jane' }, list_ids_included: [1], list_ids_excluded: [2]} + # {email: 'john@example.com', fields: { first_name: 'John' }, list_ids_included: [1], list_ids_excluded: [2]}, + # {email: 'jane@example.com', fields: { first_name: 'Jane' }, list_ids_included: [1], list_ids_excluded: [2]} # ]) # # @param contacts [Mailtrap::ContactsImportRequest, Array] The contacts import request From eda72443c5a89c22d0de08882dcc53fc59b4cd73 Mon Sep 17 00:00:00 2001 From: Ivan Yurchanka Date: Mon, 22 Sep 2025 13:14:35 +0200 Subject: [PATCH 08/11] Clean up contact import api tests --- lib/mailtrap/contact_imports_api.rb | 2 +- spec/mailtrap/contact_imports_api_spec.rb | 241 +++++----------------- spec/mailtrap/contact_spec.rb | 16 +- 3 files changed, 57 insertions(+), 202 deletions(-) diff --git a/lib/mailtrap/contact_imports_api.rb b/lib/mailtrap/contact_imports_api.rb index 019693b..4ec2219 100644 --- a/lib/mailtrap/contact_imports_api.rb +++ b/lib/mailtrap/contact_imports_api.rb @@ -12,7 +12,7 @@ class ContactImportsAPI self.response_class = ContactImport # Retrieves a specific contact import - # @param import_id [String] The contact import identifier + # @param import_id [Integer] The contact import identifier # @return [ContactImport] Contact import object # @!macro api_errors def get(import_id) diff --git a/spec/mailtrap/contact_imports_api_spec.rb b/spec/mailtrap/contact_imports_api_spec.rb index 86f4752..b96fb79 100644 --- a/spec/mailtrap/contact_imports_api_spec.rb +++ b/spec/mailtrap/contact_imports_api_spec.rb @@ -1,235 +1,96 @@ # frozen_string_literal: true RSpec.describe Mailtrap::ContactImportsAPI do - let(:client) { described_class.new('1111111', Mailtrap::Client.new(api_key: 'correct-api-key')) } - let(:base_url) { 'https://mailtrap.io/api/accounts/1111111' } + let(:imports_api) { described_class.new(123, Mailtrap::Client.new(api_key: 'correct-api-key')) } describe '#get' do - let(:import_id) { 'import-123' } - let(:expected_response) do - { - 'id' => 'import-123', - 'status' => 'finished', - 'created_contacts_count' => 10, - 'updated_contacts_count' => 2, - 'contacts_over_limit_count' => 0 - } - end - it 'returns a specific contact import' do - stub_request(:get, "#{base_url}/contacts/imports/#{import_id}") + expected_response = { + id: 1, + status: 'finished', + created_contacts_count: 10, + updated_contacts_count: 2, + contacts_over_limit_count: 0 + } + stub_request(:get, %r{/api/accounts/123/contacts/imports/1}) .to_return( status: 200, body: expected_response.to_json, headers: { 'Content-Type' => 'application/json' } ) - response = client.get(import_id) - expect(response).to have_attributes( - id: 'import-123', - status: 'finished', - created_contacts_count: 10, - updated_contacts_count: 2, - contacts_over_limit_count: 0 - ) + response = imports_api.get(1) + expect(response).to have_attributes(expected_response) end it 'raises error when contact import not found' do - stub_request(:get, "#{base_url}/contacts/imports/not-found") + stub_request(:get, %r{/api/accounts/123/contacts/imports/1}) .to_return( status: 404, body: { 'error' => 'Not Found' }.to_json, headers: { 'Content-Type' => 'application/json' } ) - expect { client.get('not-found') }.to raise_error(Mailtrap::Error) + expect { imports_api.get(1) }.to raise_error(Mailtrap::Error) end end describe '#create' do - let(:contacts) do - [ - { - email: 'example@example.com', - fields: { - fname: 'John', - age: 30, - is_subscribed: true, - birthday: '1990-05-15' - }, - list_ids_included: [1, 2], - list_ids_excluded: [3] - } - ] - end - let(:expected_response) do - { - 'id' => 'import-456', - 'status' => 'created', - 'created_contacts_count' => 1, - 'updated_contacts_count' => 0, - 'contacts_over_limit_count' => 0 - } - end + it 'creates a new contact import' do + request = Mailtrap::ContactsImportRequest.new.tap do |req| + req.upsert(email: 'j.doe@example.com', fields: { first_name: 'John' }) + req.add_to_lists(email: 'j.doe@example.com', list_ids: [1, 2]) + req.remove_from_lists(email: 'j.doe@example.com', list_ids: [3]) + end - it 'creates a new contact import with hash' do - stub_request(:post, "#{base_url}/contacts/imports") - .with(body: { contacts: }.to_json) + stub_request(:post, %r{/api/accounts/123/contacts/imports}) + .with(body: { contacts: request.to_a }.to_json) .to_return( status: 200, - body: expected_response.to_json, + body: { id: 1, status: 'started' }.to_json, headers: { 'Content-Type' => 'application/json' } ) - response = client.create(contacts) + response = imports_api.create(request) expect(response).to have_attributes( - id: 'import-456', - status: 'created', - created_contacts_count: 1, - updated_contacts_count: 0, - contacts_over_limit_count: 0 + id: 1, + status: 'started', + created_contacts_count: nil, + updated_contacts_count: nil, + contacts_over_limit_count: nil ) - end - - it 'raises error when invalid options are provided' do - invalid_contacts = [contacts.first.merge(foo: 'bar')] - expect { client.create(invalid_contacts) }.to raise_error(ArgumentError, /invalid options are given/) - end - - it 'raises error when API returns an error' do - stub_request(:post, "#{base_url}/contacts/imports") - .with(body: { contacts: }.to_json) - .to_return( - status: 422, - body: { 'errors' => { 'email' => ['is invalid'] } }.to_json, - headers: { 'Content-Type' => 'application/json' } - ) - expect { client.create(contacts) }.to raise_error(Mailtrap::Error) - end - - context 'when using ContactsImportRequest' do - let(:basic_contact_data) do - { - email: 'example@example.com', - fields: { - fname: 'John', - age: 30, - is_subscribed: true, - birthday: '1990-05-15' - } - } - end - - def expect_successful_import(request_body, response_data = expected_response) - stub_request(:post, "#{base_url}/contacts/imports") - .with(body: request_body.to_json) - .to_return( - status: 200, - body: response_data.to_json, - headers: { 'Content-Type' => 'application/json' } - ) - end - - shared_examples 'successful contact import' do |expected_id, expected_count| - it 'returns the expected import response' do - expect(response).to have_attributes( - id: expected_id, - status: 'created', - created_contacts_count: expected_count, - updated_contacts_count: 0, - contacts_over_limit_count: 0 - ) - end - end - - describe 'step-by-step method calls' do # rubocop:disable RSpec/MultipleMemoizedHelpers - let(:request) do - req = Mailtrap::ContactsImportRequest.new - req.upsert(email: basic_contact_data[:email], fields: basic_contact_data[:fields]) - req.add_to_lists(email: basic_contact_data[:email], list_ids: [1, 2]) - req.remove_from_lists(email: basic_contact_data[:email], list_ids: [3]) - end - - let(:expected_request_body) do - { - contacts: [ - basic_contact_data.merge( - list_ids_included: [1, 2], - list_ids_excluded: [3] - ) - ] - } - end - - let(:response) do - expect_successful_import(expected_request_body) - client.create(request) - end - - include_examples 'successful contact import', 'import-456', 1 - end - - it 'validates request data and raises error when invalid options are provided' do - request = Mailtrap::ContactsImportRequest.new - # Manually add invalid data to simulate corrupted request - request.instance_variable_get(:@data)['test@example.com'] = { - email: 'test@example.com', - fields: {}, - list_ids_included: [], - list_ids_excluded: [], - invalid_field: 'should not be here' - } - - expect { client.create(request) }.to raise_error(ArgumentError, /invalid options are given/) - end - end - end - - describe '#start' do - let(:contacts) do - [ - { - email: 'example@example.com', - fields: { - fname: 'John', - age: 30, - is_subscribed: true, - birthday: '1990-05-15' - }, - list_ids_included: [1, 2], - list_ids_excluded: [3] - } - ] + response = imports_api.create(request.to_a) + expect(response).to have_attributes( + id: 1, + status: 'started', + created_contacts_count: nil, + updated_contacts_count: nil, + contacts_over_limit_count: nil + ) end - let(:expected_response) do - { - 'id' => 'import-789', - 'status' => 'created', - 'created_contacts_count' => 1, - 'updated_contacts_count' => 0, - 'contacts_over_limit_count' => 0 - } + it 'validates contacts' do + expect { imports_api.create([{ email: 'john@example.com', foo: 1 }]) }.to raise_error(ArgumentError) end - it 'is an alias for #create and works identically' do - stub_request(:post, "#{base_url}/contacts/imports") - .with(body: { contacts: }.to_json) + it 'handles api errors' do + stub_request(:post, %r{/api/accounts/123/contacts/imports}) + .with(body: { contacts: [{ email: 'john@example' }] }.to_json) .to_return( - status: 200, - body: expected_response.to_json, + status: 422, + body: { + 'errors' => { + 'email' => 'john@example', + 'errors' => { + 'email' => ['is invalid'] + } + } + }.to_json, headers: { 'Content-Type' => 'application/json' } ) - response = client.start(contacts) - expect(response).to have_attributes( - id: 'import-789', - status: 'created', - created_contacts_count: 1, - updated_contacts_count: 0, - contacts_over_limit_count: 0 - ) + expect { imports_api.create([{ email: 'john@example' }]) }.to raise_error(Mailtrap::Error) end end end diff --git a/spec/mailtrap/contact_spec.rb b/spec/mailtrap/contact_spec.rb index 329be08..34c6c1b 100644 --- a/spec/mailtrap/contact_spec.rb +++ b/spec/mailtrap/contact_spec.rb @@ -13,30 +13,24 @@ list_ids: [1, 2], status: 'subscribed', created_at: 1_700_000_000, - updated_at: 1_700_000_100, - action: + updated_at: 1_700_000_100 } end - let(:action) { 'created' } describe '#newly_created?' do + it { is_expected.to be_newly_created } + context "when action is 'created'" do - let(:action) { 'created' } + let(:attributes) { super().merge(action: 'created') } it { is_expected.to be_newly_created } end context "when action is 'updated'" do - let(:action) { 'updated' } + let(:attributes) { super().merge(action: 'updated') } it { is_expected.not_to be_newly_created } end - - context 'when action is nil' do - let(:action) { nil } - - it { is_expected.to be_newly_created } - end end describe '#to_h' do From bfd6489460ab1d9c8533f61b17960ee125cc3fcc Mon Sep 17 00:00:00 2001 From: Ivan Yurchanka Date: Mon, 22 Sep 2025 13:24:04 +0200 Subject: [PATCH 09/11] Fix return example --- examples/contacts_api.rb | 43 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/examples/contacts_api.rb b/examples/contacts_api.rb index 8838e19..598cb11 100644 --- a/examples/contacts_api.rb +++ b/examples/contacts_api.rb @@ -17,38 +17,38 @@ # Create new contact list list = contact_lists.create(name: 'Test List') -# => # +# => # # Get all contact lists contact_lists.list -# => [#] +# => [#] # Update contact list contact_lists.update(list.id, name: 'Test List Updated') -# => # +# => # # Get contact list list = contact_lists.get(list.id) -# => # +# => # # Delete contact list contact_lists.delete(list.id) # Create new contact field field = contact_fields.create(name: 'Nickname', data_type: 'text', merge_tag: 'nickname') -# => # +# => # # Get all contact fields contact_fields.list -# => [#] +# => [#] # Update contact field contact_fields.update(field.id, name: 'Nickname 2', merge_tag: 'nickname') -# => # +# => # # Get contact field field = contact_fields.get(field.id) -# => # +# => # # Create new contact with all possible fields contact = contacts.create( @@ -56,7 +56,7 @@ fields: { field.merge_tag => 'John Doe' }, list_ids: [list.id] ) -# => # # "John Doe" }, @@ -68,7 +68,7 @@ # Get contact contact = contacts.get(contact.id) -# => # # "John Doe" }, @@ -83,7 +83,7 @@ email: 'test2@example.com', fields: { field.merge_tag => 'Jane Doe' } ) -# => # # "Jane Doe" }, @@ -99,7 +99,7 @@ email: 'test3@example.com', fields: { field.merge_tag => 'Jane Doe' } ) -# => # # "Jane Doe" }, @@ -111,7 +111,7 @@ # Remove contact from lists contacts.remove_from_lists(contact.id, [list.id]) -# => # # "Jane Doe" }, @@ -122,7 +122,7 @@ # Add contact to lists contacts.add_to_lists(contact.id, [list.id]) -# => # # "Jane Doe" }, @@ -150,7 +150,7 @@ # Execute the import contact_import = contact_imports.create(import_request) -# => # # ContactImport.new( -# id: 1, -# status: 'finished', -# created_contacts_count: 2, -# updated_contacts_count: 0, -# contacts_over_limit_count: 0 -# ) +# => # # Import using plain hash contact_imports.create( From 87d118d6fc63855d32f62bfc19b79dcdc55fc1be Mon Sep 17 00:00:00 2001 From: Ivan Yurchanka Date: Mon, 22 Sep 2025 16:25:51 +0200 Subject: [PATCH 10/11] Add email validation to contacts import request --- lib/mailtrap/contacts_import_request.rb | 12 +++++++++++- spec/mailtrap/contacts_import_request_spec.rb | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/mailtrap/contacts_import_request.rb b/lib/mailtrap/contacts_import_request.rb index a924754..731a507 100644 --- a/lib/mailtrap/contacts_import_request.rb +++ b/lib/mailtrap/contacts_import_request.rb @@ -16,6 +16,8 @@ def initialize # ISO-8601 date string (yyyy-mm-dd) # @return [ContactsImportRequest] Returns self for method chaining def upsert(email:, fields: {}) + validate_email!(email) + @data[email][:fields].merge!(fields) self @@ -26,6 +28,8 @@ def upsert(email:, fields: {}) # @param list_ids [Array] Array of list IDs to add the contact to # @return [ContactsImportRequest] Returns self for method chaining def add_to_lists(email:, list_ids:) + validate_email!(email) + append_list_ids email:, list_ids:, key: :list_ids_included self @@ -36,6 +40,8 @@ def add_to_lists(email:, list_ids:) # @param list_ids [Array] Array of list IDs to remove the contact from # @return [ContactsImportRequest] Returns self for method chaining def remove_from_lists(email:, list_ids:) + validate_email!(email) + append_list_ids email:, list_ids:, key: :list_ids_excluded self @@ -48,8 +54,12 @@ def to_a private + def validate_email!(email) + raise ArgumentError, 'email must be present' if email.nil? || email.empty? + end + def append_list_ids(email:, list_ids:, key:) - raise ArgumentError, 'list_ids must not be empty' if list_ids.empty? + raise ArgumentError, 'list_ids must be present' if list_ids.empty? @data[email][key] |= list_ids end diff --git a/spec/mailtrap/contacts_import_request_spec.rb b/spec/mailtrap/contacts_import_request_spec.rb index 9600d6a..3a9c622 100644 --- a/spec/mailtrap/contacts_import_request_spec.rb +++ b/spec/mailtrap/contacts_import_request_spec.rb @@ -2,6 +2,11 @@ RSpec.describe Mailtrap::ContactsImportRequest do describe '#upsert' do + it 'validates email' do + expect { described_class.new.upsert(email: nil) }.to raise_error(ArgumentError) + expect { described_class.new.upsert(email: '') }.to raise_error(ArgumentError) + end + it 'adds contact to the request' do request = described_class.new.tap do |req| req.upsert(email: 'one@example.com') @@ -18,6 +23,11 @@ end describe '#add_to_lists' do + it 'validates email' do + expect { described_class.new.add_to_lists(email: nil, list_ids: [1]) }.to raise_error(ArgumentError) + expect { described_class.new.add_to_lists(email: '', list_ids: [1]) }.to raise_error(ArgumentError) + end + it 'does not allow empty list' do expect { described_class.new.add_to_lists(email: 'one@example.com', list_ids: []) }.to raise_error(ArgumentError) end @@ -39,6 +49,11 @@ end describe '#remove_from_lists' do + it 'validates email' do + expect { described_class.new.remove_from_lists(email: nil, list_ids: [1]) }.to raise_error(ArgumentError) + expect { described_class.new.remove_from_lists(email: '', list_ids: [1]) }.to raise_error(ArgumentError) + end + it 'does not allow empty list' do expect do described_class.new.remove_from_lists(email: 'one@example.com', list_ids: []) From 618e7e3f348ccfe56c9e17519228cf1311e17822 Mon Sep 17 00:00:00 2001 From: Ivan Yurchanka Date: Mon, 22 Sep 2025 16:32:45 +0200 Subject: [PATCH 11/11] Fix typos --- lib/mailtrap/contact_import.rb | 2 +- lib/mailtrap/contact_imports_api.rb | 2 +- spec/mailtrap/contacts_import_request_spec.rb | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/mailtrap/contact_import.rb b/lib/mailtrap/contact_import.rb index 9194157..df2a9b9 100644 --- a/lib/mailtrap/contact_import.rb +++ b/lib/mailtrap/contact_import.rb @@ -2,7 +2,7 @@ module Mailtrap # Data Transfer Object for Contact Import - # @attr_reader id [String] The contact import ID + # @attr_reader id [Integer] The contact import ID # @attr_reader status [String] The status of the import (created, started, finished, failed) # @attr_reader created_contacts_count [Integer, nil] Number of contacts created in this import # @attr_reader updated_contacts_count [Integer, nil] Number of contacts updated in this import diff --git a/lib/mailtrap/contact_imports_api.rb b/lib/mailtrap/contact_imports_api.rb index 4ec2219..3b6a754 100644 --- a/lib/mailtrap/contact_imports_api.rb +++ b/lib/mailtrap/contact_imports_api.rb @@ -40,7 +40,7 @@ def get(import_id) # # @param contacts [Mailtrap::ContactsImportRequest, Array] The contacts import request # - # @return [ContactImport] Created contact list object + # @return [ContactImport] Created contact import object # @!macro api_errors # @raise [ArgumentError] If invalid options are provided def create(contacts) diff --git a/spec/mailtrap/contacts_import_request_spec.rb b/spec/mailtrap/contacts_import_request_spec.rb index 3a9c622..37b36cb 100644 --- a/spec/mailtrap/contacts_import_request_spec.rb +++ b/spec/mailtrap/contacts_import_request_spec.rb @@ -11,13 +11,13 @@ request = described_class.new.tap do |req| req.upsert(email: 'one@example.com') .upsert(email: 'two@example.com', fields: { first_name: 'John', last_name: 'Doe' }) - .upsert(email: 'trhee@example.com', fields: { first_name: 'Jack' }) - .upsert(email: 'trhee@example.com', fields: { first_name: 'Joe', last_name: 'Blow', age: 33 }) + .upsert(email: 'three@example.com', fields: { first_name: 'Jack' }) + .upsert(email: 'three@example.com', fields: { first_name: 'Joe', last_name: 'Blow', age: 33 }) end expect(request.to_a).to contain_exactly( hash_including(email: 'one@example.com', fields: {}), hash_including(email: 'two@example.com', fields: { first_name: 'John', last_name: 'Doe' }), - hash_including(email: 'trhee@example.com', fields: { first_name: 'Joe', last_name: 'Blow', age: 33 }) + hash_including(email: 'three@example.com', fields: { first_name: 'Joe', last_name: 'Blow', age: 33 }) ) end end @@ -37,13 +37,13 @@ req.add_to_lists(email: 'one@example.com', list_ids: [1]) .add_to_lists(email: 'two@example.com', list_ids: [1]) .add_to_lists(email: 'two@example.com', list_ids: [2]) - .add_to_lists(email: 'trhee@example.com', list_ids: [1]) - .add_to_lists(email: 'trhee@example.com', list_ids: [1, 2]) + .add_to_lists(email: 'three@example.com', list_ids: [1]) + .add_to_lists(email: 'three@example.com', list_ids: [1, 2]) end expect(request.to_a).to contain_exactly( hash_including(email: 'one@example.com', list_ids_included: [1]), hash_including(email: 'two@example.com', list_ids_included: [1, 2]), - hash_including(email: 'trhee@example.com', list_ids_included: [1, 2]) + hash_including(email: 'three@example.com', list_ids_included: [1, 2]) ) end end @@ -65,13 +65,13 @@ req.remove_from_lists(email: 'one@example.com', list_ids: [1]) .remove_from_lists(email: 'two@example.com', list_ids: [1]) .remove_from_lists(email: 'two@example.com', list_ids: [2]) - .remove_from_lists(email: 'trhee@example.com', list_ids: [1]) - .remove_from_lists(email: 'trhee@example.com', list_ids: [1, 2]) + .remove_from_lists(email: 'three@example.com', list_ids: [1]) + .remove_from_lists(email: 'three@example.com', list_ids: [1, 2]) end expect(request.to_a).to contain_exactly( hash_including(email: 'one@example.com', list_ids_excluded: [1]), hash_including(email: 'two@example.com', list_ids_excluded: [1, 2]), - hash_including(email: 'trhee@example.com', list_ids_excluded: [1, 2]) + hash_including(email: 'three@example.com', list_ids_excluded: [1, 2]) ) end end