acts_as_icontact 0.1.1 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -23,26 +23,26 @@ Using ActsAsIcontact is easy, but going through iContact's authorization process
23
23
 
24
24
  $ sudo gem install acts_as_icontact
25
25
 
26
- 2. _Optional but recommended:_ Go to <http://beta.icontact.com> and sign up for an iContact Beta account. This will let you test your app without risk of blowing away your production mailing lists.
26
+ 2. _Optional but recommended:_ Go to <http://sandbox.icontact.com> and sign up for an iContact Sandbox account. This will let you test your app without risk of blowing away your production mailing lists.
27
27
 
28
- 3. Enable the ActsAsIcontact gem for use with your iContact account. The URL and credentials you'll use are different between the beta and production environments:
28
+ 3. Enable the ActsAsIcontact gem for use with your iContact account. The URL and credentials you'll use are different between the sandbox and production environments:
29
29
 
30
- * **BETA:** Go to <http://app.beta.icontact.com/icp/core/externallogin> and enter `Ml5SnuFhnoOsuZeTOuZQnLUHTbzeUyhx` for the Application Id. Choose a password for ActsAsIcontact that's different from your account password.
30
+ * **Sandbox:** Go to <http://app.sandbox.icontact.com/icp/core/externallogin> and enter `Ml5SnuFhnoOsuZeTOuZQnLUHTbzeUyhx` for the Application Id. Choose a password for ActsAsIcontact that's different from your account password.
31
31
 
32
32
  * **PRODUCTION:** Go to <http://app.icontact.com/icp/core/externallogin> and enter `IYDOhgaZGUKNjih3hl1ItLln7zpAtWN2` for the Application Id. Choose a password for ActsAsIcontact that's different from your account password.
33
33
 
34
- 4. Set your _(beta, if applicable)_ account username and the password you just chose for API access. You can either set the environment variables `ICONTACT_MODE`, `ICONTACT_USERNAME`, and `ICONTACT_PASSWORD`, or you can explicitly do it with calls to the Config module:
34
+ 4. Set your _(sandbox, if applicable)_ account username and the password you just chose for API access. You can either set the environment variables `ICONTACT_MODE`, `ICONTACT_USERNAME`, and `ICONTACT_PASSWORD`, or you can explicitly do it with calls to the Config module:
35
35
 
36
36
  require 'rubygems'
37
37
  require 'acts_as_icontact'
38
38
 
39
- ActsAsIcontact::Config.mode = :beta
40
- ActsAsIcontact::Config.username = my_beta_username
39
+ ActsAsIcontact::Config.mode = :sandbox
40
+ ActsAsIcontact::Config.username = my_sandbox_username
41
41
  ActsAsIcontact::Config.password = my_api_password
42
42
 
43
43
  If you're using Rails, the recommended approach is to require the gem with `config.gem 'acts_as_icontact'` in your **config/environment.rb** file, and then set up an initializer (i.e. **config/initializers/acts\_as\_icontact.rb**) with the above code. See more about Rails below.
44
44
 
45
- 5. Rinse and repeat with production credentials when you're ready to move out of the beta environment.
45
+ 5. Rinse and repeat with production credentials when you're ready to move out of the sandbox environment.
46
46
 
47
47
  API Access
48
48
  ----------
@@ -120,20 +120,20 @@ Rails Integration
120
120
  The _real_ power of ActsAsIcontact is its automatic syncing with ActiveRecord. At this time this feature is focused entirely on Contacts.
121
121
 
122
122
  ### Activation
123
- First add the line `config.gem 'acts_as_icontact'` to your **config/environment.rb** file. Then create an initializer (e.g. **config/initializers/acts\_as\_icontact.rb**) and set it up with your username and password. If applicable, you can give it both the beta _and_ production credentials:
123
+ First add the line `config.gem 'acts_as_icontact'` to your **config/environment.rb** file. Then create an initializer (e.g. **config/initializers/acts\_as\_icontact.rb**) and set it up with your username and password. If applicable, you can give it both the sandbox _and_ production credentials:
124
124
 
125
125
  module ActsAsIcontact
126
126
  case Config.mode
127
- when :beta
128
- Config.username = my_beta_username
129
- Config.password = my_beta_password
127
+ when :sandbox
128
+ Config.username = my_sandbox_username
129
+ Config.password = my_sandbox_password
130
130
  when :production
131
131
  Config.username = my_production_username
132
132
  Config.password = my_production_password
133
133
  end
134
134
  end
135
135
 
136
- If ActsAsIcontact detects that it's running in a Rails app, the default behavior is to set the mode to `:production` if RAILS\_ENV is equal to "production" and `:beta` if RAILS\_ENV is set to anything else. (Incidentally, if you're _not_ in a Rails app but running Rack, the same logic applies for the RACK\_ENV environment variable.)
136
+ If ActsAsIcontact detects that it's running in a Rails app, the default behavior is to set the mode to `:production` if RAILS\_ENV is equal to "production" and `:sandbox` if RAILS\_ENV is set to anything else. (Incidentally, if you're _not_ in a Rails app but running Rack, the same logic applies for the RACK\_ENV environment variable.)
137
137
 
138
138
  Finally, enable one of your models to synchronize with iContact with a simple declaration:
139
139
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.1.5
@@ -2,11 +2,11 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{acts_as_icontact}
5
- s.version = "0.1.1"
5
+ s.version = "0.1.5"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Stephen Eley"]
9
- s.date = %q{2009-07-24}
9
+ s.date = %q{2009-07-25}
10
10
  s.description = %q{ActsAsIcontact connects Ruby applications with the iContact e-mail marketing service using the iContact API v2.0. Building on the RestClient gem, it offers two significant feature sets:
11
11
 
12
12
  * Simple, consistent access to all resources in the iContact API; and
@@ -33,6 +33,8 @@ Gem::Specification.new do |s|
33
33
  "lib/acts_as_icontact/resources/account.rb",
34
34
  "lib/acts_as_icontact/resources/client.rb",
35
35
  "lib/acts_as_icontact/resources/contact.rb",
36
+ "lib/acts_as_icontact/resources/list.rb",
37
+ "lib/acts_as_icontact/resources/message.rb",
36
38
  "spec/config_spec.rb",
37
39
  "spec/connection_spec.rb",
38
40
  "spec/resource_collection_spec.rb",
@@ -40,6 +42,8 @@ Gem::Specification.new do |s|
40
42
  "spec/resources/account_spec.rb",
41
43
  "spec/resources/client_spec.rb",
42
44
  "spec/resources/contact_spec.rb",
45
+ "spec/resources/list_spec.rb",
46
+ "spec/resources/message_spec.rb",
43
47
  "spec/spec.opts",
44
48
  "spec/spec_fakeweb.rb",
45
49
  "spec/spec_helper.rb"
@@ -58,6 +62,8 @@ Gem::Specification.new do |s|
58
62
  "spec/resources/account_spec.rb",
59
63
  "spec/resources/client_spec.rb",
60
64
  "spec/resources/contact_spec.rb",
65
+ "spec/resources/list_spec.rb",
66
+ "spec/resources/message_spec.rb",
61
67
  "spec/spec_fakeweb.rb",
62
68
  "spec/spec_helper.rb"
63
69
  ]
