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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 79c751c1af951762a1fe11184842ac7959b4e43a
4
- data.tar.gz: c7b91641ccb763315b14cc0e2eadf2e4651a5fe8
3
+ metadata.gz: 480e18da2657efd7c3645a753ce1cc105c8bd4d4
4
+ data.tar.gz: fb4600f19714aa5eb6f0a49b4e45bcc05be3d1ca
5
5
  SHA512:
6
- metadata.gz: ce34b69d0059e0a5af071de68fe64340066b726c984c71f86213edb1ca6995d7250a97b28b3c081572325fd91af6e1dbb4418db310362bb87b7dbcb05a19d731
7
- data.tar.gz: 23abd33b373b5bbd6676a5a9ae10d6a1ae7c97b228ec182c250c81cae0c14d8bc8f173e4909c663883536fb4809baf257c9cb1661b6d8f0dbc971d7f141c9012
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(404).to match_status(:not_found)
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
- [![Build Status](https://travis-ci.org/rspec-api/rspec-api-matchers.png)](https://travis-ci.org/rspec-api/rspec-api-matchers)
10
+ [![Build Status](https://travis-ci.org/rspec-api/rspec-api-matchers.png?branch=master)](https://travis-ci.org/rspec-api/rspec-api-matchers)
11
11
  [![Code Climate](https://codeclimate.com/github/rspec-api/rspec-api-matchers.png)](https://codeclimate.com/github/rspec-api/rspec-api-matchers)
12
12
  [![Coverage Status](https://coveralls.io/repos/rspec-api/rspec-api-matchers/badge.png)](https://coveralls.io/r/rspec-api/rspec-api-matchers)
13
13
  [![Dependency Status](https://gemnasium.com/rspec-api/rspec-api-matchers.png)](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(http_status_code).to match_status(http_status)
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
@@ -1,3 +1,28 @@
1
- require 'rspec-api/matchers/version'
2
- require 'rspec-api/matchers/matchers'
3
- require 'rspec-api/matchers/match_status'
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 MatchStatus
6
- def initialize(expected_status)
7
- @expected_status = expected_status
5
+ class Status
6
+ def initialize(status)
7
+ @expected_status = status
8
8
  end
9
9
 
10
- def matches?(actual_code)
11
- @actual_code = actual_code
12
- actual_code == expected_code
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 #{@actual_code} to #{description}"
17
+ "expected HTTP status code #{expected_code}, got #{@status}"
18
18
  end
19
19
 
20
20
  def failure_message_for_should_not
21
- "expected #{@actual_code} not to #{description}"
21
+ "expected HTTP status code not to be #{expected_code}, but it was"
22
22
  end
23
23
 
24
24
  def description
25
- "be #{@expected_status}"
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}"
@@ -1,5 +1,5 @@
1
1
  module RSpecApi
2
2
  module Matchers
3
- VERSION = '0.0.1'
3
+ VERSION = '0.1.0'
4
4
  end
5
5
  end
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.1
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-10-29 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
@@ -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/matchers/match_status.rb
91
- - lib/rspec-api/matchers/matchers.rb
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