rspec-twirp 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2f0ca51df29824ba1c1f2d1944ca94119501b3ec6366d09144f077bd99c92f2
4
- data.tar.gz: 513bdae144ce76ecec652954cff14d3f8467037f6ec706ab67181be0ac036d61
3
+ metadata.gz: 2634a27685c93991e30afab03cf80cb5fe650047a2b91342eeadff69a7bb920e
4
+ data.tar.gz: f87529044e688df0b546e65cd527989842e983576bf62a70ea7aabf72d920d09
5
5
  SHA512:
6
- metadata.gz: 4a4ebdce6d3d96826eca31484d1d4c22dc5b9e07fb109bea659315c5cfc7265f497f8d0a0bb498115e654ee139b232d6c6eb74bfd6da03a325a44411e636486c
7
- data.tar.gz: 4342593db5f6e34db3fb0c3829a04ff1b8bc5806960e6b0efb144f650c38b80b08559b7f1f1a616a1c36712e2fb4a1c9cb963e457accd9451ac1ad3a6314096f
6
+ metadata.gz: 01e6fbab6f8649f6c8dbcc8df72269699e099eb1d924d6ff6656781d5339d060c6138e465a5c43b5ec2a9320614d7924bed1c79eac088c9c10f881d99c6588d7
7
+ data.tar.gz: ce566d5fd9e1e91cca7a100d205e4a1b0615c743f808383d5180bd007049aeaaaf1298bfe382ad6e0385404dd3c04fadb5980ac53fbc9de54c9aeee335b4834b
@@ -14,9 +14,22 @@ RSpec::Matchers.define :be_a_twirp_error do |*matchers, **meta_matcher|
14
14
 
15
15
  @fail_msg = "Expected #{actual} to have code: #{matcher.inspect}, found #{actual.code}"
16
16
  return false unless actual.code == matcher
17
+ when Integer
18
+ # match http status code
19
+
20
+ if code = Twirp::ERROR_CODES_TO_HTTP_STATUS.key(matcher)
21
+ @fail_msg = "Expected #{actual} to have status: #{matcher}, found #{actual.code}"
22
+ return false unless actual.code == code
23
+ else
24
+ raise ArgumentError, "invalid error status code: #{matcher}"
25
+ end
26
+ when Twirp::Error
27
+ # match instance
28
+
29
+ @fail_msg = "Expected #{actual} to equal #{matcher}"
30
+ return false unless values_match?(matcher.to_h, actual.to_h)
17
31
  else
18
32
  # match msg
19
-
20
33
  @fail_msg = "Expected #{actual} to have msg: #{matcher.inspect}, found #{actual.msg}"
21
34
  return false unless values_match?(matcher, actual.msg)
22
35
  end
