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