rspec-resources 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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