rspec-api 0.2.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dc5683375460c14700afc17d12e045bb5acac528
4
- data.tar.gz: c218c3741e024db99c24862a87092b5b4fe54594
3
+ metadata.gz: f66889873ee973973a2f419e950bd9f405abe7e2
4
+ data.tar.gz: bbd9a7c7f60d30eaad0e46a28de310e538933bc3
5
5
  SHA512:
6
- metadata.gz: 1d9f72802f506ee2532df49b3d5e64b0cebb0535fc802f557b74e297c4996da73c80d1ded42dc6cdd44bd4e5a745c137df8823049ee1eabce14a96313f8f4050
7
- data.tar.gz: e2d6254c1fe55f4a2ae6ce6086d92a734e5cdd3bfd905ac8090423a37278e40f71eb58c7d56ef4ad6528a183a21048506ff4033658a287fb5c1ad3f5834ee6c8
6
+ metadata.gz: 0bfaa8bfff6796c6620802a92505c7c4d89c29bfac3283123ab8d9aec942bb8805c02030434646898f119a36f817ca838b4182fcf776cfe02484af0e624a138e
7
+ data.tar.gz: 6eb04e4cd39cbf6275a1a8ce4e850996e3d27417bdd004947830ca1a11d484633fcb742636ea9c25f3d7f571b634b72efabc4f69a4dc0b4029c8dfa9a3e4ebb1
data/README.md CHANGED
@@ -1,16 +1,102 @@
1
1
  RSpec API
2
2
  =========
3
3
 
