rspec-twirp 0.0.1 → 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: 5fd6d0300d49b6797b5e346b3c9091dc946a7dde949aeebe8a188ece2a68ed24
4
- data.tar.gz: 634c8748918a04c36a576bcfc377fcfa122d01f3b58d4b5a7576098a11e882d9
3
+ metadata.gz: 2634a27685c93991e30afab03cf80cb5fe650047a2b91342eeadff69a7bb920e
4
+ data.tar.gz: f87529044e688df0b546e65cd527989842e983576bf62a70ea7aabf72d920d09
5
5
  SHA512:
6
- metadata.gz: 7a2cb6d3d9d72219a255d03905f8007dec32263f938067c27986a308542fbe953ff372e8adb6a841b86f498d6d66c32a065a7b550eaf85a771a06792367daca0
7
- data.tar.gz: 6f748768287f63658564c30ebff9bc6ef1bbb7965bc3236f9af5dd52c1e1ae01b5e41001f349a7510d763e8f2401824b358f89cdc3fb8c8f6b19688965991d49
6
+ metadata.gz: 01e6fbab6f8649f6c8dbcc8df72269699e099eb1d924d6ff6656781d5339d060c6138e465a5c43b5ec2a9320614d7924bed1c79eac088c9c10f881d99c6588d7
7
+ data.tar.gz: ce566d5fd9e1e91cca7a100d205e4a1b0615c743f808383d5180bd007049aeaaaf1298bfe382ad6e0385404dd3c04fadb5980ac53fbc9de54c9aeee335b4834b
@@ -0,0 +1,62 @@
1
+ RSpec::Matchers.define :be_a_twirp_error do |*matchers, **meta_matcher|
2
+ match do |actual|
3
+ @fail_msg = "Expected #{actual} to be a Twirp::Error, found #{actual.class}"
4
+ return false unless actual.is_a?(Twirp::Error)
5
+
6
+ matchers.each do |matcher|
7
+ case matcher
8
+ when Symbol
9
+ # match code
10
+
11
+ unless Twirp::Error.valid_code?(matcher)
12
+ raise ArgumentError, "invalid error code: #{matcher.inspect}"
13
+ end
14
+
15
+ @fail_msg = "Expected #{actual} to have code: #{matcher.inspect}, found #{actual.code}"
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)
31
+ else
32
+ # match msg
33
+ @fail_msg = "Expected #{actual} to have msg: #{matcher.inspect}, found #{actual.msg}"
34
+ return false unless values_match?(matcher, actual.msg)
35
+ end
36
+ end
37
+
38
+ # match meta
39
+ unless meta_matcher.empty?
40
+ @cur_match = { meta: meta_matcher }
41
+
42
+ # sanity check...values must be Strings or Regexp
43
+ discrete_attrs = meta_matcher.transform_values do |attr|
44
+ attr.is_a?(Regexp) ? attr.inspect : attr
45
+ end
46
+ actual.send(:validate_meta, discrete_attrs)
47
+
48
+ @fail_msg = "Expected #{actual} to have meta: #{meta_matcher.inspect}, found #{actual.meta}"
49
+ return false unless values_match?(meta_matcher, actual.meta)
50
+ end
51
+
52
+ true
53
+ end
54
+
55
+ description do
56
+ "a Twirp::Error"
57
+ end
58
+
59
+ failure_message { @fail_msg }
60
+ end
61
+
62
+ RSpec::Matchers.alias_matcher :a_twirp_error, :be_a_twirp_error
@@ -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
@@ -0,0 +1,57 @@
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
+
7
+ match do |actual|
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
20
+ end
21
+
22
+ @fail_msg = "Expected a Twirp::ClientResp, found #{actual}"
23
+ return false unless actual.is_a?(Twirp::ClientResp)
24
+
25
+ # match expected response type
26
+ @fail_msg = "Expected a Twirp::ClientResp of type #{type}, found #{actual.data&.class}"
27
+ return false if type && actual.data&.class != type
28
+
29
+ if @with_error
30
+ unless attrs.empty?
31
+ raise ArgumentError, "match data attributes or error, but not both"
32
+ end
33
+
34
+ @fail_msg = "Expected #{actual} to have an error, but found none"
35
+ return false unless actual.error
36
+
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
42
+
43
+ expect(actual.data).to be_a_twirp_message(**attrs)
44
+ end
45
+ rescue RSpec::Expectations::ExpectationNotMetError => err
46
+ @fail_msg = err.message
47
+ false
48
+ end
49
+
50
+ description do
51
+ type ? "a #{type} Twirp response" : "a Twirp response"
52
+ end
53
+
54
+ failure_message { @fail_msg }
55
+ end
56
+
57
+ RSpec::Matchers.alias_matcher :a_twirp_response, :be_a_twirp_response
@@ -0,0 +1,5 @@
1
+ module RSpec
2
+ module Twirp
3
+ VERSION = "0.2.0"
4
+ end
5
+ end
@@ -0,0 +1,54 @@
1
+ require "google/protobuf"
2
+ require "rspec"
3
+ require "twirp"
4
+
5
+ require "rspec/twirp/helpers"
6
+ require "rspec/twirp/make_request_matcher"
7
+ require "rspec/twirp/error_matcher"
8
+ require "rspec/twirp/message_matcher"
9
+ require "rspec/twirp/response_matcher"
10
+
11
+ module RSpec
12
+ module Twirp
13
+ extend RSpec::Mocks::ExampleMethods
14
+
15
+ # private
16
+
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)
30
+ else
31
+ raise ArgumentError, "invalid error code: #{arg}"
32
+ end
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
42
+
43
+ if res.is_a?(Google::Protobuf::MessageExts)
44
+ ::Twirp::ClientResp.new(res, nil)
45
+ else
46
+ ::Twirp::ClientResp.new(nil, res)
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ RSpec.configure do |config|
53
+ config.include(RSpec::Twirp::Helpers)
54
+ end
data/lib/rspec-twirp.rb CHANGED
@@ -1,5 +1 @@
1
- require "rspec-twirp/version"
2
-
3
- module RSpecTwirp
4
-
5
- end
1
+ require "rspec/twirp"
@@ -0,0 +1,116 @@
1
+ describe "be_a_twirp_error" do
2
+ subject { Twirp::Error.new(code, msg, meta) }
3
+
4
+ let(:code) { :not_found }
5
+ let(:msg) { "Not Found" }
6
+ let(:meta) { { is_meta: "true" } }
7
+
8
+ it { is_expected.to be_a_twirp_error }
9
+
10
+ it "catches type mismatches" do
11
+ expect {
12
+ expect(Object).to be_a_twirp_error
13
+ }.to fail_with /to be a Twirp::Error/
14
+ end
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
+
28
+ describe "code matches" do
29
+ it { is_expected.to be_a_twirp_error(:not_found) }
30
+
31
+ it { expect { is_expected.to be_a_twirp_error(:unknown) }.to fail }
32
+
33
+ it "catches erroneous codes" do
34
+ expect {
35
+ is_expected.to be_a_twirp_error(:not_a_valid_code)
36
+ }.to raise_error(ArgumentError, /:not_a_valid_code/)
37
+ end
38
+ end
39
+
40
+ describe "msg matches" do
41
+ it { is_expected.to be_a_twirp_error("Not Found") }
42
+
43
+ it "supports Regex matches" do
44
+ is_expected.to be_a_twirp_error(/Not/)
45
+ end
46
+
47
+ it "catches mismatches" do
48
+ expect {
49
+ is_expected.to be_a_twirp_error("Not")
50
+ }.to fail_with /to have msg: "Not"/
51
+
52
+ expect {
53
+ is_expected.to be_a_twirp_error(/Nope/)
54
+ }.to fail_with /to have msg: \/Nope\//
55
+ end
56
+ end
57
+
58
+ describe "meta matches" do
59
+ it { is_expected.to be_a_twirp_error(is_meta: "true") }
60
+ it { is_expected.to be_a_twirp_error(is_meta: /^t/) }
61
+
62
+ it "catches mismatches" do
63
+ expect {
64
+ is_expected.to be_a_twirp_error(is_meta: "false")
65
+ }.to fail_with /to have meta.*is_meta/
66
+
67
+ expect {
68
+ is_expected.to be_a_twirp_error(not_meta: "")
69
+ }.to fail_with /to have meta.*not_meta/
70
+ end
71
+
72
+ it "catches type errors" do
73
+ expect {
74
+ is_expected.to be_a_twirp_error(is_meta: true)
75
+ }.to raise_error(ArgumentError, /meta values must be Strings/)
76
+ end
77
+ end
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
+
97
+ describe "multi matches" do
98
+ it { is_expected.to be_a_twirp_error(:not_found, "Not Found") }
99
+ it { is_expected.to be_a_twirp_error(:not_found, /Not/) }
100
+ it { is_expected.to be_a_twirp_error(:not_found, is_meta: "true") }
101
+
102
+ it "catches mismatches" do
103
+ expect {
104
+ is_expected.to be_a_twirp_error(:unknown, "Not Found")
105
+ }.to fail_with /unknown/
106
+
107
+ expect {
108
+ is_expected.to be_a_twirp_error(:not_found, "Nope")
109
+ }.to fail_with /Nope/
110
+
111
+ expect {
112
+ is_expected.to be_a_twirp_error(:not_found, is_meta: "false")
113
+ }.to fail_with /false/
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,181 @@
1
+ describe "make_twirp_request" do
2
+ let(:client) { HelloWorldClient.new(conn) }
3
+ let(:conn) { mock_twirp_connection(response) }
4
+ let!(:other_client) { HelloWorldClient.new(conn) }
5
+ let(:request) { HelloRequest.new(name: "World", count: 3) }
6
+ let(:response) { HelloResponse.new(message: ["Hello", "Hello", "Hello World"]) }
7
+
8
+ def hello
9
+ client.hello(request)
10
+ end
11
+
12
+ describe "block mode" do
13
+ it { expect { hello }.to make_twirp_request }
14
+
15
+ it "returns nil" do
16
+ expect {
17
+ expect(hello).to be_nil
18
+ }.to make_twirp_request
19
+ end
20
+
21
+ context "with client instance matcher" do
22
+ it "matches a client instance" do
23
+ expect { hello }.to make_twirp_request(client)
24
+ end
25
+
26
+ it "matches a pre-existing client instance" do
27
+ expect {
28
+ other_client.hello(HelloRequest.new)
29
+ }.to make_twirp_request(other_client)
30
+ end
31
+
32
+ it "catches client mismaches" do
33
+ expect {
34
+ expect { hello }.to make_twirp_request(other_client)
35
+ }.to fail
36
+ end
37
+ end
38
+
39
+ context "with client class matcher" do
40
+ it "matches a client class" do
41
+ expect { hello }.to make_twirp_request(HelloWorldClient)
42
+ end
43
+
44
+ it "matches subclasses" do
45
+ expect { hello }.to make_twirp_request(Twirp::Client)
46
+ end
47
+
48
+ it "catches mismatches" do
49
+ expect {
50
+ expect { hello }.to make_twirp_request(GoodbyeClient)
51
+ }.to fail
52
+ end
53
+
54
+ it "catches type errors" do
55
+ expect {
56
+ expect { hello }.to make_twirp_request(Object)
57
+ }.to raise_error(TypeError)
58
+ end
59
+ end
60
+
61
+ context "with rpc name matcher" do
62
+ it { expect { hello }.to make_twirp_request(:Hello) }
63
+ it { expect { hello }.to make_twirp_request("Hello") }
64
+ it { expect { hello }.to make_twirp_request(/^He/) }
65
+
66
+ it "catches mismatches" do
67
+ expect {
68
+ expect { hello }.to make_twirp_request(:Bye)
69
+ }.to fail
70
+
71
+ expect {
72
+ expect { hello }.to make_twirp_request(/B/)
73
+ }.to fail
74
+ end
75
+ end
76
+
77
+ context "with input matcher" do
78
+ it "matches messages" do
79
+ expect { hello }.to make_twirp_request.with(request)
80
+ end
81
+
82
+ it "matches message type" do
83
+ expect { hello }.to make_twirp_request.with(HelloRequest)
84
+ end
85
+
86
+ it "matches with attrs" do
87
+ expect { hello }.to make_twirp_request.with(count: 3)
88
+ end
89
+
90
+ context "with json request" do
91
+ it "matches messages" do
92
+ expect {
93
+ client.hello(name: "World")
94
+ }.to make_twirp_request.with(HelloRequest.new(name: "World"))
95
+ end
96
+
97
+ it "matches attrs" do
98
+ expect {
99
+ client.hello(name: "World")
100
+ }.to make_twirp_request.with(name: "World")
101
+ end
102
+ end
103
+
104
+ it "catches mismatches" do
105
+ expect {
106
+ expect { hello }.to make_twirp_request.with(count: 1)
107
+ }.to fail
108
+ end
109
+ end
110
+
111
+ context "with service matcher" do
112
+ it { expect { hello }.to make_twirp_request(HelloWorldService) }
113
+
114
+ it "catches mismatches" do
115
+ expect {
116
+ expect { hello }.to make_twirp_request(GoodbyeService)
117
+ }.to fail
118
+ end
119
+ end
120
+
121
+ describe ".and_call_original" do
122
+ it "calls original" do
123
+ expect {
124
+ expect(hello).to be_a_twirp_response(response)
125
+ }.to make_twirp_request.and_call_original
126
+ end
127
+ end
128
+
129
+ describe ".and_return" do
130
+ it "returns the specified value" do
131
+ expect {
132
+ expect(hello).to be_a_twirp_response(HelloRequest.new)
133
+ }.to make_twirp_request.and_return(HelloRequest.new)
134
+ end
135
+
136
+ it "returns the specified value" do
137
+ expect {
138
+ expect(hello).to be_a_twirp_response(HelloRequest.new)
139
+ }.to make_twirp_request.and_return(HelloRequest)
140
+ end
141
+ end
142
+ end
143
+
144
+ describe "inline mode" do
145
+ after { hello }
146
+
147
+ it { expect(client).to make_twirp_request }
148
+
149
+ it "matches a specific rpc method" do
150
+ expect(client).to make_twirp_request(:hello)
151
+ end
152
+
153
+ it "matches a specific rpc method by rpc name" do
154
+ expect(client).to make_twirp_request(:Hello)
155
+ end
156
+
157
+ it "fails when client is not called within scope" do
158
+ expect_expectation_failure {
159
+ expect(client).to make_twirp_request
160
+ }
161
+ end
162
+
163
+ describe ".with" do
164
+ let(:request) { HelloRequest.new(name: "Daniel") }
165
+
166
+ it "matches the request message" do
167
+ expect(client).to make_twirp_request(:hello).with(request)
168
+ end
169
+
170
+ it "matches the request arguments" do
171
+ expect(client).to make_twirp_request(:hello).with(name: "Daniel")
172
+ end
173
+
174
+ it "fails when params don't match" do
175
+ expect_expectation_failure {
176
+ expect(client).to make_twirp_request.with(name: "Bob")
177
+ }
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,83 @@
1
+ describe "be_a_twirp_message" do
2
+ subject { HelloRequest.new(**attrs) }
3
+
4
+ let(:attrs) { {} }
5
+
6
+ it { is_expected.to be_a_twirp_message }
7
+
8
+ it "works with responses also" do
9
+ expect(HelloResponse.new).to be_a_twirp_message
10
+ end
11
+
12
+ it "supports compound matchers" do
13
+ expect([ subject ]).to include(a_twirp_message)
14
+ end
15
+
16
+ it "catches non-twirp subjects" do
17
+ expect {
18
+ expect(Object).to be_a_twirp_message
19
+ }.to fail_with /Expected a Twirp message, found Object/
20
+ end
21
+
22
+ it "matches a specific message type" do
23
+ is_expected.to be_a_twirp_message(HelloRequest)
24
+ end
25
+
26
+ it "catches type mismatches" do
27
+ expect {
28
+ is_expected.to be_a_twirp_message(GoodbyeRequest)
29
+ }.to fail_with /message of type GoodbyeRequest/
30
+ end
31
+
32
+ it "catches erroneous message types" do
33
+ expect {
34
+ is_expected.to be_a_twirp_message(Object)
35
+ }.to raise_error(TypeError, /Object/)
36
+ end
37
+
38
+ context "with attributes" do
39
+ let(:attrs) { { name: "Bob", count: 3 } }
40
+
41
+ it "can match attributes" do
42
+ is_expected.to be_a_twirp_message(HelloRequest, **attrs)
43
+ end
44
+
45
+ it "supports regex matches" do
46
+ is_expected.to be_a_twirp_message(name: /^B/)
47
+ end
48
+
49
+ it "supports range matches" do
50
+ is_expected.to be_a_twirp_message(count: 1..5)
51
+ end
52
+
53
+ it "catches mismatches" do
54
+ expect {
55
+ is_expected.to be_a_twirp_message(GoodbyeRequest, name: "Bob")
56
+ }.to fail_with /message of type/
57
+
58
+ expect {
59
+ is_expected.to be_a_twirp_message(name: "nope")
60
+ }.to fail_with /to have name: "nope"/
61
+
62
+ expect {
63
+ is_expected.to be_a_twirp_message(name: /no/)
64
+ }.to fail_with /to have name: \/no\//
65
+
66
+ expect {
67
+ is_expected.to be_a_twirp_message(count: 1)
68
+ }.to fail_with /to have count: 1/
69
+ end
70
+
71
+ it "catches the erroneous attribute matches" do
72
+ expect {
73
+ is_expected.to be_a_twirp_message(namezzz: "Bob")
74
+ }.to raise_error(ArgumentError, /namezzz/)
75
+ end
76
+
77
+ it "catches type mismatches" do
78
+ expect {
79
+ is_expected.to be_a_twirp_message(name: 123)
80
+ }.to raise_error(TypeError, /string field.*given Integer/)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,103 @@
1
+ describe :mock_twirp_connection do
2
+ subject { client.bye(request) }
3
+
4
+ let(:client) { GoodbyeClient.new(conn) }
5
+ let(:request) { GoodbyeRequest.new }
6
+ let(:response) { GoodbyeResponse.new(**response_attrs) }
7
+ let(:response_attrs) { { message: "bye" } }
8
+
9
+ context "without a mock" do
10
+ let(:conn) { "http://localhost:3000/twirp/Goodbye/Bye" }
11
+
12
+ it { expect { subject }.to raise_error(Faraday::Error) }
13
+ end
14
+
15
+ describe "with a response instance" do
16
+ let(:conn) { mock_twirp_connection(response) }
17
+
18
+ it { is_expected.to be_a(Twirp::ClientResp) }
19
+ it { is_expected.to be_a_twirp_response }
20
+ it { is_expected.to be_a_twirp_response(response) }
21
+ it { is_expected.to be_a_twirp_response(message: "bye") }
22
+
23
+ it "catches mismatches" do
24
+ expect {
25
+ is_expected.to be_a_twirp_response(message: "nope")
26
+ }.to fail
27
+ end
28
+ end
29
+
30
+ describe "with a response type" do
31
+ let(:conn) { mock_twirp_connection(GoodbyeResponse) }
32
+
33
+ it { is_expected.to be_a_twirp_response(GoodbyeResponse) }
34
+
35
+ it "catches mismatches" do
36
+ expect {
37
+ is_expected.to be_a_twirp_response(HelloResponse)
38
+ }.to fail
39
+ end
40
+ end
41
+
42
+ describe "with default response" do
43
+ let(:conn) { mock_twirp_connection }
44
+
45
+ it { is_expected.to be_a_twirp_response(GoodbyeResponse) }
46
+
47
+ context "and with attrs" do
48
+ let(:conn) { mock_twirp_connection(message: "adios") }
49
+
50
+ it { is_expected.to be_a_twirp_response(message: "adios") }
51
+ end
52
+ end
53
+
54
+ describe "with an error" do
55
+ let(:conn) { mock_twirp_connection(error) }
56
+ let(:error) { Twirp::Error.not_found("nope") }
57
+
58
+ it { is_expected.to be_a_twirp_response.with_error(error) }
59
+
60
+ context "when error is symbol" do
61
+ let(:error) { :not_found }
62
+
63
+ it { is_expected.to be_a_twirp_response.with_error(404) }
64
+ it { is_expected.to be_a_twirp_response.with_error(:not_found) }
65
+ end
66
+
67
+ context "when error is integer" do
68
+ let(:error) { 500 }
69
+
70
+ it { is_expected.to be_a_twirp_response.with_error(500) }
71
+ it { is_expected.to be_a_twirp_response.with_error(:internal) }
72
+ end
73
+ end
74
+
75
+ describe "with a block" do
76
+ context "with a response object" do
77
+ let(:conn) { mock_twirp_connection { response } }
78
+
79
+ it { is_expected.to be_a_twirp_response(response) }
80
+ end
81
+
82
+ context "with response attrs" do
83
+ let(:conn) { mock_twirp_connection { response_attrs } }
84
+
85
+ it "determines the correct response type and incorporates the attrs" do
86
+ is_expected.to be_a_twirp_response(GoodbyeResponse, **response_attrs)
87
+ end
88
+ end
89
+
90
+ context "with error" do
91
+ let(:conn) { mock_twirp_connection { error } }
92
+ let(:error) { Twirp::Error.not_found("nope") }
93
+
94
+ it { is_expected.to be_a_twirp_response.with_error(error) }
95
+ end
96
+
97
+ context "with error code" do
98
+ let(:conn) { mock_twirp_connection { :not_found } }
99
+
100
+ it { is_expected.to be_a_twirp_response.with_error(:not_found) }
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,124 @@
1
+ describe "be_a_twirp_response" do
2
+ context "with a response" do
3
+ subject { Twirp::ClientResp.new(response, nil) }
4
+
5
+ let(:response) { GoodbyeResponse.new }
6
+
7
+ it { is_expected.to be_a_twirp_response }
8
+
9
+ it "catches non-twirp response" do
10
+ expect {
11
+ expect(Object).to be_a_twirp_response
12
+ }.to fail_with /found Object/
13
+ end
14
+
15
+ it "matches a specific response type" do
16
+ is_expected.to be_a_twirp_response(GoodbyeResponse)
17
+ end
18
+
19
+ it "matches a specific response" do
20
+ is_expected.to be_a_twirp_response(response)
21
+ end
22
+
23
+ it "catches type mismatches" do
24
+ expect {
25
+ is_expected.to be_a_twirp_response(HelloResponse)
26
+ }.to fail_with /of type HelloResponse/
27
+ end
28
+
29
+ it "catches erroneous response types" do
30
+ expect {
31
+ is_expected.to be_a_twirp_response(Object)
32
+ }.to raise_error(ArgumentError, /Object/)
33
+ end
34
+
35
+ context "with attributes" do
36
+ subject { Twirp::ClientResp.new(GoodbyeResponse.new(**attrs), nil) }
37
+
38
+ let(:attrs) { { message: "bye", name: "Bob" } }
39
+
40
+ it "can match attributes" do
41
+ is_expected.to be_a_twirp_response(GoodbyeResponse, **attrs)
42
+ end
43
+
44
+ it "supports regex matches" do
45
+ is_expected.to be_a_twirp_response(name: /^B/)
46
+ end
47
+
48
+ it "catches mismatches" do
49
+ expect {
50
+ is_expected.to be_a_twirp_response(name: "nope")
51
+ }.to fail_with /to have name: "nope"/
52
+
53
+ expect {
54
+ is_expected.to be_a_twirp_response(name: /no/)
55
+ }.to fail_with /to have name: \/no\//
56
+ end
57
+
58
+ it "catches the erroneous attributes" do
59
+ expect {
60
+ is_expected.to be_a_twirp_response(namezzz: "Bob")
61
+ }.to raise_error(ArgumentError, /namezzz/)
62
+ end
63
+
64
+ it "catches type mismatches" do
65
+ expect {
66
+ is_expected.to be_a_twirp_response(name: 123)
67
+ }.to raise_error(TypeError, /string field.*given Integer/)
68
+ end
69
+
70
+ it "can't also match a specific response" do
71
+ expect {
72
+ is_expected.to be_a_twirp_response(response, name: "Bob")
73
+ }.to raise_error(ArgumentError, /but not both/)
74
+ end
75
+ end
76
+ end
77
+
78
+ context "with error" do
79
+ subject { Twirp::ClientResp.new(nil, error) }
80
+
81
+ let(:error) { Twirp::Error.new(code, msg, meta) }
82
+ let(:code) { :not_found }
83
+ let(:msg) { "Not Found" }
84
+ let(:meta) { { is_meta: "true" } }
85
+
86
+ it { is_expected.to be_a_twirp_response.with_error }
87
+ it { is_expected.to be_a_twirp_response.with_error(code) }
88
+ it { is_expected.to be_a_twirp_response.with_error(msg) }
89
+ it { is_expected.to be_a_twirp_response.with_error(**meta) }
90
+ it { is_expected.to be_a_twirp_response.with_error(/Not/) }
91
+
92
+ it "catches mismatches" do
93
+ expect {
94
+ is_expected.to be_a_twirp_response.with_error(:internal)
95
+ }.to fail_with /code: :internal/
96
+ end
97
+ end
98
+
99
+ context "with neither response nor error" do
100
+ subject { Twirp::ClientResp.new(nil, nil) }
101
+
102
+ it "fails the response match" do
103
+ expect {
104
+ is_expected.to be_a_twirp_response
105
+ }.to fail_with /to have data/
106
+ end
107
+
108
+ it "fails the error match" do
109
+ expect {
110
+ is_expected.to be_a_twirp_response.with_error
111
+ }.to fail_with /to have an error/
112
+ end
113
+ end
114
+
115
+ context "with both response and error" do
116
+ subject { Twirp::ClientResp.new(GoodbyeResponse.new, Twirp::Error.not_found("Not Found")) }
117
+
118
+ it "fails" do
119
+ expect {
120
+ is_expected.to be_a_twirp_response(name: "Bob").with_error
121
+ }.to raise_error(ArgumentError, /but not both/)
122
+ end
123
+ end
124
+ end
metadata CHANGED
@@ -1,31 +1,59 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-twirp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-25 00:00:00.000000000 Z
11
+ date: 2022-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: byebug
14
+ name: google-protobuf
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
19
+ version: '3'
20
+ type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '3'
27
27
  - !ruby/object:Gem::Dependency
