spec_forge 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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.rspec +1 -0
  4. data/.standard.yml +3 -0
  5. data/CHANGELOG.md +5 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +426 -0
  9. data/Rakefile +6 -0
  10. data/bin/spec_forge +5 -0
  11. data/flake.lock +61 -0
  12. data/flake.nix +41 -0
  13. data/lib/spec_forge/attribute/chainable.rb +86 -0
  14. data/lib/spec_forge/attribute/factory.rb +63 -0
  15. data/lib/spec_forge/attribute/faker.rb +54 -0
  16. data/lib/spec_forge/attribute/literal.rb +27 -0
  17. data/lib/spec_forge/attribute/matcher.rb +118 -0
  18. data/lib/spec_forge/attribute/parameterized.rb +76 -0
  19. data/lib/spec_forge/attribute/resolvable.rb +21 -0
  20. data/lib/spec_forge/attribute/resolvable_array.rb +24 -0
  21. data/lib/spec_forge/attribute/resolvable_hash.rb +24 -0
  22. data/lib/spec_forge/attribute/transform.rb +39 -0
  23. data/lib/spec_forge/attribute/variable.rb +36 -0
  24. data/lib/spec_forge/attribute.rb +208 -0
  25. data/lib/spec_forge/cli/actions.rb +23 -0
  26. data/lib/spec_forge/cli/command.rb +127 -0
  27. data/lib/spec_forge/cli/init.rb +29 -0
  28. data/lib/spec_forge/cli/new.rb +161 -0
  29. data/lib/spec_forge/cli/run.rb +17 -0
  30. data/lib/spec_forge/cli.rb +43 -0
  31. data/lib/spec_forge/config.rb +84 -0
  32. data/lib/spec_forge/environment.rb +71 -0
  33. data/lib/spec_forge/error.rb +150 -0
  34. data/lib/spec_forge/factory.rb +104 -0
  35. data/lib/spec_forge/http/backend.rb +106 -0
  36. data/lib/spec_forge/http/client.rb +33 -0
  37. data/lib/spec_forge/http/request.rb +93 -0
  38. data/lib/spec_forge/http/verb.rb +118 -0
  39. data/lib/spec_forge/http.rb +6 -0
  40. data/lib/spec_forge/normalizer/config.rb +104 -0
  41. data/lib/spec_forge/normalizer/constraint.rb +47 -0
  42. data/lib/spec_forge/normalizer/expectation.rb +85 -0
  43. data/lib/spec_forge/normalizer/factory.rb +65 -0
  44. data/lib/spec_forge/normalizer/factory_reference.rb +66 -0
  45. data/lib/spec_forge/normalizer/spec.rb +73 -0
  46. data/lib/spec_forge/normalizer.rb +183 -0
  47. data/lib/spec_forge/runner.rb +91 -0
  48. data/lib/spec_forge/spec/expectation/constraint.rb +52 -0
  49. data/lib/spec_forge/spec/expectation.rb +53 -0
  50. data/lib/spec_forge/spec.rb +77 -0
  51. data/lib/spec_forge/type.rb +45 -0
  52. data/lib/spec_forge/version.rb +5 -0
  53. data/lib/spec_forge.rb +90 -0
  54. data/lib/templates/config.tt +19 -0
  55. data/spec_forge/config.yml +19 -0
  56. data/spec_forge/factories/user.yml +4 -0
  57. data/spec_forge/specs/users.yml +63 -0
  58. metadata +234 -0
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Normalizer
5
+ class Config < Normalizer
6
+ STRUCTURE = {
7
+ environment: {
8
+ # Allows for a shorthand:
9
+ # environment: rails
10
+ # Long form:
11
+ # environment:
12
+ # use: rails
13
+ type: [String, Hash],
14
+ default: "rails",
15
+ structure: {
16
+ use: {type: String, default: "rails"},
17
+ preload: {type: String, default: ""},
18
+ models_path: {
19
+ type: String,
20
+ aliases: %i[models],
21
+ default: ""
22
+ }
23
+ }
24
+ },
25
+ base_url: {type: String}, # Required
26
+ authorization: {
27
+ type: Hash,
28
+ default: {
29
+ # Default is a key on this hash
30
+ default: {}
31
+ },
32
+ structure: {
33
+ default: {
34
+ type: Hash,
35
+ structure: {
36
+ header: {type: String, default: ""},
37
+ value: {type: String, default: ""}
38
+ }
39
+ }
40
+ }
41
+ },
42
+ factories: {
43
+ type: Hash,
44
+ default: {},
45
+ structure: {
46
+ paths: {
47
+ type: Array,
48
+ default: []
49
+ },
50
+ auto_discover: {
51
+ type: [TrueClass, FalseClass],
52
+ default: true
53
+ }
54
+ }
55
+ }
56
+ }.freeze
57
+ end
58
+
59
+ # On Normalizer
60
+ class << self
61
+ #
62
+ # Generates an empty config hash
63
+ #
64
+ # @return [Hash]
65
+ #
66
+ def default_config
67
+ Config.default
68
+ end
69
+
70
+ #
71
+ # Normalizes a config hash by standardizing its keys while ensuring the required data
72
+ # is provided or defaulted.
73
+ # Raises InvalidStructureError if anything is missing/invalid type
74
+ #
75
+ # @param input [Hash] The hash to normalize
76
+ #
77
+ # @return [Hash] A normalized hash as a new instance
78
+ #
79
+ def normalize_config!(input)
80
+ raise_errors! do
81
+ normalize_config(input)
82
+ end
83
+ end
84
+
85
+ #
86
+ # Normalize a config hash
87
+ # Used internally by .normalize_config, but is available for utility
88
+ #
89
+ # @param config [Hash] Config representation as a Hash
90
+ #
91
+ # @return [Array] Two item array
92
+ # First - The normalized hash
93
+ # Second - Array of errors, if any
94
+ #
95
+ # @private
96
+ #
97
+ def normalize_config(config)
98
+ raise InvalidTypeError.new(config, Hash, for: "config") unless Type.hash?(config)
99
+
100
+ Normalizer::Config.new("config", config).normalize
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Normalizer
5
+ class Constraint < Normalizer
6
+ STRUCTURE = {
7
+ status: {
8
+ type: Integer
9
+ },
10
+ json: {
11
+ type: Hash,
12
+ default: {}
13
+ }
14
+ }.freeze
15
+ end
16
+
17
+ # On Normalizer
18
+ class << self
19
+ #
20
+ # Generates an empty constraint hash
21
+ #
22
+ # @return [Hash]
23
+ #
24
+ def default_constraint
25
+ Constraint.default
26
+ end
27
+
28
+ #
29
+ # Normalize a constraint hash
30
+ # Used internally by .normalize_spec, but is available for utility
31
+ #
32
+ # @param constraint [Hash] Constraint representation as a Hash
33
+ #
34
+ # @return [Array] Two item array
35
+ # First - The normalized hash
36
+ # Second - Array of errors, if any
37
+ #
38
+ # @private
39
+ #
40
+ def normalize_constraint(constraint)
41
+ raise InvalidTypeError.new(constraint, Hash, for: "expect") unless Type.hash?(constraint)
42
+
43
+ Normalizer::Constraint.new("expect", constraint).normalize
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Normalizer
5
+ class Expectation < Normalizer
6
+ STRUCTURE = {
7
+ name: {type: String, default: ""},
8
+ base_url: Normalizer::SHARED_ATTRIBUTES[:base_url],
9
+ url: Normalizer::SHARED_ATTRIBUTES[:url],
10
+ http_method: Normalizer::SHARED_ATTRIBUTES[:http_method],
11
+ headers: Normalizer::SHARED_ATTRIBUTES[:headers],
12
+ query: Normalizer::SHARED_ATTRIBUTES[:query],
13
+ body: Normalizer::SHARED_ATTRIBUTES[:body],
14
+ variables: Normalizer::SHARED_ATTRIBUTES[:variables],
15
+ expect: {type: Hash}
16
+ }.freeze
17
+ end
18
+
19
+ # On Normalizer
20
+ class << self
21
+ #
22
+ # Generates an empty expectation hash
23
+ #
24
+ # @return [Hash]
25
+ #
26
+ def default_expectation
27
+ Expectation.default
28
+ end
29
+
30
+ #
31
+ # Normalize an array of expectation hashes
32
+ #
33
+ # @raises InvalidStructureError if anything is missing/invalid type
34
+ #
35
+ # @param input [Hash] The hash to normalize
36
+ #
37
+ # @return [Hash] A normalized hash as a new instance
38
+ #
39
+ def normalize_expectations!(input)
40
+ raise_errors! do
41
+ normalize_expectations(input)
42
+ end
43
+ end
44
+
45
+ #
46
+ # Normalize an array of expectation hashes
47
+ # Used internally by .normalize_spec, but is available for utility
48
+ #
49
+ # @param expectations [Array<Hash>] An array of expectation hashes
50
+ #
51
+ # @return [Array] Two item array
52
+ # First - The normalized Array<Hash>
53
+ # Second - Array of errors, if any
54
+ #
55
+ # @private
56
+ #
57
+ def normalize_expectations(expectations)
58
+ if !Type.array?(expectations)
59
+ raise InvalidTypeError.new(expectations, Array, for: "\"expectations\" on spec")
60
+ end
61
+
62
+ final_errors = Set.new
63
+ final_output = expectations.map.with_index do |expectation, index|
64
+ normalizer = Normalizer::Expectation.new("expectation (item #{index})", expectation)
65
+ output, errors = normalizer.normalize
66
+
67
+ # If expect is not provided, skip the constraints
68
+ if (constraint = expectation[:expect])
69
+ constraint_output, constraint_errors = Normalizer::Constraint.new(
70
+ "expect (item #{index})", constraint
71
+ ).normalize
72
+
73
+ output[:expect] = constraint_output
74
+ errors.merge(constraint_errors) if constraint_errors.size > 0
75
+ end
76
+
77
+ final_errors.merge(errors) if errors.size > 0
78
+ output
79
+ end
80
+
81
+ [final_output, final_errors]
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Normalizer
5
+ class Factory < Normalizer
6
+ STRUCTURE = {
7
+ model_class: {
8
+ type: String,
9
+ aliases: %i[class],
10
+ default: ""
11
+ },
12
+ variables: Normalizer::SHARED_ATTRIBUTES[:variables],
13
+ attributes: {
14
+ type: Hash,
15
+ default: {}
16
+ }
17
+ }.freeze
18
+ end
19
+
20
+ # On Normalizer
21
+ class << self
22
+ #
23
+ # Generates an empty factory hash
24
+ #
25
+ # @return [Hash]
26
+ #
27
+ def default_factory
28
+ Factory.default
29
+ end
30
+
31
+ #
32
+ # Normalizes a factory hash by standardizing its keys while ensuring the required data
33
+ # is provided or defaulted.
34
+ # Raises InvalidStructureError if anything is missing/invalid type
35
+ #
36
+ # @param input [Hash] The hash to normalize
37
+ #
38
+ # @return [Hash] A normalized hash as a new instance
39
+ #
40
+ def normalize_factory!(input)
41
+ raise_errors! do
42
+ normalize_factory(input)
43
+ end
44
+ end
45
+
46
+ #
47
+ # Normalize a factory hash
48
+ # Used internally by .normalize_factory, but is available for utility
49
+ #
50
+ # @param factory [Hash] Factory representation as a Hash
51
+ #
52
+ # @return [Array] Two item array
53
+ # First - The normalized hash
54
+ # Second - Array of errors, if any
55
+ #
56
+ # @private
57
+ #
58
+ def normalize_factory(factory)
59
+ raise InvalidTypeError.new(factory, Hash, for: "factory") unless Type.hash?(factory)
60
+
61
+ Normalizer::Factory.new("factory", factory).normalize
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Normalizer
5
+ class FactoryReference < Normalizer
6
+ STRUCTURE = {
7
+ attributes: {
8
+ type: Hash,
9
+ default: {}
10
+ },
11
+ build_strategy: {
12
+ type: String,
13
+ aliases: %i[strategy],
14
+ default: "create"
15
+ }
16
+ }.freeze
17
+ end
18
+
19
+ # On Normalizer
20
+ class << self
21
+ #
22
+ # Generates an empty Attribute::Factory hash
23
+ #
24
+ # @return [Hash]
25
+ #
26
+ def default_factory_reference
27
+ FactoryReference.default
28
+ end
29
+
30
+ #
31
+ # Normalizes a Attribute::Factory hash by standardizing
32
+ # its keys while ensuring the required data is provided or defaulted.
33
+ # Raises InvalidStructureError if anything is missing/invalid type
34
+ #
35
+ # @param input [Hash] The hash to normalize
36
+ #
37
+ # @return [Hash] A normalized hash as a new instance
38
+ #
39
+ def normalize_factory_reference!(input, **)
40
+ raise_errors! do
41
+ normalize_factory_reference(input, **)
42
+ end
43
+ end
44
+
45
+ #
46
+ # Normalize a factory hash
47
+ # Used internally by .normalize_factory_reference, but is available for utility
48
+ #
49
+ # @param factory [Hash] Attribute::Factory representation as a Hash
50
+ #
51
+ # @return [Array] Two item array
52
+ # First - The normalized hash
53
+ # Second - Array of errors, if any
54
+ #
55
+ # @private
56
+ #
57
+ def normalize_factory_reference(factory, label: "factory reference")
58
+ if !Type.hash?(factory)
59
+ raise InvalidTypeError.new(factory, Hash, for: "factory reference")
60
+ end
61
+
62
+ Normalizer::FactoryReference.new(label, factory).normalize
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Normalizer
5
+ class Spec < Normalizer
6
+ STRUCTURE = {
7
+ base_url: Normalizer::SHARED_ATTRIBUTES[:base_url],
8
+ url: Normalizer::SHARED_ATTRIBUTES[:url],
9
+ http_method: Normalizer::SHARED_ATTRIBUTES[:http_method],
10
+ headers: Normalizer::SHARED_ATTRIBUTES[:headers],
11
+ query: Normalizer::SHARED_ATTRIBUTES[:query],
12
+ body: Normalizer::SHARED_ATTRIBUTES[:body],
13
+ variables: Normalizer::SHARED_ATTRIBUTES[:variables],
14
+ expectations: {type: Array}
15
+ }.freeze
16
+ end
17
+
18
+ # On Normalizer
19
+ class << self
20
+ #
21
+ # Generates an empty spec hash
22
+ #
23
+ # @return [Hash]
24
+ #
25
+ def default_spec
26
+ Spec.default
27
+ end
28
+
29
+ #
30
+ # Normalizes a complete spec hash by standardizing its keys while ensuring the required data
31
+ # is provided or defaulted.
32
+ # Raises InvalidStructureError if anything is missing/invalid type
33
+ #
34
+ # @param input [Hash] The hash to normalize
35
+ #
36
+ # @return [Hash] A normalized hash as a new instance
37
+ #
38
+ def normalize_spec!(input)
39
+ raise_errors! do
40
+ output, errors = normalize_spec(input)
41
+
42
+ # Process expectations
43
+ if (expectations = input[:expectations]) && Type.array?(expectations)
44
+ expectation_output, expectation_errors = normalize_expectations(expectations)
45
+
46
+ output[:expectations] = expectation_output
47
+ errors += expectation_errors if expectation_errors.size > 0
48
+ end
49
+
50
+ [output, errors]
51
+ end
52
+ end
53
+
54
+ #
55
+ # Normalize a spec hash
56
+ # Used internally by .normalize_spec, but is available for utility
57
+ #
58
+ # @param spec [Hash] Spec representation as a Hash
59
+ #
60
+ # @return [Array] Two item array
61
+ # First - The normalized hash
62
+ # Second - Array of errors, if any
63
+ #
64
+ # @private
65
+ #
66
+ def normalize_spec(spec)
67
+ raise InvalidTypeError.new(spec, Hash, for: "spec") unless Type.hash?(spec)
68
+
69
+ Normalizer::Spec.new("spec", spec).normalize
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Normalizer
5
+ SHARED_ATTRIBUTES = {
6
+ base_url: {
7
+ type: String,
8
+ default: ""
9
+ },
10
+ url: {
11
+ type: String,
12
+ aliases: %i[path],
13
+ default: ""
14
+ },
15
+ http_method: {
16
+ type: String,
17
+ aliases: %i[method],
18
+ default: ""
19
+ },
20
+ headers: {
21
+ type: Hash,
22
+ default: {}
23
+ },
24
+ query: {
25
+ type: Hash,
26
+ aliases: %i[params],
27
+ default: {}
28
+ },
29
+ body: {
30
+ type: Hash,
31
+ aliases: %i[data],
32
+ default: {}
33
+ },
34
+ variables: {
35
+ type: Hash,
36
+ default: {}
37
+ }
38
+ }.freeze
39
+
40
+ STRUCTURE = {}
41
+
42
+ class << self
43
+ #
44
+ # Raises any errors collected by the block
45
+ #
46
+ # @raises InvalidStructureError
47
+ #
48
+ # @private
49
+ #
50
+ def raise_errors!(&block)
51
+ errors = Set.new
52
+
53
+ begin
54
+ output, new_errors = yield
55
+ errors.merge(new_errors) if new_errors.size > 0
56
+ rescue => e
57
+ errors << e
58
+ end
59
+
60
+ raise InvalidStructureError.new(errors) if errors.size > 0
61
+
62
+ output
63
+ end
64
+
65
+ #
66
+ # Returns a default version of this normalizer
67
+ #
68
+ # @private
69
+ #
70
+ def default
71
+ new("", "").default
72
+ end
73
+ end
74
+
75
+ attr_reader :label, :input, :structure
76
+
77
+ #
78
+ # Creates a normalizer for normalizing Hash data based on a structure
79
+ #
80
+ # @param label [String] A label that describes the data itself
81
+ # @param input [Hash] The data to normalize
82
+ # @param structure [Hash] The structure to normalize the data to
83
+ #
84
+ def initialize(label, input, structure: self.class::STRUCTURE)
85
+ @label = label
86
+ @input = input
87
+ @structure = structure
88
+ end
89
+
90
+ #
91
+ # Normalizes the data and returns the result
92
+ #
93
+ # @return [Hash] The normalized data
94
+ #
95
+ def normalize
96
+ normalize_to_structure
97
+ end
98
+
99
+ #
100
+ # Returns a hash with the default structure
101
+ #
102
+ # @return [Hash]
103
+ #
104
+ def default
105
+ structure.transform_values do |value|
106
+ if (default = value[:default])
107
+ default.dup
108
+ elsif value[:type] == Integer # Can't call new on int
109
+ 0
110
+ else
111
+ value[:type].new
112
+ end
113
+ end
114
+ end
115
+
116
+ protected
117
+
118
+ def normalize_to_structure
119
+ output, errors = {}, Set.new
120
+
121
+ structure.each do |key, attribute|
122
+ type_class = attribute[:type]
123
+ aliases = attribute[:aliases] || []
124
+ sub_structure = attribute[:structure]
125
+ default = attribute[:default]
126
+ required = !attribute.key?(:default)
127
+
128
+ # Get the value
129
+ value = value_from_keys(input, [key] + aliases)
130
+
131
+ # Default the value if needed
132
+ value = default.dup if !required && value.nil?
133
+
134
+ # Type + existence check
135
+ if !valid_class?(value, type_class)
136
+ raise InvalidTypeError.new(value, type_class, for: "\"#{key}\" on #{label}")
137
+ end
138
+
139
+ value =
140
+ case [value.class, sub_structure.class]
141
+ when [Hash, Hash]
142
+ new_value, new_errors = self.class
143
+ .new(label, value, structure: sub_structure)
144
+ .normalize
145
+
146
+ errors += new_errors if new_errors.size > 0
147
+ new_value
148
+ else
149
+ value
150
+ end
151
+
152
+ # Store
153
+ output[key] = value
154
+ rescue => e
155
+ errors << e
156
+ end
157
+
158
+ [output, errors]
159
+ end
160
+
161
+ def value_from_keys(hash, keys)
162
+ hash.find { |k, v| keys.include?(k) }&.second
163
+ end
164
+
165
+ def valid_class?(value, expected_type)
166
+ if expected_type.instance_of?(Array)
167
+ expected_type.any? { |type| value.is_a?(type) }
168
+ else
169
+ value.is_a?(expected_type)
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ #######################################################################
176
+ # These need to be required after the base class due to them requiring
177
+ # a constant
178
+ require_relative "normalizer/config"
179
+ require_relative "normalizer/constraint"
180
+ require_relative "normalizer/expectation"
181
+ require_relative "normalizer/factory_reference"
182
+ require_relative "normalizer/factory"
183
+ require_relative "normalizer/spec"
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Runner
5
+ #
6
+ # Creates a spec runner and defines the spec with RSpec
7
+ #
8
+ # @param spec [Spec] The spec to run
9
+ #
10
+ def initialize(spec)
11
+ define_spec(spec)
12
+ end
13
+
14
+ #
15
+ # Runs any RSpec specs
16
+ #
17
+ def run
18
+ RSpec::Core::Runner.disable_autorun!
19
+ RSpec::Core::Runner.run([], $stderr, $stdout)
20
+ end
21
+
22
+ #
23
+ # Defines a spec with RSpec
24
+ #
25
+ # @param spec_forge [Spec] The spec to define
26
+ #
27
+ def define_spec(spec_forge)
28
+ runner_forge = self
29
+
30
+ RSpec.describe(spec_forge.name) do
31
+ spec_forge.expectations.each do |expectation_forge|
32
+ describe(expectation_forge.name) do
33
+ runner_forge.define_variables(self, expectation_forge)
34
+ runner_forge.define_examples(self, expectation_forge)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ #
41
+ # Defines any variables as let statements in RSpec
42
+ #
43
+ # @param context [RSpec::ExampleGroup] The rspec example group for this spec
44
+ # @param expectation [Expectation] The expectation that holds the variables
45
+ #
46
+ def define_variables(context, expectation)
47
+ expectation.variables.each do |variable_name, attribute|
48
+ context.let(variable_name, &attribute.to_proc)
49
+ end
50
+ end
51
+
52
+ #
53
+ # Defines the expectation itself using the constraint
54
+ #
55
+ # @param context [RSpec::ExampleGroup] The RSpec example group for this spec
56
+ # @param expectation [Expectation] The expectation that holds the constraint
57
+ #
58
+ def define_examples(context, expectation)
59
+ context.instance_exec(expectation) do |expectation|
60
+ # Ensures the only one API call occurs per expectation
61
+ before(:all) { @response = expectation.http_client.call }
62
+
63
+ constraints = expectation.constraints.resolve
64
+ request = expectation.http_client.request
65
+
66
+ # Define the example group
67
+ context "#{request.http_method} #{request.url}" do
68
+ subject(:response) { @response }
69
+
70
+ # Status check
71
+ expected_status = constraints[:status]
72
+ it "expects the response to return a status code of #{expected_status}" do
73
+ expect(response.status).to eq(expected_status)
74
+ end
75
+
76
+ # JSON check
77
+ expected_json = constraints[:json]
78
+ if expected_json.size > 0
79
+ it "expects the body to return valid JSON" do
80
+ expect(response.body).to be_kind_of(Hash)
81
+ end
82
+
83
+ it "expects the body to include values" do
84
+ expect(response.body).to include(expected_json)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end