SFEley-acts_as_icontact 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/acts_as_icontact.gemspec +4 -1
- data/lib/acts_as_icontact/exceptions.rb +6 -0
- data/lib/acts_as_icontact/resource.rb +59 -9
- data/lib/acts_as_icontact/resources/contact.rb +10 -2
- data/lib/acts_as_icontact/resources/list.rb +17 -0
- data/spec/resource_spec.rb +30 -0
- data/spec/resources/contact_spec.rb +20 -3
- data/spec/resources/list_spec.rb +32 -0
- data/spec/spec_fakeweb.rb +20 -11
- metadata +4 -1
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.4
|
data/acts_as_icontact.gemspec
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = %q{acts_as_icontact}
|
5
|
-
s.version = "0.1.
|
5
|
+
s.version = "0.1.4"
|
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"]
|
@@ -33,6 +33,7 @@ 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",
|
36
37
|
"spec/config_spec.rb",
|
37
38
|
"spec/connection_spec.rb",
|
38
39
|
"spec/resource_collection_spec.rb",
|
@@ -40,6 +41,7 @@ Gem::Specification.new do |s|
|
|
40
41
|
"spec/resources/account_spec.rb",
|
41
42
|
"spec/resources/client_spec.rb",
|
42
43
|
"spec/resources/contact_spec.rb",
|
44
|
+
"spec/resources/list_spec.rb",
|
43
45
|
"spec/spec.opts",
|
44
46
|
"spec/spec_fakeweb.rb",
|
45
47
|
"spec/spec_helper.rb"
|
@@ -58,6 +60,7 @@ Gem::Specification.new do |s|
|
|
58
60
|
"spec/resources/account_spec.rb",
|
59
61
|
"spec/resources/client_spec.rb",
|
60
62
|
"spec/resources/contact_spec.rb",
|
63
|
+
"spec/resources/list_spec.rb",
|
61
64
|
"spec/spec_fakeweb.rb",
|
62
65
|
"spec/spec_helper.rb"
|
63
66
|
]
|
@@ -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
|
@@ -33,10 +33,18 @@ module ActsAsIcontact
|
|
33
33
|
if property =~ /(.*)=$/ # It's a value assignment
|
34
34
|
@newvalues ||= []
|
35
35
|
@newvalues << $1
|
36
|
-
|
36
|
+
if self.class.boolean_fields.include?($1)
|
37
|
+
@properties[$1] = params[0] ? 1 : 0
|
38
|
+
else
|
39
|
+
@properties[$1] = params[0]
|
40
|
+
end
|
37
41
|
else
|
38
42
|
if @properties.has_key?(property)
|
39
|
-
|
43
|
+
if self.class.boolean_fields.include?(property)
|
44
|
+
(@properties[property] == 1)
|
45
|
+
else
|
46
|
+
@properties[property]
|
47
|
+
end
|
40
48
|
else
|
41
49
|
super
|
42
50
|
end
|
@@ -56,11 +64,15 @@ module ActsAsIcontact
|
|
56
64
|
# error, raises an exception with it.
|
57
65
|
def save
|
58
66
|
if new_record?
|
67
|
+
fields = create_fields
|
68
|
+
validate_on_create(fields)
|
59
69
|
result_type = self.class.collection_name
|
60
|
-
response = self.class.connection.post([
|
70
|
+
response = self.class.connection.post([fields].to_json)
|
61
71
|
else
|
72
|
+
fields = update_fields
|
73
|
+
validate_on_update(fields)
|
62
74
|
result_type = self.class.resource_name
|
63
|
-
response = connection.post(
|
75
|
+
response = connection.post(fields.to_json)
|
64
76
|
end
|
65
77
|
parsed = JSON.parse(response)
|
66
78
|
if parsed[result_type].empty?
|
@@ -97,7 +109,10 @@ module ActsAsIcontact
|
|
97
109
|
|
98
110
|
# Returns an array of resources starting at the base.
|
99
111
|
def self.find(type, options={})
|
100
|
-
|
112
|
+
query_options = default_options.merge(options)
|
113
|
+
query_options.merge(:limit => 1) if type == :first # Minor optimization
|
114
|
+
validate_options(query_options)
|
115
|
+
uri_extension = uri_component + build_query(query_options)
|
101
116
|
response = base[uri_extension].get
|
102
117
|
parsed = JSON.parse(response)
|
103
118
|
case type
|
@@ -109,13 +124,13 @@ module ActsAsIcontact
|
|
109
124
|
end
|
110
125
|
|
111
126
|
# Returns an array of resources starting at the base.
|
112
|
-
def self.all
|
113
|
-
find(:all)
|
127
|
+
def self.all(options={})
|
128
|
+
find(:all, options)
|
114
129
|
end
|
115
130
|
|
116
131
|
# Returns the first account associated with this username.
|
117
|
-
def self.first
|
118
|
-
find(:first)
|
132
|
+
def self.first(options={})
|
133
|
+
find(:first, options)
|
119
134
|
end
|
120
135
|
|
121
136
|
protected
|
@@ -173,6 +188,11 @@ module ActsAsIcontact
|
|
173
188
|
resource_name + "Id"
|
174
189
|
end
|
175
190
|
|
191
|
+
# Options that are always passed on 'find' requests unless overridden.
|
192
|
+
def self.default_options
|
193
|
+
{:limit => 500}
|
194
|
+
end
|
195
|
+
|
176
196
|
# Fields that _must_ be included for this resource upon creation.
|
177
197
|
def self.required_on_create
|
178
198
|
[]
|
@@ -193,11 +213,41 @@ module ActsAsIcontact
|
|
193
213
|
[]
|
194
214
|
end
|
195
215
|
|
216
|
+
# Fields that operate as 0/1 boolean toggles. Can be assigned to with true and false.
|
217
|
+
def self.boolean_fields
|
218
|
+
[]
|
219
|
+
end
|
220
|
+
|
221
|
+
# Validation rules that ensure proper parameters are passed to iContact on querying.
|
222
|
+
def self.validate_options(options)
|
223
|
+
# See: http://developer.icontact.com/forums/api-beta-moderated-support/there-upper-limit-result-sets#comment-136
|
224
|
+
raise ActsAsIcontact::QueryError, "Limit must be between 1 and 500" if options[:limit].to_i < 1 or options[:limit].to_i > 500
|
225
|
+
end
|
226
|
+
|
227
|
+
# Validation rules that ensure proper data is passed to iContact on resource creation.
|
228
|
+
def validate_on_create(fields)
|
229
|
+
check_required_fields(fields, self.class.required_on_create)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Validation rules that ensure proper data is passed to iContact on resource update.
|
233
|
+
def validate_on_update(fields)
|
234
|
+
check_required_fields(fields, self.class.required_on_update)
|
235
|
+
end
|
236
|
+
|
196
237
|
private
|
197
238
|
def self.build_query(options={})
|
198
239
|
return "" if options.empty?
|
199
240
|
terms = options.collect{|k,v| "#{k}=#{URI.escape(v.to_s)}"}
|
200
241
|
build = "?" + terms.join('&')
|
201
242
|
end
|
243
|
+
|
244
|
+
def check_required_fields(fields, required)
|
245
|
+
# Check that all required fields are filled in
|
246
|
+
missing = required.select{|f| fields[f].blank?}
|
247
|
+
unless missing.empty?
|
248
|
+
missing_fields = missing.join(', ')
|
249
|
+
raise ActsAsIcontact::ValidationError, "Missing required fields: #{missing_fields}"
|
250
|
+
end
|
251
|
+
end
|
202
252
|
end
|
203
253
|
end
|
@@ -1,11 +1,19 @@
|
|
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
|
6
8
|
|
9
|
+
# Derived from clientFolder
|
7
10
|
def self.base
|
8
|
-
ActsAsIcontact
|
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)
|
9
17
|
end
|
10
18
|
end
|
11
19
|
end
|
@@ -0,0 +1,17 @@
|
|
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
|
+
end
|
17
|
+
end
|
data/spec/resource_spec.rb
CHANGED
@@ -51,6 +51,30 @@ 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(:find).with(:first,{:foo=>:bar}).returns(nil)
|
70
|
+
ActsAsIcontact::Resource.first(:foo=>:bar)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "maps the 'all' method to find(:all)" do
|
74
|
+
ActsAsIcontact::Resource.expects(:find).with(:all,{:foo=>:bar}).returns(nil)
|
75
|
+
ActsAsIcontact::Resource.all(:foo=>:bar)
|
76
|
+
end
|
77
|
+
|
54
78
|
end
|
55
79
|
|
56
80
|
it "knows its properties" do
|
@@ -94,6 +118,7 @@ describe ActsAsIcontact::Resource do
|
|
94
118
|
ActsAsIcontact::Resource.never_on_create.should == ["resourceId"]
|
95
119
|
end
|
96
120
|
|
121
|
+
|
97
122
|
context "updating records" do
|
98
123
|
before(:each) do
|
99
124
|
@res = ActsAsIcontact::Resource.first
|
@@ -117,6 +142,11 @@ describe ActsAsIcontact::Resource do
|
|
117
142
|
@res.send(:update_fields).should == {"resourceId" => "1", "too" => "tar"}
|
118
143
|
end
|
119
144
|
|
145
|
+
it "throws an exception if required fields aren't included" do
|
146
|
+
@res.resourceId = nil
|
147
|
+
lambda{@res.save}.should raise_error(ActsAsIcontact::ValidationError, "Missing required fields: resourceId")
|
148
|
+
end
|
149
|
+
|
120
150
|
context "with successful save" do
|
121
151
|
before(:each) do
|
122
152
|
@res.too = "sar"
|
@@ -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,32 @@
|
|
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
|
+
it "knows its welcome message"
|
31
|
+
end
|
32
|
+
end
|
data/spec/spec_fakeweb.rb
CHANGED
@@ -2,22 +2,31 @@ require 'rubygems'
|
|
2
2
|
require 'fakeweb'
|
3
3
|
|
4
4
|
FakeWeb.allow_net_connect = false
|
5
|
+
i = "https://app.beta.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, "
|
8
|
-
FakeWeb.register_uri(:get, "
|
9
|
-
FakeWeb.register_uri(:get, "
|
10
|
-
FakeWeb.register_uri(:get, "
|
11
|
-
FakeWeb.register_uri(:get, "
|
12
|
-
FakeWeb.register_uri(:post, "
|
13
|
-
FakeWeb.register_uri(:post, "
|
14
|
-
FakeWeb.register_uri(:post, "
|
9
|
+
FakeWeb.register_uri(:get, "#{i}/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, "#{i}/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}>)
|
11
|
+
FakeWeb.register_uri(:get, "#{i}/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}>)
|
12
|
+
FakeWeb.register_uri(:get, "#{i}/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}>)
|
13
|
+
FakeWeb.register_uri(:get, "#{i}/resources?offset=10&limit=5", :body => %q<{"resources":[{"foo":"kar","resourceId":"11"},{"foo":"yar","resourceId":"12"}],"total":12,"limit":5,"offset":10}>)
|
14
|
+
FakeWeb.register_uri(:post, "#{i}/resources/1", :body => %q<{"resource":{"foo":"bar","resourceId":"1","too":"sar"}}>)
|
15
|
+
FakeWeb.register_uri(:post, "#{i}/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"]}>)
|
16
|
+
FakeWeb.register_uri(:post, "#{i}/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
17
|
|
16
18
|
# Time
|
17
|
-
FakeWeb.register_uri(:get, "
|
19
|
+
FakeWeb.register_uri(:get, "#{i}/time", :body => %q<{"time":"2009-07-13T01:28:18-04:00","timestamp":1247462898}>)
|
18
20
|
|
19
21
|
# Accounts
|
20
|
-
FakeWeb.register_uri(:get, "
|
22
|
+
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
23
|
|
22
24
|
# Clients
|
23
|
-
FakeWeb.register_uri(:get, "
|
25
|
+
FakeWeb.register_uri(:get, "#{i}/a/111111/c?limit=500", :body => %q<{"clientfolders":[{"clientFolderId":"222222","logoId":null,"emailRecipient":"bob@example.org"}],"total":1}>)
|
26
|
+
|
27
|
+
# Contacts
|
28
|
+
FakeWeb.register_uri(:get, "#{ic}/contacts?limit=500&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"}]}>)
|
29
|
+
|
30
|
+
# Lists
|
31
|
+
FakeWeb.register_uri(:get, "#{ic}/lists?limit=500&name=First%20Test", :body => %q<{"lists":[{"listId":"444444","name":"First Test","emailOwnerOnChange":"0","welcomeOnManualAdd":"0","welcomeOnSignupAdd":"0","welcomeMessageId":"555555","description":"Just a test list."}]}>)
|
32
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: SFEley-acts_as_icontact
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen Eley
|
@@ -38,6 +38,7 @@ files:
|
|
38
38
|
- lib/acts_as_icontact/resources/account.rb
|
39
39
|
- lib/acts_as_icontact/resources/client.rb
|
40
40
|
- lib/acts_as_icontact/resources/contact.rb
|
41
|
+
- lib/acts_as_icontact/resources/list.rb
|
41
42
|
- spec/config_spec.rb
|
42
43
|
- spec/connection_spec.rb
|
43
44
|
- spec/resource_collection_spec.rb
|
@@ -45,6 +46,7 @@ files:
|
|
45
46
|
- spec/resources/account_spec.rb
|
46
47
|
- spec/resources/client_spec.rb
|
47
48
|
- spec/resources/contact_spec.rb
|
49
|
+
- spec/resources/list_spec.rb
|
48
50
|
- spec/spec.opts
|
49
51
|
- spec/spec_fakeweb.rb
|
50
52
|
- spec/spec_helper.rb
|
@@ -82,5 +84,6 @@ test_files:
|
|
82
84
|
- spec/resources/account_spec.rb
|
83
85
|
- spec/resources/client_spec.rb
|
84
86
|
- spec/resources/contact_spec.rb
|
87
|
+
- spec/resources/list_spec.rb
|
85
88
|
- spec/spec_fakeweb.rb
|
86
89
|
- spec/spec_helper.rb
|