rspec-api-matchers 0.5.0 → 0.6.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -19
  3. data/lib/rspec-api/matchers.rb +38 -21
  4. data/lib/rspec-api/matchers/attributes/have_attributes.rb +23 -0
  5. data/lib/rspec-api/matchers/attributes/matcher.rb +101 -0
  6. data/lib/rspec-api/matchers/collection/be_a_collection.rb +20 -0
  7. data/lib/rspec-api/matchers/collection/matcher.rb +19 -0
  8. data/lib/rspec-api/matchers/content_type/have_content_type.rb +24 -0
  9. data/lib/rspec-api/matchers/content_type/matcher.rb +23 -0
  10. data/lib/rspec-api/matchers/filter/be_filtered.rb +20 -0
  11. data/lib/rspec-api/matchers/filter/matcher.rb +57 -0
  12. data/lib/rspec-api/matchers/headers/have_headers.rb +20 -0
  13. data/lib/rspec-api/matchers/headers/matcher.rb +28 -0
  14. data/lib/rspec-api/matchers/json/be_valid_json.rb +20 -0
  15. data/lib/rspec-api/matchers/json/matcher.rb +41 -0
  16. data/lib/rspec-api/matchers/jsonp/be_wrapped_in_callback.rb +24 -0
  17. data/lib/rspec-api/matchers/jsonp/matcher.rb +23 -0
  18. data/lib/rspec-api/matchers/page_links/have_page_links.rb +25 -0
  19. data/lib/rspec-api/matchers/page_links/matcher.rb +18 -0
  20. data/lib/rspec-api/matchers/response/be_a_valid_response.rb +20 -0
  21. data/lib/rspec-api/matchers/response/matcher.rb +52 -0
  22. data/lib/rspec-api/matchers/sort/be_sorted.rb +20 -0
  23. data/lib/rspec-api/matchers/sort/matcher.rb +56 -0
  24. data/lib/rspec-api/matchers/status/have_status.rb +25 -0
  25. data/lib/rspec-api/matchers/status/matcher.rb +46 -0
  26. data/lib/rspec-api/matchers/version.rb +1 -1
  27. metadata +24 -18
  28. data/lib/rspec-api/dsl/be_a_collection.rb +0 -24
  29. data/lib/rspec-api/dsl/be_a_jsonp.rb +0 -24
  30. data/lib/rspec-api/dsl/be_filtered.rb +0 -24
  31. data/lib/rspec-api/dsl/be_sorted.rb +0 -24
  32. data/lib/rspec-api/dsl/have_attributes.rb +0 -37
  33. data/lib/rspec-api/dsl/have_prev_page_link.rb +0 -20
  34. data/lib/rspec-api/dsl/have_status.rb +0 -20
  35. data/lib/rspec-api/dsl/include_content_type.rb +0 -24
  36. data/lib/rspec-api/matchers/attributes.rb +0 -171
  37. data/lib/rspec-api/matchers/collection.rb +0 -42
  38. data/lib/rspec-api/matchers/content_type.rb +0 -26
  39. data/lib/rspec-api/matchers/filter.rb +0 -58
  40. data/lib/rspec-api/matchers/jsonp.rb +0 -27
  41. data/lib/rspec-api/matchers/prev_page_link.rb +0 -23
  42. data/lib/rspec-api/matchers/sort.rb +0 -65
  43. data/lib/rspec-api/matchers/status.rb +0 -60
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 52d99afa65e3dc027e30e06fe06e5abdb6ca7b77
4
- data.tar.gz: 670f2f96336bf3b7ffdd41434f7a756fc358da4c
3
+ metadata.gz: 7a9cb40b04968465419bdc9816a1002fe6a29761
4
+ data.tar.gz: f1ca85d85451586be343f9edf7bb2d6405590578
5
5
  SHA512:
