rspec-twirp 0.1.0 → 0.2.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 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/) }