dotmailer 0.0.1 → 0.0.2

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.
@@ -0,0 +1,94 @@
1
+ require 'csv'
2
+ require 'active_support/core_ext/object/blank'
3
+
4
+ module DotMailer
5
+ class ContactImport
6
+ def self.import(account, contacts)
7
+ contact_import = new(account, contacts)
8
+
9
+ contact_import.start
10
+
11
+ contact_import
12
+ end
13
+
14
+ attr_reader :id
15
+
16
+ def initialize(account, contacts)
17
+ self.account = account
18
+ self.contacts = contacts
19
+ end
20
+
21
+ def start
22
+ validate_headers
23
+
24
+ response = client.post_csv '/contacts/import', contacts_csv
25
+
26
+ self.id = response['id']
27
+ end
28
+
29
+ def status
30
+ if id.nil?
31
+ 'NotStarted'
32
+ else
33
+ response = client.get "/contacts/import/#{id}"
34
+
35
+ response['status']
36
+ end
37
+ end
38
+
39
+ def finished?
40
+ status == 'Finished'
41
+ end
42
+
43
+ def errors
44
+ raise ImportNotFinished unless finished?
45
+
46
+ client.get_csv "/contacts/import/#{id}/report-faults"
47
+ end
48
+
49
+ def to_s
50
+ "#{self.class.name} contacts: #{contacts.to_s}"
51
+ end
52
+
53
+ def inspect
54
+ to_s
55
+ end
56
+
57
+ private
58
+ attr_accessor :contacts, :account
59
+ attr_writer :id
60
+
61
+ def client
62
+ account.client
63
+ end
64
+
65
+ def contact_headers
66
+ @contact_headers ||= contacts.map(&:keys).flatten.uniq
67
+ end
68
+
69
+ def contacts_csv
70
+ @contacts_csv ||= CSV.generate do |csv|
71
+ csv << contact_headers
72
+
73
+ contacts.each do |contact|
74
+ csv << contact_headers.map { |header| contact[header] }
75
+ end
76
+ end
77
+ end
78
+
79
+ # Check that the contact_headers are all valid (case insensitive)
80
+ def validate_headers
81
+ raise UnknownDataField, unknown_headers.join(',') if unknown_headers.present?
82
+ end
83
+
84
+ def unknown_headers
85
+ @unknown_headers ||= contact_headers.reject do |header|
86
+ valid_headers.map(&:downcase).include?(header.downcase)
87
+ end
88
+ end
89
+
90
+ def valid_headers
91
+ @valid_headers ||= %w(id email optInType emailType) + account.data_fields.map(&:name)
92
+ end
93
+ end
94
+ end
@@ -1,5 +1,24 @@
1
- module Dotmailer
1
+ module DotMailer
2
2
  class DataField
3
+ def self.all(account)
4
+ fields = account.client.get '/data-fields'
5
+
6
+ fields.map { |attributes| new(attributes) }
7
+ end
8
+
9
+ def self.create(account, name, options = {})
10
+ options[:type] ||= 'String'
11
+ options[:visibility] ||= 'Public'
12
+
13
+ account.client.post_json(
14
+ '/data-fields',
15
+ 'name' => name,
16
+ 'type' => options[:type],
17
+ 'visibility' => options[:visibility],
18
+ 'defaultValue' => options[:default]
19
+ )
20
+ end
21
+
3
22
  def initialize(attributes)
4
23
  self.attributes = attributes
5
24
  end
@@ -32,6 +51,10 @@ module Dotmailer
32
51
  attributes == other.attributes
33
52
  end
34
53
 
54
+ def date?
55
+ type == 'Date'
56
+ end
57
+
35
58
  protected
36
59
  attr_accessor :attributes
37
60
  end