@@ -0,0 +1,16 @@
1
+ require "rspec/twirp/mock_client"
2
+ require "rspec/twirp/mock_connection"
3
+
4
+ module RSpec
5
+ module Twirp
6
+ module Helpers
7
+ def mock_twirp_connection(...)
8
+ RSpec::Twirp.mock_connection(...)
9
+ end
10
+
11
+ def mock_twirp_client(...)
12
+ RSpec::Twirp.mock_client(...)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,169 @@
1
+ # expect {
2
+ # do_the_thing
3
+ # }.to make_twirp_request(client | service | rpc_method).with(request | attrs)
4
+ #
5
+ # expect(client).to make_twirp_request(rpc_method).with(...)
6
+
7
+ RSpec::Matchers.define :make_twirp_request do |*matchers|
8
+ chain :with do |input_matcher = nil|
9
+ @input_matcher = if input_matcher
10
+ if input_matcher.is_a?(Google::Protobuf::MessageExts)
11
+ defaults = input_matcher.class.new.to_h
12
+ hash_form = input_matcher.to_h.reject {|k,v| v == defaults[k] }
13
+
14
+ ->(input) do
15
+ if input.is_a?(Google::Protobuf::MessageExts)
16
+ values_match?(input_matcher, input)
17
+ else
18
+ values_match?(include(**hash_form), input.to_h)
19
+ end
20
+ end
21
+ elsif input_matcher.is_a?(Class) && input_matcher < Google::Protobuf::MessageExts
22
+ ->(input) { values_match?(be_a(input_matcher), input) }
23
+ elsif input_matcher.is_a?(Hash)
24
+ ->(input) { values_match?(include(**input_matcher), input.to_h) }
25
+ else
26
+ raise TypeError, "Expected an input_matcher of type `Google::Protobuf::MessageExts`, found #{input_matcher.class}"
27
+ end
28
+ end
29
+ end
30
+
31
+ chain(:and_call_original) { @and_call_original = true }
32
+ chain(:and_return) do |arg|
33
+ @and_return = case arg
34
+ when Google::Protobuf::MessageExts
35
+ Twirp::ClientResp.new(arg, nil)
36
+ when Twirp::Error
37
+ Twirp::ClientResp.new(nil, arg)
38
+ when Class
39
+ if arg < Google::Protobuf::MessageExts
40
+ Twirp::ClientResp.new(arg.new, nil)
41
+ end
42
+ end
43
+
44
+ unless @and_return
45
+ raise TypeError, "Expected type `Google::Protobuf::MessageExts`, found #{arg}"
46
+ end
47
+ end
48
+
49
+ supports_block_expectations
50
+
51
+ match do |client_or_block|
52
+ @input_matcher ||= ->(*){ true }
53
+
54
+ if @and_call_original && @and_return
55
+ raise ArgumentError, "use `and_call_original` or `and_return`, but not both"
56
+ end
57
+
58
+ if client_or_block.is_a? Proc
59
+ RSpec::Mocks.with_temporary_scope do
60
+ match_block_request(client_or_block, matchers)
61
+ end
62
+ elsif client_or_block.is_a?(Twirp::Client)
63
+ match_client_request(client_or_block, matchers)
64
+ else
65
+ raise ArgumentError, "Expected Twirp::Client or block, found: #{client_or_block}"
66
+ end
67
+ end
68
+
69
+ def match_block_request(block, matchers)
70
+ expected_client = be_a(Twirp::Client)
71
+ expected_service = anything
72
+ expected_rpc_name = anything
73
+
74
+ matchers.each do |matcher|
75
+ case matcher
76
+ when Twirp::Client
77
+ expected_client = be(matcher)
78
+ when Class
79
+ if matcher <= Twirp::Client
80
+ expected_client = be_a(matcher)
81
+ elsif matcher <= Twirp::Service
82
+ expected_service = matcher.service_full_name
83
+ else
84
+ raise TypeError
85
+ end
86
+ when String
87
+ expected_rpc_name = be(matcher.to_sym)
88
+ when Symbol
89
+ expected_rpc_name = be(matcher)
90
+ when Regexp
91
+ expected_rpc_name = matcher
92
+ end
93
+ end
94
+
95
+ @twirp_request_made = false
96
+
97
+ # stub pre-existing client instances
98
+ ObjectSpace.each_object(Twirp::Client) do |client|
99
+ if expected_client === client && (expected_service === client.class.service_full_name)
100
+ stub_client(client, expected_rpc_name)
101
+ end
102
+ end
103
+
104
+ # stub future client instances
105
+ ObjectSpace.each_object(Twirp::Client.singleton_class).select do |obj|
106
+ obj < Twirp::Client && obj != Twirp::ClientJSON
107
+ end.each do |client_type|
108
+ next unless client_type.name && expected_service === client_type.service_full_name
109
+
110
+ allow(client_type).to receive(:new).and_wrap_original do |orig, *args, **kwargs|
111
+ orig.call(*args, **kwargs).tap do |client|
112
+ if expected_client === client
113
+ stub_client(client, expected_rpc_name)
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ block.call
120
+
121
+ @twirp_request_made
122
+ end
123
+
124
+ def stub_client(client, rpc_matcher)
125
+ allow(client).to receive(:rpc).and_wrap_original do |orig, rpc_name, input, req_opts|
126
+ if values_match?(rpc_matcher, rpc_name) && @input_matcher.call(input)
127
+ @twirp_request_made = true
128
+ end
129
+
130
+ if @and_call_original
131
+ orig.call(rpc_name, input, req_opts)
132
+ elsif @and_return
133
+ @and_return
134
+ end
135
+ end
136
+ end
137
+
138
+ def match_client_request(client, matchers)
139
+ rpc_name = matchers.first.to_sym if matchers.any?
140
+
141
+ if rpc_name
142
+ rpc_info = client.class.rpcs.values.find do |x|
143
+ x[:rpc_method] == rpc_name || x[:ruby_method] == rpc_name
144
+ end
145
+
146
+ raise ArgumentError, "invalid rpc method: #{rpc_name}" unless rpc_info
147
+
148
+ msg = "Expected #{client} to make a twirp request to #{rpc_name}"
149
+ rpc_matcher = eq(rpc_info[:rpc_method])
150
+ else
151
+ rpc_info = nil
152
+ msg = "Expected #{client} to make a twirp request"
153
+ rpc_matcher = anything
154
+ end
155
+
156
+ expect(client).to receive(:rpc) do |rpc_name, input, req_opts|
157
+ expect(rpc_name).to match(rpc_matcher), msg
158
+ expect(@input_matcher.call(input)).to be(true), msg
159
+ end
160
+ end
161
+
162
+ description do
163
+ "make a Twirp request"
164
+ end
165
+
166
+ failure_message { @fail_msg || super() }
167
+ end
168
+
169
+ RSpec::Matchers.alias_matcher :make_a_twirp_request, :make_twirp_request
@@ -0,0 +1,61 @@
1
+ RSpec::Matchers.define :be_a_twirp_message do |type = nil, **attrs|
2
+ match do |actual|
3
+ # ensure type is a valid twirp message type
4
+ if type && !(type < Google::Protobuf::MessageExts)
5
+ raise TypeError, "Expected `type` to be a Google::Protobuf::MessageExts, found: #{type}"
6
+ end
7
+
8
+ @fail_msg = "Expected a Twirp message, found #{actual}"
9
+ return false unless actual.is_a?(Google::Protobuf::MessageExts)
10
+
11
+ # match expected message type
12
+ @fail_msg = "Expected a Twirp message of type #{type}, found #{actual.class}"
13
+ return false if type && actual.class != type
14
+
15
+ return true if attrs.empty?
16
+
17
+ # sanity check inputs
18
+ validate_types(attrs, actual.class)
19
+
20
+ # match attributes which are present
21
+ attrs.each do |attr_name, expected_attr|
22
+ actual_attr = actual.send(attr_name)
23
+
24
+ @fail_msg = "Expected #{actual} to have #{attr_name}: #{expected_attr.inspect}, found #{actual_attr}"
25
+ return false unless values_match?(expected_attr, actual_attr)
26
+ end
27
+
28
+ true
29
+ end
30
+
31
+ description do
32
+ type ? "a #{type} Twirp message" : "a Twirp message"
33
+ end
34
+
35
+ failure_message { @fail_msg }
36
+
37
+ private
38
+
39
+ def validate_types(attrs, klass)
40
+ # check names and types of attrs by constructing an actual proto object
41
+ discrete_attrs = attrs.transform_values do |attr|
42
+ case attr
43
+ when Regexp
44
+ attr.inspect
45
+ when Range
46
+ attr.first
47
+ when RSpec::Matchers::BuiltIn::BaseMatcher
48
+ # no good substitute, so skip attr and hope for the best
49
+ nil
50
+ else
51
+ attr
52
+ end
53
+ end.compact
54
+
55
+ klass.new(**discrete_attrs)
56
+ rescue Google::Protobuf::TypeError => e
57
+ raise TypeError, e.message
58
+ end
59
+ end
60
+
61
+ RSpec::Matchers.alias_matcher :a_twirp_message, :be_a_twirp_message
@@ -0,0 +1,59 @@
1
+ # mock_twirp_client(Twirp::Client, { rpc => response } )
2
+
3
+ module RSpec
4
+ module Twirp
5
+ def mock_client(client, **responses)
6
+ unless client.is_a?(Class) && client < ::Twirp::Client
7
+ raise ArgumentError, "Expected Twirp::Client, found: #{client.class}"
8
+ end
9
+
10
+ rpcs = client.rpcs.values
11
+
12
+ client_instance = client.new(mock_connection)
13
+
14
+ unless responses.empty?
15
+ # validate input
16
+ responses.transform_keys! do |rpc_name|
17
+ rpc_info = rpcs.find do |x|
18
+ x[:rpc_method] == rpc_name || x[:ruby_method] == rpc_name
19
+ end
20
+
21
+ unless rpc_info
22
+ raise ArgumentError, "invalid rpc method: #{rpc_name}"
23
+ end
24
+
25
+ rpc_info[:rpc_method]
26
+ end
27
+
28
+ # repackage responses
29
+ client_responses = {}
30
+ responses.each do |rpc_method, response|
31
+ if response.is_a?(Hash)
32
+ response = client.rpcs[rpc_method.to_s][:output_class].new(**response)
33
+ end
34
+
35
+ client_responses[rpc_method] = RSpec::Twirp.generate_client_response(response)
36
+ end
37
+
38
+ # mock
39
+ rpcs.each do |info|
40
+ response = client_responses[info[:rpc_method]]
41
+
42
+ if response
43
+ allow(client_instance).to receive(:rpc).with(
44
+ info[:rpc_method],
45
+ any_args,
46
+ ).and_return(response)
47
+ else
48
+ allow(client_instance).to receive(:rpc).with(
49
+ info[:rpc_method],
50
+ any_args,
51
+ ).and_raise(::RSpec::Mocks::MockExpectationError)
52
+ end
53
+ end
54
+ end
55
+
56
+ client_instance
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,54 @@
1
+ # GoodbyeClient.new(mock_twirp_connection)
2
+
3
+ module RSpec
4
+ module Twirp
5
+ extend self
6
+
7
+ def mock_connection(response = nil)
8
+ if block_given? && response
9
+ raise ArgumentError, "can not specify both block and response"
10
+ end
11
+
12
+ Faraday.new do |conn|
13
+ conn.adapter :test do |stub|
14
+ stub.post(/.*/) do |env|
15
+ response = yield(env) if block_given?
16
+ response ||= {}
17
+
18
+ if response.is_a?(Hash)
19
+ # create default response
20
+
21
+ # determine which client would make this rpc call
22
+ service_full_name, rpc_method = env.url.path.split("/").last(2)
23
+ client = ObjectSpace.each_object(::Twirp::Client.singleton_class).find do |client|
24
+ next unless client.name
25
+
26
+ client.service_full_name == service_full_name && client.rpcs.key?(rpc_method)
27
+ end
28
+
29
+ unless client
30
+ raise TypeError, "could not determine Twirp::Client for: #{env.url.path}"
31
+ end
32
+
33
+ response = client.rpcs[rpc_method][:output_class].new(**response)
34
+ end
35
+
36
+ res = RSpec::Twirp.generate_client_response(response)
37
+
38
+ if res.data
39
+ status = 200
40
+ headers = { "Content-Type" => ::Twirp::Encoding::PROTO }
41
+ body = res.data.to_proto
42
+ else
43
+ status = ::Twirp::ERROR_CODES_TO_HTTP_STATUS[res.error.code]
44
+ headers = { "Content-Type" => ::Twirp::Encoding::JSON } # errors are always JSON
45
+ body = ::Twirp::Encoding.encode_json(res.error.to_h)
46
+ end
47
+
48
+ [ status, headers, body ]
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,30 +1,50 @@
1
1
  RSpec::Matchers.define :be_a_twirp_response do |type = nil, **attrs|
