rspec-api-matchers 0.0.1 → 0.1.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 +10 -3
- data/lib/rspec-api/dsl/be_a_jsonp.rb +24 -0
- data/lib/rspec-api/dsl/be_filtered.rb +24 -0
- data/lib/rspec-api/dsl/be_sorted.rb +24 -0
- data/lib/rspec-api/dsl/be_valid_json.rb +29 -0
- data/lib/rspec-api/dsl/have_attributes.rb +37 -0
- data/lib/rspec-api/dsl/have_prev_page_link.rb +20 -0
- data/lib/rspec-api/dsl/have_status.rb +20 -0
- data/lib/rspec-api/dsl/include_content_type.rb +23 -0
- data/lib/rspec-api/dsl/run_if.rb +24 -0
- data/lib/rspec-api/matchers.rb +28 -3
- data/lib/rspec-api/matchers/attributes.rb +171 -0
- data/lib/rspec-api/matchers/content_type.rb +30 -0
- data/lib/rspec-api/matchers/filter.rb +54 -0
- data/lib/rspec-api/matchers/json.rb +46 -0
- data/lib/rspec-api/matchers/jsonp.rb +27 -0
- data/lib/rspec-api/matchers/prev_page_link.rb +27 -0
- data/lib/rspec-api/matchers/run_if.rb +39 -0
- data/lib/rspec-api/matchers/sort.rb +65 -0
- data/lib/rspec-api/matchers/{match_status.rb → status.rb} +11 -11
- data/lib/rspec-api/matchers/version.rb +1 -1
- metadata +34 -4
- data/lib/rspec-api/matchers/matchers.rb +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 480e18da2657efd7c3645a753ce1cc105c8bd4d4
|
4
|
+
data.tar.gz: fb4600f19714aa5eb6f0a49b4e45bcc05be3d1ca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dfc14d736db8c6d4f9c059d32ab0012c579c585a2b903ae2cf8920211be1ed95f5b42c07e4d56a710c8357df97d50fb7539ee8ea97d05bc030f2c8846adca79b
|
7
|
+
data.tar.gz: 12d82522d32fb0154b6e5e7c20464a2c01e47b4adb863a2475db3aa0ca8a584db430afb1ba84556240b8ab76398bf2b1a911f88d9b2196578f5a42137e7850a7
|
data/README.md
CHANGED
@@ -3,11 +3,11 @@ RSpec API Matchers
|
|
3
3
|
|
4
4
|
RSpecApi::Matchers lets you express outcomes on the response of web APIs.
|
5
5
|
|
6
|
-
expect(
|
6
|
+
expect(response).to have_status(:not_found)
|
7
7
|
|
8
8
|
More documentation and examples about RSpec Api are available at [http://rspec-api.github.io](http://rspec-api.github.io)
|
9
9
|
|
10
|
-
[](https://travis-ci.org/rspec-api/rspec-api-matchers)
|
10
|
+
[](https://travis-ci.org/rspec-api/rspec-api-matchers)
|
11
11
|
[](https://codeclimate.com/github/rspec-api/rspec-api-matchers)
|
12
12
|
[](https://coveralls.io/r/rspec-api/rspec-api-matchers)
|
13
13
|
[](https://gemnasium.com/rspec-api/rspec-api-matchers)
|
@@ -21,7 +21,14 @@ Or install yourself by running `gem install rspec-api-matchers`.
|
|
21
21
|
Available matchers
|
22
22
|
------------------
|
23
23
|
|
24
|
-
expect(
|
24
|
+
expect(response).to have_status(:ok)
|
25
|
+
expect(response).to include_content_type(:json)
|
26
|
+
expect(response).to have_prev_page_link
|
27
|
+
expect(response).to be_valid_json(Array)
|
28
|
+
expect(response).to be_a_jsonp('alert')
|
29
|
+
expect(response).to be_sorted(by: :id, verse: :desc)
|
30
|
+
expect(response).to be_filtered(10, by: :id)
|
31
|
+
expect(response).to have_attributes(id: {value: 1.2}, url: {type: {string: :url}})
|
25
32
|
|
26
33
|
How to contribute
|
27
34
|
=================
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rspec-api/matchers/jsonp'
|
2
|
+
|
3
|
+
module RSpecApi
|
4
|
+
module Matchers
|
5
|
+
# Convert RSpecAPI::Matchers classes into RSpec-compatible matchers, e.g.:
|
6
|
+
# makes RSpecApi::Matchers::Status available as expect(...).to match_status
|
7
|
+
module DSL
|
8
|
+
# Passes if response body is in JSONP format
|
9
|
+
#
|
10
|
+
# @note
|
11
|
+
#
|
12
|
+
# A JSONP should actually return application/javascript...
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
#
|
16
|
+
# # Passes if the body is a JSON wrapped in a callback
|
17
|
+
# body = 'alert([{"website":"http://www.example.com","flag":null}])'
|
18
|
+
# expect(OpenStruct.new body: body).to be_a_jsonp(:alert)
|
19
|
+
def be_a_jsonp(callback = nil)
|
20
|
+
RSpecApi::Matchers::Jsonp.new(callback)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rspec-api/matchers/filter'
|
2
|
+
|
3
|
+
module RSpecApi
|
4
|
+
module Matchers
|
5
|
+
# Convert RSpecAPI::Matchers classes into RSpec-compatible matchers, e.g.:
|
6
|
+
# makes RSpecApi::Matchers::Status available as expect(...).to match_status
|
7
|
+
module DSL
|
8
|
+
# Passes if response body is filtered by +options[:by]+ comparing with +options[:comparing_with]+
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
#
|
12
|
+
# # Passes if the body only contains ID = 1
|
13
|
+
# body = '[{"id": 1}, {"id": 1}, {"id": 1}]'
|
14
|
+
# expect(OpenStruct.new body: body).to be_filtered(1, by: :id)
|
15
|
+
#
|
16
|
+
# # Passes if the body only contains ID < 10
|
17
|
+
# body = '[{"id": 1}, {"id": 6}, {"id": 3}]'
|
18
|
+
# expect(OpenStruct.new body: body).to be_filtered(10, by: :id, comparing_with: :<)
|
19
|
+
def be_filtered(value = nil, options = {})
|
20
|
+
RSpecApi::Matchers::Filter.new value, options
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rspec-api/matchers/sort'
|
2
|
+
|
3
|
+
module RSpecApi
|
4
|
+
module Matchers
|
5
|
+
# Convert RSpecAPI::Matchers classes into RSpec-compatible matchers, e.g.:
|
6
|
+
# makes RSpecApi::Matchers::Status available as expect(...).to match_status
|
7
|
+
module DSL
|
8
|
+
# Passes if response body is sorted by +options[:by]+ with +options[:verse]+
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
#
|
12
|
+
# # Passes if the body is sorted by ascending IDs
|
13
|
+
# body = '[{"id": 1}, {"id": 2}, {"id": 3}]'
|
14
|
+
# expect(OpenStruct.new body: body).to be_sorted(by: :id)
|
15
|
+
#
|
16
|
+
# # Passes if the body is sorted by descending timestamps
|
17
|
+
# body = '[{"t": "2013-10-29T18:09:43Z"}, {"t": "2009-01-12T18:09:43Z"}]'
|
18
|
+
# expect(OpenStruct.new body: body).to be_sorted(by: :t, verse: :desc)
|
19
|
+
def be_sorted(options = {})
|
20
|
+
RSpecApi::Matchers::Sort.new options
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rspec-api/matchers/json'
|
2
|
+
|
3
|
+
module RSpecApi
|
4
|
+
module Matchers
|
5
|
+
# Convert RSpecAPI::Matchers classes into RSpec-compatible matchers, e.g.:
|
6
|
+
# makes RSpecApi::Matchers::Status available as expect(...).to match_status
|
7
|
+
module DSL
|
8
|
+
# Passes if response body is JSON. Optionally check if Array or Hash.
|
9
|
+
# Skips if if response body is JSON. Optionally check if Array or Hash.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
#
|
13
|
+
# # Passes if the body is valid JSON
|
14
|
+
# body = '{"id": 1}'
|
15
|
+
# expect(OpenStruct.new body: body).to be_valid_json
|
16
|
+
#
|
17
|
+
# # Passes if the body is a valid JSON-marshalled Hash
|
18
|
+
# body = '{"id": 1}'
|
19
|
+
# expect(OpenStruct.new body: body).to be_valid_json(Hash)
|
20
|
+
#
|
21
|
+
# # Passes if the body is a valid JSON-marshalled Array
|
22
|
+
# body = '[{"id": 1}, {"id": 2}]'
|
23
|
+
# expect(OpenStruct.new body: body).to be_valid_json(Array)
|
24
|
+
def be_valid_json(type = nil)
|
25
|
+
RSpecApi::Matchers::Json.new(type)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rspec-api/matchers/attributes'
|
2
|
+
|
3
|
+
module RSpecApi
|
4
|
+
module Matchers
|
5
|
+
# Convert RSpecAPI::Matchers classes into RSpec-compatible matchers, e.g.:
|
6
|
+
# makes RSpecApi::Matchers::Status available as expect(...).to match_status
|
7
|
+
module DSL
|
8
|
+
# Passes if response body is an object or array of objects with +key+.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
#
|
12
|
+
# # Passes if the body is an object with key :id
|
13
|
+
# body = '{"id": 1, "url": "http://www.example.com"}'
|
14
|
+
# expect(OpenStruct.new body: body).to have_attributes(:id)
|
15
|
+
#
|
16
|
+
# # Passes if the body is an object with key :id and value 1
|
17
|
+
# body = '{"id": 1, "url": "http://www.example.com"}'
|
18
|
+
# expect(OpenStruct.new body: body).to have_attributes(id: {value: 1})
|
19
|
+
#
|
20
|
+
# # Passes if the body is an array of objects, all with key :id
|
21
|
+
# body = '{"id": 1, "name": ""}, {"id": 2}]'
|
22
|
+
# expect(OpenStruct.new body: body).to have_attributes(:id)
|
23
|
+
#
|
24
|
+
# # Passes if the body is an array of object with key :id and odd values
|
25
|
+
# body = '{"id": 1, "name": ""}, {"id": 3}], {"id": 5}]'
|
26
|
+
# expect(OpenStruct.new body: body).to have_attributes(id: {value: -> v {v.odd?}})
|
27
|
+
|
28
|
+
# TODO: add &block options
|
29
|
+
def have_attributes(attributes = {})
|
30
|
+
RSpecApi::Matchers::Attributes.new attributes
|
31
|
+
end
|
32
|
+
# alias have_attribute have_attributes
|
33
|
+
# alias have_keys have_attributes
|
34
|
+
# alias have_key have_attributes
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rspec-api/matchers/prev_page_link'
|
2
|
+
|
3
|
+
module RSpecApi
|
4
|
+
module Matchers
|
5
|
+
# Convert RSpecAPI::Matchers classes into RSpec-compatible matchers, e.g.:
|
6
|
+
# makes RSpecApi::Matchers::Status available as expect(...).to match_status
|
7
|
+
module DSL
|
8
|
+
# Passes if response includes the pagination Link rel=prev in the headers
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
#
|
12
|
+
# # Passes if the headers specify the pagination link for Prev
|
13
|
+
# headers = {'Link' => '<https://example.com/1>; rel="prev"'}
|
14
|
+
# expect(OpenStruct.new headers: headers).to have_prev_page_link
|
15
|
+
def have_prev_page_link
|
16
|
+
RSpecApi::Matchers::PrevPageLink.new
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rspec-api/matchers/status'
|
2
|
+
|
3
|
+
module RSpecApi
|
4
|
+
module Matchers
|
5
|
+
module DSL
|
6
|
+
# Passes if response has the given HTTP status code.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
#
|
10
|
+
# # Passes if the status is 200 OK (passed as a symbol)
|
11
|
+
# expect(OpenStruct.new status: 200).to match_status :ok
|
12
|
+
#
|
13
|
+
# # Passes if the status is 200 OK (passed as a number)
|
14
|
+
# expect(OpenStruct.new status: 200).to match_status 200
|
15
|
+
def have_status(status)
|
16
|
+
RSpecApi::Matchers::Status.new status
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rspec-api/matchers/content_type'
|
2
|
+
|
3
|
+
module RSpecApi
|
4
|
+
module Matchers
|
5
|
+
# Convert RSpecAPI::Matchers classes into RSpec-compatible matchers, e.g.:
|
6
|
+
# makes RSpecApi::Matchers::Status available as expect(...).to match_status
|
7
|
+
module DSL
|
8
|
+
# Passes if response includes the given Content-Type JSON in the headers
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
#
|
12
|
+
# # Passes if the headers specify that Content-Type is JSON
|
13
|
+
# headers ={'Content-Type' => 'application/json; charset=utf-8'}
|
14
|
+
# expect(OpenStruct.new headers: headers).to include_content_type(:json)
|
15
|
+
def include_content_type(type = nil)
|
16
|
+
content_type = case type
|
17
|
+
when :json then 'application/json; charset=utf-8'
|
18
|
+
end
|
19
|
+
RSpecApi::Matchers::ContentType.new(content_type)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rspec-api/matchers/run_if'
|
2
|
+
|
3
|
+
module RSpecApi
|
4
|
+
module Matchers
|
5
|
+
module DSL
|
6
|
+
# Creates a +method_name+_if method for each DSL method
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
#
|
10
|
+
# def have_prev_page_link_if(condition, *args)
|
11
|
+
# RSpecApi::Matchers::RunIf.new condition, have_prev_page_link(*args)
|
12
|
+
# end
|
13
|
+
RSpecApi::Matchers::DSL.instance_methods.each do |method|
|
14
|
+
define_method("#{method}_if") do |condition, *args|
|
15
|
+
run_if condition, send(method, *args)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def run_if(run, matcher)
|
20
|
+
RSpecApi::Matchers::RunIf.new run, matcher
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/rspec-api/matchers.rb
CHANGED
@@ -1,3 +1,28 @@
|
|
1
|
-
require 'rspec-api/
|
2
|
-
require 'rspec-api/
|
3
|
-
require 'rspec-api/
|
1
|
+
require 'rspec-api/dsl/have_status'
|
2
|
+
require 'rspec-api/dsl/include_content_type'
|
3
|
+
require 'rspec-api/dsl/have_prev_page_link'
|
4
|
+
require 'rspec-api/dsl/be_a_jsonp'
|
5
|
+
require 'rspec-api/dsl/be_sorted'
|
6
|
+
require 'rspec-api/dsl/be_valid_json'
|
7
|
+
require 'rspec-api/dsl/be_filtered'
|
8
|
+
require 'rspec-api/dsl/have_attributes'
|
9
|
+
require 'rspec-api/dsl/run_if' # should be the last, for metaprogramming
|
10
|
+
|
11
|
+
require 'rspec/matchers'
|
12
|
+
|
13
|
+
# Convert RSpecAPI::Matchers classes into RSpec-compatible matchers, e.g.:
|
14
|
+
# makes RSpecApi::Matchers::Status available as expect(...).to match_status
|
15
|
+
|
16
|
+
module RSpecApi
|
17
|
+
module Matchers
|
18
|
+
module DSL
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module RSpec
|
24
|
+
module Matchers
|
25
|
+
# Make RSpecApi::Matchers::DSL methods available inside RSpec
|
26
|
+
include ::RSpecApi::Matchers::DSL
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'active_support/core_ext/array/wrap'
|
3
|
+
require 'active_support/core_ext/hash/keys'
|
4
|
+
require 'active_support/core_ext/array/conversions'
|
5
|
+
|
6
|
+
module RSpecApi
|
7
|
+
module Matchers
|
8
|
+
class Attributes
|
9
|
+
def initialize(attributes = {})
|
10
|
+
@attributes = as_hash(attributes)
|
11
|
+
end
|
12
|
+
|
13
|
+
def matches?(response)
|
14
|
+
@body = response.body
|
15
|
+
json = extract_symbolized_json_from @body
|
16
|
+
# NOTE: Might add this... but might be too strict. For instance, if I
|
17
|
+
# ask for ?page=2, I might get an empty array. Maybe in that case I
|
18
|
+
# should not even check for attributes? Maybe it can be another best
|
19
|
+
# practice: adding query params will not affect the attributes (so I
|
20
|
+
# can just check them when there are no query params)... of course
|
21
|
+
# unless the query params is ?fields=id,name
|
22
|
+
# if json.is_a?(Array) and json.empty?
|
23
|
+
# raise RSpec::Core::Pending::PendingDeclaredInExample.new "You are testing if an array is sorted, but the array is empty. Try with more fixtures"
|
24
|
+
# end
|
25
|
+
has_attributes? json, @attributes
|
26
|
+
rescue JSON::ParserError, JSON::GeneratorError
|
27
|
+
false
|
28
|
+
end
|
29
|
+
alias == matches?
|
30
|
+
|
31
|
+
def failure_message_for_should
|
32
|
+
"expected body to #{description}, but got #{@body}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def failure_message_for_should_not
|
36
|
+
"expected body not to #{description}, but it did"
|
37
|
+
end
|
38
|
+
|
39
|
+
def description
|
40
|
+
desc = @attributes.map do |name, options|
|
41
|
+
text = "#{name}"
|
42
|
+
if options.key?(:value)
|
43
|
+
text << "="
|
44
|
+
text << case options[:value]
|
45
|
+
when NilClass then 'nil'
|
46
|
+
when Proc then '[Proc value]'
|
47
|
+
else "#{options[:value]}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
if options.key?(:type)
|
51
|
+
expected_types = Array.wrap(options.fetch :type, :any)
|
52
|
+
types = expected_types.map do |type|
|
53
|
+
case type
|
54
|
+
when Hash
|
55
|
+
type.map do |k, format|
|
56
|
+
case format
|
57
|
+
when Hash, Array
|
58
|
+
"#{k} with attributes"
|
59
|
+
else # Symbol
|
60
|
+
"#{format} #{k}"
|
61
|
+
end
|
62
|
+
end.to_sentence(two_words_connector: ' or ', last_word_connector: ', or ')
|
63
|
+
else "#{type}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
text << " (#{types.to_sentence(two_words_connector: ' or ', last_word_connector: ', or ')})" if types.any?
|
67
|
+
end
|
68
|
+
text
|
69
|
+
end.to_sentence
|
70
|
+
['have attributes', desc].join(' ').strip
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def has_attributes?(item_or_items, attributes)
|
76
|
+
attributes.deep_symbolize_keys!
|
77
|
+
items = Array.wrap item_or_items
|
78
|
+
attributes.all? do |name, options|
|
79
|
+
has_attribute? items, name, options
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def has_attribute?(items, name, options)
|
84
|
+
if items.all?{|item| has_key? item, name}
|
85
|
+
values = items.map{|item| item[name]}
|
86
|
+
expected_types = Array.wrap(options.fetch :type, :any)
|
87
|
+
expected_value = options.fetch :value, :any
|
88
|
+
values.all? do |value|
|
89
|
+
matches_value?(value, expected_value) && matches_types?(value, expected_types)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def has_key?(item, name)
|
95
|
+
item.key? name
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
def matches_value?(value, expected_value)
|
100
|
+
case expected_value
|
101
|
+
when :any then true
|
102
|
+
when Proc then expected_value.call value
|
103
|
+
else value == expected_value
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def matches_types?(value, expected_types)
|
108
|
+
expected_types.any? do |type|
|
109
|
+
matches_type_and_format? value, type
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def matches_type_and_format?(value, type)
|
114
|
+
type = Hash[type, :any] unless type.is_a?(Hash)
|
115
|
+
type.any? do |type, format|
|
116
|
+
matches_type?(value, type) && matches_format?(value, format)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def matches_type?(value, type)
|
121
|
+
type_to_classes(type).any?{|klass| value.is_a? klass}
|
122
|
+
end
|
123
|
+
|
124
|
+
def matches_format?(value, format)
|
125
|
+
case format
|
126
|
+
when :url then value =~ URI::regexp
|
127
|
+
when :integer then value.is_a? Integer
|
128
|
+
when :timestamp then DateTime.iso8601 value rescue false
|
129
|
+
when :email then value =~ %r{(?<name>.+?)@(?<host>.+?)\.(?<domain>.+?)}
|
130
|
+
when Hash, Array then value.any? ?has_attributes?(value, as_hash(format)) : true
|
131
|
+
when String then matches_format?(value, format.to_sym)
|
132
|
+
when :any then true
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def type_to_classes(type)
|
137
|
+
Array.wrap case type
|
138
|
+
when :string then String
|
139
|
+
when :array then Array
|
140
|
+
when :object then Hash
|
141
|
+
when :null then NilClass
|
142
|
+
when :boolean then [TrueClass, FalseClass]
|
143
|
+
when :number then Numeric
|
144
|
+
when String then type_to_classes(type.to_sym)
|
145
|
+
when :any then Object
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# These go elsewhere
|
150
|
+
|
151
|
+
def as_hash(anything)
|
152
|
+
if anything.is_a? Hash
|
153
|
+
anything
|
154
|
+
elsif anything.nil?
|
155
|
+
{}
|
156
|
+
else
|
157
|
+
Hash[*Array.wrap(anything).map{|x| x.is_a?(Hash) ? [x.keys.first, x.values.first] : [x, {}]}.flatten]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def extract_symbolized_json_from(something)
|
162
|
+
JSON without_callbacks(something), symbolize_names: true
|
163
|
+
end
|
164
|
+
|
165
|
+
def without_callbacks(something)
|
166
|
+
callback_pattern = %r[^.+?\((.*?)\)$]
|
167
|
+
something =~ callback_pattern ? something.match(callback_pattern)[1] : something
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module RSpecApi
|
2
|
+
module Matchers
|
3
|
+
class ContentType
|
4
|
+
def initialize(content_type)
|
5
|
+
@content_type = content_type
|
6
|
+
end
|
7
|
+
|
8
|
+
def matches?(response)
|
9
|
+
@headers = response.headers
|
10
|
+
@headers['Content-Type'] == @content_type if @headers.key? 'Content-Type'
|
11
|
+
end
|
12
|
+
|
13
|
+
def failure_message_for_should
|
14
|
+
"expected headers to #{description}, but got #{@headers}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def failure_message_for_should_not
|
18
|
+
"expected headers not to #{description}, but got #{@headers}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def description
|
22
|
+
%Q{include 'Content-Type': '#{@content_type}'}
|
23
|
+
end
|
24
|
+
|
25
|
+
def description_for_run_if
|
26
|
+
%Q{include any specific 'Content-Type'}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module RSpecApi
|
4
|
+
module Matchers
|
5
|
+
class Filter
|
6
|
+
def initialize(value, options = {})
|
7
|
+
@value = value
|
8
|
+
@field = options[:by]
|
9
|
+
@comparing_with = options.fetch :comparing_with, Proc.new{|x,y| x == y}
|
10
|
+
end
|
11
|
+
|
12
|
+
def matches?(response)
|
13
|
+
@desc = " by #{@field}#{@comparing_with}#{@value}"
|
14
|
+
@body = response.body
|
15
|
+
array = extract_json_from @body
|
16
|
+
array.all? do |item| # TODO: Don't always use string
|
17
|
+
@comparing_with.call @value, item[@field.to_s].to_s
|
18
|
+
end
|
19
|
+
end
|
20
|
+
alias == matches?
|
21
|
+
|
22
|
+
def failure_message_for_should
|
23
|
+
"expected body to #{description}, but got #{@body}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def failure_message_for_should_not
|
27
|
+
"expected body not to #{description}, but it was"
|
28
|
+
end
|
29
|
+
|
30
|
+
def description
|
31
|
+
%Q(be filtered#{@desc})
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# These go elsewhere
|
37
|
+
|
38
|
+
def extract_json_from(something)
|
39
|
+
array = JSON without_callbacks(something)
|
40
|
+
if array.is_a?(Array) and array.empty?
|
41
|
+
raise RSpec::Core::Pending::PendingDeclaredInExample.new "You are testing if an array is sorted, but the array is empty. Try with more fixtures"
|
42
|
+
end
|
43
|
+
array
|
44
|
+
rescue JSON::ParserError, JSON::GeneratorError
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def without_callbacks(something)
|
49
|
+
callback_pattern = %r[^.+?\((.*?)\)$]
|
50
|
+
something =~ callback_pattern ? something.match(callback_pattern)[1] : something
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module RSpecApi
|
4
|
+
module Matchers
|
5
|
+
class Json
|
6
|
+
def initialize(type)
|
7
|
+
@type = type
|
8
|
+
@desc = " #{@type}" if @type
|
9
|
+
end
|
10
|
+
|
11
|
+
def matches?(response)
|
12
|
+
@body = response.body
|
13
|
+
json = parse_json_of @body
|
14
|
+
@type ? json.is_a?(@type) : true
|
15
|
+
end
|
16
|
+
alias == matches?
|
17
|
+
|
18
|
+
def failure_message_for_should
|
19
|
+
"expected body to #{description}, but got #{@body}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def failure_message_for_should_not
|
23
|
+
"expected body not to #{description}, but it was"
|
24
|
+
end
|
25
|
+
|
26
|
+
def description
|
27
|
+
%Q(be a valid JSON#{@desc})
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# These go elsewhere
|
33
|
+
|
34
|
+
def parse_json_of(something)
|
35
|
+
JSON without_callbacks(something)
|
36
|
+
rescue JSON::ParserError, JSON::GeneratorError
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def without_callbacks(something)
|
41
|
+
callback_pattern = %r[^.+?\((.*?)\)$]
|
42
|
+
something =~ callback_pattern ? something.match(callback_pattern)[1] : something
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module RSpecApi
|
2
|
+
module Matchers
|
3
|
+
class Jsonp
|
4
|
+
def initialize(callback)
|
5
|
+
@callback = callback
|
6
|
+
end
|
7
|
+
|
8
|
+
def matches?(response)
|
9
|
+
@body = response.body
|
10
|
+
@body =~ %r{^#{@callback || '.+?'}\((.*?)\)$}
|
11
|
+
end
|
12
|
+
alias == matches?
|
13
|
+
|
14
|
+
def failure_message_for_should
|
15
|
+
"expected body to #{description}, but got #{@body}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def failure_message_for_should_not
|
19
|
+
"expected body not to #{description}, but it was"
|
20
|
+
end
|
21
|
+
|
22
|
+
def description
|
23
|
+
%Q(be wrapped in a JSONP callback #{@callback}).rstrip
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module RSpecApi
|
2
|
+
module Matchers
|
3
|
+
class PrevPageLink
|
4
|
+
def matches?(response)
|
5
|
+
@headers = response.headers
|
6
|
+
links = @headers['Link'] || '' # not fetch, see http://git.io/CUz3-Q
|
7
|
+
links =~ %r{<.+?>. rel\="prev"}
|
8
|
+
end
|
9
|
+
|
10
|
+
def failure_message_for_should
|
11
|
+
"expected headers to #{description}, but got #{@headers}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def failure_message_for_should_not
|
15
|
+
"expected headers not to #{description}, but got #{@headers}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def description
|
19
|
+
%Q{include a 'Link' to the previous page}
|
20
|
+
end
|
21
|
+
|
22
|
+
def description_for_run_if
|
23
|
+
%Q{include any specific pagination 'Link'}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module RSpecApi
|
2
|
+
module Matchers
|
3
|
+
class RunIf
|
4
|
+
def initialize(run, matcher)
|
5
|
+
@run = run
|
6
|
+
@matcher = matcher
|
7
|
+
end
|
8
|
+
|
9
|
+
def matches?(response)
|
10
|
+
!@run || @matcher.matches?(response)
|
11
|
+
end
|
12
|
+
alias == matches?
|
13
|
+
|
14
|
+
def does_not_match?(response)
|
15
|
+
!@run || !matches?(response)
|
16
|
+
end
|
17
|
+
|
18
|
+
def failure_message_for_should
|
19
|
+
@matcher.failure_message_for_should
|
20
|
+
end
|
21
|
+
|
22
|
+
def failure_message_for_should_not
|
23
|
+
@matcher.failure_message_for_should_not
|
24
|
+
end
|
25
|
+
|
26
|
+
def description
|
27
|
+
if @run
|
28
|
+
@matcher.description
|
29
|
+
else
|
30
|
+
if @matcher.respond_to? :description_for_run_if
|
31
|
+
%Q{not be expected to #{@matcher.description_for_run_if}}
|
32
|
+
else
|
33
|
+
%Q{not be expected to #{@matcher.description}}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module RSpecApi
|
4
|
+
module Matchers
|
5
|
+
class Sort
|
6
|
+
def initialize(options = {})
|
7
|
+
@field = options[:by]
|
8
|
+
@reverse = options[:verse].to_s == 'desc' || options[:verse].to_s == 'descending' || options[:reverse] == true || options[:ascending] == true || options[:asc] == true || options[:descending] == false || options[:desc] == false
|
9
|
+
end
|
10
|
+
|
11
|
+
def matches?(response)
|
12
|
+
# If we don't get which field the body should be sorted by, then we
|
13
|
+
# say that it's sorted. For instance sort by random... no expectations
|
14
|
+
# We might still want to do some check about being a JSON array, though
|
15
|
+
if @field.nil?
|
16
|
+
true
|
17
|
+
else
|
18
|
+
@desc = " by #{'descending ' if @reverse}#{@field}"
|
19
|
+
@body = response.body
|
20
|
+
array = extract_array of: @field, from: @body # what if this fails?
|
21
|
+
is_sorted? array, @reverse
|
22
|
+
end
|
23
|
+
end
|
24
|
+
alias == matches?
|
25
|
+
|
26
|
+
def failure_message_for_should
|
27
|
+
# NOTE: might just print the (unsorted) fields, not the whole @body
|
28
|
+
"expected body to #{description}, but got #{@body}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def failure_message_for_should_not
|
32
|
+
"expected body not to #{description}, but it was"
|
33
|
+
end
|
34
|
+
|
35
|
+
def description
|
36
|
+
%Q(be sorted#{@desc})
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def is_sorted?(array, reverse)
|
42
|
+
return false unless array.is_a?(Array)
|
43
|
+
array.reverse! if reverse
|
44
|
+
array == array.sort
|
45
|
+
end
|
46
|
+
|
47
|
+
# These go elsewhere
|
48
|
+
|
49
|
+
def extract_array(options = {})
|
50
|
+
array = JSON without_callbacks(options[:from])
|
51
|
+
if array.is_a?(Array) and array.empty?
|
52
|
+
raise RSpec::Core::Pending::PendingDeclaredInExample.new "You are testing if an array is sorted, but the array is empty. Try with more fixtures"
|
53
|
+
end
|
54
|
+
array.map{|item| item[options[:of].to_s]} # what if it's not an array of hashes?
|
55
|
+
rescue JSON::ParserError, JSON::GeneratorError
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def without_callbacks(something)
|
60
|
+
callback_pattern = %r[^.+?\((.*?)\)$]
|
61
|
+
something =~ callback_pattern ? something.match(callback_pattern)[1] : something
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -2,27 +2,27 @@ require 'rack/utils'
|
|
2
2
|
|
3
3
|
module RSpecApi
|
4
4
|
module Matchers
|
5
|
-
class
|
6
|
-
def initialize(
|
7
|
-
@expected_status =
|
5
|
+
class Status
|
6
|
+
def initialize(status)
|
7
|
+
@expected_status = status
|
8
8
|
end
|
9
9
|
|
10
|
-
def matches?(
|
11
|
-
@
|
12
|
-
|
10
|
+
def matches?(response)
|
11
|
+
@status = response.status
|
12
|
+
@status == expected_code
|
13
13
|
end
|
14
14
|
alias == matches?
|
15
15
|
|
16
16
|
def failure_message_for_should
|
17
|
-
"expected #{
|
17
|
+
"expected HTTP status code #{expected_code}, got #{@status}"
|
18
18
|
end
|
19
19
|
|
20
20
|
def failure_message_for_should_not
|
21
|
-
"expected
|
21
|
+
"expected HTTP status code not to be #{expected_code}, but it was"
|
22
22
|
end
|
23
23
|
|
24
24
|
def description
|
25
|
-
"be #{
|
25
|
+
"be HTTP status code #{expected_code}"
|
26
26
|
end
|
27
27
|
|
28
28
|
private
|
@@ -36,7 +36,7 @@ module RSpecApi
|
|
36
36
|
# @example
|
37
37
|
# status_to_code(:ok) # => 200
|
38
38
|
# status_to_code(200) # => 200
|
39
|
-
# status_to_code(987) # => raise
|
39
|
+
# status_to_code(987) # => raise ArgumentError
|
40
40
|
def status_to_numeric_code(status)
|
41
41
|
code = status.is_a?(Symbol) ? Rack::Utils.status_code(status) : status
|
42
42
|
validate_status_code! code
|
@@ -47,7 +47,7 @@ module RSpecApi
|
|
47
47
|
#
|
48
48
|
# @example
|
49
49
|
# validate_status_code!(200) # => (no error)
|
50
|
-
# validate_status_code!(999) # => raise
|
50
|
+
# validate_status_code!(999) # => raise ArgumentError
|
51
51
|
def validate_status_code!(code)
|
52
52
|
valid_codes = Rack::Utils::SYMBOL_TO_STATUS_CODE.values
|
53
53
|
message = "#{code} must be a valid HTTP status code: #{valid_codes}"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec-api-matchers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.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
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - '>='
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activesupport
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: bundler
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -87,8 +101,24 @@ executables: []
|
|
87
101
|
extensions: []
|
88
102
|
extra_rdoc_files: []
|
89
103
|
files:
|
90
|
-
- lib/rspec-api/
|
91
|
-
- lib/rspec-api/
|
104
|
+
- lib/rspec-api/dsl/be_a_jsonp.rb
|
105
|
+
- lib/rspec-api/dsl/be_filtered.rb
|
106
|
+
- lib/rspec-api/dsl/be_sorted.rb
|
107
|
+
- lib/rspec-api/dsl/be_valid_json.rb
|
108
|
+
- lib/rspec-api/dsl/have_attributes.rb
|
109
|
+
- lib/rspec-api/dsl/have_prev_page_link.rb
|
110
|
+
- lib/rspec-api/dsl/have_status.rb
|
111
|
+
- lib/rspec-api/dsl/include_content_type.rb
|
112
|
+
- lib/rspec-api/dsl/run_if.rb
|
113
|
+
- lib/rspec-api/matchers/attributes.rb
|
114
|
+
- lib/rspec-api/matchers/content_type.rb
|
115
|
+
- lib/rspec-api/matchers/filter.rb
|
116
|
+
- lib/rspec-api/matchers/json.rb
|
117
|
+
- lib/rspec-api/matchers/jsonp.rb
|
118
|
+
- lib/rspec-api/matchers/prev_page_link.rb
|
119
|
+
- lib/rspec-api/matchers/run_if.rb
|
120
|
+
- lib/rspec-api/matchers/sort.rb
|
121
|
+
- lib/rspec-api/matchers/status.rb
|
92
122
|
- lib/rspec-api/matchers/version.rb
|
93
123
|
- lib/rspec-api/matchers.rb
|
94
124
|
- lib/rspec-api-matchers.rb
|
@@ -1,26 +0,0 @@
|
|
1
|
-
require 'rspec/matchers'
|
2
|
-
|
3
|
-
module RSpecApi
|
4
|
-
module Matchers
|
5
|
-
# Passes if receiver is the same HTTP status code as the argument.
|
6
|
-
# The receiver can either be in a symbolic or numeric form.
|
7
|
-
#
|
8
|
-
# @example
|
9
|
-
#
|
10
|
-
# # Passes if 200 corresponds to 200
|
11
|
-
# expect(200).to match_status(200)
|
12
|
-
#
|
13
|
-
# # Passes if :ok corresponds to :ok
|
14
|
-
# expect(:ok).to match_status(200)
|
15
|
-
def match_status(expected_status)
|
16
|
-
RSpecApi::Matchers::MatchStatus.new(expected_status)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
module RSpec
|
22
|
-
module Matchers
|
23
|
-
# Make RSpecApi::Matchers available inside RSpec
|
24
|
-
include ::RSpecApi::Matchers
|
25
|
-
end
|
26
|
-
end
|