@@ -3,15 +3,15 @@ module ActsAsIcontact
3
3
  # required by the iContact API for authentication.
4
4
  module Config
5
5
 
6
- # Sets :production or :beta. This changes the AppId and URL.
6
+ # Sets :production or :sandbox. This changes the AppId and URL.
7
7
  def self.mode=(val)
8
8
  @mode = val
9
9
  end
10
10
 
11
- # Determines whether to return the beta or production AppId and URL.
11
+ # Determines whether to return the sandbox or production AppId and URL.
12
12
  # If not explicitly set, it will first look for an ICONTACT_MODE environment variable.
13
13
  # If it doesn't find one, it will attempt to detect a Rails or Rack environment; in either
14
- # case it will default to :production if RAILS_ENV or RACK_ENV is 'production', and :beta
14
+ # case it will default to :production if RAILS_ENV or RACK_ENV is 'production', and :sandbox
15
15
  # otherwise. If none of these conditions apply, it assumes :production. (Because that
16
16
  # probably means someone's doing ad hoc queries.)
17
17
  def self.mode
@@ -19,9 +19,9 @@ module ActsAsIcontact
19
19
  when ENV["ICONTACT_MODE"]
20
20
  ENV["ICONTACT_MODE"].to_sym
21
21
  when Object.const_defined?(:Rails)
22
- (ENV["RAILS_ENV"] == 'production' ? :production : :beta)
22
+ (ENV["RAILS_ENV"] == 'production' ? :production : :sandbox)
23
23
  when Object.const_defined?(:Rack)
24
- (ENV["RACK_ENV"] == 'production' ? :production : :beta)
24
+ (ENV["RACK_ENV"] == 'production' ? :production : :sandbox)
25
25
  else
26
26
  :production
27
27
  end
@@ -32,7 +32,7 @@ module ActsAsIcontact
32
32
  # to change this. Ever.
33
33
  def self.app_id
34
34
  case mode
35
- when :beta
35
+ when :beta, :sandbox
36
36
  "Ml5SnuFhnoOsuZeTOuZQnLUHTbzeUyhx"
37
37
  when :production
38
38
  "IYDOhgaZGUKNjih3hl1ItLln7zpAtWN2"
@@ -45,14 +45,16 @@ module ActsAsIcontact
45
45
  end
46
46
 
47
47
  # Prefixed to the beginning of every API request. You can override this if you have some special
48
- # need (e.g. working against a testing server, or if iContact takes their API out of beta and
49
- # changes the URI before the gem gets updated), but for the most part you can leave it alone.
48
+ # need (e.g. working against a testing server, or if iContact changes their URL and you have to
49
+ # fix it before the gem gets updated), but for the most part you can leave it alone.
50
50
  def self.url
51
51
  @url ||= case mode
52
- when :beta
53
- "https://app.beta.icontact.com/icp/"
54
52
  when :production
55
53
  "https://app.icontact.com/icp/"
54
+ when :sandbox
55
+ "https://app.sandbox.icontact.com/icp/"
56
+ when :beta # The 'beta' environment still works as of 7/25/2009
57
+ "https://app.beta.icontact.com/icp/"
56
58
  end
57
59
  end
58
60
 
@@ -2,6 +2,12 @@ module ActsAsIcontact
2
2
  # Thrown when a configuration value isn't provided or is invalid.
3
3
  class ConfigError < StandardError; end
4
4
 
5
+ # Thrown when a bad parameter is passed to a resource find.
6
+ class QueryError < StandardError; end
7
+
8
+ # Thrown before saving if iContact validation rules are not met.
9
+ class ValidationError < StandardError; end
10
+
5
11
  # Thrown when a resource calls save! and fails. Contains the +.errors+ array from
6
12
  # the resource.
7
13
  class RecordNotSaved < StandardError
@@ -8,11 +8,10 @@ module ActsAsIcontact
8
8
 
9
9
  # Creates a new resource object from a values hash. (Which is passed to us via the magic of JSON.)
10
10
  def initialize(properties={})
11
- @properties = properties
11
+ @properties = clean_properties(properties)
12
12
  @new_record = !@properties.has_key?(self.class.primary_key)
13
13
  # Initialize other useful attributes
14
14
  @errors = []
15
-
16
15
  end
17
16
 
18
17
  # Returns the primary key ID for an existing resource. Returns nil if the resource is a new record.
@@ -33,10 +32,14 @@ module ActsAsIcontact
33
32
  if property =~ /(.*)=$/ # It's a value assignment
34
33
  @newvalues ||= []
35
34
  @newvalues << $1
36
- @properties[$1] = params[0]
37
- else
35
+ @properties[$1] = clean_value(params[0])
36
+ else
38
37
  if @properties.has_key?(property)
39
- @properties[property]
38
+ if self.class.boolean_fields.include?(property)
39
+ (@properties[property] == 1)
40
+ else
41
+ @properties[property]
42
+ end
40
43
  else
41
44
  super
42
45
  end
@@ -56,12 +59,15 @@ module ActsAsIcontact
56
59
  # error, raises an exception with it.
57
60
  def save
58
61
  if new_record?
62
+ fields = create_fields
63
+ validate_on_create(fields)
59
64
  result_type = self.class.collection_name
