rspec-api-matchers 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![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(
|
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
|