rspec-resources 0.1.0 → 0.2.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: dfb441b28de8568ca1eb074a1e62b67aec0e3f1f
4
- data.tar.gz: 78e04e0fe569a3a90be7fa90dc3eb4df37c12693
3
+ metadata.gz: 6c30cfbc718cfa5916c574b2048ce257fb58f24a
4
+ data.tar.gz: 6b2e3296e3e40ddf30890139d98e087b8cb356a1
5
5
  SHA512:
6
- metadata.gz: 549c60cbf01019fe906f1f571e2a8edb0d57674ff5485bcfe7ff4d727c12b357ba8dd3417bbbd0f522006a6241dd474658561fee2fcfa7e040c5c244657b103e
7
- data.tar.gz: ffcc0265869be1451e0bef0fda95d3870dc6ca82da8fa62a2f976a129ee63d22a8fca2fce841fec72dafa35e0f3dfc4838bc8250c10a92b5c31fecdd47dd17d5
6
+ metadata.gz: 9a83281b35c83f65012fc0e2c43465171315e8474eac7b8f4ba229db3aba34dc7d91e5cf9c7f99bee173baa7118e78dc3a168bec5e757722f746f59a6115ff2b
7
+ data.tar.gz: 3485b1539671d3345d331e9857afa10732c972fb872a25da91d5c192fbed8093f91327810dcf1042beb82a5297c5d66f4212cf25c0016bf18ad6bbbf8a79b581
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rspec-resources (0.1.0)
4
+ rspec-resources (0.2.0)
5
5
  activesupport (>= 3.0.0)
6
6
  rspec (~> 3.0)
7
7
 
@@ -1,12 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_support'
4
+ require 'active_support/core_ext/object'
4
5
 
6
+ require 'rspec'
7
+
8
+ require 'rspec/resources/document_formats'
9
+ require 'rspec/resources/configuration'
5
10
  require 'rspec/resources/util'
6
11
  require 'rspec/resources/dsl'
7
12
  require 'rspec/resources/version'
8
13
 
9
14
  module RSpec
10
15
  module Resources
16
+ def self.configuration
17
+ @configuration ||= Configuration.new
18
+ end
19
+
20
+ # Configures rspec-resources
21
+ #
22
+ # See RSpec::Resources::Configuration for more information on configuring.
23
+ #
24
+ # RSpec::Resources.configure do |config|
25
+ # config.document_format = :jsonapi
26
+ # end
27
+ def self.configure
28
+ yield configuration if block_given?
29
+ end
11
30
  end
12
31
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Resources
5
+ class Configuration
6
+ def self.add_setting(name, opts = {})
7
+ define_method("#{name}=") { |value| settings[name] = value }
8
+ define_method(name.to_s) do
9
+ if settings.key?(name)
10
+ settings[name]
11
+ elsif opts[:default].respond_to?(:call)
12
+ opts[:default].call(self)
13
+ else
14
+ opts[:default]
15
+ end
16
+ end
17
+ end
18
+
19
+ # Specify how the returned document look like
20
+ # See RSpec::Resources::DOCUMENT_FORMATS
21
+ add_setting :document_format, default: :jsonapi
22
+
23
+ def settings
24
+ @settings ||= {}
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Resources
5
+ DOCUMENT_FORMATS = {
6
+ # Resource end points with most simple data format
7
+ # Example response:
8
+ # {
9
+ # "id": "5",
10
+ # "text": "Loren ipsum"
11
+ # }
12
+ simple: {
13
+ # path to the content of the response
14
+ base_path: '',
15
+ # path to the resource id relative to base path
16
+ id_path: 'id',
17
+ # path to the attributes section of the resource relative to base path
18
+ attributes_path: '',
19
+ # path to error objects; may either be an array or a single object
20
+ error_path: '',
21
+ # the path to the error message relative to an error object (see error_path)
22
+ error_detail_path: 'message',
23
+ }.freeze,
24
+
25
+ # Resource end points with simple data format wrapped in a data field
26
+ # Example response:
27
+ # {
28
+ # "data": {
29
+ # "id": "5",
30
+ # "text": "Loren ipsum"
31
+ # }
32
+ # }
33
+ simple_wrapped: {
34
+ # path to the content of the response
35
+ base_path: 'data',
36
+ # path to the resource id relative to base path
37
+ id_path: 'id',
38
+ # path to the attributes section of the resource relative to base path
39
+ attributes_path: '',
40
+ # path to error objects; may either be an array or a single object
41
+ error_path: 'errors',
42
+ # the path to the error message relative to an error object (see error_path)
43
+ error_detail_path: 'message',
44
+ }.freeze,
45
+
46
+ # Resource end points conforming to json api specification (http://jsonapi.org/)
47
+ # Example response:
48
+ # {
49
+ # "data": {
50
+ # "id": "5",
51
+ # "type": "article",
52
+ # "attributes": {
53
+ # "text": "Loren ipsum"
54
+ # }
55
+ # }
56
+ # }
57
+ jsonapi: {
58
+ # path to the content of the response
59
+ base_path: 'data',
60
+ # path to the resource id relative to base path
61
+ id_path: 'id',
62
+ # path to the attributes section of the resource relative to base path
63
+ attributes_path: 'attributes',
64
+ # path to error objects; may either be an array or a single object
65
+ error_path: 'errors',
66
+ # the path to the error message relative to an error object (see error_path)
67
+ error_detail_path: 'detail',
68
+ }.freeze,
69
+ }.freeze
70
+ end
71
+ end
@@ -50,6 +50,7 @@ module RSpec
50
50
  base_path: base_path,