60
- payload = {self.class.collection_name => [create_fields]}
61
- response = self.class.connection.post(payload.to_json)
65
+ response = self.class.connection.post([fields].to_json)
62
66
  else
67
+ fields = update_fields
68
+ validate_on_update(fields)
63
69
  result_type = self.class.resource_name
64
- response = connection.post(update_fields.to_json)
70
+ response = connection.post(fields.to_json)
65
71
  end
66
72
  parsed = JSON.parse(response)
67
73
  if parsed[result_type].empty?
@@ -98,25 +104,46 @@ module ActsAsIcontact
98
104
 
99
105
  # Returns an array of resources starting at the base.
100
106
  def self.find(type, options={})
101
- uri_extension = uri_component + build_query(options)
102
- response = base[uri_extension].get
103
- parsed = JSON.parse(response)
104
107
  case type
105
- when :first then
106
- self.new(parsed[collection_name].first) if parsed[collection_name]
107
- when :all then
108
- ResourceCollection.new(self, parsed)
108
+ when :first
109
+ first(options)
110
+ when :all
111
+ all(options)
112
+ when Integer
113
+ find_by_id(type)
114
+ else
115
+ raise ActsAsIcontact::QueryError, "Don't know how to find '#{type.to_s}'"
109
116
  end
110
117
  end
111
118
 
112
119
  # Returns an array of resources starting at the base.
113
- def self.all
114
- find(:all)
120
+ def self.all(options={})
121
+ query_options = default_options.merge(options)
122
+ validate_options(query_options)
123
+ result = query_collection(query_options)
124
+ ResourceCollection.new(self, result)
115
125
  end
116
126
 
117
127
  # Returns the first account associated with this username.
118
- def self.first
119
- find(:first)
128
+ def self.first(options={})
129
+ query_options = default_options.merge(options).merge(:limit => 1) # Minor optimization
130
+ validate_options(query_options)
131
+ result = query_collection(query_options)
132
+ self.new(result[collection_name].first)
133
+ end
134
+
135
+ # Returns the single resource at the URL identified by the passed integer. Takes no options; this is
136
+ # not a search, this is a single record retrieval. Raises an exception if the record can't be found.
137
+ def self.find_by_id(id)
138
+ response = self.connection[id].get
139
+ parsed = JSON.parse(response)
140
+ raise ActsAsiContact::QueryError, "iContact's response did not contain a #{resource_name}!" unless parsed[resource_name]
141
+ self.new(parsed[resource_name])
142
+ end
143
+
144
+ # Two resources are identical if they have exactly the same property array.
145
+ def ==(obj)
146
+ properties == obj.properties
120
147
  end
121
148
 
122
149
  protected
@@ -139,10 +166,10 @@ module ActsAsIcontact
139
166
  @properties
140
167
  end
141
168
 
142
- # The base RestClient resource that this particular class nests from. Starts with
143
- # the resource connection at 'https://api.icontact.com/icp/' and works its way up.
169
+ # The base RestClient resource that this particular class nests from. Defaults to
170
+ # the clientFolders path since that's the most common case.
144
171
  def self.base
145
- ActsAsIcontact.connection
172
+ ActsAsIcontact.client
146
173
  end
147
174
 
148
175
  # The name of the singular resource type pulled from iContact. Defaults to the lowercase
@@ -174,6 +201,11 @@ module ActsAsIcontact
174
201
  resource_name + "Id"
175
202
  end
176
203
 
204
+ # Options that are always passed on 'find' requests unless overridden.
205
+ def self.default_options
206
+ {:limit => 500}
207
+ end
208
+
177
209
  # Fields that _must_ be included for this resource upon creation.
178
210
  def self.required_on_create
179
211
  []
@@ -194,11 +226,86 @@ module ActsAsIcontact
194
226
  []
195
227
  end
196
228
 
229
+ # Fields that operate as 0/1 boolean toggles. Can be assigned to with true and false.
230
+ def self.boolean_fields
231
+ []
232
+ end
233
+
234
+ # Validation rules that ensure proper parameters are passed to iContact on querying.
235
+ def self.validate_options(options)
236
+ # See: http://developer.icontact.com/forums/api-beta-moderated-support/there-upper-limit-result-sets#comment-136
237
+ raise ActsAsIcontact::QueryError, "Limit must be between 1 and 500" if options[:limit].to_i < 1 or options[:limit].to_i > 500
238
+ end
239
+
240
+ # Validation rules that ensure proper data is passed to iContact on resource creation.
241
+ def validate_on_create(fields)
242
+ check_required_fields(fields, self.class.required_on_create)
243
+ validate_on_save(fields)
244
+ end
245
+
246
+ # Validation rules that ensure proper data is passed to iContact on resource update.
247
+ def validate_on_update(fields)
248
+ check_required_fields(fields, self.class.required_on_update)
249
+ validate_on_save(fields)
250
+ end
251
+
252
+ # Validation rules that apply to both creates and updates. The method on the abstract Resource class is just a placeholder;
253
+ # this is intended to be used by resource subclasses.
254
+ def validate_on_save(fields)
255
+ end
256
+
257
+ # Finesses the properties hash passed in to make iContact and Ruby idioms compatible.
258
+ # Turns symbol keys into strings and runs the clean_value method on values.
259
+ # Subclasses may add additional conversions.
260
+ def clean_properties(properties)
261
+ newhash = {}
262
+ properties.each_pair do |key, value|
263
+ newhash[key.to_s] = clean_value(value)
264
+ end
265
+ newhash
266
+ end
267
+
268
+ # Finesses values passed in to properties to make iContact and Ruby idioms compatible.
269
+ # Turns symbols into strings, numbers into integers or floats, true/false into 1 and 0,
270
+ # and empty strings into nil. Subclasses may add additional conversions.
271
+ def clean_value(value)
272
+ case value
273
+ when Symbol then value.to_s
274
+ when TrueClass then 1
275
+ when FalseClass then 0
276
+ when /^\d+$/ then value.to_i # Integer
277
+ when /^\d+(\.\d+)?([eE]\d+)?$/ then value.to_f # Float
278
+ when blank? then nil
279
+ else value
280
+ end
281
+ end
282
+
283
+ # The properties array, for comparison against other resources or debugging.
284
+ def properties
285
+ @properties
286
+ end
287
+
197
288
  private
198
289
  def self.build_query(options={})