2
+ chain :with_error do |*matchers, **meta_matchers|
3
+ # code, msg, meta
4
+ @with_error = [ matchers, meta_matchers ]
5
+ end
6
+
2
7
  match do |actual|
3
- # ensure type is a valid twirp response type
4
- if type && !(type < Google::Protobuf::MessageExts)
5
- raise ArgumentError, "Expected `type` to be a Twirp response, found: #{type}"
8
+ # ensure type is a valid twirp request type
9
+ if type
10
+ if type.is_a?(Google::Protobuf::MessageExts)
11
+ unless attrs.empty?
12
+ raise ArgumentError, "Expected Google::Protobuf::MessageExts instance or attrs, but not both"
13
+ end
14
+
15
+ attrs = type.to_h
16
+ type = type.class
17
+ elsif !(type < Google::Protobuf::MessageExts)
18
+ raise ArgumentError, "Expected `type` to be a Google::Protobuf::MessageExts, found: #{type}"
19
+ end
6
20
  end
7
21
 
8
- @fail_msg = "Expected a Twirp response, found #{actual}"
9
- return false unless actual.is_a?(Google::Protobuf::MessageExts)
22
+ @fail_msg = "Expected a Twirp::ClientResp, found #{actual}"
23
+ return false unless actual.is_a?(Twirp::ClientResp)
10
24
 
