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 +4 -4
- data/README.md +93 -7
- data/lib/rspec-api/dsl.rb +9 -1
- data/lib/rspec-api/dsl/request.rb +3 -70
- data/lib/rspec-api/dsl/request/body.rb +63 -67
- data/lib/rspec-api/dsl/request/headers.rb +26 -10
- data/lib/rspec-api/dsl/request/request.rb +14 -0
- data/lib/rspec-api/dsl/request/response.rb +86 -0
- data/lib/rspec-api/dsl/request/status.rb +8 -3
- data/lib/rspec-api/dsl/resource.rb +28 -17
- data/lib/rspec-api/dsl/route.rb +30 -14
- data/lib/rspec-api/version.rb +1 -1
- metadata +19 -14
- data/lib/rspec-api/matchers.rb +0 -10
- data/lib/rspec-api/matchers/attributes.rb +0 -33
- data/lib/rspec-api/matchers/body.rb +0 -13
- data/lib/rspec-api/matchers/content_type.rb +0 -13
- data/lib/rspec-api/matchers/fields.rb +0 -32
- data/lib/rspec-api/matchers/filter.rb +0 -28
- data/lib/rspec-api/matchers/fixtures.rb +0 -16
- data/lib/rspec-api/matchers/jsonp.rb +0 -17
- data/lib/rspec-api/matchers/page.rb +0 -19
- data/lib/rspec-api/matchers/sort.rb +0 -25
- data/lib/rspec-api/matchers/status.rb +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f66889873ee973973a2f419e950bd9f405abe7e2
|
4
|
+
data.tar.gz: bbd9a7c7f60d30eaad0e46a28de310e538933bc3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
+
[](https://travis-ci.org/rspec-api/rspec-api)
|
7
11
|
[](https://codeclimate.com/github/rspec-api/rspec-api)
|
8
12
|
[](https://coveralls.io/r/rspec-api/rspec-api)
|
9
13
|
[](https://gemnasium.com/rspec-api/rspec-api)
|
10
14
|
|
11
|
-
|
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`
|
data/lib/rspec-api/dsl.rb
CHANGED
@@ -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
|
-
|
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 '
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
29
|
+
def should_be_valid_json(type)
|
30
|
+
expect(response).to be_valid_json_if response_is_successful?, type
|
31
|
+
end
|
35
32
|
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
66
|
+
def should_have_attributes(attributes)
|
67
|
+
expect(response).to have_attributes_if response_is_successful?, attributes
|
68
|
+
end
|
65
69
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
75
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
22
|
+
private
|
14
23
|
|
15
|
-
|
16
|
-
|
17
|
-
|
24
|
+
def should_include_content_type(type)
|
25
|
+
expect(response).to include_content_type_if response_has_body?, type
|
26
|
+
end
|
18
27
|
|
19
|
-
|
20
|
-
|
21
|
-
|
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,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
|
-
|
9
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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[:
|
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[:
|
60
|
+
(rspec_api[:filters] ||= []) << options.merge(name: filter_parameter)
|
42
61
|
end
|
43
62
|
|
44
63
|
def accepts_callback(callback_parameter)
|
45
|
-
rspec_api[:
|
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
|
data/lib/rspec-api/dsl/route.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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(
|
62
|
-
|
63
|
-
|
64
|
-
|
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[
|
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[
|
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
|
-
(@
|
109
|
+
(@url_params ||= {})[key] = value
|
94
110
|
else
|
95
111
|
body[key] = body[key].call if body[key].is_a?(Proc)
|
96
112
|
end
|
data/lib/rspec-api/version.rb
CHANGED
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.
|
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-
|
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.
|
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
|
data/lib/rspec-api/matchers.rb
DELETED
@@ -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
|