proto_pharm 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/rspec.yml +35 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +3 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +113 -0
- data/LICENSE.txt +21 -0
- data/README.md +238 -0
- data/Rakefile +8 -0
- data/bin/console +11 -0
- data/bin/regen_examples +7 -0
- data/bin/release +16 -0
- data/bin/setup +8 -0
- data/lib/proto_pharm.rb +47 -0
- data/lib/proto_pharm/action_stub.rb +57 -0
- data/lib/proto_pharm/adapter.rb +13 -0
- data/lib/proto_pharm/api.rb +31 -0
- data/lib/proto_pharm/configuration.rb +11 -0
- data/lib/proto_pharm/errors.rb +21 -0
- data/lib/proto_pharm/grpc_stub_adapter.rb +24 -0
- data/lib/proto_pharm/grpc_stub_adapter/mock_stub.rb +74 -0
- data/lib/proto_pharm/introspection.rb +18 -0
- data/lib/proto_pharm/introspection/rpc_inspector.rb +58 -0
- data/lib/proto_pharm/introspection/service_resolver.rb +24 -0
- data/lib/proto_pharm/matchers/hash_argument_matcher.rb +43 -0
- data/lib/proto_pharm/matchers/request_including_matcher.rb +39 -0
- data/lib/proto_pharm/operation_stub.rb +30 -0
- data/lib/proto_pharm/request_pattern.rb +29 -0
- data/lib/proto_pharm/request_stub.rb +67 -0
- data/lib/proto_pharm/response.rb +36 -0
- data/lib/proto_pharm/response_sequence.rb +40 -0
- data/lib/proto_pharm/rspec.rb +28 -0
- data/lib/proto_pharm/rspec/action_stub_builder.rb +37 -0
- data/lib/proto_pharm/rspec/action_stub_proxy.rb +58 -0
- data/lib/proto_pharm/rspec/dsl.rb +15 -0
- data/lib/proto_pharm/rspec/matchers/have_received_rpc.rb +72 -0
- data/lib/proto_pharm/stub_components/failure_response.rb +30 -0
- data/lib/proto_pharm/stub_registry.rb +36 -0
- data/lib/proto_pharm/version.rb +5 -0
- data/proto_pharm.gemspec +38 -0
- data/rakelib/regen_examples.rake +11 -0
- metadata +248 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProtoPharm
|
4
|
+
module Matchers
|
5
|
+
# Base class for Hash matchers
|
6
|
+
# https://github.com/rspec/rspec-mocks/blob/master/lib/rspec/mocks/argument_matchers.rb
|
7
|
+
class HashArgumentMatcher
|
8
|
+
def self.stringify_keys!(arg, options = {})
|
9
|
+
case arg
|
10
|
+
when Array
|
11
|
+
arg.map do |elem|
|
12
|
+
options[:deep] ? stringify_keys!(elem, options) : elem
|
13
|
+
end
|
14
|
+
when Hash
|
15
|
+
Hash[
|
16
|
+
*arg.map do |key, value|
|
17
|
+
k = key.is_a?(Symbol) ? key.to_s : key
|
18
|
+
v = (options[:deep] ? stringify_keys!(value, options) : value)
|
19
|
+
[k, v]
|
20
|
+
end.inject([]) { |r, x| r + x }]
|
21
|
+
else
|
22
|
+
arg
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(expected)
|
27
|
+
@expected = Hash[
|
28
|
+
ProtoPharm::Matchers::HashArgumentMatcher.stringify_keys!(expected, deep: true).sort,
|
29
|
+
]
|
30
|
+
end
|
31
|
+
|
32
|
+
def ==(_actual, &block)
|
33
|
+
@expected.all?(&block)
|
34
|
+
rescue NoMethodError
|
35
|
+
false
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.from_rspec_matcher(matcher)
|
39
|
+
new(matcher.instance_variable_get(:@expected))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "proto_pharm/matchers/hash_argument_matcher"
|
4
|
+
|
5
|
+
module ProtoPharm
|
6
|
+
module Matchers
|
7
|
+
class RequestIncludingMatcher < HashArgumentMatcher
|
8
|
+
def ==(actual)
|
9
|
+
if actual.respond_to?(:to_h)
|
10
|
+
actual = actual.to_h
|
11
|
+
end
|
12
|
+
|
13
|
+
actual = Hash[ProtoPharm::Matchers::HashArgumentMatcher.stringify_keys!(actual, deep: true)]
|
14
|
+
super { |key, value| inner_including(value, key, actual) }
|
15
|
+
rescue NoMethodError
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def inner_including(expect, key, actual)
|
22
|
+
if actual.key?(key)
|
23
|
+
actual_value = actual[key]
|
24
|
+
if expect.is_a?(Hash)
|
25
|
+
RequestIncludingMatcher.new(expect) == actual_value
|
26
|
+
else
|
27
|
+
expect === actual_value
|
28
|
+
end
|
29
|
+
else
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def inspect
|
35
|
+
"reqeust_including(#{@expected.inspect})"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProtoPharm
|
4
|
+
class OperationStub
|
5
|
+
attr_reader :response_proc, :metadata, :trailing_metadata, :deadline
|
6
|
+
|
7
|
+
# @param metadata [Hash] Any metadata passed into the GRPC request
|
8
|
+
# @param deadline [Time] The deadline set on the GRPC request
|
9
|
+
# @yieldreturn [*] The stubbed value or error expected to be returned from the request
|
10
|
+
def initialize(metadata: nil, deadline: nil, &response_proc)
|
11
|
+
@response_proc = response_proc
|
12
|
+
@metadata = metadata
|
13
|
+
@deadline = deadline
|
14
|
+
|
15
|
+
# TODO: support stubbing
|
16
|
+
@trailing_metadata = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
# Calls the block given upon instantiation and returns the result
|
20
|
+
def response
|
21
|
+
response_proc.call
|
22
|
+
end
|
23
|
+
alias_method :execute, :response
|
24
|
+
|
25
|
+
# TODO: support stubbing
|
26
|
+
def cancelled?
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProtoPharm
|
4
|
+
class RequestPattern
|
5
|
+
attr_reader :path, :request, :block
|
6
|
+
|
7
|
+
# @param path [String]
|
8
|
+
def initialize(path)
|
9
|
+
@path = path
|
10
|
+
@block = nil
|
11
|
+
@request = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def with(request = nil, &block)
|
15
|
+
if request.nil? && !block_given?
|
16
|
+
raise ArgumentError, "#with method invoked with no arguments. Either options request or block must be specified."
|
17
|
+
end
|
18
|
+
|
19
|
+
@request = request
|
20
|
+
@block = block
|
21
|
+
end
|
22
|
+
|
23
|
+
def match?(match_path, match_request)
|
24
|
+
path == match_path &&
|
25
|
+
(request.nil? || request == match_request) &&
|
26
|
+
(block.nil? || block.call(match_path))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "proto_pharm/request_pattern"
|
4
|
+
require "proto_pharm/response"
|
5
|
+
require "proto_pharm/response_sequence"
|
6
|
+
require "proto_pharm/errors"
|
7
|
+
|
8
|
+
module ProtoPharm
|
9
|
+
class RequestStub
|
10
|
+
attr_reader :received_requests, :request_pattern, :response_sequence
|
11
|
+
|
12
|
+
delegate :path, to: :request_pattern, allow_nil: true
|
13
|
+
|
14
|
+
# @param path [String] gRPC path like /${service_name}/${method_name}
|
15
|
+
def initialize(path)
|
16
|
+
@request_pattern = RequestPattern.new(path)
|
17
|
+
@response_sequence = []
|
18
|
+
@received_requests = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def with(request = nil, &block)
|
22
|
+
@request_pattern.with(request, &block)
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_return(*values)
|
27
|
+
responses = [*values].flatten.map { |v| Response::Value.new(v) }
|
28
|
+
@response_sequence << ProtoPharm::ResponsesSequence.new(responses)
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_raise(*exceptions)
|
33
|
+
responses = [*exceptions].flatten.map { |e| Response::ExceptionValue.new(e) }
|
34
|
+
@response_sequence << ProtoPharm::ResponsesSequence.new(responses)
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def response
|
39
|
+
if @response_sequence.empty?
|
40
|
+
raise ProtoPharm::NoResponseError, "Must be set some values by using ProtoPharm::RequestStub#to_run"
|
41
|
+
elsif @response_sequence.size == 1
|
42
|
+
@response_sequence.first.next
|
43
|
+
else
|
44
|
+
if @response_sequence.first.end?
|
45
|
+
@response_sequence.shift
|
46
|
+
end
|
47
|
+
|
48
|
+
@response_sequence.first.next
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def received!(request)
|
53
|
+
@received_requests << request
|
54
|
+
end
|
55
|
+
|
56
|
+
def received_count
|
57
|
+
received_requests.size
|
58
|
+
end
|
59
|
+
|
60
|
+
# @param path [String]
|
61
|
+
# @param request [Object]
|
62
|
+
# @return [Bool]
|
63
|
+
def match?(path, request)
|
64
|
+
@request_pattern.match?(path, request)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProtoPharm
|
4
|
+
module Response
|
5
|
+
class ExceptionValue
|
6
|
+
attr_reader :exception
|
7
|
+
|
8
|
+
def initialize(exception)
|
9
|
+
@exception = case exception
|
10
|
+
when String
|
11
|
+
StandardError.new(exception)
|
12
|
+
when Class
|
13
|
+
exception.new("Exception from ProtoPharm")
|
14
|
+
when Exception
|
15
|
+
exception
|
16
|
+
else
|
17
|
+
raise ArgumentError.new(message: "Invalid exception class: #{exception.class}")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def evaluate
|
22
|
+
raise @exception.dup
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Value
|
27
|
+
def initialize(value)
|
28
|
+
@value = value
|
29
|
+
end
|
30
|
+
|
31
|
+
def evaluate
|
32
|
+
@value.dup
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProtoPharm
|
4
|
+
class ResponsesSequence
|
5
|
+
attr_accessor :repeat
|
6
|
+
attr_reader :responses
|
7
|
+
|
8
|
+
def initialize(responses)
|
9
|
+
@repeat = 1
|
10
|
+
@responses = responses
|
11
|
+
@current = 0
|
12
|
+
@last = @responses.length - 1
|
13
|
+
end
|
14
|
+
|
15
|
+
def end?
|
16
|
+
@repeat == 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def next
|
20
|
+
if @repeat > 0
|
21
|
+
response = @responses[@current]
|
22
|
+
next_pos
|
23
|
+
response
|
24
|
+
else
|
25
|
+
@responses.last
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def next_pos
|
32
|
+
if @last == @current
|
33
|
+
@current = 0
|
34
|
+
@repeat -= 1
|
35
|
+
else
|
36
|
+
@current += 1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rspec"
|
4
|
+
|
5
|
+
require_relative "../proto_pharm"
|
6
|
+
|
7
|
+
require_relative "rspec/action_stub_proxy"
|
8
|
+
require_relative "rspec/action_stub_builder"
|
9
|
+
|
10
|
+
require_relative "rspec/dsl"
|
11
|
+
require_relative "rspec/matchers/have_received_rpc"
|
12
|
+
|
13
|
+
RSpec.configure do |config|
|
14
|
+
config.before(:suite) do
|
15
|
+
ProtoPharm.enable!
|
16
|
+
end
|
17
|
+
|
18
|
+
config.after(:suite) do
|
19
|
+
ProtoPharm.disable!
|
20
|
+
end
|
21
|
+
|
22
|
+
config.after(:each) do
|
23
|
+
ProtoPharm.reset!
|
24
|
+
end
|
25
|
+
|
26
|
+
config.include ProtoPharm::RSpec::DSL
|
27
|
+
config.include ProtoPharm::RSpec::Matchers
|
28
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProtoPharm
|
4
|
+
module RSpec
|
5
|
+
class ActionStubBuilder
|
6
|
+
attr_accessor :grpc_service, :action_stub_proxy
|
7
|
+
|
8
|
+
delegate :rpc_action, :expectations, to: :action_stub_proxy
|
9
|
+
|
10
|
+
def initialize(grpc_service)
|
11
|
+
@grpc_service = grpc_service
|
12
|
+
end
|
13
|
+
|
14
|
+
def to(action_stub_proxy)
|
15
|
+
@action_stub_proxy = action_stub_proxy
|
16
|
+
|
17
|
+
ProtoPharm.stub_registry.register_request_stub(action_stub)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def action_stub
|
23
|
+
@action_stub ||= ActionStub.new(grpc_service, rpc_action).tap do |stub|
|
24
|
+
expectations.each do |ex|
|
25
|
+
if ex.kwargs.blank?
|
26
|
+
stub.public_send(ex.method, *ex.args)
|
27
|
+
elsif ex.args.blank?
|
28
|
+
stub.public_send(ex.method, **ex.kwargs)
|
29
|
+
else
|
30
|
+
stub.public_send(ex.method, *ex.args, **ex.kwargs)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProtoPharm
|
4
|
+
module RSpec
|
5
|
+
class ActionStubProxy
|
6
|
+
attr_reader :rpc_action, :expectations
|
7
|
+
|
8
|
+
def initialize(rpc_action)
|
9
|
+
@rpc_action = rpc_action
|
10
|
+
@expectations = []
|
11
|
+
end
|
12
|
+
|
13
|
+
# Proxies ActionStub#with
|
14
|
+
def with(*args, **kwargs)
|
15
|
+
expectations << Expectation.new(:with, args, kwargs)
|
16
|
+
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
# Proxies ActionStub#to_return
|
21
|
+
def and_return(*args, **kwargs)
|
22
|
+
expectations << Expectation.new(:to_return, args, kwargs)
|
23
|
+
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
# Proxies ActionStub#to_raise
|
28
|
+
def and_raise(*args, **kwargs)
|
29
|
+
expectations << Expectation.new(:to_raise, args, kwargs)
|
30
|
+
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
# Proxies ActionStub#to_fail_with
|
35
|
+
def and_fail_with(*args, **kwargs)
|
36
|
+
expectations << Expectation.new(:to_fail_with, args, kwargs)
|
37
|
+
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def and_fail
|
42
|
+
expectations << Expectation.new(:to_fail, [], {})
|
43
|
+
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
class Expectation
|
48
|
+
attr_reader :method, :args, :kwargs
|
49
|
+
|
50
|
+
def initialize(method, args, kwargs)
|
51
|
+
@method = method
|
52
|
+
@args = args
|
53
|
+
@kwargs = kwargs
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProtoPharm
|
4
|
+
module RSpec
|
5
|
+
module Matchers
|
6
|
+
extend ::RSpec::Matchers::DSL
|
7
|
+
|
8
|
+
matcher :have_received_rpc do |endpoint_reference| # rubocop:disable Metrics/BlockLength
|
9
|
+
include Introspection
|
10
|
+
|
11
|
+
attr_reader :service_reference, :endpoint_reference, :expected_proto, :expected_kwargs
|
12
|
+
|
13
|
+
description { "receive rpc #{endpoint_reference.inspect}#{with_parameters_description}" }
|
14
|
+
|
15
|
+
match do |service_reference|
|
16
|
+
@service_reference = service_reference
|
17
|
+
@endpoint_reference = endpoint_reference
|
18
|
+
|
19
|
+
raise RpcNotStubbedError, "RPC '#{grpc_path}' has not been stubbed. Stub it with stub_grpc_action before asserting." unless matching_request_stubs.any?
|
20
|
+
|
21
|
+
if expected_request_proto.blank?
|
22
|
+
expect(received_request_stubs.size).to be > 0
|
23
|
+
else
|
24
|
+
expect(received_request_stubs.flat_map(&:received_requests)).to include expected_request_proto
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
chain :with do |expected_proto = nil, **expected_kwargs|
|
29
|
+
raise ArgumentError, "assert only with proto or keyword arguments, not both" if expected_proto.present? && expected_kwargs.present?
|
30
|
+
raise ArgumentError, "cannot assert expected arguments multiple times in the same expectation" if (@expected_proto || @expected_kwargs).present?
|
31
|
+
|
32
|
+
@expected_proto = expected_proto
|
33
|
+
@expected_kwargs = expected_kwargs
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
delegate :grpc_path, :input_type, to: :endpoint
|
39
|
+
|
40
|
+
def endpoint
|
41
|
+
@endpoint ||= inspect_rpc(service_reference, endpoint_reference)
|
42
|
+
end
|
43
|
+
|
44
|
+
def received_request_stubs
|
45
|
+
matching_request_stubs.select { |stub| stub.received_count > 0 }
|
46
|
+
end
|
47
|
+
|
48
|
+
def matching_request_stubs
|
49
|
+
@matching_request_stubs ||= ProtoPharm.stub_registry.all_requests_matching(grpc_path, expected_request_proto)
|
50
|
+
end
|
51
|
+
|
52
|
+
def expected_request_proto
|
53
|
+
return if expected_proto.nil? && expected_kwargs.nil?
|
54
|
+
|
55
|
+
ensure_valid_proto_type!(expected_proto)
|
56
|
+
|
57
|
+
@expected_request_proto ||= expected_proto.presence || input_type.new(**expected_kwargs)
|
58
|
+
end
|
59
|
+
|
60
|
+
def ensure_valid_proto_type!(expected_proto)
|
61
|
+
raise InvalidProtoType, "Invalid proto type #{expected_proto.class} for #{grpc_path}, expected #{input_type}" if expected_proto.present? && expected_proto.class != input_type
|
62
|
+
end
|
63
|
+
|
64
|
+
def with_parameters_description
|
65
|
+
return if expected_request_proto.blank?
|
66
|
+
|
67
|
+
" with request #{expected_request_proto.inspect}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|