4
- More info at [http://rspec-api.github.io](http://rspec-api.github.io)
4
+ RSpec API aims to make it easy to document and test [pragmatic RESTful web APIs](http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api).
5
5
 
6
- [![Build Status](https://travis-ci.org/rspec-api/rspec-api.png)](https://travis-ci.org/rspec-api/rspec-api)
6
+ It is still under development, and you can follow its progress by checking out the code on Github.
7
+
8
+ More documentation and examples are available at [http://rspec-api.github.io](http://rspec-api.github.io)
9
+
10
+ [![Build Status](https://travis-ci.org/rspec-api/rspec-api.png?branch=master)](https://travis-ci.org/rspec-api/rspec-api)
7
11
  [![Code Climate](https://codeclimate.com/github/rspec-api/rspec-api.png)](https://codeclimate.com/github/rspec-api/rspec-api)
8
12
  [![Coverage Status](https://coveralls.io/repos/rspec-api/rspec-api/badge.png)](https://coveralls.io/r/rspec-api/rspec-api)
9
13
  [![Dependency Status](https://gemnasium.com/rspec-api/rspec-api.png)](https://gemnasium.com/rspec-api/rspec-api)
10
14
 
11
- How to test
12
- ===========
15
+ A basic example
16
+ ---------------
17
+
18
+ RSpec API can help develop and document your own web APIs.
19
+ A basic example of running RSpec API locally is provided, comprised of:
20
+
21
+ * a Ruby on Rails *app* that provides a RESTful API for concerts (in [spec/dummy](https://github.com/rspec-api/rspec-api/tree/master/spec/dummy))
22
+ * a *test suite* that verifies the expected behavior of the API (in [spec/features/local/](https://github.com/rspec-api/rspec-api/blob/master/spec/features/local/gigs/gigs_spec.rb))
23
+
24
+ Run the basic example with the following commands:
25
+
26
+ git clone https://github.com/rspec-api/rspec-api.git
27
+ cd rspec-api
28
+ bundle
29
+ bundle exec rake db:migrate
30
+ bundle exec rspec spec/features/local
31
+
32
+ And you should see all the successful promises matched by the concerts API:
33
+
34
+ Concerts
35
+ GET /concerts
36
+ by default
37
+ responds with a status code that
38
+ should be 200
39
+ responds with headers that
40
+ should include 'Content-Type': 'application/json; charset=utf-8'
41
+ should include 'Link' (for pagination)
42
+ ...
43
+ DELETE /concerts/:id
44
+ given an existing id
45
+ responds with a status code that
46
+ should be 204
47
+
48
+ Finished in 0.73864 seconds
49
+ 151 examples, 0 failures
50
+
51
+
52
+ The GitHub API example
53
+ ----------------------
54
+
55
+ RSpec API can help specify and verify promises for remote APIs.
56
+ An example of running RSpec API for a remote API is provided in [spec/features/remote](https://github.com/rspec-api/rspec-api/blob/master/spec/features/remote).
57
+ The code verifies the expected behavior of a number of endpoints of the [GitHub API](http://developer.github.com):
58
+
59
+ * Activity resources (events, feeds, notifications, starring, watching)
60
+ * Gists resources (gists, gist comments)
61
+ * Git data resources (blobs, commits)
62
+ * Repository resources (repos)
63
+
64
+ Before running the example, get a GitHub Personal Access Token:
65
+
66
+ * Browse to your [GitHub settings](https://github.com/settings/applications)
67
+ * Click on 'Create new token' under 'Personal Access Token' (name it as you want)
68
+ * Copy the generated token and store it on your machine as the environment variable called `RSPEC_API_GITHUB_TOKEN`:
69
+ * On OSX and bash, accomplish this by running the command `export RSPEC_API_GITHUB_TOKEN=` followed by your pasted key (no spaces after `=`)
70
+
71
+ Now, run the GitHub API example with the following commands:
72
+
73
+ git clone https://github.com/rspec-api/rspec-api.git
74
+ cd rspec-api
75
+ bundle
76
+ bundle exec rspec spec/features/remote
77
+
78
+ And you should see all the successful promises matched by the GitHub API:
79
+
80
+ Events
81
+ GET https://api.github.com/events
82
+ by default
83
+ responds with a status code that
84
+ should be 200
85
+ responds with headers that
86
+ should include 'Content-Type': 'application/json; charset=utf-8'
87
+ should include 'Link' (for pagination)
88
+ ...
89
+ DELETE https://api.github.com/gists/:id/star
90
+ given an existing id 0d7b597d822102148810
91
+ responds with a status code that
92
+ should be 204
93
+
94
+ Finished in 1 minute 19.74 seconds
95
+ 1237 examples, 1 failure, 4 pending
96
+
97
+ How to contribute
98
+ =================
99
+
100
+ Don’t hesitate to send me code comments, issues or pull requests through GitHub!
101
+ All feedback is appreciated. Thanks :)
13
102
 
14
- * Create a new [GitHub personal authentication token](https://github.com/settings/applications)
15
- * Store it in an environment variable called `RSPEC_API_GITHUB_TOKEN` (e.g., on OS X, type `export RSPEC_API_GITHUB_TOKEN=...`)
16
- * Run `rspec`
@@ -5,6 +5,11 @@ require 'rspec-api/dsl/request'
5
5
  module DSL
6
6
  end
7
7
 
8
+ # Just like RSpec Core’s `describe` method, `resource` generates a subclass of
9
+ # {ExampleGroup} and is available at the top-level namespace.
10
+ # The difference is that examples declared inside a `resource` block have access
11
+ # to RSpec API own methods, defined in DSL::Resource, such as `has_attribute`,
12
+ # `accepts_filter`, `get`, `post`, and so on.
8
13
  def resource(name, args = {}, &block)
9
14
  args.merge! rspec_api_dsl: :resource, rspec_api: {resource_name: name}
10
15
  describe name, args, &block
@@ -13,4 +18,7 @@ end
13
18
  RSpec.configuration.include DSL::Resource, rspec_api_dsl: :resource
14
19
  RSpec.configuration.include DSL::Route, rspec_api_dsl: :route
15
20
  RSpec.configuration.include DSL::Request, rspec_api_dsl: :request
16
- # requires rspec >= 2.14 : RSpec.configuration.backtrace_exclusion_patterns << %r{lib/rspec-api/dsl\.rb}
21
+
22
+ if RSpec::Core::Version::STRING >= '2.14'
23
+ RSpec.configuration.backtrace_exclusion_patterns << %r{lib/rspec-api/dsl\.rb}
24
+ end
@@ -1,72 +1,5 @@
1
- require 'rack/utils'
1
+ require 'rspec-api/dsl/request/request'
2
+ require 'rspec-api/dsl/request/response'
2
3
  require 'rspec-api/dsl/request/status'
3
4
  require 'rspec-api/dsl/request/headers'
4
- require 'rspec-api/dsl/request/body'
5
-
6
- module DSL
7
- module Request
8
- extend ActiveSupport::Concern
9
-
10
- def response
11
- # To be overriden by more specific modules
12
- OpenStruct.new # body: nil, status: nil, headers: {}
13
- end
14
-
15
- def response_body
16
- JSON response_body_without_callbacks
17
- rescue JSON::ParserError, JSON::GeneratorError
18
- nil
19
- end
20
-
21
- def response_headers
22
- response.headers || {}
23
- end
24
-
25
- def response_status
26
- response.status
27
- end
28
-
29
- def request_params
30
- {} # To be overriden by more specific modules
31
- end
32
-
33
- module ClassMethods
34
- def respond_with(status_symbol, &block)
35
- status_code = to_code status_symbol
36
-
37
- context 'responds with a status code that' do
38
- should_match_status_expectations(status_code)
39
- end
40
- context 'responds with headers that' do
41
- should_match_headers_expectations(status_code)
42
- end
43
- context 'responds with a body that' do
44
- should_match_body_expectations(status_code, &block)
45
- end
46
- end
47
-
48
- private
49
-
50
- def to_code(status_symbol)
51
- Rack::Utils.status_code status_symbol
52
- end
53
-
54
- def has_entity_body?(status_code)
55
- !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status_code
56
- end
57
-
58
- def success?(status_code)
59
- has_entity_body?(status_code) && status_code < 400
60
- end
61
- end
62
-
63
- private
64
-
65
- def response_body_without_callbacks
66
- body = response.body
67
- # TODO: extract the 'a_callback' constant
68
- callback_pattern = %r[a_callback\((.*?)\)]
69
- body =~ callback_pattern ? body.match(callback_pattern)[1] : body
70
- end
71
- end
72
- end
5
+ require 'rspec-api/dsl/request/body'
@@ -1,3 +1,4 @@
1
+ require 'active_support'
1
2
  require 'rspec-api/matchers'
2
3
 
3
4
  module DSL
@@ -5,85 +6,80 @@ module DSL
5
6
  extend ActiveSupport::Concern
6
7
 
7
8
  module ClassMethods
8
- def should_match_body_expectations(status_code, &block)
9
- should_return_a_jsonp rspec_api[:callback] if rspec_api[:callback]
10
- should_return_a_json rspec_api[:array] if success? status_code
11
- should_include_attributes rspec_api.fetch(:attributes, {}) if success? status_code
12
- if rspec_api[:array]
13
- should_include_fixture_data unless rspec_api[:page]
14
- should_be_sorted_by(rspec_api[:sort]) if rspec_api[:sort]
15
- should_be_filtered_by(rspec_api[:filter]) if rspec_api[:filter]
9
+ # Creates an example group for expectations on the response body of the
10
+ # last API request and runs it to verify that it matches best practices:
11
+ # * if response is succesful and has a body
12
+ # - the body should be a JSON-marshalled Array or Hash
13
+ # - if request has a callback parameter, the body should be JSONP
14
+ # - if request has a sort parameter, the body should be sorted
15
+ # - if request has a filter parameter, the body should be filtered
16
+ # - if custom expectations are passed, they should pass
17
+ # - if some attributes are expected, the body should include them
18
+ def should_respond_with_expected_body(options = {})
19
+ context 'responds with a body that' do
20
+ it { should_be_valid_json options[:type] }
21
+ it { should_be_wrapped_by options[:callbacks] }
22
+ it { should_be_sorted_by options[:sorts] }
23
+ it { should_be_filtered_by options[:filters] }
24
+ it { should_have_attributes options[:attributes] }
16
25
  end
17
- should_satisfy_expectations_in &block if block_given?
18
- end
19
-
20
- private
21
-
22
- def should_return_a_jsonp(callback_options)
23
- it {
24
- if callback_options[:value] == request_params[callback_options[:name].to_s]
25
- expect(response_body).to be_a_jsonp(callback_options[:value])
26
- else
27
- expect(response_body).to be_a_jsonp(nil)
28
- end
29
- }
30
26
  end
27
+ end
31
28
 
32
- def should_return_a_json(is_array)
33
- it { expect(response_body).to be_a_json(is_array ? Array : Hash) }
34
- end
29
+ def should_be_valid_json(type)
30
+ expect(response).to be_valid_json_if response_is_successful?, type
31
+ end
35
32
 
36
- def should_include_fixture_data
37
- it { expect(response_body).to include_fixture_data }
38
- end
33
+ # If the request had a 'callback' query parameter, then the body should be
34
+ # JSONP, otherwise it should not. For instance if the request was
35
+ # `GET /?method=alert` and the request `accepts_callback :method`, then
36
+ # the body must be a JSON wrapped in the alert(...) callback
37
+ # The +callback+ param says how the request might have been made, e.g.
38
+ # name: 'method', value: 'alert'... however to make sure that it was
39
+ # really made, we need to check that request_params['method'] is present
40
+ # and that is actually 'alert'
41
+ def should_be_wrapped_by(callback_params_sets)
42
+ callback_params = response_is_successful? && get_request_param_for_list(callback_params_sets)
43
+ value = callback_params[:value] if callback_params
44
+ expect(response).to be_a_jsonp_if callback_params, value
45
+ end
39
46
 
40
- def should_be_sorted_by(sort_options)
41
- it {
42
- if sort_options[:parameter].to_s == request_params['sort']
43
- expect(response_body).to be_sorted_by(sort_options[:attribute], verse: :asc)
44
- elsif "-#{sort_options[:parameter].to_s}" == request_params['sort']
45
- expect(response_body).to be_sorted_by(sort_options[:attribute], verse: :desc)
46
- else
47
- expect(response_body).to be_sorted_by(nil)
48
- end
49
- }
50
- end
47
+ def should_be_sorted_by(sort_params_sets)
48
+ sort_params = response_is_successful? && get_request_param_for_list(sort_params_sets)
49
+ options = sort_params ? sort_params.slice(:by, :verse) : {}
50
+ expect(response).to be_sorted_if sort_params, options
51
+ end
51
52
 
52
- def should_be_filtered_by(filter_options)
53
- it {
54
- if json_value = request_params[filter_options[:name].to_s]
55
- expect(response_body).to be_filtered_by(json_value, filter_options)
56
- else
57
- expect(response_body).to be_filtered_by(nil)
58
- end
59
- }
53
+ def should_be_filtered_by(filter_params_sets)
54
+ # TODO: The following is just so the condition does not match if it's nil
55
+ # but this should be fixed in get_request_param_for_list
56
+ if filter_params_sets
57
+ filter_params_sets = filter_params_sets.dup
58
+ filter_params_sets.each{|x| x[:value] = request_params.fetch(x[:name].to_s, :something_nil)}
60
59
  end
60
+ filter_params = response_is_successful? && get_request_param_for_list(filter_params_sets)
61
+ value = filter_params[:value] if filter_params
62
+ options = filter_params ? filter_params.slice(:by, :comparing_with) : {}
63
+ expect(response).to be_filtered_if filter_params, value, options
64
+ end
61
65
 
62
- def should_satisfy_expectations_in(&block)
63
- it { instance_exec(response_body, @request_params, &block) }
64
- end
66
+ def should_have_attributes(attributes)
67
+ expect(response).to have_attributes_if response_is_successful?, attributes
68
+ end
65
69
 
66
- ## Attributes... might clean them up
67
- def should_include_attributes(attributes, ancestors = [], can_be_nil=false)
68
- attributes.each do |name, options = {}|
69
- should_match_attributes name, options, ancestors, can_be_nil
70
- should_include_nested_attributes name, options, ancestors
70
+ def get_request_param_for_list(params_sets)
71
+ (params_sets || []).find do |params|
72
+ conditions = []
73
+ conditions << (request_params[params[:name].to_s] == params[:value])
74
+ params.fetch(:extra_fields, {}).each do |name, value|
75
+ conditions << (request_params[name.to_s] == value)
71
76
  end
77
+ conditions.all?
72
78
  end
79
+ end
73
80
 
74
- def should_match_attributes(name, options, ancestors, can_be_nil)
75
- it {
76
- parent = ancestors.inject(response_body) do |chain, ancestor|
77
- Array.wrap(chain).map{|item| item[ancestor.to_s]}.flatten
78
- end
79
- expect(parent).to have_attribute(name, options.merge(parent_can_be_nil: can_be_nil, parent_can_be_empty: true))
80
- }
81
- end
82
-
83
- def should_include_nested_attributes(name, options, ancestors)
84
- attributes = options.fetch :attributes, {}
85
- should_include_attributes attributes, ancestors + [name], options[:can_be_nil]
86
- end
81
+ def response_is_successful?
82
+ response.status < 400 && !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(response.status)
87
83
  end
88
84
  end
89
85
  end
@@ -1,3 +1,5 @@
1
+ require 'active_support'
2
+ require 'rack/utils'
1
3
  require 'rspec-api/matchers'
2
4
 
3
5
  module DSL
@@ -5,20 +7,34 @@ module DSL
5
7
  extend ActiveSupport::Concern
6
8
 
7
9
  module ClassMethods
8
- def should_match_headers_expectations(status_code)
9
- should_have_json_content_type if has_entity_body? status_code
10
- should_be_paginated(rspec_api[:page][:name]) if rspec_api[:array] && rspec_api[:page]
10
+ # Creates an example group for expectations on the response headers of
11
+ # last API request and runs it to verify that it matches best practices:
12
+ # * if request has entity body, the Content-Type header should be JSON
13
+ # * if request has pages, the Link header should have a 'rel=prev' link
14
+ def should_respond_with_expected_headers(options = {})
15
+ context 'responds with headers that' do
16
+ it { should_include_content_type :json }
17
+ it { should_have_prev_page_link options[:page] }
18
+ end
11
19
  end
20
+ end
12
21
 
13
- private
22
+ private
14
23
 
15
- def should_have_json_content_type
16
- it { expect(response_headers).to have_json_content_type }
17
- end
24
+ def should_include_content_type(type)
25
+ expect(response).to include_content_type_if response_has_body?, type
26
+ end
18
27
 
19
- def should_be_paginated(page_parameter)
20
- it { expect(response_headers).to have_pagination_links(request_params[page_parameter.to_s]) }
21
- end
28
+ def should_have_prev_page_link(page)
29
+ expect(response).to have_prev_page_link_if request_is_paginated?(page)
30
+ end
31
+
32
+ def response_has_body?
33
+ !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(response.status)
34
+ end
35
+
36
+ def request_is_paginated?(page_parameter)
37
+ request_params.key? page_parameter.to_s
22
38
  end
23
39
  end
24
40
  end
@@ -0,0 +1,14 @@
1
+ require 'active_support'
2
+
3
+ module DSL
4
+ module Request
5
+ extend ::ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ end
9
+
10
+ def request_params
11
+ {} # To be overriden by more specific modules
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,86 @@
1
+ require 'active_support'
2
+
3
+ module DSL
4
+ module Request
5
+ extend ::ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ # Runs a set of expectations on the result of the last API request.
9
+ # The most basic way to call `respond_with` is with a status code:
10
+ #
11
+ # respond_with :ok
12
+ #
13
+ # This simple line will run a number of expectations, based on best
14
+ # practices that any RESTful API is expected to match:
15
+ # * the returned HTTP status will be matched against 200 OK
16
+ # * the response headers will be checked for Content-Type etc.
17
+ # * the respone body will be checked for attributes etc.
18
+ #
19
+ # Additionally, the user can specify custom expectations for the
20
+ # response by passing a block to the method:
21
+ #
22
+ # respond_with :ok do |response|
23
+ # expect(response).to be_successful?
24
+ # end
25
+ #
26
+ # In this case, after all the *implicit* expectations are run, the
27
+ # JSON-parsed response body is passed to the block to make sure that
28
+ # (in the example above), the body is a hash with a 'color' key and
29
+ # the 'green' value
30
+ def respond_with(status_symbol, &block)
31
+ should_respond_with_status status_symbol
32
+ should_respond_with_expected_headers headers_options
33
+ should_respond_with_expected_body body_options
34
+ should_match_custom_response_expectations &block if block_given?
35
+ end
36
+
37
+ private
38
+
39
+ def headers_options
40
+ {page: rspec_api.fetch(:page, {})[:name].to_s}
41
+ end
42
+
43
+ def body_options
44
+ {
45
+ type: rspec_api[:array] ? Array : Hash,
46
+ callbacks: rspec_api.fetch(:callbacks, []),
47
+ sorts: rspec_api.fetch(:sorts, []),
48
+ filters: rspec_api.fetch(:filters, []),
49
+ attributes: rspec_api.fetch(:attributes, {})
50
+ }
51
+ end
52
+
53
+ def should_match_custom_response_expectations(&block)
54
+ it { instance_exec response, @url_params, &block }
55
+ end
56
+ end
57
+
58
+ def response
59
+ # To be overriden by more specific modules
60
+ OpenStruct.new # body: nil, status: nil, headers: {}
61
+ end
62
+
63
+ def response_body
64
+ JSON response_body_without_callbacks
65
+ rescue JSON::ParserError, JSON::GeneratorError
66
+ nil
67
+ end
68
+
69
+ def response_headers
70
+ response.headers || {}
71
+ end
72
+
73
+ def response_status
74
+ response.status
75
+ end
76
+
77
+ private
78
+
79
+ def response_body_without_callbacks
80
+ body = response.body
81
+ # TODO: extract the 'a_callback' constant
82
+ callback_pattern = %r[a_callback\((.*?)\)]
83
+ body =~ callback_pattern ? body.match(callback_pattern)[1] : body
84
+ end
85
+ end
86
+ end
@@ -1,12 +1,17 @@
1
+ require 'active_support'
1
2
  require 'rspec-api/matchers'
2
3
 
3
4
  module DSL
4
5
  module Request
5
- extend ActiveSupport::Concern
6
+ extend ::ActiveSupport::Concern
6
7
 
7
8
  module ClassMethods
8
- def should_match_status_expectations(status_code)
9
- it { expect(response_status).to be_status status_code }
9
+ # Creates an example group for expectations on the HTTP status code of the
10
+ # last API request and runs it to verify that it matches +status+.
11
+ def should_respond_with_status(status)
12
+ context 'responds with a status code that' do
13
+ it { expect(response).to have_status status }
14
+ end
10
15
  end
11
16
  end
12
17
  end
@@ -22,10 +22,30 @@ module DSL
22
22
  define_action :post
23
23
  define_action :delete
24
24
 
25
- def has_attribute(name, type, options = {})
26
- parent = (@attribute_ancestors || []).inject(rspec_api) {|chain, step| chain[:attributes][step]}
27
- (parent[:attributes] ||= {})[name] = options.merge(type: type)
28
- nested_attribute(name, &Proc.new) if block_given?
25
+
26
+ def has_attribute(name, options = {}, &block)
27
+ if block_given?
28
+ # options[:type] can be symbol, hash or array
29
+ # but if you have a block we must make it a hash
30
+ options[:type] = Hash[*Array.wrap(options[:type]).map{|x| x.is_a?(Hash) ? [x.keys.first, x.values.first] : [x, {}]}.flatten] unless options[:type].is_a? Hash
31
+ # we only set the block as the new format of Object and Array
32
+ nest_attributes(options[:type], &Proc.new)
33
+ end
34
+ if @attribute_ancestors.present?
35
+ hash = @attribute_ancestors.last
36
+ hash.slice(:object, :array).each do |type, _|
37
+ (hash[type] ||= {})[name] = options
38
+ end
39
+ else
40
+ hash = (rspec_api[:attributes] ||= {})
41
+ hash[name] = options
42
+ end
43
+ end
44
+
45
+ def nest_attributes(hash, &block)
46
+ (@attribute_ancestors ||= []).push hash
47
+ yield
48
+ @attribute_ancestors.pop
29
49
  end
30
50
 
31
51
  def accepts_page(page_parameter)
@@ -33,25 +53,16 @@ module DSL
33
53
  end
34
54
 
35
55
  def accepts_sort(sort_parameter, options={})
36
- rspec_api[:sort] = {name: sort_parameter, attribute: options[:on]}
56
+ (rspec_api[:sorts] ||= []) << options.merge(name: 'sort', value: sort_parameter)
37
57
  end
38
58
 
39
- # TODO: the second 'accepts_filter' should not override the first, but add
40
59
  def accepts_filter(filter_parameter, options={})
41
- rspec_api[:filter] = options.merge(name: filter_parameter)
60
+ (rspec_api[:filters] ||= []) << options.merge(name: filter_parameter)
42
61
  end
43
62
 
44
63
  def accepts_callback(callback_parameter)
45
- rspec_api[:callback] = {name: callback_parameter, value: 'a_callback'}
46
- end
47
-
48
- private
49
-
50
- def nested_attribute(name)
51
- (@attribute_ancestors ||= []).push name
52
- yield
53
- @attribute_ancestors.pop
64
+ (rspec_api[:callbacks] ||= []) << {name: callback_parameter.to_s, value: 'a_callback'}
54
65
  end
55
66
  end
56
67
  end
57
- end
68
+ end
@@ -44,12 +44,25 @@ module DSL
44
44
  def sets_of_parameters
45
45
  [].tap do |sets_of_params|
46
46
  sets_of_params.push no_params
47
- sets_of_params.push callback_params if rspec_api[:callback]
47
+ if rspec_api[:callbacks]
48
+ rspec_api[:callbacks].each do |callback|
49
+ sets_of_params.push callback_params(callback)
50
+ end
51
+ end
48
52
  if rspec_api[:array]
49
- sets_of_params.push sort_params(verse: :asc) if rspec_api[:sort]
50
- sets_of_params.push sort_params(verse: :desc) if rspec_api[:sort]
51
- sets_of_params.push page_params if rspec_api[:page]
52
- sets_of_params.push filter_params if rspec_api[:filter]
53
+ if rspec_api[:sorts]
54
+ rspec_api[:sorts].each do |sort|
55
+ sets_of_params.push sort_params(sort)
56
+ end
57
+ end
58
+ if rspec_api[:filters]
59
+ rspec_api[:filters].each do |filter|
60
+ sets_of_params.push filter_params(filter)
61
+ end
62
+ end
63
+ if rspec_api[:page]
64
+ sets_of_params.push page_params
65
+ end
53
66
  end
54
67
  end
55
68
  end
@@ -58,10 +71,13 @@ module DSL
58
71
  {} # always send the original request without extra parameters
59
72
  end
60
73
 
61
- def sort_params(options = {})
62
- ascending = options[:verse] == :asc
63
- sort = rspec_api[:sort][:name]
64
- {sort: ascending ? "#{sort}" : "-#{sort}"}
74
+ def sort_params(sort)
75
+ {}.tap do |params|
76
+ params[sort[:name]] = sort[:value]
77
+ sort.fetch(:extra_fields, {}).each do |name, value|
78
+ params[name] = value
79
+ end
80
+ end
65
81
  end
66
82
 
67
83
  def page_params
@@ -70,15 +86,15 @@ module DSL
70
86
  end
71
87
  end
72
88
 
73
- def filter_params
89
+ def filter_params(filter)
74
90
  {}.tap do |params|
75
- params[rspec_api[:filter][:name]] = existing rspec_api[:filter][:on]
91
+ params[filter[:name]] = existing filter[:by]
76
92
  end
77
93
  end
78
94
 
79
- def callback_params
95
+ def callback_params(callback)
80
96
  {}.tap do |params|
81
- params[rspec_api[:callback][:name]] = rspec_api[:callback][:value]
97
+ params[callback[:name]] = callback[:value]
82
98
  end
83
99
  end
84
100
 
@@ -90,7 +106,7 @@ module DSL
90
106
  value = body.delete(key)
91
107
  value = value.call if value.is_a?(Proc)
92
108
  interpolated_route[":#{key}"] = value.to_s
93
- (@request_params ||= {})[key] = value
109
+ (@url_params ||= {})[key] = value
94
110
  else
95
111
  body[key] = body[key].call if body[key].is_a?(Proc)
96
112
  end
@@ -1,3 +1,3 @@
1
1
  module RspecApi
2
- VERSION = '0.2.0'
2
+ VERSION = '0.4.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - claudiob
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-10-21 00:00:00.000000000 Z
11
+ date: 2013-11-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - '>='
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec-api-matchers
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rack
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -245,6 +259,8 @@ extra_rdoc_files: []
245
259
  files:
246
260
  - lib/rspec-api/dsl/request/body.rb
247
261
  - lib/rspec-api/dsl/request/headers.rb
262
+ - lib/rspec-api/dsl/request/request.rb
263
+ - lib/rspec-api/dsl/request/response.rb
248
264
  - lib/rspec-api/dsl/request/status.rb
249
265
  - lib/rspec-api/dsl/request.rb
250
266
  - lib/rspec-api/dsl/resource.rb
@@ -257,17 +273,6 @@ files:
257
273
  - lib/rspec-api/http/remote/resource.rb
258
274
  - lib/rspec-api/http/remote/route.rb
259
275
  - lib/rspec-api/http/remote.rb
260
- - lib/rspec-api/matchers/attributes.rb
261
- - lib/rspec-api/matchers/body.rb
262
- - lib/rspec-api/matchers/content_type.rb
263
- - lib/rspec-api/matchers/fields.rb
264
- - lib/rspec-api/matchers/filter.rb
265
- - lib/rspec-api/matchers/fixtures.rb
266
- - lib/rspec-api/matchers/jsonp.rb
267
- - lib/rspec-api/matchers/page.rb
268
- - lib/rspec-api/matchers/sort.rb
269
- - lib/rspec-api/matchers/status.rb
270
- - lib/rspec-api/matchers.rb
271
276
  - lib/rspec-api/version.rb
272
277
  - MIT-LICENSE
273
278
  - README.md
@@ -291,7 +296,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
291
296
  version: 1.3.6
292
297
  requirements: []
293
298
  rubyforge_project:
294
- rubygems_version: 2.1.5
299
+ rubygems_version: 2.1.10
295
300
  signing_key:
296
301
  specification_version: 4
297
302
  summary: Methods to write more compact and meaningful, auto-documented specs for web
@@ -1,10 +0,0 @@
1
- require_relative 'matchers/status'
2
- require_relative 'matchers/body'
3
- require_relative 'matchers/content_type'
4
- require_relative 'matchers/fields'
5
- require_relative 'matchers/attributes'
6
- require_relative 'matchers/fixtures'
7
- require_relative 'matchers/page'
8
- require_relative 'matchers/sort'
9
- require_relative 'matchers/filter'
10
- require_relative 'matchers/jsonp'
@@ -1,33 +0,0 @@
1
- RSpec::Matchers.define :have_attribute do |name, options = {}|
2
- name, can_be_nil, type = name.to_s, options[:can_be_nil], options[:type]
3
-
4
- match do |json|
5
- Array.wrap(json).all? do |item|
6
- if (options[:parent_can_be_nil] and item.nil?) || (options[:parent_can_be_empty] and item.empty?)
7
- true
8
- elsif can_be_nil
9
- item.key?(name)
10
- else
11
- matches_type?(item[name], type)
12
- end
13
- end
14
- end
15
-
16
- description do # TODO: add parent name
17
- type = "#{options[:type]}#{' or nil' if can_be_nil}"
18
- %Q(include the field #{name.to_json} of type #{type})
19
- end
20
-
21
- failure_message_for_should do |json|
22
- %Q(should #{description}, but is #{json})
23
- end
24
- end
25
-
26
- def matches_type?(value, type)
27
- case type
28
- when :url then value =~ URI::regexp
29
- when :timestamp then DateTime.iso8601 value rescue false
30
- when :boolean then [TrueClass, FalseClass].include? value.class
31
- else value.is_a? type.to_s.classify.constantize
32
- end
33
- end
@@ -1,13 +0,0 @@
1
- RSpec::Matchers.define :be_a_json do |expected_type|
2
- match do |json|
3
- json.is_a? expected_type
4
- end
5
-
6
- description do
7
- "be a JSON #{expected_type}"
8
- end
9
-
10
- failure_message_for_should do |json|
11
- %Q(should #{description}, but is #{json})
12
- end
13
- end
@@ -1,13 +0,0 @@
1
- RSpec::Matchers.define :have_json_content_type do
2
- match do |response|
3
- response_headers['Content-Type'] == 'application/json; charset=utf-8'
4
- end
5
-
6
- description do
7
- "include 'Content-Type': 'application/json; charset=utf-8'"
8
- end
9
-
10
- failure_message_for_should do |item|
11
- %Q(should #{description}, but are #{response_headers})
12
- end
13
- end
@@ -1,32 +0,0 @@
1
- RSpec::Matchers.define :have_field do |key, options = {}|
2
- value = options[:value]
3
-
4
- match do |item|
5
- item[key.to_s] == value
6
- end
7
-
8
- description do
9
- %Q(have the value #{value.to_json} in the field #{key})
10
- end
11
-
12
- failure_message_for_should do |item|
13
- %Q(should #{description}, but got #{item})
14
- end
15
- end
16
-
17
- RSpec::Matchers.define :have_fields do |key, options = {}|
18
- value = options[:value]
19
- after = options[:after]
20
-
21
- match do |items|
22
- items.all?{|item| item[key.to_s].send(after) == value }
23
- end
24
-
25
- description do
26
- %Q(have the value #{value.to_json} in the field #{key} after #{after})
27
- end
28
-
29
- failure_message_for_should do |items|
30
- %Q(should #{description}, but got #{items})
31
- end
32
- end
@@ -1,28 +0,0 @@
1
- RSpec::Matchers.define :be_filtered_by do |json_value, options = {}|
2
- filtered_attribute = options[:on]
3
- compare = options[:comparing_with] || Proc.new{|x,y| x == y}
4
-
5
- match do |items|
6
- if filtered_attribute.nil?
7
- true
8
- else
9
- items.all? do |item|
10
- # TODO: Don't always use string
11
- compare.call json_value, item[filtered_attribute.to_s].to_s
12
- end
13
- end
14
- end
15
-
16
- description do
17
- if filtered_attribute.nil?
18
- %Q(not be filtered by any specific attribute)
19
- else
20
- # TODO: Change description based on operator
21
- %Q(be filtered by #{filtered_attribute.to_json} => #{json_value})
22
- end
23
- end
24
-
25
- failure_message_for_should do |items|
26
- %Q(should #{description}, but is #{items})
27
- end
28
- end
@@ -1,16 +0,0 @@
1
- RSpec::Matchers.define :include_fixture_data do
2
- match do |body|
3
- if body.empty? #&& did_not_declare_fixtures
4
- pending 'Make your tests more meaningful by declaring fixtures!'
5
- end
6
- !body.empty?
7
- end
8
-
9
- description do
10
- %Q(have some values from the fixtures)
11
- end
12
-
13
- failure_message_for_should do |response|
14
- %Q(should #{description}, but got an empty array)
15
- end
16
- end
@@ -1,17 +0,0 @@
1
- RSpec::Matchers.define :be_a_jsonp do |callback_name|
2
- match do |response_body|
3
- if callback_name.nil?
4
- true
5
- else
6
- response.body =~ %r[^#{callback_name}\((.*?)\)$]
7
- end
8
- end
9
-
10
- description do
11
- %Q(be a JSONP callback)
12
- end
13
-
14
- failure_message_for_should do |response_body|
15
- %Q(should #{description}, but is #{response_body})
16
- end
17
- end
@@ -1,19 +0,0 @@
1
- RSpec::Matchers.define :have_pagination_links do |page|
2
- match do |response_headers|
3
- if page.nil?
4
- true
5
- else
6
- links = response_headers['Link'] || '' # see http://git.io/CUz3-Q
7
- rels = links.split(',').map{|link| link[/<.+?>; rel="(.*)"$/, 1]}
8
- rels.sort == ['first', 'prev']
9
- end
10
- end
11
-
12
- description do
13
- %Q(include 'Link' (for pagination))
14
- end
15
-
16
- failure_message_for_should do |response_headers|
17
- %Q(should #{description}, but are #{response_headers})
18
- end
19
- end
@@ -1,25 +0,0 @@
1
- RSpec::Matchers.define :be_sorted_by do |sorting_field, options = {}|
2
- match do |items|
3
- if sorting_field.nil?
4
- true
5
- else
6
- values = items.map{|item| item[sorting_field.to_s]}
7
- values.reverse! if options[:verse] == :desc
8
- values == values.sort
9
- end
10
- end
11
-
12
- description do
13
- # NOTE: Since `accepts_sort random: nil` is acceptable, this description
14
- # should say "you should not expect any sorting by any specific field"
15
- if sorting_field.nil?
16
- %Q(not be sorted by any specific attribute)
17
- else
18
- %Q(be sorted by #{sorting_field.to_json} #{options[:verse]})
19
- end
20
- end
21
-
22
- failure_message_for_should do |items|
23
- %Q(should #{description}, but is #{items})
24
- end
25
- end
@@ -1,13 +0,0 @@
1
- RSpec::Matchers.define :be_status do |expected|
2
- match do |actual|
3
- actual == expected
4
- end
5
-
6
- description do
7
- %Q(be #{expected})
8
- end
9
-
10
- failure_message_for_should do |actual|
11
- %Q(should #{description}, but is #{actual})
12
- end
13
- end