28
- name: codecov
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: twirp
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: byebug
29
57
  requirement: !ruby/object:Gem::Requirement
30
58
  requirements:
31
59
  - - ">="
@@ -39,7 +67,7 @@ dependencies:
39
67
  - !ruby/object:Gem::Version
40
68
  version: '0'
41
69
  - !ruby/object:Gem::Dependency
42
- name: rspec
70
+ name: codecov
43
71
  requirement: !ruby/object:Gem::Requirement
44
72
  requirements:
45
73
  - - ">="
@@ -73,7 +101,20 @@ extensions: []
73
101
  extra_rdoc_files: []
74
102
  files:
75
103
  - lib/rspec-twirp.rb
76
- - lib/rspec-twirp/version.rb
104
+ - lib/rspec/twirp.rb
105
+ - lib/rspec/twirp/error_matcher.rb
106
+ - lib/rspec/twirp/helpers.rb
107
+ - lib/rspec/twirp/make_request_matcher.rb
108
+ - lib/rspec/twirp/message_matcher.rb
109
+ - lib/rspec/twirp/mock_client.rb
110
+ - lib/rspec/twirp/mock_connection.rb
111
+ - lib/rspec/twirp/response_matcher.rb
112
+ - lib/rspec/twirp/version.rb
113
+ - spec/error_spec.rb
114
+ - spec/make_request_spec.rb
115
+ - spec/message_spec.rb
116
+ - spec/mock_connection_spec.rb
117
+ - spec/response_spec.rb
77
118
  homepage: https://github.com/dpep/rspec-twirp_rb
78
119
  licenses:
79
120
  - MIT
@@ -93,8 +134,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
134
  - !ruby/object:Gem::Version
94
135
  version: '0'
95
136
  requirements: []
96
- rubygems_version: 3.1.6
137
+ rubygems_version: 3.3.7
97
138
  signing_key:
98
139
  specification_version: 4
99
- summary: RSpecTwirp
100
- test_files: []
140
+ summary: Gem::Specification::RSpec::Twirp
141
+ test_files:
142
+ - spec/error_spec.rb
143
+ - spec/make_request_spec.rb
144
+ - spec/message_spec.rb
145
+ - spec/mock_connection_spec.rb
146
+ - spec/response_spec.rb
@@ -1,3 +0,0 @@
1
- module RSpecTwirp
2
- VERSION = "0.0.1"
3
- end