199
290
  return "" if options.empty?
200
291
  terms = options.collect{|k,v| "#{k}=#{URI.escape(v.to_s)}"}
201
292
  build = "?" + terms.join('&')
202
293
  end
294
+
295
+ def self.query_collection(options={})
296
+ uri_extension = uri_component + build_query(options)
297
+ response = base[uri_extension].get
298
+ parsed = JSON.parse(response)
299
+ parsed
300
+ end
301
+
302
+ def check_required_fields(fields, required)
303
+ # Check that all required fields are filled in
304
+ missing = required.select{|f| fields[f].blank?}
305
+ unless missing.empty?
306
+ missing_fields = missing.join(', ')
307
+ raise ActsAsIcontact::ValidationError, "Missing required fields: #{missing_fields}"
308
+ end
309
+ end
203
310
  end
204
311
  end
@@ -2,6 +2,11 @@ module ActsAsIcontact
2
2
  # The top-level Accounts resource from iContact. Currently only supports retrieval -- and is
3
3
  # highly targeted toward the _first_ account, since that seems to be the dominant use case.
4
4
  class Account < Resource
5
+ # This is the one major resource that comes directly from the main path.
6
+ def self.base
7
+ ActsAsIcontact.connection
8
+ end
9
+
5
10
  def self.uri_component
6
11
  'a'
7
12
  end
@@ -15,7 +20,7 @@ module ActsAsIcontact
15
20
  # The accountId retrieved from iContact. Can also be set manually for performance optimization,
16
21
  # but remembers it so that it won't be pulled more than once anyway.
17
22
  def self.account_id
18
- @account_id ||= Account.first.accountId.to_i
23
+ @account_id ||= Account.first.id.to_i
19
24
  end
20
25
 
21
26
  # Manually sets the accountId used in subsequent calls. Setting this in your initializer will save
@@ -2,6 +2,11 @@ module ActsAsIcontact
2
2
  # The nested Client Folder resource from iContact. Currently only supports retrieval -- and is
3
3
  # highly targeted toward the _first_ client folder, since that seems to be the dominant use case.
4
4
  class Client < Resource
5
+ # Derives from the Account resource.
6
+ def self.base
7
+ ActsAsIcontact.account
8
+ end
9
+
5
10
  def self.resource_name
6
11
  'clientfolder'
7
12
  end
@@ -1,7 +1,20 @@
1
1
  module ActsAsIcontact
2
2
  class Contact < Resource
3
+
4
+ # Email is required
3
5
  def self.required_on_create
4
- ['email']
6
+ super << ['email']
5
7
  end
8
+
9
+ # Derived from clientFolder
10
+ def self.base
11
+ ActsAsIcontact.client
12
+ end
13
+
14
+ # Defaults to status=total to return contacts on or off lists
15
+ def self.default_options
16
+ super.merge(:status=>:total)
17
+ end
18
+
6
19
  end
7
20
  end
@@ -0,0 +1,24 @@
1
+ module ActsAsIcontact
2
+ class List < Resource
3
+ # Derives from clientFolder.
4
+ def self.base
5
+ ActsAsIcontact.client
6
+ end
7
+
8
+ # Requires name, emailOwnerOnChange, welcomeOnManualAdd, welcomeOnSignupAdd, and welcomeMessageId.
9
+ def self.required_on_create
10
+ super << "name" << "emailOwnerOnChange" << "welcomeOnManualAdd" << "welcomeOnSignupAdd" << "welcomeMessageId"
11
+ end
12
+
13
+ def self.boolean_fields
14
+ super << "emailOwnerOnChange" << "welcomeOnManualAdd" << "welcomeOnSignupAdd"
15
+ end
16
+
17
+ # The welcome message pointed to by the welcomeMessageId.
18
+ def welcomeMessage
19
+ return nil unless welcomeMessageId
20
+ ActsAsIcontact::Message.find(welcomeMessageId)
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ module ActsAsIcontact
2
+ class Message < Resource
3
+ # Has a default messageType of "normal" if another isn't passed as an option.
4
+ def initialize(properties={})
5
+ super({:messageType => "normal"}.merge(properties))
6
+ end
7
+
8
+ # Requires messageType and subject
9
+ def self.required_on_create
10
+ super << "messageType" << "subject"
11
+ end
12
+
13
+ # messageType must be one of four values: normal, autoresponder, welcome, or confirmation
14
+ def validate_on_save(fields)
15
+ messageType = %w(normal autoresponder welcome confirmation)
16
+ raise ActsAsIcontact::ValidationError, "messageType must be one of: " + messageType.join(', ') unless messageType.include?(fields["messageType"])
17
+ end
18
+ end
19
+ end
data/spec/config_spec.rb CHANGED
@@ -41,9 +41,9 @@ describe "Configuration" do
41
41
  Object.expects(:const_defined?).with(:Rails).returns(true)
42
42
  end
43
43
 
44
- it "is beta if RAILS_ENV is not production" do
44
+ it "is sandbox if RAILS_ENV is not production" do
45
45
  ENV["RAILS_ENV"] = 'staging'
46
- ActsAsIcontact::Config.mode.should == :beta
46
+ ActsAsIcontact::Config.mode.should == :sandbox
47
47
  end
48
48
 
49
49
  it "is production if RAILS_ENV is production" do
@@ -65,9 +65,9 @@ describe "Configuration" do
65
65
  Object.expects(:const_defined?).with(:Rack).returns(true)
66
66
  end
67
67
 
68
- it "is beta if RACK_ENV is not production" do
68
+ it "is sandbox if RACK_ENV is not production" do
69
69
  ENV["RACK_ENV"] = 'staging'
70
- ActsAsIcontact::Config.mode.should == :beta
70
+ ActsAsIcontact::Config.mode.should == :sandbox
71
71
  end
72
72
 
73
73
  it "is production if RACK_ENV is production" do
@@ -81,17 +81,17 @@ describe "Configuration" do
81
81
  end
82
82
 
83
83
 
84
- context ":beta" do
84
+ context ":sandbox" do
85
85
  before(:each) do
86
- ActsAsIcontact::Config.mode = :beta
86
+ ActsAsIcontact::Config.mode = :sandbox
87
87
  end
88
88
 
