proto_pharm 0.6.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.
- 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
|