11
25
  # match expected response type
12
- @fail_msg = "Expected a Twirp response of type #{type}, found #{actual.class}"
13
- return false if type && actual.class != type
26
+ @fail_msg = "Expected a Twirp::ClientResp of type #{type}, found #{actual.data&.class}"
27
+ return false if type && actual.data&.class != type
14
28
 
15
- return true if attrs.empty?
29
+ if @with_error
30
+ unless attrs.empty?
31
+ raise ArgumentError, "match data attributes or error, but not both"
32
+ end
16
33
 
17
- RSpec::Twirp.validate_types(attrs, actual.class)
34
+ @fail_msg = "Expected #{actual} to have an error, but found none"
35
+ return false unless actual.error
18
36
 
19
- # match attributes which are present
20
- attrs.each do |attr_name, expected_attr|
21
- actual_attr = actual.send(attr_name)
37
+ matchers, meta_matchers = @with_error
38
+ expect(actual.error).to be_a_twirp_error(*matchers, **meta_matchers)
39
+ else
40
+ @fail_msg = "Expected #{actual} to have data, but found none"
41
+ return false unless actual.data
22
42
 
23
- @fail_msg = "Expected #{actual} to have #{attr_name}: #{expected_attr.inspect}, found #{actual_attr}"
24
- return false unless values_match?(expected_attr, actual_attr)
43
+ expect(actual.data).to be_a_twirp_message(**attrs)
25
44
  end