89
- it "returns the beta AppId" do
89
+ it "returns the sandbox AppId" do
90
90
  ActsAsIcontact::Config.app_id.should == "Ml5SnuFhnoOsuZeTOuZQnLUHTbzeUyhx"
91
91
  end
92
92
 
93
- it "returns the beta URL" do
94
- ActsAsIcontact::Config.url.should == "https://app.beta.icontact.com/icp/"
93
+ it "returns the sandbox URL" do
94
+ ActsAsIcontact::Config.url.should == "https://app.sandbox.icontact.com/icp/"
95
95
  end
96
96
 
97
97
  end
@@ -2,7 +2,7 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
 
3
3
  describe ActsAsIcontact::Resource do
4
4
  it "has a RestClient connection" do
5
- ActsAsIcontact::Resource.connection.url.should == ActsAsIcontact.connection['resources'].url
5
+ ActsAsIcontact::Resource.connection.url.should == ActsAsIcontact.client['resources'].url
6
6
  end
7
7
 
8
8
  it "can return all resources for the given URL" do
@@ -51,6 +51,35 @@ describe ActsAsIcontact::Resource do
51
51
  r.first.foo.should == "kar"
52
52
  r[1].foo.should == "yar"
53
53
  end
54
+
55
+ it "defaults to a limit of 500" do
56
+ ActsAsIcontact::Resource.base.expects(:[]).with(regexp_matches(/limit=500/)).returns(stub(:get => '{"resources":[]}'))
57
+ r = ActsAsIcontact::Resource.find(:all)
58
+ end
59
+
60
+ it "throws an exception if a limit higher than 500 is attempted" do
61
+ lambda{r = ActsAsIcontact::Resource.find(:all, :limit => 501)}.should raise_error(ActsAsIcontact::QueryError, "Limit must be between 1 and 500")
62
+ end
63
+
64
+ it "throws an exception if a limit lower than 500 is attempted" do
65
+ lambda{r = ActsAsIcontact::Resource.find(:all, :limit => 501)}.should raise_error(ActsAsIcontact::QueryError, "Limit must be between 1 and 500")
66
+ end
67
+
68
+ it "maps the 'first' method to find(:first)" do
69
+ ActsAsIcontact::Resource.expects(:first).with({:foo=>:bar}).returns(nil)
70
+ ActsAsIcontact::Resource.find(:first, :foo=>:bar)
71
+ end
72
+
73
+ it "maps the 'all' method to find(:all)" do
74
+ ActsAsIcontact::Resource.expects(:all).with({:foo=>:bar}).returns(nil)
75
+ ActsAsIcontact::Resource.find(:all, :foo=>:bar)
76
+ end
77
+
78
+ it "can find a single resource by ID" do
79
+ a = ActsAsIcontact::Resource.find(1)
80
+ a.too.should == "sar"
81
+ end
82
+
54
83
  end
55
84
 
56
85
  it "knows its properties" do
@@ -70,7 +99,7 @@ describe ActsAsIcontact::Resource do
70
99
 
71
100
  it "has its own connection if it's not new" do
72
101
  r = ActsAsIcontact::Resource.first
73
- r.connection.url.should == ActsAsIcontact.connection['resources/1'].url
102
+ r.connection.url.should == ActsAsIcontact.client['resources/1'].url
74
103
  end
75
104
 
76
105
  it "does not have a connection if it's new" do
@@ -79,7 +108,7 @@ describe ActsAsIcontact::Resource do
79
108
  end
80
109
 
81
110
  it "knows its REST base resource" do
82
- ActsAsIcontact::Resource.base.should == ActsAsIcontact.connection
111
+ ActsAsIcontact::Resource.base.should == ActsAsIcontact.client
83
112
  end
84
113
 
85
114
  it "knows its primary key" do
@@ -94,6 +123,16 @@ describe ActsAsIcontact::Resource do
94
123
  ActsAsIcontact::Resource.never_on_create.should == ["resourceId"]
95
124
  end
96
125
 
126
+ it "accepts symbols for properties on creation" do
127
+ a = ActsAsIcontact::Resource.new(:foofoo => "bunny")
128
+ a.foofoo.should == "bunny"
129
+ end
130
+
131
+ it "typecasts all integer property values if it can" do
132
+ a = ActsAsIcontact::Resource.new("indianaPi" => "3")
133
+ a.indianaPi.should == 3
134
+ end
135
+
97
136
  context "updating records" do
98
137
  before(:each) do
99
138
  @res = ActsAsIcontact::Resource.first
@@ -114,7 +153,12 @@ describe ActsAsIcontact::Resource do
114
153
 
115
154
  it "knows the minimum set of properties that changed or must be sent" do
116
155
  @res.too = "tar"
117
- @res.send(:update_fields).should == {"resourceId" => "1", "too" => "tar"}
156
+ @res.send(:update_fields).should == {"resourceId" => 1, "too" => "tar"}
157
+ end
158
+
159
+ it "throws an exception if required fields aren't included" do
160
+ @res.resourceId = nil
161
+ lambda{@res.save}.should raise_error(ActsAsIcontact::ValidationError, "Missing required fields: resourceId")
118
162
  end
119
163
 
120
164
  context "with successful save" do
@@ -219,7 +263,7 @@ describe ActsAsIcontact::Resource do
219
263
 
220
264
  context "with successful save" do
221
265
  before(:each) do
222
- FakeWeb.register_uri(:post, "https://app.beta.icontact.com/icp/resources", :body => %q<{"resources":[{"resourceId":"100","foo":"flar","kroo":"krar","too":"sar"}]}>)
266
+ FakeWeb.register_uri(:post, "https://app.sandbox.icontact.com/icp/a/111111/c/222222/resources", :body => %q<{"resources":[{"resourceId":"100","foo":"flar","kroo":"krar","too":"sar"}]}>)
223
267
  @res.too = "sar"
224
268
  end
225
269
 
@@ -249,7 +293,7 @@ describe ActsAsIcontact::Resource do
249
293
 
250
294
  context "with failed save but status 200" do
251
295
  before(:each) do
