webmock-twirp 0.2.1 → 0.4.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: b68a561786519f4bf91828a3a01e4ae803e36e03af7fd327af7780ba5da4d838
4
- data.tar.gz: 151994b76bc7d4494be3f302f20633a19fdb681a092d4f1d489e694f89d43189
3
+ metadata.gz: 292380b89f2b938b849edb2e4ce46cb84e197b6a39592d16425440446adce9a1
4
+ data.tar.gz: 64be8b4452a96b256adc5bfe9d08a479392d9cb916f4fe0ab1135cc763728f87
5
5
  SHA512:
6
- metadata.gz: b764858403de27f0b9cc28a6bddb7c827379308c1619be1b8bcbeff8d1824a3cb1c3f892644ce63c78c2bc4f1331e4fdb4a60eb88713541e0161d899a4ddaa1c
7
- data.tar.gz: 43a24c5182e1aab84cbc16897faae7ea54098be328ad07e5754b939c6b89783985c23f1d565db89fa751fb5e1edb719d72a0fef264b64ae1ceadbc877e8b92d0
6
+ metadata.gz: c4f52185995140dc0d2aa2646227bbd9195a87941a1ab40a20d6e1dfb1b29bf24c9b0a50796db26fecd453f802fecc5bd5003eee284f325ccea3a96bf4f152a6
7
+ data.tar.gz: fa2e3ed00ed69f960dae3c500979fc8a745cd6e78015c68d6b963ee85ffad842070855abcdd8d50e6f37263f2789ead2f231d50092698c496bd0e4e5b8251970
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ### v0.4.0 (2022-11-16)
2
+ - .to_return(Twirp::ClientResp)
3
+ - revert client class name filter
4
+ - unqualified client class filter
5
+ - uri matching
6
+
7
+ ### v0.3.0 (2022-10-28)
8
+ - twirpify all the things
9
+ - improve refinements
10
+ - make_twirp_request rspec matcher
11
+
1
12
  ### v0.2.1 (2022-10-19)
2
13
  - fix client matching
3
14
 
data/README.md CHANGED
@@ -97,6 +97,45 @@ stub_twirp_request.to_return do |request|
97
97
  end