26
-
27
- true
45
+ rescue RSpec::Expectations::ExpectationNotMetError => err
46
+ @fail_msg = err.message
47
+ false
28
48
  end
29
49
 
30
50
  description do
@@ -1,5 +1,5 @@
1
1
  module RSpec
2
2
  module Twirp
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
data/lib/rspec/twirp.rb CHANGED
@@ -1,32 +1,54 @@
1
- require "rspec/expectations"
2
- require "rspec/twirp/client_response_matcher"
1
+ require "google/protobuf"
2
+ require "rspec"
3
+ require "twirp"
4
+
5
+ require "rspec/twirp/helpers"
6
+ require "rspec/twirp/make_request_matcher"
3
7
  require "rspec/twirp/error_matcher"
4
- require "rspec/twirp/request_matcher"
8
+ require "rspec/twirp/message_matcher"
5
9
  require "rspec/twirp/response_matcher"
6
10
 
7
11
  module RSpec
8
12
  module Twirp
9
- extend self
13
+ extend RSpec::Mocks::ExampleMethods
14
+
15
+ # private
10
16
 
11
- def validate_types(attrs, klass)
12
- # sanity check type and names of attrs by constructing an actual
13
- # proto object
14
- discrete_attrs = attrs.transform_values do |attr|
15
- case attr
16
- when Regexp
17
- attr.inspect
18
- when Range
19
- attr.first
20
- when RSpec::Matchers::BuiltIn::BaseMatcher
21
- nil
17
+ def generate_client_response(arg)
18
+ res = case arg
19
+ when Google::Protobuf::MessageExts, ::Twirp::Error
20
+ arg
21
+ when Class
22
+ if arg < Google::Protobuf::MessageExts
23
+ arg.new
24
+ else
25
+ raise TypeError, "Expected type `Google::Protobuf::MessageExts`, found: #{arg}"
26
+ end
27
+ when Symbol
28
+ if ::Twirp::Error.valid_code?(arg)
29
+ ::Twirp::Error.new(arg, arg)
22
30
  else
