intermodal 0.0.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/Gemfile +4 -0
- data/LICENSE +1 -1
- data/README +6 -4
- data/Rakefile +6 -0
- data/intermodal.gemspec +51 -0
- data/lib/generators/intermodal_generator.rb +8 -0
- data/lib/intermodal.rb +122 -0
- data/lib/intermodal/api.rb +246 -0
- data/lib/intermodal/api/configuration.rb +40 -0
- data/lib/intermodal/api/railties.rb +21 -0
- data/lib/intermodal/concerns/acceptors/named_resource.rb +12 -0
- data/lib/intermodal/concerns/acceptors/resource.rb +12 -0
- data/lib/intermodal/concerns/controllers/accountability.rb +17 -0
- data/lib/intermodal/concerns/controllers/anonymous.rb +24 -0
- data/lib/intermodal/concerns/controllers/authenticatable.rb +24 -0
- data/lib/intermodal/concerns/controllers/paginated_collection.rb +25 -0
- data/lib/intermodal/concerns/controllers/presentation.rb +17 -0
- data/lib/intermodal/concerns/controllers/resource.rb +51 -0
- data/lib/intermodal/concerns/controllers/resource_linking.rb +58 -0
- data/lib/intermodal/concerns/let.rb +21 -0
- data/lib/intermodal/concerns/models/access_credential.rb +28 -0
- data/lib/intermodal/concerns/models/account.rb +16 -0
- data/lib/intermodal/concerns/models/accountability.rb +47 -0
- data/lib/intermodal/concerns/models/db_access_token.rb +29 -0
- data/lib/intermodal/concerns/models/has_parent_resource.rb +45 -0
- data/lib/intermodal/concerns/models/presentation.rb +25 -0
- data/lib/intermodal/concerns/models/redis_access_token.rb +60 -0
- data/lib/intermodal/concerns/models/resource_linking.rb +126 -0
- data/lib/intermodal/concerns/models/sanitize_html.rb +35 -0
- data/lib/intermodal/concerns/presenters/named_resource.rb +12 -0
- data/lib/intermodal/concerns/presenters/resource.rb +14 -0
- data/lib/intermodal/concerns/rails/rails_3_stack.rb +42 -0
- data/lib/intermodal/concerns/rails/rails_4_stack.rb +17 -0
- data/lib/intermodal/concerns/rails/use_warden.rb +21 -0
- data/lib/intermodal/config.rb +15 -0
- data/lib/intermodal/configuration.rb +11 -0
- data/lib/intermodal/controllers/api_controller.rb +26 -0
- data/lib/intermodal/controllers/linking_resource_controller.rb +8 -0
- data/lib/intermodal/controllers/nested_resource_controller.rb +18 -0
- data/lib/intermodal/controllers/resource_controller.rb +11 -0
- data/lib/intermodal/dsl/controllers.rb +125 -0
- data/lib/intermodal/dsl/mapping.rb +79 -0
- data/lib/intermodal/dsl/presentation_helpers.rb +107 -0
- data/lib/intermodal/mapping/acceptor.rb +2 -2
- data/lib/intermodal/mapping/mapper.rb +39 -13
- data/lib/intermodal/mapping/presenter.rb +12 -6
- data/lib/intermodal/proxies/linking_resources.rb +58 -0
- data/lib/intermodal/proxies/will_paginate.rb +85 -0
- data/lib/intermodal/rack/auth.rb +29 -0
- data/lib/intermodal/rack/dummy_store.rb +24 -0
- data/lib/intermodal/rack/rescue.rb +82 -0
- data/lib/intermodal/responders/linking_resource_responder.rb +21 -0
- data/lib/intermodal/responders/resource_responder.rb +64 -0
- data/lib/intermodal/rspec/acceptors.rb +79 -0
- data/lib/intermodal/rspec/models/accountability.rb +114 -0
- data/lib/intermodal/rspec/models/has_parent_resource.rb +132 -0
- data/lib/intermodal/rspec/models/resource_linking.rb +234 -0
- data/lib/intermodal/rspec/models/sanitization.rb +84 -0
- data/lib/intermodal/rspec/presenters.rb +92 -0
- data/lib/intermodal/rspec/requests/authenticated_requests.rb +17 -0
- data/lib/intermodal/rspec/requests/linked_resources.rb +180 -0
- data/lib/intermodal/rspec/requests/paginated_collection.rb +60 -0
- data/lib/intermodal/rspec/requests/rack.rb +142 -0
- data/lib/intermodal/rspec/requests/request_validations.rb +36 -0
- data/lib/intermodal/rspec/requests/resources.rb +275 -0
- data/lib/intermodal/rspec/requests/rfc2616_status_codes.rb +51 -0
- data/lib/intermodal/rspec/validators.rb +86 -0
- data/lib/intermodal/validators/account_validator.rb +27 -0
- data/lib/intermodal/validators/different_account_validator.rb +27 -0
- data/lib/intermodal/version.rb +3 -0
- data/spec/mapping/acceptors_spec.rb +142 -0
- data/spec/mapping/presenters_spec.rb +186 -0
- data/spec/models/accountability_spec.rb +13 -0
- data/spec/models/has_parent_resource_spec.rb +18 -0
- data/spec/models/resource_linking_spec.rb +21 -0
- data/spec/proxies/will_paginate_spec.rb +163 -0
- data/spec/rack/auth_spec.rb +51 -0
- data/spec/requests/linked_resources.rb +37 -0
- data/spec/requests/nested_resources_spec.rb +54 -0
- data/spec/requests/resources_spec.rb +50 -0
- data/spec/spec_helper.rb +53 -0
- data/spec/support/api.rb +50 -0
- data/spec/support/app/class_builder.rb +41 -0
- data/spec/support/app/db/adapter_helper.rb +53 -0
- data/spec/support/app/db/authentication_schema_helper.rb +62 -0
- data/spec/support/app/db/migration_helper.rb +44 -0
- data/spec/support/app/schema.rb +101 -0
- data/spec/support/application.rb +23 -0
- data/spec/support/blueprints.rb +41 -0
- data/spec/support/epiphyte.rb +29 -0
- metadata +393 -52
- data/lib/intermodal/base.rb +0 -13
- data/lib/intermodal/declare_controllers.rb +0 -102
- data/lib/intermodal/mapping.rb +0 -4
- data/lib/intermodal/mapping/dsl.rb +0 -76
@@ -0,0 +1,84 @@
|
|
1
|
+
module Intermodal
|
2
|
+
module RSpec
|
3
|
+
module Sanitization
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def expects_sanitization_of(_field, _options, &additional_examples)
|
8
|
+
# We are not trying to retest the sanitizer so much as lightly demonstrating
|
9
|
+
# idempotence. That is, repeated calls to the sanitizer should produce the
|
10
|
+
# same output
|
11
|
+
|
12
|
+
context _field.inspect do
|
13
|
+
subject { resource.update_attributes!(updated_attributes); resource }
|
14
|
+
let(:updated_attributes) { { _field => value } }
|
15
|
+
let(:accepted_tags) { _options[:accepted_tags] }
|
16
|
+
let(:rejected_tags) { _options[:rejected_tags] }
|
17
|
+
|
18
|
+
context 'with a random string' do
|
19
|
+
let(:value) { SecureRandom.hex(16) }
|
20
|
+
it 'should leave it alone' do
|
21
|
+
expect(subject).not_to be_changed # Check update has persisted
|
22
|
+
expect(subject.send(_field)).to eql(value)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'with approved html tag' do
|
27
|
+
let(:tag) { accepted_tags.sample }
|
28
|
+
let(:content) { SecureRandom.hex(16) }
|
29
|
+
let(:value) { "<#{tag}>#{content}</#{tag}>" }
|
30
|
+
it 'should leave it alone' do
|
31
|
+
expect(subject).not_to be_changed # Check update has persisted
|
32
|
+
expect(subject.send(_field)).to eql(value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'with tag not on whitelist' do
|
37
|
+
let(:tag) { rejected_tags.sample }
|
38
|
+
let(:content) { SecureRandom.hex(16) }
|
39
|
+
let(:value) { "<#{tag}>#{content}</#{tag}>" }
|
40
|
+
it 'should sanitize tag' do
|
41
|
+
expect(subject).not_to be_changed # Check update has persisted
|
42
|
+
expect(subject.send(_field)).to eql(content)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
instance_eval(&additional_examples) if additional_examples
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def expects_stripping_of(_field, &additional_examples)
|
51
|
+
# We are not trying to retest the sanitizer so much as lightly demonstrating
|
52
|
+
# idempotence. That is, repeated calls to the sanitizer should produce the
|
53
|
+
# same output
|
54
|
+
|
55
|
+
context _field.inspect do
|
56
|
+
subject { resource.update_attributes!(updated_attributes); resource }
|
57
|
+
let(:updated_attributes) { { _field => value } }
|
58
|
+
let(:rejected_tags) { %w(p div span ol ul li em strong) }
|
59
|
+
|
60
|
+
context 'with a random string' do
|
61
|
+
let(:value) { SecureRandom.hex(16) }
|
62
|
+
it 'should leave it alone' do
|
63
|
+
expect(subject).not_to be_changed # Check update has persisted
|
64
|
+
expect(subject.send(_field)).to eql(value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'with any tag' do
|
69
|
+
let(:tag) { rejected_tags.sample }
|
70
|
+
let(:content) { SecureRandom.hex(16) }
|
71
|
+
let(:value) { "<#{tag}>#{content}</#{tag}>" }
|
72
|
+
it 'should sanitize tag' do
|
73
|
+
expect(subject).not_to be_changed # Check update has persisted
|
74
|
+
expect(subject.send(_field)).to eql(content)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
instance_eval(&additional_examples) if additional_examples
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Intermodal
|
2
|
+
module RSpec
|
3
|
+
module Presenters
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
included do
|
6
|
+
extend ::Intermodal::RSpec::Macros::Presenters
|
7
|
+
|
8
|
+
let(:resource) { model.make! }
|
9
|
+
let(:resource_name) { model.name.demodulize.underscore }
|
10
|
+
let(:presenter) { api.presenter_for(model) }
|
11
|
+
let(:presentation) { presenter.call(resource, :scope => scope).with_indifferent_access }
|
12
|
+
let(:scope) { :default }
|
13
|
+
|
14
|
+
def expects_presentation(input, expectation)
|
15
|
+
field = input.keys[0]
|
16
|
+
presenter.call(input.with_indifferent_access, :scope => scope)[field].should eql(expectation)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Macros
|
22
|
+
module Presenters
|
23
|
+
def concerned_with_presentation(_model, &blk)
|
24
|
+
describe _model do
|
25
|
+
let(:model) { _model }
|
26
|
+
subject { model }
|
27
|
+
|
28
|
+
context "when concerned with presentation" do
|
29
|
+
instance_eval(&blk) if blk
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def scoped_to(_scope, &blk)
|
35
|
+
context "when scoped to #{_scope}" do
|
36
|
+
let(:scope) { _scope }
|
37
|
+
instance_eval(&blk) if blk
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def exposes(*fields)
|
42
|
+
fields.each do |field|
|
43
|
+
it "should present #{field}" do
|
44
|
+
presentation.should contain(field)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def hides(*fields)
|
50
|
+
fields.each do |field|
|
51
|
+
it "should not present #{field}" do
|
52
|
+
presentation.should_not contain(field)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def exposes_resource
|
58
|
+
context 'when exposing resource fields' do
|
59
|
+
exposes 'id', 'created_at', 'updated_at'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def exposes_named_resource
|
64
|
+
context 'when exposing named resource fields' do
|
65
|
+
exposes_resource
|
66
|
+
exposes 'name'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def exposes_linked_resources(linked_resources_name, options = {})
|
71
|
+
_linked_resource_name, _linking_association = linked_resources_name.to_s.singularize, options[:with]
|
72
|
+
context "when exposing linked resources, #{_linked_resource_name}" do
|
73
|
+
let(:linked_resource_ids_name) { :"#{_linked_resource_name}_ids" }
|
74
|
+
let(:linking_association) { :"#{_linking_association}" }
|
75
|
+
let(:linked_resource_ids) { resource.send(linking_association).send("to_#{linked_resource_ids_name}").to_a }
|
76
|
+
let(:presented_linked_ids) { presentation[linked_resource_ids_name].to_a }
|
77
|
+
|
78
|
+
exposes "#{_linked_resource_name}_ids"
|
79
|
+
it "should present \"#{_linked_resource_name}_ids\" as a collection of ids" do
|
80
|
+
resource
|
81
|
+
(presented_linked_ids - linked_resource_ids).should be_empty
|
82
|
+
(linked_resource_ids - presented_linked_ids).should be_empty
|
83
|
+
end
|
84
|
+
|
85
|
+
pending "should only present \"#{_linked_resource_name}_ids\" scoped to account"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Intermodal
|
2
|
+
module RSpec
|
3
|
+
module AuthenticatedRequests
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def expects_unauthorized_access_to_respond_with_401
|
8
|
+
context 'with unauthorized access credentials' do
|
9
|
+
let(:http_headers) { { 'X-Auth-Token' => '', 'Accept' => 'application/json' } }
|
10
|
+
|
11
|
+
expects_status(401)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
module Intermodal
|
2
|
+
module RSpec
|
3
|
+
module LinkedResources
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include Intermodal::RSpec::Resources
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def link_resource(parent_resource, options = {}, &blk)
|
12
|
+
metadata[:parent_resource] = options[:parent_resource] = parent_resource
|
13
|
+
metadata[:target_resources] = options[:to]
|
14
|
+
metadata[:linking_resource_model] = options[:with]
|
15
|
+
|
16
|
+
_target_resources = options[:to]
|
17
|
+
|
18
|
+
resource_options = {
|
19
|
+
:namespace => options[:namespace]
|
20
|
+
}
|
21
|
+
resources [metadata[:parent_resource].to_s.pluralize, metadata[:target_resources]], resource_options do
|
22
|
+
let(:model) { options[:with] }
|
23
|
+
let(:model_parents) { [ send(options[:parent_resource]) ] }
|
24
|
+
let(:unlinked_targets) { raise "Override :unlinked_targets with let()" }
|
25
|
+
let(:target_resources) { _target_resources }
|
26
|
+
let(:collection_element_name) { "#{target_resources.to_s.singularize}_ids" }
|
27
|
+
let(:paginated_collection) { collection }
|
28
|
+
let(:resource_element_name) { parent_resource }
|
29
|
+
let(:presentation_root) { options[:parent_resource] }
|
30
|
+
let(:collection_proxy) do
|
31
|
+
Intermodal::Proxies::LinkingResources.new presentation_root,
|
32
|
+
:to => target_resources,
|
33
|
+
:with => collection,
|
34
|
+
:parent_id => model_parents.first.id
|
35
|
+
end
|
36
|
+
let(:presented_collection) { parser.decode(collection_proxy.send("to_#{format}")) }
|
37
|
+
instance_eval(&blk)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def expects_crud_for_linked_resource
|
42
|
+
expects_index :skip_pagination_examples => true do
|
43
|
+
it 'should include the parent id' do
|
44
|
+
collection.should_not be_empty
|
45
|
+
body[resource_element_name.to_s]['id'].should eql(model_parents.first.id)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
expects_list_replace
|
50
|
+
expects_list_append
|
51
|
+
expects_list_remove
|
52
|
+
end
|
53
|
+
|
54
|
+
def expects_list_replace(&blk)
|
55
|
+
_metadata = metadata
|
56
|
+
request :post, metadata[:collection_url] do
|
57
|
+
let(:request_url) { collection_url }
|
58
|
+
let(:replacement_target_ids) { unlinked_targets.map(&:id) }
|
59
|
+
let(:request_payload) { { collection_element_name => replacement_target_ids } }
|
60
|
+
let(:updated_target_ids) { model.by_parent(model_parent).to_target_ids }
|
61
|
+
|
62
|
+
instance_eval(&blk) if blk
|
63
|
+
|
64
|
+
expects_status(201)
|
65
|
+
expects_content_type(metadata[:mime_type], metadata[:encoding])
|
66
|
+
|
67
|
+
with_malformed_data_should_respond_with_400
|
68
|
+
with_nil_target_ids_should_respond_with_422
|
69
|
+
expects_unauthorized_access_to_respond_with_401
|
70
|
+
|
71
|
+
context 'with empty payload' do
|
72
|
+
let(:request_payload) { { collection_element_name => [] } }
|
73
|
+
|
74
|
+
expects_status(201)
|
75
|
+
expects_content_type(metadata[:mime_type], metadata[:encoding])
|
76
|
+
|
77
|
+
it 'should reset resource linking' do
|
78
|
+
response.should_not be_empty
|
79
|
+
updated_target_ids.should be_empty
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should link #{metadata[:target_resources]} to #{metadata[:parent_resource]}" do
|
84
|
+
model_collection.should_not be_empty
|
85
|
+
response.should_not be_empty
|
86
|
+
replacement_target_ids.each do |replacement_target_id|
|
87
|
+
updated_target_ids.should include(replacement_target_id)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should delete original linked #{metadata[:target_resources]}" do
|
92
|
+
model_collection.should_not be_empty
|
93
|
+
response.should_not be_empty
|
94
|
+
model_collection.each do |original_target_id|
|
95
|
+
updated_target_ids.should_not include(original_target_id)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def expects_list_append(&blk)
|
102
|
+
_metadata = metadata
|
103
|
+
request :put, metadata[:collection_url] do
|
104
|
+
instance_eval(&blk) if blk
|
105
|
+
let(:request_url) { collection_url }
|
106
|
+
let(:additional_target_ids) { unlinked_targets.map(&:id) }
|
107
|
+
let(:request_payload) { { collection_element_name => additional_target_ids } }
|
108
|
+
let(:updated_target_ids) { model.by_parent(model_parent).to_target_ids }
|
109
|
+
|
110
|
+
expects_status(200)
|
111
|
+
expects_content_type(metadata[:mime_type], metadata[:encoding])
|
112
|
+
|
113
|
+
it 'should link additional targets to manufacturer' do
|
114
|
+
model_collection.should_not be_empty
|
115
|
+
response.should_not be_empty
|
116
|
+
additional_target_ids.each do |additional_target_id|
|
117
|
+
updated_target_ids.should include(additional_target_id)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'should append to, but not delete original linked targets' do
|
122
|
+
model_collection.should_not be_empty
|
123
|
+
response.should_not be_empty
|
124
|
+
model_collection.each do |original_target_id|
|
125
|
+
updated_target_ids.should include(original_target_id)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
with_malformed_data_should_respond_with_400
|
130
|
+
with_nil_target_ids_should_respond_with_422
|
131
|
+
expects_unauthorized_access_to_respond_with_401
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def expects_list_remove(&blk)
|
136
|
+
_metadata = metadata
|
137
|
+
request :delete, metadata[:collection_url] do
|
138
|
+
instance_eval(&blk) if blk
|
139
|
+
let(:request_url) { collection_url }
|
140
|
+
let(:deleted_target_ids) { model_collection[0..1] }
|
141
|
+
let(:remaining_target_ids) { model_collection - deleted_target_ids }
|
142
|
+
let(:request_payload) { { collection_element_name => deleted_target_ids } }
|
143
|
+
let(:updated_target_ids) { model.by_parent(model_parent).to_target_ids }
|
144
|
+
|
145
|
+
expects_status(200)
|
146
|
+
expects_content_type(metadata[:mime_type], metadata[:encoding])
|
147
|
+
|
148
|
+
it 'should delete linked targets' do
|
149
|
+
deleted_target_ids.should_not be_empty
|
150
|
+
response.should_not be_empty
|
151
|
+
deleted_target_ids.each do |deleted_target_id|
|
152
|
+
updated_target_ids.should_not include(deleted_target_id)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'should keep remaining targets' do
|
157
|
+
deleted_target_ids.should_not be_empty
|
158
|
+
response.should_not be_empty
|
159
|
+
remaining_target_ids.each do |remaining_target_id|
|
160
|
+
updated_target_ids.should include(remaining_target_id)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
with_malformed_data_should_respond_with_400
|
165
|
+
with_nil_target_ids_should_respond_with_422
|
166
|
+
expects_unauthorized_access_to_respond_with_401
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def with_nil_target_ids_should_respond_with_422
|
171
|
+
context 'without nil ids ' do
|
172
|
+
let(:request_payload) { { collection_element_name => nil } }
|
173
|
+
expects_status(422)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Intermodal
|
2
|
+
module RSpec
|
3
|
+
module PaginatedCollection
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def expects_paginated_resource(options = {}, &customizations)
|
8
|
+
# Default behavior for will_paginate
|
9
|
+
options[:page] ||= 1
|
10
|
+
options[:collection_name]
|
11
|
+
|
12
|
+
context 'when paginating' do
|
13
|
+
let(:expected_total_pages) { collection.size/per_page + 1 }
|
14
|
+
let(:expected_total_entries) { collection.size }
|
15
|
+
let(:collection_element_name) { options[:collection_name] } if options[:collection_name]
|
16
|
+
let(:responded_collection_metadata) do
|
17
|
+
case format
|
18
|
+
when :xml
|
19
|
+
body[collection_element_name.to_s]
|
20
|
+
else
|
21
|
+
body
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
instance_eval(&customizations) if customizations
|
26
|
+
|
27
|
+
if options[:empty_collection]
|
28
|
+
it 'should have an empty collection' do
|
29
|
+
body[collection_element_name.to_s].should be_empty
|
30
|
+
end
|
31
|
+
else
|
32
|
+
it 'should have a collection' do
|
33
|
+
collection
|
34
|
+
body[collection_element_name.to_s].should_not be_empty
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should be on page #{options[:page]}" do
|
39
|
+
collection
|
40
|
+
responded_collection_metadata['page'].should eql(options[:page])
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should have total_pages' do
|
44
|
+
collection
|
45
|
+
responded_collection_metadata['total_pages'].should eql(expected_total_pages)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should have total_entries' do
|
49
|
+
collection
|
50
|
+
responded_collection_metadata['total_entries'].should eql(expected_total_entries)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Some version of WillPaginate steps on this
|
56
|
+
alias expects_pagination expects_paginated_resource
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Intermodal
|
2
|
+
module RSpec
|
3
|
+
module Rack
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class SimpleXMLParser
|
7
|
+
def self.decode(*args)
|
8
|
+
Hash.from_xml(*args)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
included do
|
13
|
+
subject { response }
|
14
|
+
|
15
|
+
# Define the Rack Application you are testing
|
16
|
+
let(:application) { fail NotImplementedError, 'Must define let(:application)' }
|
17
|
+
let(:http_headers) { Hash.new }
|
18
|
+
# let(:request_headers) { rack_compliant_headers(http_headers) }
|
19
|
+
let(:request_url_prefix) { '' }
|
20
|
+
|
21
|
+
let(:response) { application.call(request) }
|
22
|
+
let(:status) { response[0] }
|
23
|
+
let(:response_headers) { response[1] }
|
24
|
+
let(:content_type) { response_headers['Content-Type'] }
|
25
|
+
let(:raw_body) do
|
26
|
+
# http://rack.rubyforge.org/doc/files/SPEC.html
|
27
|
+
# Rack Spec says response[2] should respond to #each and we
|
28
|
+
# cannot gaurantee this is an ActionDispatch::Response
|
29
|
+
|
30
|
+
# This calls each on all the chunks in-memory and gives it back as a whole string
|
31
|
+
|
32
|
+
[].tap do |out|
|
33
|
+
response[2].each do |chunk|
|
34
|
+
out << chunk
|
35
|
+
end
|
36
|
+
end.join
|
37
|
+
end
|
38
|
+
let(:body) { parser.decode(raw_body) }
|
39
|
+
|
40
|
+
let(:json_parser) { MultiJson }
|
41
|
+
let(:xml_parser) { SimpleXMLParser }
|
42
|
+
|
43
|
+
let(:parser) do
|
44
|
+
case format
|
45
|
+
when :json
|
46
|
+
json_parser
|
47
|
+
when :xml
|
48
|
+
xml_parser
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Override to change payload Content-Type
|
53
|
+
let(:request_payload_mime_type) { Mime::Type.lookup_by_extension(format).to_s }
|
54
|
+
|
55
|
+
# Override to change JSON payload builder
|
56
|
+
let(:request_json_payload) { request_payload.to_json }
|
57
|
+
|
58
|
+
# Override to change XML payload builder
|
59
|
+
let(:request_xml_payload) { request_payload.to_xml }
|
60
|
+
|
61
|
+
# Override to send arbitrary payloads
|
62
|
+
let(:request_raw_payload) { send("request_#{format}_payload") unless request_payload.nil? }
|
63
|
+
|
64
|
+
let(:expected_mime_type) { Mime::Types.lookup_by_extension(format).to_s }
|
65
|
+
let(:expected_encoding) { 'utf-8' }
|
66
|
+
|
67
|
+
# Broken out so you can use this outside of let()
|
68
|
+
def rack_compliant_headers(_headers = {})
|
69
|
+
_headers.to_a.inject({}) do |m,kv|
|
70
|
+
m.tap do |_m|
|
71
|
+
_h, _v = kv
|
72
|
+
_m["HTTP_#{_h}".underscore.upcase] = _v
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def rack_compliant_request(_method, _url, _headers, _body, format = :json)
|
78
|
+
_payload_mime_type = (format.is_a?(Symbol) ? Mime::Type.lookup_by_extension(format) : format)
|
79
|
+
req_opts = { :method => _method }.merge(rack_compliant_headers(_headers))
|
80
|
+
req_opts[:input] = _body
|
81
|
+
::Rack::MockRequest.env_for(_url, req_opts).tap do |_r|
|
82
|
+
_r.merge!({ 'CONTENT_TYPE' => _payload_mime_type.to_s }) unless _body.nil?
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
module ClassMethods
|
88
|
+
def request(method, _url, payload = nil, &blk)
|
89
|
+
describe "#{method.to_s.upcase} #{_url}" do
|
90
|
+
rack_request(method, _url, &payload)
|
91
|
+
instance_eval(&blk)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def rack_request(method, _url, _format = nil, &_payload)
|
96
|
+
_format = _format # Scope for closure
|
97
|
+
_payload = proc do nil end unless _payload
|
98
|
+
|
99
|
+
let(:format) { _format } if _format
|
100
|
+
let(:request) { rack_compliant_request(method,
|
101
|
+
request_url_prefix + request_url,
|
102
|
+
http_headers,
|
103
|
+
request_raw_payload,
|
104
|
+
(request_raw_payload ? request_payload_mime_type : nil) ) }
|
105
|
+
let(:request_url) { _url }
|
106
|
+
let(:request_payload, &_payload)
|
107
|
+
end
|
108
|
+
|
109
|
+
def expects_status(response_status)
|
110
|
+
it "should respond with status #{response_status} #{Intermodal::RSpec::HTTP::STATUS_CODES[response_status.to_s]}" do
|
111
|
+
status.should eql(response_status)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def expects_content_type(mime_type, charset = nil)
|
116
|
+
if mime_type
|
117
|
+
it "should respond with content type of #{mime_type}" do
|
118
|
+
content_type.should match(%r{^#{mime_type}})
|
119
|
+
end
|
120
|
+
if charset
|
121
|
+
it "should respond encoded in #{charset}" do
|
122
|
+
content_type.should match(%r{charset=#{charset}})
|
123
|
+
end
|
124
|
+
end
|
125
|
+
else
|
126
|
+
it "should not respond with content type" do
|
127
|
+
content_type.should be_nil
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
def expects_empty_body
|
134
|
+
it "should respond with a Rack-compliant empty body" do
|
135
|
+
response[2].should be_empty
|
136
|
+
response[2].should be_respond_to(:each)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|