252
- FakeWeb.register_uri(:post, "https://app.beta.icontact.com/icp/resources", :body => %q<{"resources":[],"warnings":["You did not provide a foo. foo is a required field. Please provide a foo","This was not a good record"]}>)
296
+ FakeWeb.register_uri(:post, "https://app.sandbox.icontact.com/icp/a/111111/c/222222/resources", :body => %q<{"resources":[],"warnings":["You did not provide a foo. foo is a required field. Please provide a foo","This was not a good record"]}>)
253
297
  @res = ActsAsIcontact::Resource.new
254
298
  @res.foo = nil
255
299
  @result = @res.save
@@ -274,7 +318,7 @@ describe ActsAsIcontact::Resource do
274
318
 
275
319
  context "with failed save on HTTP failure exception" do
276
320
  before(:each) do
277
- FakeWeb.register_uri(:post, "https://app.beta.icontact.com/icp/resources", :status => ["400","Bad Request"], :body => %q<{"errors":["You did not provide a clue. Clue is a required field. Please provide a clue"]}>)
321
+ FakeWeb.register_uri(:post, "https://app.sandbox.icontact.com/icp/a/111111/c/222222/resources", :status => ["400","Bad Request"], :body => %q<{"errors":["You did not provide a clue. Clue is a required field. Please provide a clue"]}>)
278
322
  @res = ActsAsIcontact::Resource.new
279
323
  @res.foo = nil
280
324
  @result = @res.save
@@ -1,8 +1,25 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
2
 
3
3
  describe ActsAsIcontact::Contact do
4
- it "defaults to a limit of 500"
5
- it "defaults to searching on all contacts regardless of list status"
6
- it "throws an exception if a limit higher than 500 is attempted"
7
4
 
5
+ it "defaults to searching on all contacts regardless of list status" do
6
+ ActsAsIcontact::Contact.base.expects(:[]).with(regexp_matches(/status=total/)).returns(stub(:get => '{"contacts":[]}'))
7
+ r = ActsAsIcontact::Contact.find(:all)
8
+ end
9
+
10
+ it "requires email address" do
11
+ c = ActsAsIcontact::Contact.new
12
+ lambda{c.save}.should raise_error(ActsAsIcontact::ValidationError, "Missing required fields: email")
13
+ end
14
+
15
+ context "associations" do
16
+ # We have _one_ really good contact set up here
17
+ before(:each) do
18
+ @john = ActsAsIcontact::Contact.first(:firstName => "John", :lastName => "Test")
19
+ end
20
+
21
+ it "knows which lists it's subscribed to"
22
+ it "knows its history"
23
+ end
24
+
8
25
  end
@@ -0,0 +1,35 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe ActsAsIcontact::List do
4
+ it "requires name, emailOwnerOnChange, welcomeOnManualAdd, welcomeOnSignupAdd, welcomeMessageId" do
5
+ l = ActsAsIcontact::List.new
6
+ lambda{l.save}.should raise_error(ActsAsIcontact::ValidationError, "Missing required fields: name, emailOwnerOnChange, welcomeOnManualAdd, welcomeOnSignupAdd, welcomeMessageId")
7
+ end
8
+
9
+ it "uses true and false to assign boolean fields" do
10
+ l = ActsAsIcontact::List.new
11
+ l.emailOwnerOnChange = true
12
+ l.welcomeOnSignupAdd = false
13
+ l.instance_variable_get(:@properties)["emailOwnerOnChange"].should == 1
14
+ l.instance_variable_get(:@properties)["welcomeOnSignupAdd"].should == 0
15
+ end
16
+
17
+ it "uses true and false to retrieve boolean fields" do
18
+ l = ActsAsIcontact::List.new
19
+ l.instance_variable_set(:@properties,{"welcomeOnManualAdd" => 1, "emailOwnerOnChange" => 0})
20
+ l.emailOwnerOnChange.should be_false
21
+ l.welcomeOnManualAdd.should be_true
22
+ end
23
+
24
+ context "associations" do
25
+ # Create one good list
26
+ before(:each) do
27
+ @list = ActsAsIcontact::List.first(:name => "First Test")
28
+ end
29
+ it "knows its subscribers"
30
+
31
+ it "knows its welcome message" do
32
+ @list.welcomeMessage.should == ActsAsIcontact::Message.find(555555)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe ActsAsIcontact::Message do
4
+
5
+ it "defaults messageType to normal" do
6
+ m = ActsAsIcontact::Message.new
7
+ m.messageType.should == "normal"
8
+ end
9
+
10
+ it "requires messageType and subject" do
11
+ m = ActsAsIcontact::Message.new(:messageType => nil)
12
+ lambda{m.save}.should raise_error(ActsAsIcontact::ValidationError, "Missing required fields: messageType, subject")
13
+ end
14
+
15
+ it "must have an acceptable messageType" do
16
+ m = ActsAsIcontact::Message.new(:messageType => "dummy", :subject => "test")
17
+ lambda{m.save}.should raise_error(ActsAsIcontact::ValidationError, "messageType must be one of: normal, autoresponder, welcome, confirmation")
18
+ end
19
+
20
+ context "associations" do
21
+ before(:each) do
22
+ @message = ActsAsIcontact::Message.first(:subject => "Test Message")
23
+ end
24
+
25
+ it "knows which campaign it has (if any)"
26
+ end
27
+
28
+ end
data/spec/spec_fakeweb.rb CHANGED
@@ -2,22 +2,41 @@ require 'rubygems'
2
2
  require 'fakeweb'
3
3
 
4
4
  FakeWeb.allow_net_connect = false
5
+ i = "https://app.sandbox.icontact.com/icp"
6
+ ic = "#{i}/a/111111/c/222222"
5
7
 
6
8
  # Resources (this one's a fake stub for pure testing)
