remotely 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ module Remotely
2
+ VERSION = "0.0.4"
3
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/remotely/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Matte Noble"]
6
+ gem.email = ["me@mattenoble.com"]
7
+ gem.description = %q{Remote API based model associations.}
8
+ gem.summary = %q{Remote API based model associations.}
9
+ gem.homepage = ''
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "remotely"
15
+ gem.require_paths = ['lib']
16
+ gem.version = Remotely::VERSION
17
+
18
+ gem.add_development_dependency "rake"
19
+ gem.add_development_dependency "rspec", "~> 2.6.0"
20
+ gem.add_development_dependency "ZenTest"
21
+ gem.add_development_dependency "autotest-growl"
22
+ gem.add_development_dependency "webmock"
23
+ gem.add_development_dependency "ruby-debug19"
24
+ gem.add_development_dependency "ruby-debug-completion"
25
+
26
+ gem.add_dependency "activesupport"
27
+ gem.add_dependency "activemodel"
28
+ gem.add_dependency "faraday", "~> 0.7.4"
29
+ gem.add_dependency "yajl-ruby", "~> 0.8.2"
30
+ end
@@ -0,0 +1,146 @@
1
+ require "spec_helper"
2
+
3
+ describe Remotely::Associations do
4
+ let(:app) { "http://localhost:1234" }
5
+
6
+ shared_examples_for "an association" do
7
+ it "keeps track of it's remote associations" do
8
+ subject.remote_associations.should include(assoc)
9
+ end
10
+
11
+ it "creates a method for the association" do
12
+ subject.should respond_to(assoc)
13
+ end
14
+
15
+ it "creates a setter for the associations" do
16
+ subject.public_send(:"#{assoc}=", "guy")
17
+ subject.public_send(assoc).should == "guy"
18
+ end
19
+ end
20
+
21
+ shared_examples_for "an association with a path" do
22
+ it "generates the correct path" do
23
+ subject.path_to(assoc, type).should == path
24
+ end
25
+
26
+ it "requests the correct path" do
27
+ subject.send(assoc)
28
+ a_request(:get, "#{app}#{path}").should have_been_made
29
+ end
30
+ end
31
+
32
+ describe "has_many_remote" do
33
+ subject { HasMany.new(id: 1) }
34
+ let(:type) { :has_many }
35
+ let(:assoc) { :things }
36
+
37
+ it_behaves_like "an association"
38
+
39
+ it "returns a Collection" do
40
+ subject.things.should be_a Remotely::Collection
41
+ end
42
+
43
+ it "returns a Collection of the appropriate model" do
44
+ subject.things.first.should be_a Thing
45
+ end
46
+
47
+ it "returns nil when it can not fetch the association" do
48
+ subject.id = nil
49
+ subject.things.should be_nil
50
+ end
51
+
52
+ context "with no options" do
53
+ subject { HasMany.new(id: 1) }
54
+ let(:path) { "/has_manies/1/things" }
55
+ it_behaves_like "an association with a path"
56
+ end
57
+
58
+ context "with the :path option" do
59
+ subject { HasManyWithPath.new(id: 1) }
60
+ let(:path) { "/custom/things" }
61
+ it_behaves_like "an association with a path"
62
+ end
63
+
64
+ context "with :path variables" do
65
+ subject { HasManyWithPathVariables.new(name: "stuff") }
66
+ let(:path) { "/custom/stuff/things" }
67
+ it_behaves_like "an association with a path"
68
+ end
69
+
70
+ context "with the :foreign_key option" do
71
+ subject { HasManyWithForeignKey.new }
72
+ specify { expect { subject.path_to(:things, :has_many) }.to raise_error(Remotely::HasManyForeignKeyError) }
73
+ end
74
+ end
75
+
76
+ describe "has_one_remote" do
77
+ subject { HasOne.new(id: 1) }
78
+ let(:type) { :has_one }
79
+ let(:assoc) { :thing }
80
+
81
+ it_behaves_like "an association"
82
+
83
+ it "returns an object of the appropriate model" do
84
+ subject.thing.should be_a Thing
85
+ end
86
+
87
+ context "with no options" do
88
+ subject { HasOne.new(id: 1) }
89
+ let(:path) { "/has_ones/1/thing" }
90
+ it_behaves_like "an association with a path"
91
+ end
92
+
93
+ context "with the :path option" do
94
+ subject { HasOneWithPath.new(id: 1) }
95
+ let(:path) { "/custom/thing" }
96
+ it_behaves_like "an association with a path"
97
+ end
98
+
99
+ context "with :path variables" do
100
+ subject { HasOneWithPathVariables.new(name: "stuff") }
101
+ let(:path) { "/custom/stuff/thing" }
102
+ it_behaves_like "an association with a path"
103
+ end
104
+
105
+ context "with the :foreign_key option" do
106
+ subject { HasOneWithForeignKey.new }
107
+ specify { expect { subject.path_to(:thing, :has_one) }.to raise_error(Remotely::HasManyForeignKeyError) }
108
+ end
109
+ end
110
+
111
+ describe "belongs_to_remote" do
112
+ subject { BelongsTo.new(id: 1, thing_id: 1) }
113
+ let(:type) { :belongs_to }
114
+ let(:assoc) { :thing }
115
+
116
+ it_behaves_like "an association"
117
+
118
+ it "returns an object of the appropriate model" do
119
+ subject.thing.should be_a Thing
120
+ end
121
+
122
+ it "should not fetch an association when the foreign key is nil" do
123
+ subject.thing_id = nil
124
+ subject.thing.should be_nil
125
+ a_request(:get, %r(/things)).should_not have_been_made
126
+ end
127
+
128
+ context "with no options" do
129
+ subject { BelongsTo.new(thing_id: 1) }
130
+ let(:path) { "/things/1" }
131
+ it_behaves_like "an association with a path"
132
+ end
133
+
134
+ context "with the :path option" do
135
+ subject { BelongsToWithPath.new }
136
+ let(:path) { "/custom/thing" }
137
+ it_behaves_like "an association with a path"
138
+ end
139
+
140
+ context "with :path variables" do
141
+ subject { BelongsToWithPathVariables.new(name: "stuff") }
142
+ let(:path) { "/custom/stuff/thing" }
143
+ it_behaves_like "an association with a path"
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,57 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Remotely::Collection do
4
+ let(:finn) { Member.new(id: 1, name: "Finn", type: "human") }
5
+ let(:jake) { Member.new(id: 2, name: "Jake", type: "dog") }
6
+ let(:adventure) { Adventure.new(id: 3) }
7
+
8
+ subject { Remotely::Collection.new(adventure, Member, [jake, finn]) }
9
+
10
+ describe "#find" do
11
+ it "finds by id" do
12
+ subject.find(1).should == finn
13
+ end
14
+ end
15
+
16
+ describe "#where" do
17
+ it "is searchable by attributes and values" do
18
+ subject.where(name: "Jake", type: "dog").should == [jake]
19
+ end
20
+
21
+ it "returns a new Collection" do
22
+ subject.where(name: "Jake", type: "dog").should be_a Remotely::Collection
23
+ end
24
+ end
25
+
26
+ describe "#order" do
27
+ it "orders by an attribute" do
28
+ subject.order(:name).should == [finn, jake]
29
+ end
30
+ end
31
+
32
+ describe "#build" do
33
+ it "creates a new model object with the foreign key automatically defined" do
34
+ adventure.members.build.adventure_id.should == 3
35
+ end
36
+
37
+ it "adds the new object to itself" do
38
+ new_member = adventure.members.build
39
+ adventure.members.should include(new_member)
40
+ end
41
+ end
42
+
43
+ describe "#create" do
44
+ before do
45
+ stub_request(:post, %r(/members)).to_return(lambda { |req| {body: req.body} })
46
+ end
47
+
48
+ it "creates and saves a new model object with the foreign key automatically defined" do
49
+ adventure.members.create.adventure_id.should == 3
50
+ end
51
+
52
+ it "adds the new object to itself" do
53
+ new_member = adventure.members.create
54
+ adventure.members.should include(new_member)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,27 @@
1
+ require "spec_helper"
2
+
3
+ describe URL do
4
+ it "takes n number of arguments and joins them" do
5
+ URL.new("a", "b", "c").should == "/a/b/c"
6
+ end
7
+
8
+ it "removes duplicate slashes" do
9
+ URL.new("a", "/", "b").should == "/a/b"
10
+ end
11
+
12
+ it "is comparable" do
13
+ URL.new("a", "b").should == URL.new("a", "b")
14
+ end
15
+
16
+ it "is addable" do
17
+ (URL.new("a", "b") + URL.new("c")).to_s.should == "/a/b/c"
18
+ end
19
+
20
+ it "is subtractable" do
21
+ (URL.new("a", "b") - URL.new("b")).to_s.should == "/a"
22
+ end
23
+
24
+ it "creatable using URL()" do
25
+ URL("a", "b").should == "/a/b"
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ require "spec_helper"
2
+
3
+ describe Remotely::HTTPMethods do
4
+ include Remotely::HTTPMethods
5
+
6
+ it "raises NonJsonResponseError when HTML is returned on GET" do
7
+ stub_request(:get, %r(/things)).to_return(body: "<html><head><title></title></head></html>")
8
+ expect { get("/things") }.to raise_error(Remotely::NonJsonResponseError)
9
+ end
10
+
11
+ it "raises NonJsonResponseError when HTML is returned on POST" do
12
+ stub_request(:post, %r(/things)).to_return(body: "<html><head><title></title></head></html>")
13
+ expect { post("/things") }.to raise_error(Remotely::NonJsonResponseError)
14
+ end
15
+
16
+ it "raises NonJsonResponseError when HTML is returned on PUT" do
17
+ stub_request(:put, %r(/things)).to_return(body: "<html><head><title></title></head></html>")
18
+ expect { put("/things") }.to raise_error(Remotely::NonJsonResponseError)
19
+ end
20
+
21
+ it "raises NonJsonResponseError when HTML is returned on DELETE" do
22
+ stub_request(:delete, %r(/things)).to_return(body: "<html><head><title></title></head></html>")
23
+ expect { http_delete("/things") }.to raise_error(Remotely::NonJsonResponseError)
24
+ end
25
+ end
@@ -0,0 +1,368 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Remotely::Model do
4
+ let(:app) { "http://localhost:1234" }
5
+ let(:attributes) { {id: 1, name: "Marceline Quest", type: "MATHEMATICAL!"} }
6
+
7
+ subject { Adventure.new(attributes) }
8
+
9
+ describe ".attr_savable" do
10
+ let(:attrs) { {id: 2, name: "Wishes!", type: "MATHEMATICAL!", length: 9} }
11
+ let(:saved) { to_json({name: "OMG New Name!", type: "MATHEMATICAL!", id: 2}) }
12
+
13
+ subject { Adventure.new(attrs) }
14
+
15
+ it "stores which attributes are savable" do
16
+ Adventure.savable_attributes.should == [:name, :type]
17
+ end
18
+
19
+ it "only sends the specified attributes when saving an existing record" do
20
+ stub_request(:put, "#{app}/adventures/2").to_return(body: saved)
21
+ subject.update_attribute(:name, "OMG New Name!")
22
+ a_request(:put, "#{app}/adventures/2").with(body: saved).should have_been_made
23
+ end
24
+ end
25
+
26
+ describe ".find" do
27
+ it "retreives an individual resource" do
28
+ Adventure.find(1)
29
+ a_request(:get, "#{app}/adventures/1").should have_been_made
30
+ end
31
+ end
32
+
33
+ describe ".where" do
34
+ it "searches for resources" do
35
+ Adventure.where(:type => "MATHEMATICAL!")
36
+ a_request(:get, "#{app}/adventures/search?type=MATHEMATICAL!").should have_been_made
37
+ end
38
+
39
+ it "returns a collection of resources" do
40
+ Adventure.where(:type => "MATHEMATICAL!").should be_a Remotely::Collection
41
+ end
42
+ end
43
+
44
+ describe ".destroy" do
45
+ it "destroys the resource" do
46
+ Adventure.destroy(1)
47
+ a_request(:delete, "#{app}/adventures/1").should have_been_made
48
+ end
49
+
50
+ it "returns true on success" do
51
+ Adventure.destroy(1).should be_true
52
+ end
53
+
54
+ it "returns false on failure" do
55
+ stub_request(:delete, %r[/adventures/1]).to_return(status: 500)
56
+ Adventure.destroy(1).should be_false
57
+ end
58
+ end
59
+
60
+ describe ".create" do
61
+ let(:attrs) { attributes.except(:id) }
62
+
63
+ before do
64
+ stub_request(:post, "#{app}/adventures").to_return(lambda { |req| { body: req.body, status: 201 }})
65
+ end
66
+
67
+ it "creates the resource" do
68
+ Adventure.create(attrs)
69
+ a_request(:post, "#{app}/adventures").with(attrs).should have_been_made
70
+ end
71
+
72
+ it "returns the new resource on creation" do
73
+ Adventure.create(attrs).name.should == "Marceline Quest"
74
+ end
75
+
76
+ it "returns an instance with errors when the creation fails" do
77
+ body = Yajl.dump({errors: {base: ["error"]}})
78
+ stub_request(:post, %r[/adventures]).to_return(status: 500, body: body)
79
+ Adventure.create(attrs).errors[:base].should include("error")
80
+ end
81
+ end
82
+
83
+ describe ".find_or_" do
84
+ let(:body) { Yajl::Encoder.encode([{id: 1, name: "BubbleGum"}]) }
85
+ let(:stub_success) { stub_request(:get, "#{app}/adventures/search?name=BubbleGum").to_return(body: body) }
86
+ let(:stub_failure) { stub_request(:get, "#{app}/adventures/search?name=BubbleGum").to_return(body: "[]") }
87
+
88
+ describe "initialize" do
89
+ it "tries to fetch the record" do
90
+ stub_success
91
+ Adventure.find_or_initialize(name: "BubbleGum")
92
+ a_request(:get, "#{app}/adventures/search?name=BubbleGum").should have_been_made
93
+ end
94
+
95
+ it "returns the fetched object if found" do
96
+ stub_success
97
+ Adventure.find_or_initialize(name: "BubbleGum").id.should == 1
98
+ end
99
+
100
+ it "creates a new object if one is not found" do
101
+ stub_failure
102
+ Adventure.find_or_initialize(name: "BubbleGum").should be_a_new_record
103
+ end
104
+ end
105
+
106
+ describe "create" do
107
+ it "automatically saves the new object" do
108
+ stub_failure
109
+ Adventure.should_receive(:create).with(name: "BubbleGum")
110
+ Adventure.find_or_create(name: "BubbleGum")
111
+ end
112
+
113
+ it "returns the first item from the collection" do
114
+ stub_success
115
+ Adventure.find_or_create(name: "BubbleGum").should be_an Adventure
116
+ end
117
+ end
118
+ end
119
+
120
+ describe ".find_by_*" do
121
+ it "searches by a single attribute" do
122
+ Adventure.find_by_name("Fun")
123
+ a_request(:get, "#{app}/adventures/search?name=Fun").should have_been_made
124
+ end
125
+
126
+ it "searches by multiple attributes seperated by 'and'" do
127
+ Adventure.find_by_name_and_type("Fun", "MATHEMATICAL!")
128
+ a_request(:get, "#{app}/adventures/search?name=Fun&type=MATHEMATICAL!").should have_been_made
129
+ end
130
+ end
131
+
132
+ describe ".all" do
133
+ it "fetches all resources" do
134
+ Adventure.all
135
+ a_request(:get, "#{app}/adventures").should have_been_made
136
+ end
137
+ end
138
+
139
+ describe ".update_all" do
140
+ it "request an update to all entries" do
141
+ Adventure.update_all(type: "awesome")
142
+ a_request(:put, "#{app}/adventures").with(type: "awesome").should have_been_made
143
+ end
144
+ end
145
+
146
+ describe "#save" do
147
+ let(:new_name) { "City of Thieves" }
148
+ let(:new_attributes) { attributes.merge(name: new_name) }
149
+
150
+ context "when updating" do
151
+ it "updates the resource" do
152
+ adventure = Adventure.new(attributes)
153
+ adventure.name = new_name
154
+ adventure.save
155
+ a_request(:put, "#{app}/adventures/1").with(new_attributes).should have_been_made
156
+ end
157
+
158
+ it "returns true when the save succeeds" do
159
+ Adventure.new(attributes).save.should be_a Adventure
160
+ end
161
+
162
+ it "sets errors when a save fails" do
163
+ adventure = Adventure.new(attributes)
164
+ stub_request(:put, %r[/adventures/1]).to_return(status: 409, body: to_json({errors: {base: %w{this failed}}}))
165
+ adventure.save
166
+ adventure.errors[:base].should == %w{this failed}
167
+ end
168
+ end
169
+
170
+ context "when creating" do
171
+ it "merges in the response body to attributes on success" do
172
+ adventure = Adventure.new(name: "To Be Saved...")
173
+ stub_request(:post, %r(/adventures)).to_return(body: to_json(attributes.merge(name: "To Be Saved...", id: 2)), status: 201)
174
+ adventure.save
175
+ adventure.id.should == 2
176
+ end
177
+
178
+ it "sets errors on a failure" do
179
+ body = Yajl.dump({errors: {base: ["error"]}})
180
+ stub_request(:post, %r(/adventures)).to_return(status: 409, body: body)
181
+ Adventure.new(name: "name").save.errors.should_not be_empty
182
+ end
183
+ end
184
+ end
185
+
186
+ describe "#update_attribute" do
187
+ it "updates a single attribute and saves" do
188
+ subject.update_attribute(:type, "powerful")
189
+ a_request(:put, "#{app}/adventures/1").with(type: "powerful").should have_been_made
190
+ end
191
+ end
192
+
193
+ describe "#to_param" do
194
+ it "returns correct value" do
195
+ subject.to_param.should == '1'
196
+ end
197
+ end
198
+
199
+ describe "#update_attributes" do
200
+ let(:updates) { {type: "awesome"} }
201
+ let(:new_attributes) { subject.attributes.merge(updates) }
202
+
203
+ it "replaces existing attribute values" do
204
+ subject.update_attributes(updates)
205
+ subject.type.should == "awesome"
206
+ end
207
+
208
+ it "calls save" do
209
+ subject.update_attributes(updates)
210
+ a_request(:put, "#{app}/adventures/1").with(new_attributes).should have_been_made
211
+ end
212
+
213
+ it "returns true on success" do
214
+ subject.update_attributes(updates).should be_true
215
+ end
216
+
217
+ it "sets errors on failure" do
218
+ body = Yajl.dump({errors: {base: ["error"]}})
219
+ stub_request(:put, %r[/adventures/1]).to_return(status: 500, body: body)
220
+ subject.update_attributes(updates)
221
+ subject.errors[:base].should include("error")
222
+ end
223
+
224
+ it "reverts the object's attributes if the save fails" do
225
+ body = Yajl.dump({errors: {base: ["error"]}})
226
+ stub_request(:put, %r[/adventures/1]).to_return(status: 500, body: body)
227
+ subject.update_attributes(updates)
228
+ subject.type.should == "MATHEMATICAL!"
229
+ end
230
+ end
231
+
232
+ describe "#destroy" do
233
+ it "destroys a resource with the might of 60 jotun!!" do
234
+ Adventure.new(attributes).destroy
235
+ a_request(:delete, "#{app}/adventures/1").should have_been_made
236
+ end
237
+ end
238
+
239
+ describe "associations" do
240
+ let(:member) { Member.new(id: 2, name_id: 1) }
241
+
242
+ it "creates associations when instantiated" do
243
+ member.should respond_to :name
244
+ end
245
+
246
+ it "fetches the resource when accessed" do
247
+ Name.should_receive(:find).with(1)
248
+ member.name
249
+ end
250
+
251
+ it "doesn't fetch a resource twice" do
252
+ Name.should_receive(:find).with(1).once
253
+ member.name
254
+ member.name
255
+ end
256
+
257
+ it "reloads association objects" do
258
+ Name.should_receive(:find).with(1).twice
259
+ member.name
260
+ member.name(true)
261
+ end
262
+ end
263
+
264
+ context "basic auth" do
265
+ before do
266
+ Remotely.configure { app :adventure_app, "http://localhost:3000" }
267
+ end
268
+
269
+ after do
270
+ Remotely.reset!
271
+ end
272
+
273
+ it "sends Authorization headers when basic auth is configured" do
274
+ Remotely.configure { basic_auth "user", "password" }
275
+ Adventure.find(1)
276
+ a_request(:get, "#{app}/adventures/1").with(headers: {'Authorization' => "Basic dXNlcjpwYXNzd29yZA=="})
277
+ end
278
+
279
+ it "doesn't send Authorization headers when basic auth is not configured" do
280
+ Adventure.find(1)
281
+ a_request(:get, "#{app}/adventures/1").with(headers: {})
282
+ end
283
+ end
284
+
285
+ it "sets the app it belongs to" do
286
+ Adventure.app.should == :adventure_app
287
+ end
288
+
289
+ it "sets the uri to itself" do
290
+ Adventure.uri.should == "/adventures"
291
+ end
292
+
293
+ it "has a connection" do
294
+ Adventure.remotely_connection.should be_a Faraday::Connection
295
+ end
296
+
297
+ it "supports ActiveModel::Naming methods" do
298
+ Adventure.model_name.element.should == "adventure"
299
+ end
300
+
301
+ it "is reloadable" do
302
+ subject.reload
303
+ a_request(:get, "#{app}/adventures/1")
304
+ end
305
+
306
+ it "symbolizes attribute keys" do
307
+ subject.attributes.should == attributes
308
+ end
309
+
310
+ it "can be initialized with a hash of attribute/values" do
311
+ subject.name.should == "Marceline Quest"
312
+ end
313
+
314
+ it "sets an attribute value" do
315
+ subject.name = "City of Thieves"
316
+ subject.name.should == "City of Thieves"
317
+ end
318
+
319
+ it "raises a normal NoMethodError for non-existent attributes" do
320
+ expect { subject.height }.to raise_error(NoMethodError)
321
+ end
322
+
323
+ it "is a new_record when no id exists" do
324
+ subject.id = nil
325
+ subject.should be_a_new_record
326
+ end
327
+
328
+ it "creates boolean methods for each attribute" do
329
+ subject.name?.should == true
330
+ end
331
+
332
+ it "returns id from #to_key" do
333
+ subject.id = 1
334
+ subject.to_key.should == [1]
335
+ end
336
+
337
+ it "returns id from #to_param" do
338
+ subject.id = 1
339
+ subject.to_param.should == "1"
340
+ end
341
+
342
+ it "returns itself from #to_model" do
343
+ subject.to_model.should == subject
344
+ end
345
+
346
+ context "with an app uri" do
347
+ before do
348
+ Remotely.app :uri_app, "http://localhost:3000/api"
349
+ Thing.app :uri_app
350
+ end
351
+
352
+ it "prepends the app uri" do
353
+ Thing.expand("/members").should == "/api/members"
354
+ end
355
+
356
+ it "doesn't prepend when it's already there" do
357
+ Thing.expand("/api/members").should == "/api/members"
358
+ end
359
+ end
360
+
361
+ context "with errors" do
362
+ let(:attributes) { {'errors' => {:base => %w{totally failed dude}}} }
363
+
364
+ it "adds errors during #initialize" do
365
+ subject.errors[:base].should == %w{totally failed dude}
366
+ end
367
+ end
368
+ end