@@ -0,0 +1,16 @@
1
+ module DotMailer
2
+ class ImportNotFinished < Exception
3
+ end
4
+
5
+ class InvalidRequest < Exception
6
+ end
7
+
8
+ class NotFound < Exception
9
+ end
10
+
11
+ class UnknownDataField < Exception
12
+ end
13
+
14
+ class UnknownOptInType < Exception
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module DotMailer
2
+ module OptInType
3
+ DOUBLE = 'Double'
4
+ SINGLE = 'Single'
5
+ UNKNOWN = 'Unknown'
6
+ VERIFIED_DOUBLE = 'VerifiedDouble'
7
+
8
+ def self.all
9
+ constants(false).map(&method(:const_get))
10
+ end
11
+
12
+ def self.exists?(opt_in_type)
13
+ all.include?(opt_in_type)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ require 'time'
2
+
3
+ module DotMailer
4
+ class Suppression
5
+ attr_reader :contact, :date_removed, :reason
6
+
7
+ def self.suppressed_since(account, time)
8
+ response = account.client.get("/contacts/suppressed-since/#{time.utc.xmlschema}")
9
+
10
+ response.map do |attributes|
11
+ new(account, attributes)
12
+ end
13
+ end
14
+
15
+ def initialize(account, attributes)
16
+ @contact = Contact.new account, attributes['suppressedContact']
17
+ @date_removed = Time.parse attributes['dateRemoved']
18
+ @reason = attributes['reason']
19
+ end
20
+
21
+ def to_s
22
+ %{#{self.class.name} reason: #{reason}, date_removed: #{date_removed}, contact: #{contact.to_s}}
23
+ end
24
+
25
+ def inspect
26
+ to_s
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module DotMailer
2
+ VERSION = "0.0.2"
3
+ end
@@ -1,3 +1 @@
1
- require 'dotmailer/exceptions'
2
- require 'dotmailer/data_field'
3
- require 'dotmailer/client'
1
+ require 'dot_mailer'
@@ -0,0 +1,113 @@
1
+ require 'spec_helper'
2
+
3
+ describe DotMailer::Account do
4
+ let(:api_user) { double 'api user' }
5
+ let(:api_pass) { double 'api pass' }
6
+ let(:client) { double 'client' }
7
+
8
+ subject { DotMailer::Account.new(api_user, api_pass) }
9
+
10
+ before(:each) do
11
+ subject.stub :client => client
12
+ end
13
+
14
+ describe '#initialize' do
15
+ before(:each) do
16
+ DotMailer::Client.stub :new => client
17
+ end
18
+
19
+ it 'should initialize a Client with the credentials' do
20
+ DotMailer::Client.should_receive(:new).with(api_user, api_pass)
21
+
22
+ DotMailer::Account.new(api_user, api_pass)
23
+ end
24
+
25
+ it 'should set the client' do
26
+ DotMailer::Client.should_receive(:new).with(api_user, api_pass)
27
+
28
+ account = DotMailer::Account.new(api_user, api_pass)
29
+ end
30
+ end
31
+
32
+ describe '#suppress' do
33
+ let(:email) { double 'email' }
34
+
35
+ before(:each) do
36
+ client.stub :post_json
37
+ end
38
+
39
+ it 'should call post_json on the client with the correct path' do
40
+ client.should_receive(:post_json).with('/contacts/unsubscribe', anything)
41
+
42
+ subject.suppress email
43
+ end
44
+
45
+ it 'should call post_json on the client with the email address' do
46
+ client.should_receive(:post_json).with(anything, 'Email' => email)
47
+
48
+ subject.suppress email
49
+ end
50
+ end
51
+
52
+ describe '#data_fields' do
53
+ let(:data_fields) { double 'data fields' }
54
+ let(:cache) { double 'cache' }
55
+
56
+ before(:each) do
57
+ subject.stub :cache => cache
58
+ end
59
+
60
+ context 'when the cache is empty' do
61
+ before(:each) do
62
+ cache.stub(:fetch).with('data_fields').and_yield
63
+ DotMailer::DataField.stub :all => data_fields
64
+ end
65
+
66
+ it 'should call DataField.all' do
67
+ DotMailer::DataField.should_receive(:all).with(subject)
68
+
69
+ subject.data_fields
70
+ end
71
+
72
+ its(:data_fields) { should == data_fields }
73
+ end
74
+
75
+ context 'when the cache is not empty' do
76
+ before(:each) do
77
+ cache.stub(:fetch).with('data_fields').and_return(data_fields)
78
+ end
79
+
80
+ it 'should not call DataField.all' do
81
+ DotMailer::DataField.should_not_receive(:all)
82
+
83
+ subject.data_fields
84
+ end
85
+
86
+ its(:data_fields) { should == data_fields }
87
+ end
88
+ end
89
+
90
+ describe '#create_data_field' do
91
+ let(:name) { double 'name' }
92
+ let(:options) { double 'options' }
93
+ let(:cache) { double 'cache' }
94
+
95
+ before(:each) do
96
+ subject.stub :cache => cache
97
+ DotMailer::DataField.stub :create
98
+ cache.stub :delete
99
+ end
100
+
101
+ it 'should DataField.create' do
102
+ DotMailer::DataField.should_receive(:create).with(subject, name, options)
103
+
104
+ subject.create_data_field(name, options)
105
+ end
106
+
107
+ it 'should clear the data_fields from the cache' do
108
+ cache.should_receive(:delete).with('data_fields')
109
+
110
+ subject.create_data_field(name, options)
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,160 @@
1
+ require 'spec_helper'
2
+
3
+ describe DotMailer::Client do
4
+ let(:api_user) { 'john_doe' }
5
+ let(:api_pass) { 's3cr3t' }
6
+ let(:api_base_url) { "https://#{api_user}:#{api_pass}@api.dotmailer.com" }
7
+ let(:api_path) { '/some/api/path' }
8
+ let(:api_endpoint) { "#{api_base_url}/v2#{api_path}" }
9
+
10
+ subject { DotMailer::Client.new(api_user, api_pass) }
11
+
12
+ describe '#get' do
13
+ let(:response) { { 'foo' => 'bar' } }
14
+
15
+ before(:each) do
16
+ stub_request(:get, api_endpoint).to_return(:body => response.to_json)
17
+ end
18
+
19
+ it 'should GET the endpoint with a JSON accept header' do
20
+ subject.get api_path
21
+
22
+ WebMock.should have_requested(:get, api_endpoint).with(
23
+ :headers => { 'Accept' => 'application/json' }
24
+ )
25
+ end
26
+
27
+ it 'should return the response from the endpoint' do
28
+ subject.get(api_path).should == response
29
+ end
30
+
31
+ context 'when the path is not found' do
32
+ let(:error_message) { 'not found' }
33
+ let(:response) { { 'message' => error_message } }
34
+
35
+ before(:each) do
36
+ stub_request(:get, api_endpoint).to_return(:status => 404, :body => response.to_json)
37
+ end
38
+
39
+ it 'should raise a NotFound error with the error message' do
40
+ expect { subject.get(api_path).should }.to raise_error(DotMailer::NotFound, error_message)
41
+ end
42
+ end
43
+ end
44
+
45
+ describe '#get_csv' do
46
+ # The API includes a UTF-8 BOM in the response...
47
+ let(:response) { "\xEF\xBB\xBFId,Name\n1,Foo\n2,Bar" }
48
+ let(:csv) { double 'csv' }
49
+
50
+ before(:each) do
51
+ stub_request(:get, api_endpoint).to_return(:body => response)
52
+ CSV.stub(:parse => csv)
53
+ end
54
+
55
+ it 'should GET the endpoint with a CSV accept header' do
56
+ subject.get_csv api_path
57
+
58
+ WebMock.should have_requested(:get, api_endpoint).with(
59
+ :headers => { 'Accept' => 'text/csv' }
60
+ )
61
+ end
62
+
63
+ it 'should pass the response to CSV.parse with the correct options' do
64
+ CSV.should_receive(:parse).with(response, :headers => true)
65
+
66
+ subject.get_csv api_path
67
+ end
68
+
69
+ it 'should return the CSV object' do
70
+ subject.get_csv(api_path).should == csv
71
+ end
72
+ end
73
+
74
+ describe '#post' do
75
+ let(:data) { 'some random data' }
76
+ let(:response) { { 'foo' => 'bar' } }
77
+
78
+ before(:each) do
79
+ stub_request(:post, api_endpoint).to_return(:body => response.to_json)
80
+ end
81
+
82
+ it 'should POST the data to the endpoint with a JSON accept header' do
83
+ subject.post api_path, data
84
+
85
+ WebMock.should have_requested(:post, api_endpoint).with(
86
+ :headers => { 'Accept' => 'application/json' },
87
+ :body => data
88
+ )
89
+ end
90
+
91
+ it 'should return the response from the endpoint' do
92
+ subject.post(api_path, data).should == response
93
+ end
94
+
95
+ context 'when the data is invalid for the endpoint' do
96
+ let(:error_message) { 'invalid data' }
97
+ let(:response) { { 'message' => error_message } }
98
+
99
+ before(:each) do
100
+ stub_request(:post, api_endpoint).to_return(:status => 400, :body => response.to_json)
101
+ end
102
+
103
+ it 'should raise an InvalidRequest error with the error message' do
104
+ expect { subject.post(api_path, data) }.to raise_error(DotMailer::InvalidRequest, error_message)
105
+ end
106
+ end
107
+ end
108
+
109
+ describe '#post_json' do
110
+ let(:params) { { 'foo' => 'bar' } }
111
+
112
+ it 'should call post with the path' do
113
+ subject.should_receive(:post).with(api_path, anything, anything)
114
+
115
+ subject.post_json api_path, params
116
+ end
117
+
118
+ it 'should convert the params to JSON' do
119
+ subject.should_receive(:post).with(anything, params.to_json, anything)
120
+
121
+ subject.post_json api_path, params
122
+ end
123
+
124
+ it 'should pass use the correct content type' do
125
+ subject.should_receive(:post).with(anything, anything, hash_including(:content_type => :json))
126
+
127
+ subject.post_json api_path, params
128
+ end
129
+ end
130
+
131
+ describe '#post_csv' do
132
+ let(:csv) { "Some\nCSV\nString" }
133
+ let(:tempfile) { double 'tempfile', :write => true, :rewind => true }
134
+
135
+ before(:each) do
136
+ Tempfile.stub :new => tempfile
137
+ subject.stub :post => double
138
+ end
139
+
140
+ it 'should call post with the path' do
141
+ subject.should_receive(:post).with(api_path, anything)
142
+
143
+ subject.post_csv api_path, csv
144
+ end
145
+
146
+ it 'should create a Tempfile with the contents and rewind it' do
147
+ Tempfile.should_receive(:new).and_return(tempfile)
148
+ tempfile.should_receive(:write).with(csv)
149
+ tempfile.should_receive(:rewind)
150
+
151
+ subject.post_csv api_path, csv
152
+ end
153
+
154
+ it 'should call post with the tempfile' do
155
+ subject.should_receive(:post).with(api_path, hash_including(:csv => tempfile))
156
+
157
+ subject.post_csv api_path, csv
158
+ end
159
+ end
160
+ end