51
51
  resource_name: name.singularize,
52
52
  rspec_resources_dsl: :resource,
53
+ document_format: RSpec::Resources.configuration.document_format,
53
54
  }.merge(options)
54
55
 
55
56
  RSpec.describe(name.capitalize, dopts) do
@@ -9,15 +9,17 @@ module RSpec
9
9
  extend ActiveSupport::Concern
10
10
 
11
11
  module ClassMethods
12
- def describe_index(&block)
12
+ def describe_index(opts = {}, &block)
13
13
  path = metadata[:base_path]
14
14
 
15
- describe "GET #{path}" do
15
+ describe "GET #{path}", opts do
16
16
  subject { [accessible_resource] }
17
17
 
18
+ let(:params) { {} }
19
+
18
20
  before do
19
21
  subject # force subject creation
20
- get instantiate_path(path, subject.first), headers: request_headers
22
+ get instantiate_path(path, subject.first), params: params, headers: request_headers
21
23
  end
22
24
 
23
25
  instance_eval(&block)
@@ -27,10 +29,10 @@ module RSpec
27
29
  end
28
30
  end
29
31
 
30
- def describe_show(&block)
32
+ def describe_show(opts = {}, &block)
31
33
  path = id_path_template
32
34
 
33
- describe "GET #{path}" do
35
+ describe "GET #{path}", opts do
34
36
  subject { accessible_resource }
35
37
 
36
38
  before { get instantiate_path(path, subject), headers: request_headers }
@@ -42,10 +44,11 @@ module RSpec
42
44
  end
43
45
  end
44
46
 
45
- def describe_create(&block)
47
+ # rubocop:disable Metrics/AbcSize
48
+ def describe_create(opts = {}, &block)
46
49
  path = metadata[:base_path]
47
50
 
48
- describe "POST #{path}" do
51
+ describe "POST #{path}", opts do
49
52
  let(:params) { |ce| attributes_for(ce.metadata[:resource_name]) }
50
53
  let(:instantiation_resource) { accessible_resource }
51
54
 
@@ -67,11 +70,12 @@ module RSpec
67
70
  include_if_needed(:create).restricted_examples
68
71
  end
69
72
  end
73
+ # rubocop:enable Metrics/AbcSize
70
74
 
71
- def describe_update(&block)
75
+ def describe_update(opts = {}, &block)
72
76
  path = id_path_template
73
77
 
74
- describe "PATCH/PUT #{path}" do
78
+ describe "PATCH/PUT #{path}", opts do
75
79
  subject { accessible_resource }
76
80
 
77
81
  let(:params) { |ce| attributes_for(ce.metadata[:resource_name]) }
@@ -85,10 +89,10 @@ module RSpec
85
89
  end
86
90
  end
87
91
 
88
- def describe_destroy(&block)
92
+ def describe_destroy(opts = {}, &block)
89
93
  path = id_path_template
90
94
 
91
- describe "DELETE #{path}" do
95
+ describe "DELETE #{path}", opts do
92
96
  subject { accessible_resource }
93
97
 
94
98
  before { delete instantiate_path(path, subject), headers: request_headers }
@@ -53,7 +53,7 @@ module RSpec
53
53
  it { returns_status_code 404 }
54
54
  else
55
55
  it 'returns no records' do
56
- expect(json_data).to be_empty
56
+ expect(base_doc).to be_empty
57
57
  end
58
58
 
59
59
  it { returns_status_code 200 }
@@ -12,6 +12,11 @@ module RSpec
12
12
  let(:visible_attributes) { params }
13
13
  end
14
14
 
15
+ def hidden_attributes(*params)
16
+ params = params.first if params.length == 1 && params.first.is_a?(Array)
17
+ let(:hidden_attributes) { params }
18
+ end
19
+
15
20
  def it_needs_authentication(with_headers: :auth_headers, only: %i[index show create update destroy])