7
- FakeWeb.register_uri(:get, "https://app.beta.icontact.com/icp/resources", :body => %q<{"resources":[{"foo":"bar","resourceId":"1","too":"bar"},{"foo":"aar","resourceId":"2"},{"foo":"far","resourceId":"3"},{"foo":"car","resourceId":"4"},{"foo":"dar","resourceId":"5"},{"foo":"ear","resourceId":"6"},{"foo":"gar","resourceId":"7"},{"foo":"har","resourceId":"8"},{"foo":"iar","resourceId":"9"},{"foo":"jar","resourceId":"10"},{"foo":"kar","resourceId":"11"},{"foo":"yar","resourceId":"12"}],"total":12,"limit":20,"offset":0}>)
8
- FakeWeb.register_uri(:get, "https://app.beta.icontact.com/icp/resources?limit=5", :body => %q<{"resources":[{"foo":"bar","resourceId":"1"},{"foo":"aar","resourceId":"2"},{"foo":"far","resourceId":"3"},{"foo":"car","resourceId":"4"},{"foo":"dar","resourceId":"5"}],"total":12,"limit":5,"offset":0}>)
9
- FakeWeb.register_uri(:get, "https://app.beta.icontact.com/icp/resources?offset=5", :body => %q<{"resources":[{"foo":"ear","resourceId":"6"},{"foo":"gar","resourceId":"7"},{"foo":"har","resourceId":"8"},{"foo":"iar","resourceId":"9"},{"foo":"jar","resourceId":"10"},{"foo":"kar","resourceId":"11"},{"foo":"yar","resourceId":"12"}],"total":12,"limit":20,"offset":5}>)
10
- FakeWeb.register_uri(:get, "https://app.beta.icontact.com/icp/resources?offset=5&limit=5", :body => %q<{"resources":[{"foo":"ear","resourceId":"6"},{"foo":"gar","resourceId":"7"},{"foo":"har","resourceId":"8"},{"foo":"iar","resourceId":"9"},{"foo":"jar","resourceId":"10"}],"total":12,"limit":5,"offset":5}>)
11
- FakeWeb.register_uri(:get, "https://app.beta.icontact.com/icp/resources?offset=10&limit=5", :body => %q<{"resources":[{"foo":"kar","resourceId":"11"},{"foo":"yar","resourceId":"12"}],"total":12,"limit":5,"offset":10}>)
12
- FakeWeb.register_uri(:post, "https://app.beta.icontact.com/icp/resources/1", :body => %q<{"resource":{"foo":"bar","resourceId":"1","too":"sar"}}>)
13
- FakeWeb.register_uri(:post, "https://app.beta.icontact.com/icp/resources/2", :body => %q<{"resource":{},"warnings":["You did not provide a foo. foo is a required field. Please provide a foo","This was not a good record"]}>)
14
- FakeWeb.register_uri(:post, "https://app.beta.icontact.com/icp/resources/3", :status => ["400","Bad Request"], :body => %q<{"errors":["You did not provide a clue. Clue is a required field. Please provide a clue"]}>)
9
+ FakeWeb.register_uri(:get, "#{ic}/resources?limit=500", :body => %q<{"resources":[{"foo":"bar","resourceId":"1","too":"bar"},{"foo":"aar","resourceId":"2"},{"foo":"far","resourceId":"3"},{"foo":"car","resourceId":"4"},{"foo":"dar","resourceId":"5"},{"foo":"ear","resourceId":"6"},{"foo":"gar","resourceId":"7"},{"foo":"har","resourceId":"8"},{"foo":"iar","resourceId":"9"},{"foo":"jar","resourceId":"10"},{"foo":"kar","resourceId":"11"},{"foo":"yar","resourceId":"12"}],"total":12,"limit":20,"offset":0}>)
10
+ FakeWeb.register_uri(:get, "#{ic}/resources?limit=1", :body => %q<{"resources":[{"foo":"bar","resourceId":"1","too":"bar"},{"foo":"aar","resourceId":"2"},{"foo":"far","resourceId":"3"},{"foo":"car","resourceId":"4"},{"foo":"dar","resourceId":"5"},{"foo":"ear","resourceId":"6"},{"foo":"gar","resourceId":"7"},{"foo":"har","resourceId":"8"},{"foo":"iar","resourceId":"9"},{"foo":"jar","resourceId":"10"},{"foo":"kar","resourceId":"11"},{"foo":"yar","resourceId":"12"}],"total":12,"limit":20,"offset":0}>)
11
+ FakeWeb.register_uri(:get, "#{ic}/resources?limit=5", :body => %q<{"resources":[{"foo":"bar","resourceId":"1"},{"foo":"aar","resourceId":"2"},{"foo":"far","resourceId":"3"},{"foo":"car","resourceId":"4"},{"foo":"dar","resourceId":"5"}],"total":12,"limit":5,"offset":0}>)
12
+ FakeWeb.register_uri(:get, "#{ic}/resources?limit=500&offset=5", :body => %q<{"resources":[{"foo":"ear","resourceId":"6"},{"foo":"gar","resourceId":"7"},{"foo":"har","resourceId":"8"},{"foo":"iar","resourceId":"9"},{"foo":"jar","resourceId":"10"},{"foo":"kar","resourceId":"11"},{"foo":"yar","resourceId":"12"}],"total":12,"limit":20,"offset":5}>)
13
+ FakeWeb.register_uri(:get, "#{ic}/resources?offset=5&limit=5", :body => %q<{"resources":[{"foo":"ear","resourceId":"6"},{"foo":"gar","resourceId":"7"},{"foo":"har","resourceId":"8"},{"foo":"iar","resourceId":"9"},{"foo":"jar","resourceId":"10"}],"total":12,"limit":5,"offset":5}>)
14
+ FakeWeb.register_uri(:get, "#{ic}/resources?offset=10&limit=5", :body => %q<{"resources":[{"foo":"kar","resourceId":"11"},{"foo":"yar","resourceId":"12"}],"total":12,"limit":5,"offset":10}>)
15
+ FakeWeb.register_uri(:get, "#{ic}/resources/1", :body => %q<{"resource":{"foo":"bar","resourceId":"1","too":"sar"}}>)
16
+ FakeWeb.register_uri(:post, "#{ic}/resources/1", :body => %q<{"resource":{"foo":"bar","resourceId":"1","too":"sar"}}>)
17
+ FakeWeb.register_uri(:post, "#{ic}/resources/2", :body => %q<{"resource":{},"warnings":["You did not provide a foo. foo is a required field. Please provide a foo","This was not a good record"]}>)
18
+ FakeWeb.register_uri(:post, "#{ic}/resources/3", :status => ["400","Bad Request"], :body => %q<{"errors":["You did not provide a clue. Clue is a required field. Please provide a clue"]}>)
15
19
 
16
20
  # Time
17
- FakeWeb.register_uri(:get, "https://app.beta.icontact.com/icp/time", :body => %q<{"time":"2009-07-13T01:28:18-04:00","timestamp":1247462898}>)
21
+ FakeWeb.register_uri(:get, "#{i}/time", :body => %q<{"time":"2009-07-13T01:28:18-04:00","timestamp":1247462898}>)
18
22
 