98
98
  ```
99
99
 
100
+ ## Improved WebMock Errors
101
+ Before
102
+ ```ruby
103
+ > client = EchoClient.new("http://example.com/twirp")
104
+ ...
105
+ > client.echo(msg: "Hi")
106
+ /lib/webmock/http_lib_adapters/net_http.rb:104:in `request': Real HTTP connections are disabled. Unregistered request:
107
+ POST http://example.com/twirp/Echo/Echo with body ' (WebMock::NetConnectNotAllowedError)
108
+ Hi' with headers {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Type'=>'application/protobuf', 'User-Agent'=>'Faraday v1.10.2'}
109
+
110
+ You can stub this request with the following snippet:
111
+
112
+ stub_request(:post, "http://example.com/twirp/Echo/Echo").
113
+ with(
114
+ body: "\n\x02Hi",
115
+ headers: {
116
+ 'Accept'=>'*/*',
117
+ 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
118
+ 'Content-Type'=>'application/protobuf',
119
+ 'User-Agent'=>'Faraday v1.10.2'
120
+ }).
121
+ to_return(status: 200, body: "", headers: {})
122
+ ```
123
+
124
+ After
125
+ ```ruby
126
+ > require "webmock-twirp"
127
+ > client = EchoClient.new("http://example.com/twirp")
128
+ ...
129
+ > client.echo(msg: "Hi")
130
+ /lib/webmock/http_lib_adapters/net_http.rb:104:in `request': Real Twirp connections are disabled. Unregistered request:
131
+ EchoClient(http://example.com/twirp/Echo/Echo).echo(msg: "Hi") (WebMock::NetConnectNotAllowedError)
132
+
133
+ You can stub this request with the following snippet:
134
+
135
+ stub_twirp_request(:echo).with(
136
+ msg: "Hi",
137
+ ).to_return(...)
138
+ ```
100
139
 
101
140
  ----
102
141
  ## Contributing
@@ -0,0 +1,37 @@
1
+ module WebMock
2
+ module Twirp
3
+ module NetConnectNotAllowedError
4
+ def initialize(request_signature)
5
+ @request_signature = request_signature
6
+ end
7
+
8
+ def message
9
+ snippet = WebMock::RequestSignatureSnippet.new(@request_signature)
10
+
11
+ text = [
12
+ "Real Twirp connections are disabled. Unregistered request:",
13
+ @request_signature,
14
+ snippet.stubbing_instructions,
15
+ snippet.request_stubs,
16
+ "="*60
17
+ ].compact.join("\n\n")
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ # hijack creation of Twirp errors
24
+ module WebMock
25
+ class NetConnectNotAllowedError
26
+ def self.new(request_signature)
27
+ allocate.tap do |instance|
28
+ if request_signature.is_a?(WebMock::Twirp::RequestSignature)
29
+ instance.extend(WebMock::Twirp::NetConnectNotAllowedError)
30
+ end
31
+
32
+ instance.send(:initialize, request_signature)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,46 @@
1
+ module WebMock
2
+ module Twirp
3
+ module Matchers
4
+ class MakeTwirpRequest
5
+ def initialize(*matchers)
6
+ @stub = WebMock::Twirp::RequestStub.new(*matchers)
7
+ end
8
+
9
+ def method_missing(name, *args, **kwargs, &block)
10
+ super unless respond_to?(name)
11
+ @stub.send(name, *args, **kwargs, &block)
12
+
13
+ self
14
+ end
15
+
16
+ def respond_to?(method_name, include_private = false)
17
+ super || @stub.respond_to?(method_name)
18
+ end
19
+
20
+ def matches?(block)
21
+ unless block.is_a?(Proc)
22
+ raise ArgumentError, "expected block, found: #{block}"
23
+ end
24
+
25
+ WebMock::StubRegistry.instance.register_request_stub(@stub)
26
+ block.call
27
+ WebMock::StubRegistry.instance.remove_request_stub(@stub)
28
+
29
+ RequestRegistry.instance.times_executed(@stub) > 0
30
+ end
31
+
32
+ def failure_message
33
+ "expected a Twirp request but received none"
34
+ end
35
+
36
+ def failure_message_when_negated
37
+ "did not expect a Twirp request, but received one"
38
+ end
39
+
40
+ def supports_block_expectations?
41
+ true
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,13 @@
1
+ require "webmock/twirp/matchers/make_twirp_request"
2
+
3
+ module WebMock
4
+ module Twirp
5
+ module Matchers
6
+ def make_twirp_request(...)
7
+ MakeTwirpRequest.new(...)
8
+ end
9
+
10
+ alias_method :make_a_twirp_request, :make_twirp_request
11
+ end
12
+ end
13
+ end
@@ -8,16 +8,16 @@ module WebMock
8
8
  end
9
9
 
10
10
  refine Google::Protobuf::MessageExts do
11
- def normalized_hash(symbolize_keys: true)
11
+ def normalized_hash
12
12
  res = {}
13
13
 
14
14
  self.class.descriptor.each do |field|
15
- key = symbolize_keys ? field.name.to_sym : field.name
15
+ key = field.name.to_sym
16
16
  value = field.get(self)
17
17
 
18
18
  if value.is_a?(Google::Protobuf::MessageExts)
19
19
  # recursively serialize sub-message
20
- value = value.normalized_hash(symbolize_keys: symbolize_keys)
20
+ value = value.normalized_hash
21
21
  end
22
22
 
23
23
  res[key] = value unless field.default == value
@@ -71,6 +71,12 @@ module WebMock
71
71
  end
72
72
  end
73
73
  end
74
+
75
+ refine ::WebMock::RequestSignature do
76
+ def proto_headers?
77
+ !!(headers&.fetch("Content-Type", nil)&.start_with?(::Twirp::Encoding::PROTO))
78
+ end
79
+ end
74
80
  end
75
81
  end
76
82
  end
@@ -0,0 +1,39 @@
1
+ using WebMock::Twirp::Refinements
2
+
3
+ module WebMock
4
+ module Twirp
5
+ module RequestBodyDiff
6
+
7
+ private
8
+
9
+ def request_signature_diffable?
10
+ !!request_signature.twirp_request
11
+ end
12
+
13
+ def request_stub_diffable?
14
+ !!request_stub.with_attrs && !request_stub.with_attrs.is_a?(Proc)
15
+ end
16
+
17
+ def request_signature_body_hash
18
+ request_signature.twirp_request.normalized_hash
19
+ end
20
+
21
+ def request_stub_body_hash
22
+ request_stub.with_attrs
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ # hijack creation of Twirp snippets
29
+ module WebMock
30
+ class RequestBodyDiff
31
+ def self.new(request_signature, request_stub)
32
+ super.tap do |instance|
33
+ if request_signature.proto_headers? && request_stub.is_a?(WebMock::Twirp::RequestStub)
34
+ instance.extend(WebMock::Twirp::RequestBodyDiff)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,55 @@
1
+ using WebMock::Twirp::Refinements
2
+
3
+ module WebMock
4
+ module Twirp
5
+ module RequestSignature
6
+ def twirp_client
7
+ @twirp_client ||= begin
8
+ service_full_name, rpc_name = uri.path.split("/").last(2)
9
+
10
+ # find matching client
11
+ client = ObjectSpace.each_object(::Twirp::Client.singleton_class).find do |obj|
12
+ next unless obj < ::Twirp::Client && obj.name
13
+ obj.service_full_name == service_full_name && obj.rpcs.key?(rpc_name)
14
+ end
15
+ end
16
+ end
17
+
18
+ def twirp_rpc
19
+ @twirp_rpc ||= begin
20
+ rpc_name = uri.path.split("/").last
21
+ client = twirp_client.rpcs[rpc_name] if twirp_client
22
+ end
23
+ end
24
+
25
+ def twirp_request
26
+ twirp_rpc[:input_class].decode(body) if twirp_rpc
27
+ end
28
+
29
+ def to_s
30
+ return super unless twirp_rpc
31
+
32
+ uri = WebMock::Util::URI.strip_default_port_from_uri_string(self.uri.to_s)
33
+ params = twirp_request.normalized_hash.map do |k, v|
34
+ "#{k}: #{v.inspect}"
35
+ end.join(", ")
36
+
37
+ string = "#{twirp_client}(#{uri})"
38
+ string << ".#{twirp_rpc[:ruby_method]}"
39
+ string << "(#{params.empty? ? "{}" : params})"
40
+
41
+ string
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ module WebMock
48
+ class RequestSignature
49
+ def self.new(...)
50
+ super(...).tap do |instance|
51
+ instance.extend(WebMock::Twirp::RequestSignature) if instance.proto_headers?
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,40 @@
1
+ using WebMock::Twirp::Refinements
2
+
3
+ module WebMock
4
+ module Twirp
5
+ module RequestSignatureSnippet
6
+ def stubbing_instructions
7
+ return unless WebMock.show_stubbing_instructions?
8
+
9
+ client = @request_signature.twirp_client
10
+ rpc = @request_signature.twirp_rpc
11
+
12
+ return super unless client
13
+
14
+ string = "You can stub this request with the following snippet:\n\n"
15
+ string << "stub_twirp_request(#{rpc[:ruby_method].inspect})"
16
+
17
+ if request = @request_signature.twirp_request
18
+ params = request.normalized_hash.map do |k, v|
19
+ " #{k}: #{v.inspect},"
20
+ end.join("\n")
21
+
22
+ string << ".with(\n#{params}\n)" unless params.empty?
23
+ end
24
+
25
+ string << ".to_return(...)"
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ # hijack creation of Twirp snippets
32
+ module WebMock
33
+ class RequestSignatureSnippet
34
+ def self.new(request_signature)
35
+ super.tap do |instance|
36
+ instance.extend(WebMock::Twirp::RequestSignatureSnippet) if request_signature.proto_headers?
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,11 +1,11 @@
1
+ require "uri"
2
+
1
3
  module WebMock
2
4
  module Twirp
3
5
  class RequestStub < WebMock::RequestStub
4
6
  using Refinements
5
7
 
6
8
  def initialize(*filters)
7
- rpc_name = filters.snag { |x| x.is_a?(Symbol) }
8
-
9
9
  client = filters.snag { |x| x.is_a?(::Twirp::Client) }
10
10
 
11
11
  klass = client&.class
@@ -13,11 +13,21 @@ module WebMock
13
13
  x.is_a?(Class) && (x < ::Twirp::Client || x < ::Twirp::Service)
14
14
  end
15
15
 
16
+ rpc_name = filters.snag { |x| x.is_a?(Symbol) }
17
+
18
+ uri = filters.snag do |x|
19
+ x.is_a?(String) && x.start_with?("http") && x =~ URI::regexp
20
+ end
21
+
22
+ if client && uri
23
+ raise ArgumentError, "specify uri or client instance, but not both"
24
+ end
25
+
16
26
  unless filters.empty?
17
27
  raise ArgumentError, "unexpected arguments: #{filters}"
18
28
  end
19
29
 
20
- uri = ""
30
+ uri ||= ""
21
31
 
22
32
  if client
23
33
  conn = client.instance_variable_get(:@conn)
@@ -25,15 +35,11 @@ module WebMock
25
35
  end
26
36
 
27
37
  if klass
28
- @rpcs = klass.rpcs
38
+ @twirp_client = klass
29
39
  uri += "/#{klass.service_full_name}"
30
- else
31
- uri += "/[^/]+"
32
- end
33
40
 
34
- if rpc_name
35
- if klass
36
- # kindly convert ruby method to rpc method name
41
+ if rpc_name
42
+ # type check and kindly convert ruby method to rpc method name
37
43
  rpc_info = klass.rpcs.values.find do |x|
38
44
  x[:rpc_method] == rpc_name || x[:ruby_method] == rpc_name
39
45
  end
@@ -41,19 +47,29 @@ module WebMock
41
47
  raise ArgumentError, "invalid rpc method: #{rpc_name}" unless rpc_info
42
48
 
43
49
  uri += "/#{rpc_info[:rpc_method]}"
44
- else
45
- uri += "/#{rpc_name}"
46
50
  end
47
- else
48
- uri += "/[^/]+"
49
51
  end
50
52
 
51
- super(:post, /#{uri}$/)
53
+ super(:post, /#{uri}/)
52
54
 
53
55
  # filter on Twirp header
54
56
  @request_pattern.with(
55
57
  headers: { "Content-Type" => ::Twirp::Encoding::PROTO },
56
58
  )
59
+
60
+ if rpc_name
61
+ # match rpc dynamically after client resolves
62
+ @rpc_name = rpc_name
63
+
64
+ @request_pattern.with do |request|
65
+ rpc_info = request.twirp_rpc
66
+
67
+ !!rpc_info && (
68
+ rpc_info[:rpc_method] == rpc_name ||
69
+ rpc_info[:ruby_method] == rpc_name
70
+ )
71
+ end
72
+ end
57
73
  end
58
74
 
59
75
  def with(request = nil, **attrs, &block)
@@ -69,16 +85,31 @@ module WebMock
69
85
  { body: request.to_proto }
70
86
  end
71
87
 
72
- decoder = ->(request) do
73
- input_class = rpc_from_request(request)[:input_class]
74
- decoded_request = input_class.decode(request.body)
88
+ # save for diffing
89
+ @with_attrs = if block_given?
90
+ block
91
+ elsif request
92
+ request
93
+ elsif attrs.any?
94
+ attrs
95
+ end
75
96
 
97
+ decoder = ->(request_signature) do
76
98
  matched = true
77
- matched &= decoded_request.include?(attrs) if attrs.any?
78
- matched &= block.call(decoded_request) if block
99
+
100
+ request = request_signature.twirp_request
101
+ matched &&= !!request
102
+
103
+ # match request attributes
104
+ if attrs.any?
105
+ matched &&= request.include?(attrs)
106
+ end
107
+
108
+ # match block
109
+ matched &&= block.call(request) if block_given?
79
110
 
80
111
  matched
81
- end if attrs.any? || block_given?
112
+ end
82
113
 
83
114
  super(request_matcher || {}, &decoder)
84
115
  end
@@ -93,17 +124,15 @@ module WebMock
93
124
 
94
125
  response_hashes = responses.map do |response|
95
126
  ->(request) do
96
- # determine msg type and package response
97
- output_class = rpc_from_request(request)[:output_class]
98
- generate_http_response(output_class, response)
127
+ generate_http_response(request, response)
99
128
  end
100
129
  end
101
130
 
102
131
  decoder = ->(request) do
103
- # determine msg type and call provided block
104
- rpc = rpc_from_request(request)
105
- res = block.call(rpc[:input_class].decode(request.body))
106
- generate_http_response(rpc[:output_class], res)
132
+ generate_http_response(
133
+ request,
134
+ block.call(request.twirp_request),
135
+ )
107
136
  end if block_given?
108
137
 
109
138
  super(*response_hashes, &decoder)
@@ -114,29 +143,18 @@ module WebMock
114
143
  raise NotImplementedError
115
144
  end
116
145
 
117
- private
118
-
119
- def rpc_from_request(request_signature)
120
- service_full_name, rpc_name = request_signature.uri.path.split("/").last(2)
146
+ # for internal, package use only
147
+ attr_reader :twirp_client, :rpc_name, :with_attrs
121
148
 
122
- rpcs = @rpcs || begin
123
- # find matching client instance
124
- client = ObjectSpace.each_object(::Twirp::Client).find do |client|
125
- service_full_name == client.class.service_full_name && \
126
- client.class.rpcs.key?(rpc_name)
127
- end
149
+ private
128
150
 
129
- unless client
130
- raise "could not determine Twirp::Client for call to: #{service_full_name}/#{rpc_name}"
131
- end
151
+ def generate_http_response(request, obj)
152
+ msg_class = request.twirp_rpc&.fetch(:output_class)
132
153
 
133
- client.class.rpcs
154
+ if msg_class.nil?
155
+ raise "could not determine Twirp::Client for request: #{request}"
134
156
  end
135
157
 
136
- rpcs[rpc_name]
137
- end
138
-
139
- def generate_http_response(msg_class, obj)
140
158
  res = case obj
141
159
  when nil
142
160
  msg_class.new
@@ -148,6 +166,8 @@ module WebMock
148
166
  end
149
167
 
150
168
  obj
169
+ when ::Twirp::ClientResp
170
+ obj.error || obj.data
151
171
  when ::Twirp::Error
152
172
  obj
153
173
  when Symbol
@@ -0,0 +1,46 @@
1
+ module WebMock
2
+ module Twirp
3
+ module StubRequestSnippet
4
+ def to_s(with_response = true)
5
+ string = "stub_twirp_request"
6
+
7
+ filters = [
8
+ @request_stub.twirp_client,
9
+ @request_stub.rpc_name&.inspect,
10
+ ].compact.join(", ")
11
+ string << "(#{filters})" unless filters.empty?
12
+
13
+ if attrs = @request_stub.with_attrs
14
+ string << ".with(\n"
15
+
16
+ if attrs.is_a?(Hash)
17
+ string << attrs.map do |k, v|
18
+ " #{k}: #{v.inspect},"
19
+ end.join("\n")
20
+ elsif attrs.is_a?(Proc)
21
+ string << " { ... }"
22
+ else
23
+ string << " #{attrs}"
24
+ end
25
+
26
+ string << "\n)"
27
+ end
28
+
29
+ string
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ # hijack creation of Twirp snippets
36
+ module WebMock
37
+ class StubRequestSnippet
38
+ def self.new(request_stub)
39
+ super.tap do |instance|
40
+ if request_stub.is_a?(WebMock::Twirp::RequestStub)
41
+ instance.extend(WebMock::Twirp::StubRequestSnippet)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,5 +1,5 @@
1
1
  module WebMock
2
2
  module Twirp
3
- VERSION = "0.2.1"
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  end
data/lib/webmock/twirp.rb CHANGED
@@ -1,11 +1,16 @@
1
1
  require "google/protobuf"
2
2
  require "twirp"
3
3
  require "webmock"
4
+ require "webmock/twirp/error"
5
+ require "webmock/twirp/matchers"
4
6
  require "webmock/twirp/refinements"
7
+ require "webmock/twirp/request_body_diff"
8
+ require "webmock/twirp/request_signature"
9
+ require "webmock/twirp/request_signature_snippet"
5
10
  require "webmock/twirp/request_stub"
11
+ require "webmock/twirp/stub_request_snippet"
6
12
  require "webmock/twirp/version"
7
13
 
8
-
9
14
  module WebMock
10
15
  module Twirp
11
16
  extend self
@@ -26,15 +31,22 @@ module WebMock
26
31
  end
27
32
  end
28
33
 
34
+ # patch WebMock to export Twirp helpers
35
+ module WebMock
36
+ module API
37
+ include WebMock::Twirp::API
38
+ end
29
39
 
30
- if $LOADED_FEATURES.find { |x| x =~ %r{/webmock/rspec.rb} }
31
- # require "webmock/rspec"
32
- RSpec.configure { |c| c.include WebMock::Twirp::API }
33
- else
34
- # patch WebMock to also export stub_twirp_request
35
- module WebMock
36
- module API
37
- include WebMock::Twirp::API
38
- end
40
+ module Matchers
41
+ include WebMock::Twirp::Matchers
42
+ end
43
+ end
44
+
45
+ if $LOADED_FEATURES.find { |x| x =~ %r{/webmock/rspec.rb$} }
46
+ # require "webmock/rspec" was already called, so load helpers
47
+
48
+ RSpec.configure do |conf|
49
+ conf.include WebMock::Twirp::API
50
+ conf.include WebMock::Twirp::Matchers
39
51
  end
40
52
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webmock-twirp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.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-10-20 00:00:00.000000000 Z
11
+ date: 2022-11-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: webmock
@@ -133,8 +133,15 @@ files:
133
133
  - README.md
134
134
  - lib/webmock-twirp.rb
135
135
  - lib/webmock/twirp.rb
136
+ - lib/webmock/twirp/error.rb
137
+ - lib/webmock/twirp/matchers.rb
138
+ - lib/webmock/twirp/matchers/make_twirp_request.rb
136
139
  - lib/webmock/twirp/refinements.rb
140
+ - lib/webmock/twirp/request_body_diff.rb
141
+ - lib/webmock/twirp/request_signature.rb
142
+ - lib/webmock/twirp/request_signature_snippet.rb
137
143
  - lib/webmock/twirp/request_stub.rb
144
+ - lib/webmock/twirp/stub_request_snippet.rb
138
145
  - lib/webmock/twirp/version.rb
139
146
  - webmock-twirp.gemspec
140
147
  homepage: https://github.com/dpep/webmock-twirp