intermodal 0.0.1 → 0.4.0
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.
- 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
|