6
- metadata.gz: 76fcda35ccae07f245892ec5773f20dc43c875714cf2bda6f7f44efdcafafc98e493739b2d4e0c7056d91f3aabe1055397d95c3e278ec3bc11983e345fa397db
7
- data.tar.gz: 8e71adfec9954e778684d3ecce2ac4bce83c9319cb7a5f0acd7dfaf9efa6363d8ddc60101d646aa43a30c08ee5b8869cd9d4069ac4ed525e3eab927d68ff01dc
6
+ metadata.gz: a5315d2e00d92d287059754dbef2aa689305f43ed5e66b5a580b332aed9430a1cc4741b9a1f96b768f5c074e45910c1d31a4b9c6ad04301461407d6f1bdfcbbd
7
+ data.tar.gz: 59b6add713ca2f7988ffca56bddbb5fb64efc069066f01e283ac135ed00382fcf3834f573c7dba9d7920c59fad110ce850c903e4ccdc54738eb267ae429dae89
data/README.md CHANGED
@@ -1,42 +1,47 @@
1
- RSpec API Matchers
1
+ RSpecApi::Matchers
2
2
  ==================
3
3
 
4
4
  RSpecApi::Matchers lets you express outcomes on the response of web APIs.
5
5
 
6
6
  expect(response).to have_status(:not_found)
7
7
 