16
21
  metadata[:it_needs_authentication] = {
17
22
  headers: with_headers,
@@ -1,19 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rspec/resources/dsl/matchers/json_model_matcher'
4
+ require 'rspec/resources/dsl/matchers/error_matcher'
5
+
3
6
  module RSpec
4
7
  module Resources
5
8
  module DSL
6
9
  module Matchers
7
10
  extend ActiveSupport::Concern
8
11
 
12
+ def returns_status_code(code)
13
+ expect(response).to have_http_status(code)
14
+ end
15
+
16
+ #########################
17
+ ### Return Validators ###
18
+ #########################
19
+
9
20
  def returns_the_records(*records)
10
21
  try_set_description 'returns the requested records'
11
22
 
12
- expect(json).to have_key('data')
13
- expect(json_data.length).to eq(records.length)
23
+ expect(base_doc.length).to eq(records.length)
14
24
 
15
- json_data.each_with_index do |jd, i|
16
- expect(jd['attributes']).to equal_record(records[i], on: visible_attributes)
25
+ base_doc.each_with_index do |jd, i|
26
+ check_document jd, to_match: records[i]
17
27
  end
18
28
  end
19
29
 
@@ -22,23 +32,22 @@ module RSpec
22
32
  returns_the_records(*subject)
23
33
  else
24
34
  try_set_description 'returns the requested record'
25
- expect(json).to have_key('data')
26
- expect(json_data['attributes']).to equal_record(subject, on: visible_attributes)
35
+ check_document base_doc, to_match: subject
27
36
  end
28
37
  end
29
38
 
30
- def creates_a_new_record
39
+ def creates_a_new_record(check: nil)
31
40
  try_set_description 'creates a new record with the given attributes'
32
41
 
33
- record = accessible_resource.class.find_by_id(json_data['id'])
42
+ record = accessible_resource.class.find_by_id(Util.access_by_path(base_doc, id_path))
34
43
  expect(record).to be_present
35
- expect(record).to match_params
44
+ expect(record).to match_params(check || params)
36
45
  end
37
46
 
38
- def updates_the_subject
47
+ def updates_the_subject(check: nil)
39
48
  try_set_description 'updates the record with the given attributes'
40
49
 
41
- expect(subject.reload).to match_params
50
+ expect(subject.reload).to match_params(check || params)
42
51
  end
43
52
 
44
53
  def destroys_the_subject
@@ -49,10 +58,37 @@ module RSpec
49
58
 
50
59
  private
51
60
 
61
+ def check_document(doc, to_match: nil)
62
+ # check presence of attributes
63
+ expect(Util.access_by_path(doc, attributes_doc_path)).to equal_record(to_match, on: visible_attributes)
64
+
65
+ # check absence of attributes
66
+ return unless defined?(hidden_attributes)
67
+ hidden_attributes.each do |a|
68
+ expect(doc).not_to have_key(a.to_s)
69
+ end
70
+ end
71
+
52
72
  def try_set_description(desc)
53
73
  return if RSpec.current_example.metadata[:description].present?
54
74
  RSpec.current_example.metadata[:description] = desc
55
75
  end
76
+
77
+ def json_body
78
+ JSON.parse(response.body)
79
+ end
80
+
81
+ def base_doc
82
+ Util.access_by_path(json_body, Util.document_format_hash[:base_path])
83
+ end
84
+
85
+ def attributes_doc_path
86
+ Util.document_format_hash[:attributes_path]
87
+ end
88
+
89
+ def id_path
90
+ Util.document_format_hash[:id_path]
91
+ end
56
92
  end
57
93
  end
58
94
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Resources
5
+ module DSL
6
+ module Matchers
7
+ class ErrorMatcher
8
+ def initialize(code, msg)
9
+ @code = code
10
+ @msg = msg
11
+ end
12
+
13
+ def matches?(subject)
14
+ @subject = subject
15
+
16
+ error_docs = Util.access_by_path(JSON.parse(@subject.body), Util.document_format_hash[:error_path])
17
+ error_docs = [error_docs] unless error_docs.is_a?(Array)
18
+
19
+ @subject.code == @code.to_s && error_docs.any? { |d| match_doc(d) }
20
+ end
21
+
22
+ def match_doc(error_doc)
23
+ @msg.nil? || Util.access_by_path(error_doc, Util.document_format_hash[:error_detail_path]).match(@msg)
24
+ end
25
+
26
+ def failure_message
27
+ "Expected to get an error but got code #{@subject.code} and body #{@subject.body}"
28
+ end
29
+
30
+ def failure_message_when_negated
31
+ "Expected to get no error but got code #{@subject.code} and body #{@subject.body}"
32
+ end
33
+
34
+ def description
35
+ "return an error with code #{@code}"
36
+ end
37
+
38
+ private
39
+
40
+ def obj_name
41
+ @obj.class.name.downcase
42
+ end
43
+ end
44
+
45
+ def return_an_error(hash)
46
+ code = hash[:with_code]
47
+ msg = hash[:with_message] || hash[:and_message]
48
+
49
+ ErrorMatcher.new(code, msg)
50
+ end
51
+
52
+ def returns_an_error(hash)
53
+ expect(response).to return_an_error(hash)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Resources
5
+ module DSL
6
+ module Matchers
7
+ class JsonModelMatcher
8
+ def initialize(expectation, attributes)
9
+ @expectation = expectation.is_a?(Hash) ? expectation.with_indifferent_access : expectation
10
+ @attributes = attributes
11
+ end
12
+
13
+ def matches?(subject)
14
+ @subject = subject
15
+
16
+ @fails = []
17
+
18
+ @attributes.map(&:to_s).each do |a|
19
+ exp = @expectation[a]
20
+ subj = @subject[a]
21
+
22
+ @fails.push(attribute: a, expectation: exp, subject: subj) if exp != subj
23
+ end
24
+
25
+ @fails.empty?
26
+ end
27
+
28
+ def failure_message
29
+ if @subject.is_a? Hash
30
+ # we most likely check the result of a get operation
31
+ "Expected returned attributes to match:\n#{formatted_fails}"
32
+ else
33
+ # we most likely check an update or create here
34
+ "Expected model attributes to match:\n#{formatted_fails}"
35
+ end
36
+ end
37
+
38
+ def failure_message_when_negated
39
+ "Expected to not match:\n#{formatted_fails}"
40
+ end
41
+
42
+ private
43
+
44
+ def formatted_fails
45
+ @fails.map do |f|
46
+ " #{f[:attribute]} was '#{f[:subject]}' but expected to be '#{f[:expectation]}'"
47
+ end.join("\n")
48
+ end
49
+ end
50
+
51
+ def equal_record(record, hash)
52
+ JsonModelMatcher.new(record, hash[:on])
53
+ end
54
+
55
+ def match_params(iparams)
56
+ JsonModelMatcher.new(iparams, iparams.keys)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -6,6 +6,18 @@ module RSpec
6
6
  def self.nested_resource?(metadata)
7
7
  metadata[:base_path] =~ /:\w+/
8
8
  end
9
+
10
+ def self.access_by_path(doc, path)
11
+ path.split('.').each { |p| doc = doc[p] } if path.present?
12
+ doc
13
+ end
14
+
15
+ def self.document_format_hash(metadata = nil)
16
+ doc_format = (metadata || RSpec.current_example.metadata)[:document_format]
17
+
18
+ return DOCUMENT_FORMATS[doc_format] if doc_format.is_a?(Symbol) || doc_format.is_a?(String)
19
+ doc_format.respond_to?(:with_indifferent_access) ? doc_format.with_indifferent_access : doc_format
20
+ end
9
21
  end
10
22
  end
11
23
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module Resources
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
@@ -23,8 +23,8 @@ Gem::Specification.new do |spec|
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
24
  spec.require_paths = ['lib']
25
25
 
26
- spec.add_runtime_dependency 'rspec', '~> 3.0'
27
26
  spec.add_runtime_dependency 'activesupport', '>= 3.0.0'
27
+ spec.add_runtime_dependency 'rspec', '~> 3.0'
28
28
 
29
29
  spec.add_development_dependency 'bundler', '~> 1.16'
30
30
  spec.add_development_dependency 'rake', '~> 10.0'
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-resources
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Poetzsch-Heffter
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-04-13 00:00:00.000000000 Z
11
+ date: 2018-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rspec
14
+ name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '3.0'
19
+ version: 3.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '3.0'
26
+ version: 3.0.0
27
27
  - !ruby/object:Gem::Dependency
28
- name: activesupport
28
+ name: rspec
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 3.0.0
33
+ version: '3.0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ">="
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 3.0.0
40
+ version: '3.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -114,11 +114,15 @@ files:
114
114
  - bin/console
115
115
  - bin/setup
116
116
  - lib/rspec/resources.rb
117
+ - lib/rspec/resources/configuration.rb
118
+ - lib/rspec/resources/document_formats.rb
117
119
  - lib/rspec/resources/dsl.rb
118
120
  - lib/rspec/resources/dsl/actions.rb
119
121
  - lib/rspec/resources/dsl/actions/include_if_needed.rb
120
122
  - lib/rspec/resources/dsl/characteristics.rb
121
123
  - lib/rspec/resources/dsl/matchers.rb
124
+ - lib/rspec/resources/dsl/matchers/error_matcher.rb
125
+ - lib/rspec/resources/dsl/matchers/json_model_matcher.rb
122
126
  - lib/rspec/resources/util.rb
123
127
  - lib/rspec/resources/version.rb
124
128
  - rspec-resources.gemspec