19
23
  # Accounts
20
- FakeWeb.register_uri(:get, "https://app.beta.icontact.com/icp/a", :body => %q<{"accounts":[{"billingStreet":"","billingCity":"","billingState":"","billingPostalCode":"","billingCountry":"","city":"Testville","accountId":"111111","companyName":"","country":"United States","email":"bob@example.org","enabled":1,"fax":"","firstName":"Bob","lastName":"Tester","multiClientFolder":"0","multiUser":"0","phone":"","postalCode":"12345","state":"TN","street":"123 Test Street","title":"","accountType":"0","subscriberLimit":"250000"}],"total":1,"limit":20,"offset":0}>)
24
+ FakeWeb.register_uri(:get, "#{i}/a?limit=1", :body => %q<{"accounts":[{"billingStreet":"","billingCity":"","billingState":"","billingPostalCode":"","billingCountry":"","city":"Testville","accountId":"111111","companyName":"","country":"United States","email":"bob@example.org","enabled":1,"fax":"","firstName":"Bob","lastName":"Tester","multiClientFolder":"0","multiUser":"0","phone":"","postalCode":"12345","state":"TN","street":"123 Test Street","title":"","accountType":"0","subscriberLimit":"250000"}],"total":1,"limit":20,"offset":0}>)
25
+ FakeWeb.register_uri(:get, "#{i}/a?limit=500", :body => %q<{"accounts":[{"billingStreet":"","billingCity":"","billingState":"","billingPostalCode":"","billingCountry":"","city":"Testville","accountId":"111111","companyName":"","country":"United States","email":"bob@example.org","enabled":1,"fax":"","firstName":"Bob","lastName":"Tester","multiClientFolder":"0","multiUser":"0","phone":"","postalCode":"12345","state":"TN","street":"123 Test Street","title":"","accountType":"0","subscriberLimit":"250000"}],"total":1,"limit":20,"offset":0}>)
21
26
 
22
27
  # Clients
23
- FakeWeb.register_uri(:get, "https://app.beta.icontact.com/icp/a/111111/c", :body => %q<{"clientfolders":[{"clientFolderId":"222222","logoId":null,"emailRecipient":"bob@example.org"}],"total":1}>)
28
+ FakeWeb.register_uri(:get, "#{i}/a/111111/c?limit=1", :body => %q<{"clientfolders":[{"clientFolderId":"222222","logoId":null,"emailRecipient":"bob@example.org"}],"total":1}>)
29
+ FakeWeb.register_uri(:get, "#{i}/a/111111/c?limit=500", :body => %q<{"clientfolders":[{"clientFolderId":"222222","logoId":null,"emailRecipient":"bob@example.org"}],"total":1}>)
30
+
31
+ # Contacts
32
+ FakeWeb.register_uri(:get, "#{ic}/contacts?limit=1&status=total&firstName=John&lastName=Test", :body => %q<{"contacts":[{"email":"john@example.org","firstName":"John","lastName":"Test","status":"normal","contactId":"333333","createDate":"2009-07-24 01:00:00"}]}>)
33
+
34
+ # Lists
35
+ FakeWeb.register_uri(:get, "#{ic}/lists?limit=1&name=First%20Test", :body => %q<{"lists":[{"listId":"444444","name":"First Test","emailOwnerOnChange":"0","welcomeOnManualAdd":"0","welcomeOnSignupAdd":"0","welcomeMessageId":"555555","description":"Just a test list."}]}>)
36
+
37
+ # Message
38
+ #### Test message for List association
39
+ FakeWeb.register_uri(:get, "#{ic}/messages/555555", :body => %q<{"message":{"messageId":"555555","subject":"Welcome!","messageType":"welcome","textBody":"Welcome to the Test List!","htmlBody":"<p>Welcome to the <b>Test List</b>!</p>","createDate":"20090725 14:55:12"}}>)
40
+
41
+ #### Test message for associations originating from Message spec
42
+ FakeWeb.register_uri(:get, "#{ic}/messages?limit=1&subject=Test%20Message", :body => %q<{"messages":[{"messageId":"666666","subject":"Test Message","messageType":"normal","textBody":"Hi there!\nThis is just a test.","htmlBody":"<p><b>Hi there!</b></p><p>This is just a <i>test.</i></p>","createDate":"20090725 14:53:33"}]}>)
data/spec/spec_helper.rb CHANGED
@@ -14,5 +14,5 @@ Spec::Runner.configure do |config|
14
14
  config.mock_with :mocha
15
15
 
16
16
  # Set up some reasonable testing variables
17
- ActsAsIcontact::Config.mode = :beta
17
+ ActsAsIcontact::Config.mode = :sandbox
18
18
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acts_as_icontact
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Eley
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-07-24 00:00:00 -04:00
12
+ date: 2009-07-25 00:00:00 -04:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -43,6 +43,8 @@ files:
43
43
  - lib/acts_as_icontact/resources/account.rb
44
44
  - lib/acts_as_icontact/resources/client.rb
45
45
  - lib/acts_as_icontact/resources/contact.rb
46
+ - lib/acts_as_icontact/resources/list.rb
47
+ - lib/acts_as_icontact/resources/message.rb
46
48
  - spec/config_spec.rb
47
49
  - spec/connection_spec.rb
48
50
  - spec/resource_collection_spec.rb
@@ -50,6 +52,8 @@ files:
50
52
  - spec/resources/account_spec.rb
51
53
  - spec/resources/client_spec.rb
52
54
  - spec/resources/contact_spec.rb
55
+ - spec/resources/list_spec.rb
56
+ - spec/resources/message_spec.rb
53
57
  - spec/spec.opts
54
58
  - spec/spec_fakeweb.rb
55
59
  - spec/spec_helper.rb
@@ -89,5 +93,7 @@ test_files:
89
93
  - spec/resources/account_spec.rb
90
94
  - spec/resources/client_spec.rb
91
95
  - spec/resources/contact_spec.rb
96
+ - spec/resources/list_spec.rb
97
+ - spec/resources/message_spec.rb
92
98
  - spec/spec_fakeweb.rb
93
99
  - spec/spec_helper.rb