8
- More documentation and examples about RSpec Api are available at [http://rspec-api.github.io](http://rspec-api.github.io)
8
+ More documentation and examples about RSpecApi are available at [http://rspec-api.github.io](http://rspec-api.github.io)
9
9
 
10
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)
14
14
 
15
- Install
16
- -------
17
-
18
- Add `gem 'rspec-api-matchers'` to your `Gemfile` and run `bundle`.
19
- Or install yourself by running `gem install rspec-api-matchers`.
20
-
21
- Version changes respect [Semantic Versioning](http://semver.org).
22
- To ensure `bundle update` won't cause problems in your repository,
23
- always indicate the patch version until version 1.0.0 is released, e.g.:
24
-
25
- gem 'rspec-api-matchers', '~> 0.5.0'
26
-
27
-
28
15
  Available matchers
29
16
  ------------------
30
17
 
31
18
  expect(response).to have_status(:ok)
32
- expect(response).to include_content_type(:json)
33
- expect(response).to have_prev_page_link
19
+ expect(response).to have_content_type(:json)
20
+ expect(response).to have_page_links
34
21
  expect(response).to be_a_collection
35
- expect(response).to be_a_jsonp('alert')
22
+ expect(response).to be_wrapped_in_callback('alert')
36
23
  expect(response).to be_sorted(by: :id, verse: :desc)
37
- expect(response).to be_filtered(10, by: :id)
24
+ expect(response).to be_filtered(by: :id, value: 10)
38
25
  expect(response).to have_attributes(id: {value: 1.2}, url: {type: {string: :url}})
39
26
 
27
+
28
+ How to install
29
+ ==============
30
+
31
+ To install on your system, run `gem install rspec-api-matchers`.
32
+ To use inside a bundled Ruby project, add this line to the Gemfile:
33
+
34
+ gem 'rspec-api-matchers', '~> 0.5.0'
35
+
36
+ The rspec-api-matchers gem follows [Semantic Versioning](http://semver.org).
37
+ Any new release that is fully backward-compatible bumps the *patch* version (0.5.1).
38
+ Any new version that breaks compatibility bumps the *minor* version (0.6.0)
39
+
40
+ Indicating the full version in your Gemfile (*major*.*minor*.*patch*) guarantees
41
+ that your project won’t occur in any error when you `bundle update` and a new
42
+ version of RSpecApi::Matchers is released.
43
+
44
+
40
45
  How to contribute
41
46
  =================
42
47
 
@@ -1,27 +1,44 @@
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_a_collection'
7
- require 'rspec-api/dsl/be_filtered'
8
- require 'rspec-api/dsl/have_attributes'
9
-
10
- require 'rspec/matchers'
11
-
12
- # Convert RSpecAPI::Matchers classes into RSpec-compatible matchers, e.g.:
13
- # makes RSpecApi::Matchers::Status available as expect(...).to match_status
1
+ require 'rspec/core'
2
+ require 'rspec-api/matchers/attributes/have_attributes'
3
+ require 'rspec-api/matchers/collection/be_a_collection'
4
+ require 'rspec-api/matchers/content_type/have_content_type'
5
+ require 'rspec-api/matchers/filter/be_filtered'
6
+ require 'rspec-api/matchers/json/be_valid_json'
7
+ require 'rspec-api/matchers/jsonp/be_wrapped_in_callback'
8
+ require 'rspec-api/matchers/page_links/have_page_links'
9
+ require 'rspec-api/matchers/response/be_a_valid_response'
10
+ require 'rspec-api/matchers/sort/be_sorted'
11
+ require 'rspec-api/matchers/status/have_status'
14
12
 
15
13
  module RSpecApi
16
14
  module Matchers
17
- module DSL
18
- end
15
+ include Attributes
16
+ include Collection
17
+ include ContentType
18
+ include Filter
19
+ include Json
20
+ include Jsonp
21
+ include PageLinks
22
+ include Response
23
+ include Sort
24
+ include Status
19
25
  end
20
26
  end
21
27
 
22
- module RSpec
23
- module Matchers
24
- # Make RSpecApi::Matchers::DSL methods available inside RSpec
25
- include ::RSpecApi::Matchers::DSL
26
- end
27
- end
28
+ # RSpecApi::Matchers adds matchers to test RESTful APIs.
29
+ #
30
+ # To have these matchers available inside of an RSpec `describe` block, tag that
31
+ # block with the `:rspec_api` metadata:
32
+ #
33
+ # describe "Artists", rspec_api: true do
34
+ # ... # here you can write `expect(response).to have_status :ok`, etc.
35
+ # end
36
+ RSpec.configuration.include RSpecApi::Matchers, rspec_api: true
37
+
38
+ # You can also explicitly include the RSpec::Api module inside the example group:
39
+ #
40
+ # describe "Artists" do
41
+ # include RSpecApi::Matchers
42
+ # ... # here you can write `expect(response).to have_status :ok`, etc.
43
+ # end
44
+ #
@@ -0,0 +1,23 @@
1
+ require 'rspec-api/matchers/attributes/matcher'
2
+
3
+ module RSpecApi
4
+ module Matchers
5
+ module Attributes
6
+ # Passes if the response body is either a JSON object or a collection of
7
+ # JSON objects that include all the +attributes+.
8
+ #
9
+ # @example
10
+ #
11
+ # # Passes if the body is an object with a URL-formatted String :site
12
+ # response = OpenStruct.new body: '{"site": "http://www.example.com"}'
13
+ # expect(response).to have_attributes(site: {type: {string: :url}})
14
+ #
15
+ # For more examples check +have_attributes_spec.rb+.
16
+
17
+ # TODO: add &block options
18
+ def have_attributes(*attributes)
19
+ RSpecApi::Matchers::Attributes::Matcher.new *attributes
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,101 @@
1
+ require 'rspec-api/matchers/json/matcher'
2
+ require 'active_support/core_ext/array/wrap'
3
+ require 'active_support/core_ext/hash/keys'
4
+
5
+ module RSpecApi
6
+ module Matchers
7
+ module Attributes
8
+ class Matcher < Json::Matcher
9
+ attr_accessor :attributes
10
+
11
+ def initialize(*attributes, &block)
12
+ @attributes = to_hash attributes
13
+ end
14
+
15
+ def matches?(response)
16
+ super && has_attributes?(json, attributes)
17
+ end
18
+
19
+ def description
20
+ if attributes.any?
21
+ %Q(have attributes #{attributes})
22
+ else
23
+ %Q(have attributes)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def has_attributes?(items, attrs)
30
+ attrs.deep_symbolize_keys!
31
+ attrs.all?{|name, options| has_attribute? items, name, options}
32
+ end
33
+
34
+ def has_attribute?(items, name, options)
35
+ return false unless Array.wrap(items).all?{|item| item.key? name}
36
+ values = Array.wrap(items).map{|item| item[name]}
37
+ attr_types = Array.wrap(options.fetch :type, :any)
38
+ attr_value = options.fetch :value, :any
39
+ values.all? do |v|
40
+ matches_value?(v, attr_value) && matches_types?(v, attr_types)
41
+ end
42
+ end
43
+
44
+ def matches_value?(value, expected_value)
45
+ case expected_value
46
+ when :any then true
47
+ when Proc then expected_value.call value
48
+ else value == expected_value
49
+ end
50
+ end
51
+
52
+ def matches_types?(value, expected_types)
53
+ expected_types.any?{|type| matches_type_and_format? value, type}
54
+ end
55
+
56
+ def matches_type_and_format?(value, type)
57
+ type = Hash[type, :any] unless type.is_a?(Hash)
58
+ type.any? do |type, format|
59
+ matches_type?(value, type) && matches_format?(value, format)
60
+ end
61
+ end
62
+
63
+ def matches_type?(value, type)
64
+ type_to_classes(type).any?{|klass| value.is_a? klass}
65
+ end
66
+
67
+ def matches_format?(value, format)
68
+ case format
69
+ when :url then value =~ URI::regexp
70
+ when :integer then value.is_a? Integer
71
+ when :timestamp then DateTime.iso8601 value rescue false
72
+ when :email then value =~ %r{(?<name>.+?)@(?<host>.+?)\.(?<domain>.+?)}
73
+ when Hash, Array then value.any? ? has_attributes?(value, to_hash(format)) : true
74
+ when String then matches_format?(value, format.to_sym)
75
+ when :any then true
76
+ end
77
+ end
78
+
79
+ def type_to_classes(type)
80
+ Array.wrap case type
81
+ when :string then String
82
+ when :array then Array
83
+ when :object then Hash
84
+ when :null then NilClass
85
+ when :boolean then [TrueClass, FalseClass]
86
+ when :number then Numeric
87
+ when String then type_to_classes(type.to_sym)
88
+ when :any then Object
89
+ end
90
+ end
91
+
92
+ def to_hash(attrs)
93
+ array = Array.wrap(attrs).map do |item|
94
+ item.is_a?(Hash) ? [item.keys.first, item.values.first] : [item, {}]
95
+ end.flatten
96
+ Hash[*array]
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,20 @@
1
+ require 'rspec-api/matchers/collection/matcher'
2
+
3
+ module RSpecApi
4
+ module Matchers
5
+ module Collection
6
+ # Passes if the response body is a collection of JSON objects
7
+ #
8
+ # @example
9
+ #
10
+ # # Passes if the body is a JSON array
11
+ # body = '[{"id": 1}]'
12
+ # expect(OpenStruct.new body: body).to be_a_collection
13
+ #
14
+ # For more examples check +be_a_collection_spec.rb+.
15
+ def be_a_collection
16
+ RSpecApi::Matchers::Collection::Matcher.new
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ require 'rspec-api/matchers/json/matcher'
2
+
3
+ module RSpecApi
4
+ module Matchers
5
+ module Collection
6
+ class Matcher < Json::Matcher
7
+
8
+ def matches?(response)
9
+ super
10
+ json.is_a? Array
11
+ end
12
+
13
+ def description
14
+ %Q(be a collection)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ require 'rspec-api/matchers/content_type/matcher'
2
+
3
+ module RSpecApi
4
+ module Matchers
5
+ module ContentType
6
+ # Passes if the response headers specify the provided content +type+
7
+ #
8
+ # @example
9
+ #
10
+ # # Passes if the headers matches the provided JSON content type
11
+ # headers ={'Content-Type' => 'application/json; charset=utf-8'}
12
+ # expect(OpenStruct.new headers: headers).to have_content_type(:json)
13
+ #
14
+ # For more examples check +have_content_type_spec.rb+.
15
+ def have_content_type(type = nil)
16
+ content_type = case type
17
+ when :json then 'application/json; charset=utf-8'
18
+ when :any then %r{}
19
+ end
20
+ RSpecApi::Matchers::ContentType::Matcher.new content_type
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ require 'rspec-api/matchers/headers/matcher'
2
+
3
+ module RSpecApi
4
+ module Matchers
5
+ module ContentType
6
+ class Matcher < Headers::Matcher
7
+ attr_accessor :content_type
8
+
9
+ def initialize(content_type)
10
+ @content_type = content_type
11
+ end
12
+
13
+ def matches?(response)
14
+ super && headers.key?('Content-Type') && content_type.match(headers['Content-Type'])
15
+ end
16
+
17
+ def description
18
+ %Q{include 'Content-Type': '#{content_type}'}
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ require 'rspec-api/matchers/filter/matcher'
2
+
3
+ module RSpecApi
4
+ module Matchers
5
+ module Filter
6
+ # Passes if the response body is a non-empty filtered JSON collection
7
+ #
8
+ # @example
9
+ #
10
+ # # Passes if the body only contains objects with ID = 1
11
+ # body = '[{"id": 1}, {"id": 1}, {"id": 1}]'
12
+ # expect(OpenStruct.new body: body).to be_filtered by: :id, value: 1
13
+ #
14
+ # For more examples check +be_filtered_spec.rb+.
15
+ def be_filtered(options = {})
16
+ RSpecApi::Matchers::Filter::Matcher.new options
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,57 @@
1
+ require 'rspec-api/matchers/collection/matcher'
2
+
3
+ module RSpecApi
4
+ module Matchers
5
+ module Filter
6
+ class Matcher < Collection::Matcher
7
+ attr_accessor :field, :value, :compare_with
8
+
9
+ def initialize(options = {})
10
+ @field = options[:by]
11
+ @value = options[:value]
12
+ @compare_with = options.fetch :compare_with, :==
13
+ end
14
+
15
+ def matches?(response)
16
+ super && all_objects_have_field? && has_two_objects? && is_filtered?
17
+ end
18
+
19
+ def description
20
+ if field
21
+ %Q(be filtered by #{field}#{compare_with}#{value})
22
+ else
23
+ %Q(be filtered)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def all_objects_have_field?
30
+ if field
31
+ json.all?{|item| item.is_a?(Hash) && item.key?(field)}
32
+ else
33
+ true
34
+ end
35
+ end
36
+
37
+ def has_two_objects?
38
+ if json.length < 2
39
+ msg = "Cannot test filtering on an array with #{json.length} items"
40
+ raise RSpec::Core::Pending::PendingDeclaredInExample.new msg
41
+ else
42
+ true
43
+ end
44
+ end
45
+
46
+ def is_filtered?
47
+ values = json.map{|item| item[field]}
48
+ values.all?{|v| v.send compare_with, value}
49
+ end
50
+
51
+ def reverse?
52
+ ['desc', 'descending'].include? verse.to_s
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,20 @@
1
+ require 'rspec-api/matchers/headers/matcher'
2
+
3
+ module RSpecApi
4
+ module Matchers
5
+ module Headers
6
+ # Passes if the response has a non-empty headers Hash.
7
+ #
8
+ # @example
9
+ #
10
+ # # Passes if the headers include the content length
11
+ # headers = {'Content-Length' => 17372}
12
+ # expect(OpenStruct.new headers: headers).to have_headers
13
+ #
14
+ # For more examples check +have_headers_spec.rb+.
15
+ def have_headers
16
+ RSpecApi::Matchers::Headers::Matcher.new
17
+ end
18
+ end
19
+ end
20
+ end