rspec_contracts 0.1.1 → 0.1.4
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 +4 -4
- data/README.md +3 -1
- data/lib/rspec_contracts.rb +17 -17
- data/lib/rspec_contracts/contract.rb +31 -27
- data/lib/rspec_contracts/error.rb +5 -1
- data/lib/rspec_contracts/error/operation_lookup.rb +7 -1
- data/lib/rspec_contracts/error/path_validation.rb +7 -1
- data/lib/rspec_contracts/error/request_validation.rb +7 -1
- data/lib/rspec_contracts/error/response_validation.rb +7 -1
- data/lib/rspec_contracts/error/schema.rb +7 -1
- data/lib/rspec_contracts/integration.rb +25 -17
- data/lib/rspec_contracts/operation.rb +19 -14
- data/lib/rspec_contracts/path_validator.rb +16 -12
- data/lib/rspec_contracts/railtie.rb +7 -3
- data/lib/rspec_contracts/request_validator.rb +24 -17
- data/lib/rspec_contracts/response_validator.rb +19 -12
- data/lib/rspec_contracts/version.rb +1 -1
- metadata +22 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 416990094d07bd3e4a083be651518350f4a730fad14feb0e109d99710ea2b0f4
|
4
|
+
data.tar.gz: 734de59f3870c8afb9231bf55b8712c8fd61c244f8178f5dcf09ec1cbb3a7a39
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1360e61a50dd701ca97deae3228f541e897712da1c6a9ce86c7a7e7f934cc4bf35e5e2f1434bee891f758402e3d8854534feaf4bac8686ae6faff71b7a780975
|
7
|
+
data.tar.gz: 33716e95b8cad067992ce6423a3c6b58ec16e22400f90ef2f77cb3812df74690c42f824fadf28240b4389724ab6913c9ddb45c8b4452e98152f1a47f4f949dcf
|
data/README.md
CHANGED
@@ -117,7 +117,9 @@ The operation you're trying to test does not match either the HTTP method, or th
|
|
117
117
|
|
118
118
|
The POST body (if one is present) does not match the schema for a valid request as defined by the given operation.
|
119
119
|
|
120
|
-
|
120
|
+
This error is never raised if there is no POST body.
|
121
|
+
|
122
|
+
### `RspecContracts::Error::ResponseValidation`
|
121
123
|
|
122
124
|
The response body does not match the schema for a valid response as defined by the given operation.
|
123
125
|
|
data/lib/rspec_contracts.rb
CHANGED
@@ -1,20 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "active_support/lazy_load_hooks"
|
4
4
|
require "rspec_contracts/engine"
|
5
5
|
require "rspec_contracts/integration"
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
11
|
-
require
|
12
|
-
require
|
13
|
-
require
|
14
|
-
require
|
15
|
-
require
|
16
|
-
require
|
17
|
-
require
|
6
|
+
require "rspec_contracts/contract"
|
7
|
+
require "rspec_contracts/operation"
|
8
|
+
require "rspec_contracts/error"
|
9
|
+
require "rspec_contracts/error/operation_lookup"
|
10
|
+
require "rspec_contracts/error/request_validation"
|
11
|
+
require "rspec_contracts/error/response_validation"
|
12
|
+
require "rspec_contracts/error/path_validation"
|
13
|
+
require "rspec_contracts/error/schema"
|
14
|
+
require "rspec_contracts/path_validator"
|
15
|
+
require "rspec_contracts/request_validator"
|
16
|
+
require "rspec_contracts/response_validator"
|
17
|
+
require "rspec_contracts/railtie" if defined?(Rails::Railtie)
|
18
18
|
require "openapi_parser"
|
19
19
|
require "semverse"
|
20
20
|
|
@@ -37,9 +37,9 @@ module RspecContracts
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def self.valid_json?(json)
|
40
|
-
JSON.parse(json)
|
41
|
-
|
42
|
-
rescue JSON::ParserError =>
|
43
|
-
|
40
|
+
JSON.parse(json.to_s)
|
41
|
+
true
|
42
|
+
rescue JSON::ParserError => _e
|
43
|
+
false
|
44
44
|
end
|
45
45
|
end
|
@@ -1,36 +1,40 @@
|
|
1
|
-
|
2
|
-
def initialize(schema)
|
3
|
-
@schema = schema.with_indifferent_access
|
4
|
-
@root = OpenAPIParser.parse(schema)
|
5
|
-
end
|
1
|
+
# frozen_string_literal: true
|
6
2
|
|
7
|
-
|
8
|
-
|
3
|
+
module RspecContracts
|
4
|
+
class Contract
|
5
|
+
def initialize(schema)
|
6
|
+
@schema = schema.with_indifferent_access
|
7
|
+
@root = OpenAPIParser.parse(schema)
|
8
|
+
end
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
def [](key)
|
11
|
+
return RspecContracts::Operation.new(nil, self) unless operations.key?(key)
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
rescue NoMethodError => _e
|
16
|
-
raise RspecContracts::Error.new("Version not found in schema")
|
17
|
-
end
|
13
|
+
operations[key]
|
14
|
+
end
|
18
15
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
def version
|
17
|
+
@schema[:info][:version]
|
18
|
+
rescue NoMethodError => _e
|
19
|
+
raise RspecContracts::Error.new("Version not found in schema")
|
20
|
+
end
|
23
21
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
22
|
+
def operations
|
23
|
+
@operations ||= paths.map do |p|
|
24
|
+
p._openapi_all_child_objects.values.map do |op|
|
25
|
+
next unless op.respond_to?(:operation_id)
|
28
26
|
|
29
|
-
|
30
|
-
|
31
|
-
|
27
|
+
[op.operation_id, RspecContracts::Operation.new(op, self)]
|
28
|
+
end.compact.to_h
|
29
|
+
end.inject(:merge).with_indifferent_access
|
30
|
+
end
|
31
|
+
|
32
|
+
def paths
|
33
|
+
@paths ||= @root.paths._openapi_all_child_objects.values
|
34
|
+
end
|
32
35
|
|
33
|
-
|
34
|
-
|
36
|
+
def method_missing(m, *args, &block)
|
37
|
+
@root.send(m, *args, &block)
|
38
|
+
end
|
35
39
|
end
|
36
40
|
end
|
@@ -1,24 +1,32 @@
|
|
1
|
-
|
2
|
-
http_verbs = %w(get post patch put delete)
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
api_operation = kwargs.delete(:api_operation)
|
8
|
-
api_version = kwargs.delete(:api_version)
|
9
|
-
super(*args, **kwargs).tap do |status_code|
|
10
|
-
return unless api_operation # even a not found contract lookup will still be present
|
11
|
-
return if api_version.present? && !Semverse::Constraint.new(api_operation.root.version).include?(api_version)
|
12
|
-
raise RspecContracts::Error::OperationLookup.new("Operation not found") unless api_operation.valid?
|
3
|
+
module RspecContracts
|
4
|
+
module Integration
|
5
|
+
http_verbs = %w[get post patch put delete]
|
13
6
|
|
14
|
-
|
7
|
+
http_verbs.each do |method|
|
8
|
+
define_method(method) do |*args, **kwargs|
|
9
|
+
request_path = args.first
|
10
|
+
api_operation = kwargs.delete(:api_operation)
|
11
|
+
api_version = kwargs.delete(:api_version)
|
12
|
+
super(*args, **kwargs).tap do |status_code|
|
13
|
+
next unless api_operation # even a not found contract lookup will still be present
|
14
|
+
next if api_version.present? && !Semverse::Constraint.new(api_operation.root.version).include?(api_version)
|
15
|
+
raise RspecContracts::Error::OperationLookup.new("Operation not found") unless api_operation.valid?
|
15
16
|
|
16
|
-
RspecContracts
|
17
|
-
|
17
|
+
RspecContracts.config.logger.tagged("rspec_contracts", api_operation.operation_id) do
|
18
|
+
unless RspecContracts.config.path_validation_mode == :ignore
|
19
|
+
RspecContracts::PathValidator.validate_path(api_operation, method, request_path)
|
20
|
+
end
|
21
|
+
unless RspecContracts.config.request_validation_mode == :ignore
|
22
|
+
RspecContracts::RequestValidator.validate_request(api_operation, request)
|
23
|
+
end
|
18
24
|
|
19
|
-
|
20
|
-
|
21
|
-
|
25
|
+
unless RspecContracts.config.response_validation_mode == :ignore
|
26
|
+
parsed = RspecContracts.valid_json?(response.body) ? JSON.parse(response.body) : nil
|
27
|
+
vr = OpenAPIParser::RequestOperation::ValidatableResponseBody.new(status_code, parsed, response.headers)
|
28
|
+
RspecContracts::ResponseValidator.validate_response(api_operation, vr)
|
29
|
+
end
|
22
30
|
end
|
23
31
|
end
|
24
32
|
end
|
@@ -1,19 +1,24 @@
|
|
1
|
-
|
2
|
-
attr_reader :root
|
3
|
-
def initialize(op, root)
|
4
|
-
@op = op
|
5
|
-
@root = root
|
6
|
-
end
|
1
|
+
# frozen_string_literal: true
|
7
2
|
|
8
|
-
|
9
|
-
|
10
|
-
|
3
|
+
module RspecContracts
|
4
|
+
class Operation
|
5
|
+
attr_reader :root
|
11
6
|
|
12
|
-
|
13
|
-
|
14
|
-
|
7
|
+
def initialize(op, root)
|
8
|
+
@op = op
|
9
|
+
@root = root
|
10
|
+
end
|
11
|
+
|
12
|
+
def valid?
|
13
|
+
@op.present?
|
14
|
+
end
|
15
|
+
|
16
|
+
def ==(val)
|
17
|
+
val == @op
|
18
|
+
end
|
15
19
|
|
16
|
-
|
17
|
-
|
20
|
+
def method_missing(m, *args, &block)
|
21
|
+
@op.send(m, *args, &block)
|
22
|
+
end
|
18
23
|
end
|
19
24
|
end
|
@@ -1,17 +1,21 @@
|
|
1
|
-
|
2
|
-
class << self
|
3
|
-
def validate_path(op, method, path)
|
4
|
-
lookup_path = path.remove(RspecContracts.config.base_path)
|
5
|
-
return if operation_matches_request?(op, method, lookup_path)
|
1
|
+
# frozen_string_literal: true
|
6
2
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
3
|
+
module RspecContracts
|
4
|
+
class PathValidator
|
5
|
+
class << self
|
6
|
+
def validate_path(op, method, path)
|
7
|
+
lookup_path = path.remove(RspecContracts.config.base_path)
|
8
|
+
return if operation_matches_request?(op, method, lookup_path)
|
9
|
+
|
10
|
+
msg = "#{method.upcase} #{path} does not resolve to #{op.operation_id}"
|
11
|
+
raise RspecContracts::Error::PathValidation.new(msg) if RspecContracts.config.path_validation_mode == :raise
|
12
|
+
|
13
|
+
RspecContracts.config.logger.error "Contract validation warning: #{msg}"
|
14
|
+
end
|
12
15
|
|
13
|
-
|
14
|
-
|
16
|
+
def operation_matches_request?(op, method, path)
|
17
|
+
op == op.root.request_operation(method.to_sym, path)&.operation_object
|
18
|
+
end
|
15
19
|
end
|
16
20
|
end
|
17
21
|
end
|
@@ -1,5 +1,9 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RspecContracts
|
4
|
+
class Railtie < Rails::Railtie
|
5
|
+
initializer "rspec_contracts" do
|
6
|
+
RspecContracts.install
|
7
|
+
end
|
4
8
|
end
|
5
9
|
end
|
@@ -1,22 +1,29 @@
|
|
1
|
-
|
2
|
-
class << self
|
3
|
-
def validate_request(op, request)
|
4
|
-
body = request.body.read
|
5
|
-
parsed_body = RspecContracts.valid_json?(body) ? JSON.parse(request.body.read) : nil
|
6
|
-
op.validate_request_body(request.content_type, parsed_body, opts)
|
7
|
-
rescue OpenAPIParser::OpenAPIError => e
|
8
|
-
raise RspecContracts::Error::RequestValidation.new(e.message) if RspecContracts.config.request_validation_mode == :raise
|
1
|
+
# frozen_string_literal: true
|
9
2
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
3
|
+
module RspecContracts
|
4
|
+
class RequestValidator
|
5
|
+
class << self
|
6
|
+
def validate_request(op, request)
|
7
|
+
body = request.body.read
|
8
|
+
parsed_body = RspecContracts.valid_json?(body) ? JSON.parse(request.body.read) : nil
|
9
|
+
op.validate_request_body(request.content_type, parsed_body, opts)
|
10
|
+
rescue OpenAPIParser::OpenAPIError => e
|
11
|
+
if RspecContracts.config.request_validation_mode == :raise
|
12
|
+
raise RspecContracts::Error::RequestValidation.new(e.message)
|
13
|
+
end
|
14
|
+
|
15
|
+
formatted_for_logging = {
|
16
|
+
body: parsed_body,
|
17
|
+
headers: request.headers.to_h.select {|k, _| k.starts_with?("HTTP_") }
|
18
|
+
.transform_keys {|k| k.remove("HTTP_").downcase }
|
19
|
+
}
|
20
|
+
RspecContracts.config.logger.error "Contract validation warning: #{e.message}"
|
21
|
+
RspecContracts.config.logger.error "Request was: #{formatted_for_logging}"
|
22
|
+
end
|
17
23
|
|
18
|
-
|
19
|
-
|
24
|
+
def opts
|
25
|
+
OpenAPIParser::SchemaValidator::Options.new(coerce_value: true, datetime_coerce_class: DateTime)
|
26
|
+
end
|
20
27
|
end
|
21
28
|
end
|
22
29
|
end
|
@@ -1,16 +1,23 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RspecContracts
|
4
|
+
class ResponseValidator
|
5
|
+
class << self
|
6
|
+
def validate_response(op, resp)
|
7
|
+
op.validate_response(resp, opts(has_content: resp.content_type.present?))
|
8
|
+
rescue OpenAPIParser::OpenAPIError => e
|
9
|
+
if RspecContracts.config.response_validation_mode == :raise
|
10
|
+
raise RspecContracts::Error::ResponseValidation.new(e.message)
|
11
|
+
end
|
12
|
+
|
13
|
+
RspecContracts.config.logger.error "Contract validation warning: #{e.message}"
|
14
|
+
RspecContracts.config.logger.error "Response was: #{resp}"
|
15
|
+
end
|
11
16
|
|
12
|
-
|
13
|
-
|
17
|
+
def opts(has_content: true)
|
18
|
+
OpenAPIParser::SchemaValidator::ResponseValidateOptions.new(strict: has_content &&
|
19
|
+
RspecContracts.config.strict_response_validation)
|
20
|
+
end
|
14
21
|
end
|
15
22
|
end
|
16
23
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec_contracts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Steven Allen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-07-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -45,7 +45,7 @@ dependencies:
|
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: '0'
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
|
-
name:
|
48
|
+
name: semverse
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
50
50
|
requirements:
|
51
51
|
- - ">="
|
@@ -59,13 +59,13 @@ dependencies:
|
|
59
59
|
- !ruby/object:Gem::Version
|
60
60
|
version: '0'
|
61
61
|
- !ruby/object:Gem::Dependency
|
62
|
-
name:
|
62
|
+
name: active_model_serializers
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
64
64
|
requirements:
|
65
65
|
- - ">="
|
66
66
|
- !ruby/object:Gem::Version
|
67
67
|
version: '0'
|
68
|
-
type: :
|
68
|
+
type: :development
|
69
69
|
prerelease: false
|
70
70
|
version_requirements: !ruby/object:Gem::Requirement
|
71
71
|
requirements:
|
@@ -87,7 +87,7 @@ dependencies:
|
|
87
87
|
- !ruby/object:Gem::Version
|
88
88
|
version: '0'
|
89
89
|
- !ruby/object:Gem::Dependency
|
90
|
-
name:
|
90
|
+
name: bundler
|
91
91
|
requirement: !ruby/object:Gem::Requirement
|
92
92
|
requirements:
|
93
93
|
- - ">="
|
@@ -101,7 +101,7 @@ dependencies:
|
|
101
101
|
- !ruby/object:Gem::Version
|
102
102
|
version: '0'
|
103
103
|
- !ruby/object:Gem::Dependency
|
104
|
-
name:
|
104
|
+
name: byebug
|
105
105
|
requirement: !ruby/object:Gem::Requirement
|
106
106
|
requirements:
|
107
107
|
- - ">="
|
@@ -115,7 +115,7 @@ dependencies:
|
|
115
115
|
- !ruby/object:Gem::Version
|
116
116
|
version: '0'
|
117
117
|
- !ruby/object:Gem::Dependency
|
118
|
-
name:
|
118
|
+
name: factory_bot_rails
|
119
119
|
requirement: !ruby/object:Gem::Requirement
|
120
120
|
requirements:
|
121
121
|
- - ">="
|
@@ -129,7 +129,7 @@ dependencies:
|
|
129
129
|
- !ruby/object:Gem::Version
|
130
130
|
version: '0'
|
131
131
|
- !ruby/object:Gem::Dependency
|
132
|
-
name:
|
132
|
+
name: guard
|
133
133
|
requirement: !ruby/object:Gem::Requirement
|
134
134
|
requirements:
|
135
135
|
- - ">="
|
@@ -143,7 +143,7 @@ dependencies:
|
|
143
143
|
- !ruby/object:Gem::Version
|
144
144
|
version: '0'
|
145
145
|
- !ruby/object:Gem::Dependency
|
146
|
-
name: guard
|
146
|
+
name: guard-bundler
|
147
147
|
requirement: !ruby/object:Gem::Requirement
|
148
148
|
requirements:
|
149
149
|
- - ">="
|
@@ -157,7 +157,7 @@ dependencies:
|
|
157
157
|
- !ruby/object:Gem::Version
|
158
158
|
version: '0'
|
159
159
|
- !ruby/object:Gem::Dependency
|
160
|
-
name: guard-
|
160
|
+
name: guard-rspec
|
161
161
|
requirement: !ruby/object:Gem::Requirement
|
162
162
|
requirements:
|
163
163
|
- - ">="
|
@@ -171,7 +171,7 @@ dependencies:
|
|
171
171
|
- !ruby/object:Gem::Version
|
172
172
|
version: '0'
|
173
173
|
- !ruby/object:Gem::Dependency
|
174
|
-
name:
|
174
|
+
name: overcommit
|
175
175
|
requirement: !ruby/object:Gem::Requirement
|
176
176
|
requirements:
|
177
177
|
- - ">="
|
@@ -185,7 +185,7 @@ dependencies:
|
|
185
185
|
- !ruby/object:Gem::Version
|
186
186
|
version: '0'
|
187
187
|
- !ruby/object:Gem::Dependency
|
188
|
-
name:
|
188
|
+
name: pry-rescue
|
189
189
|
requirement: !ruby/object:Gem::Requirement
|
190
190
|
requirements:
|
191
191
|
- - ">="
|
@@ -199,7 +199,7 @@ dependencies:
|
|
199
199
|
- !ruby/object:Gem::Version
|
200
200
|
version: '0'
|
201
201
|
- !ruby/object:Gem::Dependency
|
202
|
-
name: pry-
|
202
|
+
name: pry-stack_explorer
|
203
203
|
requirement: !ruby/object:Gem::Requirement
|
204
204
|
requirements:
|
205
205
|
- - ">="
|
@@ -216,16 +216,16 @@ dependencies:
|
|
216
216
|
name: rubocop
|
217
217
|
requirement: !ruby/object:Gem::Requirement
|
218
218
|
requirements:
|
219
|
-
- - "
|
219
|
+
- - "~>"
|
220
220
|
- !ruby/object:Gem::Version
|
221
|
-
version:
|
221
|
+
version: 0.83.0
|
222
222
|
type: :development
|
223
223
|
prerelease: false
|
224
224
|
version_requirements: !ruby/object:Gem::Requirement
|
225
225
|
requirements:
|
226
|
-
- - "
|
226
|
+
- - "~>"
|
227
227
|
- !ruby/object:Gem::Version
|
228
|
-
version:
|
228
|
+
version: 0.83.0
|
229
229
|
- !ruby/object:Gem::Dependency
|
230
230
|
name: rubocop-rspec
|
231
231
|
requirement: !ruby/object:Gem::Requirement
|
@@ -244,16 +244,16 @@ dependencies:
|
|
244
244
|
name: simplecov
|
245
245
|
requirement: !ruby/object:Gem::Requirement
|
246
246
|
requirements:
|
247
|
-
- - "
|
247
|
+
- - "<"
|
248
248
|
- !ruby/object:Gem::Version
|
249
|
-
version: '0'
|
249
|
+
version: '0.18'
|
250
250
|
type: :development
|
251
251
|
prerelease: false
|
252
252
|
version_requirements: !ruby/object:Gem::Requirement
|
253
253
|
requirements:
|
254
|
-
- - "
|
254
|
+
- - "<"
|
255
255
|
- !ruby/object:Gem::Version
|
256
|
-
version: '0'
|
256
|
+
version: '0.18'
|
257
257
|
- !ruby/object:Gem::Dependency
|
258
258
|
name: simplecov-console
|
259
259
|
requirement: !ruby/object:Gem::Requirement
|