rspec-twirp 0.0.1 → 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: 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