23
- attr
31
+ raise ArgumentError, "invalid error code: #{arg}"
24
32
  end
25
- end.compact
33
+ when Integer
34
+ if code = ::Twirp::ERROR_CODES_TO_HTTP_STATUS.key(arg)
35
+ ::Twirp::Error.new(code, code)
36
+ else
37
+ raise ArgumentError, "invalid error code: #{arg}"
38
+ end
39
+ else
40
+ raise NotImplementedError
41
+ end
26
42
 
27
- klass.new(**discrete_attrs)
28
- rescue Google::Protobuf::TypeError => e
29
- raise TypeError, e.message
43
+ if res.is_a?(Google::Protobuf::MessageExts)
44
+ ::Twirp::ClientResp.new(res, nil)
45
+ else
46
+ ::Twirp::ClientResp.new(nil, res)
47
+ end
30
48
  end
31
49
  end
32
50
  end
51
+
52
+ RSpec.configure do |config|
53
+ config.include(RSpec::Twirp::Helpers)
54
+ end
data/spec/error_spec.rb CHANGED
@@ -13,6 +13,18 @@ describe "be_a_twirp_error" do
13
13
  }.to fail_with /to be a Twirp::Error/
14
14
  end
15
15
 
16
+ describe "status matches" do
17
+ it { is_expected.to be_a_twirp_error(404) }
18
+
19
+ it { expect { is_expected.to be_a_twirp_error(400) }.to fail }
20
+
21
+ it "catches erroneous codes" do
22
+ expect {
23
+ is_expected.to be_a_twirp_error(123)
24
+ }.to raise_error(ArgumentError, /invalid error/)
25
+ end
26
+ end
27
+
16
28
  describe "code matches" do
17
29
  it { is_expected.to be_a_twirp_error(:not_found) }
18
30
 
@@ -64,6 +76,24 @@ describe "be_a_twirp_error" do
64
76
  end
65
77
  end
66
78
 
79
+ describe "instance matches" do
80
+ it "matches similar looking instances" do
81
+ error = Twirp::Error.new(code, msg, meta)
82
+ is_expected.to be_a_twirp_error(error)
83
+ end
84
+
85
+ it "catches mismatches" do
86
+ # no meta
87
+ expect {
88
+ is_expected.to be_a_twirp_error(Twirp::Error.not_found("Not Found"))
89
+ }.to fail
90
+
91
+ expect {
92
+ is_expected.to be_a_twirp_error(Twirp::Error.internal("boom"))
93
+ }.to fail
94
+ end
95
+ end
96
+
67
97
  describe "multi matches" do
68
98
  it { is_expected.to be_a_twirp_error(:not_found, "Not Found") }
69
99
  it { is_expected.to be_a_twirp_error(:not_found, /Not/) }