remotely 0.0.4
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.
- data/.autotest +1 -0
- data/.gitignore +18 -0
- data/.rspec +0 -0
- data/Gemfile +2 -0
- data/README.md +70 -0
- data/Rakefile +6 -0
- data/lib/remotely.rb +91 -0
- data/lib/remotely/associations.rb +241 -0
- data/lib/remotely/collection.rb +75 -0
- data/lib/remotely/ext/url.rb +29 -0
- data/lib/remotely/http_methods.rb +205 -0
- data/lib/remotely/model.rb +317 -0
- data/lib/remotely/version.rb +3 -0
- data/remotely.gemspec +30 -0
- data/spec/remotely/associations_spec.rb +146 -0
- data/spec/remotely/collection_spec.rb +57 -0
- data/spec/remotely/ext/url_spec.rb +27 -0
- data/spec/remotely/http_methods_spec.rb +25 -0
- data/spec/remotely/model_spec.rb +368 -0
- data/spec/remotely_spec.rb +23 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/test_classes.rb +97 -0
- data/spec/support/webmock.rb +40 -0
- metadata +199 -0
data/remotely.gemspec
ADDED
@@ -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
|