twirp 0.0.4 → 0.1.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.
@@ -1,3 +1,3 @@
1
1
  module Twirp
2
- VERSION = "0.0.4"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -79,17 +79,16 @@ func (g *generator) generateFile(file *descriptor.FileDescriptorProto) *plugin.C
79
79
  }
80
80
  for i, service := range file.Service {
81
81
  serviceName := serviceName(service)
82
- g.P(fmt.Sprintf("%sclass %sService < Twirp::Service", indent, serviceName))
83
- g.P(fmt.Sprintf(`%s PATH_PREFIX = "/twirp/%s.%s"`, indent, pkgName, serviceName))
82
+ g.P(fmt.Sprintf("%sclass %sService < Twirp::Service", indent, CamelCase(serviceName)))
84
83
  if pkgName != "" {
85
84
  g.P(fmt.Sprintf(`%s package "%s"`, indent, pkgName))
86
85
  }
87
- g.P(fmt.Sprintf(`%s service "%s"`, indent, CamelCase(serviceName)))
86
+ g.P(fmt.Sprintf(`%s service "%s"`, indent, serviceName))
88
87
  for _, method := range service.GetMethod() {
89
88
  methName := methodName(method)
90
89
  inputName := methodInputName(method)
91
90
  outputName := methodOutputName(method)
92
- g.P(fmt.Sprintf(`%s rpc :%s, %s, %s, handler_method: "%s"`,
91
+ g.P(fmt.Sprintf(`%s rpc :%s, %s, %s, :handler_method => :%s`,
93
92
  indent, methName, inputName, outputName, SnakeCase(methName)))
94
93
  }
95
94
  g.P(fmt.Sprintf(`%send`, indent))
@@ -34,6 +34,29 @@ end
34
34
 
35
35
  class TestTwirpError < Minitest::Test
36
36
 
37
+ def test_constructors # Try out some constructors
38
+ err = Twirp::Error.internal "woops"
39
+ assert_equal :internal, err.code
40
+ assert_equal "woops", err.msg
41
+ assert_equal({}, err.meta) # empty
42
+
43
+ err = Twirp::Error.not_found "not here", who: "Waldo"
44
+ assert_equal :not_found, err.code
45
+ assert_equal "not here", err.msg
46
+ assert_equal({who: "Waldo"}, err.meta)
47
+
48
+ err = Twirp::Error.invalid_argument("required", "argument" => "size")
49
+ assert_equal :invalid_argument, err.code
50
+ assert_equal "required", err.msg
51
+ assert_equal({"argument" => "size"}, err.meta) # empty
52
+ end
53
+
54
+ def test_invalid_constructor # Make sure that only supported codes are implemented (prevent bad metaprogramming)
55
+ assert_raises NoMethodError do
56
+ Twirp::invalid_code_error "should fail"
57
+ end
58
+ end
59
+
37
60
  def test_new_with_valid_code_and_a_message
38
61
  err = Twirp::Error.new(:internal, "woops")
39
62
  assert_equal :internal, err.code
@@ -43,24 +66,18 @@ class TestTwirpError < Minitest::Test
43
66
 
44
67
  def test_new_with_valid_metadata
45
68
  err = Twirp::Error.new(:internal, "woops", "meta" => "data", "for this" => "error")
46
- assert_equal err.meta["meta"], "data"
69
+ assert_equal "data", err.meta["meta"]
47
70
  assert_equal "error", err.meta["for this"]
48
71
  assert_nil err.meta["something else"]
49
- end
50
72
 
51
- def test_invalid_code
52
- assert_raises ArgumentError do
53
- Twirp::Error.new(:invalid_code, "woops")
54
- end
73
+ err = Twirp::Error.new(:internal, "woops", meta: "data")
74
+ assert_equal err.meta[:meta], "data"
75
+ assert_nil err.meta["meta"] # no symbol/string multiaccess for now
55
76
  end
56
77
 
57
78
  def test_invalid_metadata
58
79
  Twirp::Error.new(:internal, "woops") # ensure the base case doesn't error
59
80
 
60
- assert_raises ArgumentError do
61
- Twirp::Error.new(:internal, "woops", non_string: "metadata")
62
- end
63
-
64
81
  assert_raises ArgumentError do
65
82
  Twirp::Error.new(:internal, "woops", "string key" => :non_string_value)
66
83
  end
@@ -70,14 +87,14 @@ class TestTwirpError < Minitest::Test
70
87
  end
71
88
  end
72
89
 
73
- def test_as_json
90
+ def test_to_h
74
91
  # returns a hash with attributes
75
92
  err = Twirp::Error.new(:internal, "err msg", "key" => "val")
76
- assert_equal({code: :internal, msg: "err msg", meta: {"key" => "val"}}, err.as_json)
93
+ assert_equal({code: :internal, msg: "err msg", meta: {"key" => "val"}}, err.to_h)
77
94
 
78
95
  # skips meta if not included
79
96
  err = Twirp::Error.new(:internal, "err msg")
80
- assert_equal({code: :internal, msg: "err msg"}, err.as_json)
97
+ assert_equal({code: :internal, msg: "err msg"}, err.to_h)
81
98
  end
82
99
  end
83
100
 
@@ -29,27 +29,19 @@ module Example
29
29
  class Haberdasher < Twirp::Service
30
30
  package "example"
31
31
  service "Haberdasher"
32
-
33
- rpc "MakeHat", Size, Hat, handler_method: :make_hat
32
+ rpc :MakeHat, Size, Hat, :handler_method => :make_hat
34
33
  end
35
34
  end
36
35
 
37
36
  # Example service handler.
38
37
  # It would be provided by the developer as implementation for the service.
39
38
  class HaberdasherHandler
40
- # This fake can optionally be initialized with a block for the make_hat implementation.
41
39
  def initialize(&block)
42
- if block_given?
43
- @make_hat_block = block
44
- else # default implementation
45
- @make_hat_block = Proc.new do |size|
46
- {inches: size.inches, color: "white"}
47
- end
48
- end
40
+ @block = block if block_given?
49
41
  end
50
42
 
51
- def make_hat(size)
52
- @make_hat_block.call(size)
43
+ def make_hat(input, env)
44
+ @block && @block.call(input, env)
53
45
  end
54
46
  end
55
47
 
@@ -8,14 +8,19 @@ require_relative './fake_services'
8
8
 
9
9
  class ServiceTest < Minitest::Test
10
10
 
11
- # DSL rpc builds the proper rpc data on the service
12
- def test_rpc_methods
13
- assert_equal 1, Example::Haberdasher.rpcs.size
11
+ def setup
12
+ Example::Haberdasher.raise_exceptions = true # configure for testing to make debugging easier
13
+ end
14
+
15
+ # The rpc DSL should properly build the base Twirp environment for each rpc method.
16
+ def test_base_envs_accessor
17
+ assert_equal 1, Example::Haberdasher.base_envs.size
14
18
  assert_equal({
15
- request_class: Example::Size,
16
- response_class: Example::Hat,
19
+ rpc_method: :MakeHat,
20
+ input_class: Example::Size,
21
+ output_class: Example::Hat,
17
22
  handler_method: :make_hat,
18
- }, Example::Haberdasher.rpcs["MakeHat"])
23
+ }, Example::Haberdasher.base_envs["MakeHat"])
19
24
  end
20
25
 
21
26
  # DSL package and service define the proper data on the service
@@ -23,42 +28,27 @@ class ServiceTest < Minitest::Test
23
28
  assert_equal "example", Example::Haberdasher.package_name
24
29
  assert_equal "Haberdasher", Example::Haberdasher.service_name
25
30
  assert_equal "example.Haberdasher", Example::Haberdasher.service_full_name
26
- assert_equal "/twirp/example.Haberdasher", Example::Haberdasher.path_prefix
27
31
 
28
32
  assert_equal "", EmptyService.package_name # defaults to empty string
29
33
  assert_equal "EmptyService", EmptyService.service_name # defaults to class name
30
34
  assert_equal "EmptyService", EmptyService.service_full_name # with no package is just the service name
31
- assert_equal "/twirp/EmptyService", EmptyService.path_prefix
32
35
  end
33
36
 
34
37
  def test_init_service
35
38
  svc = Example::Haberdasher.new(HaberdasherHandler.new)
36
39
  assert svc.respond_to?(:call) # so it is a Proc that can be used as Rack middleware
37
- assert_equal "example.Haberdasher", svc.service_full_name
38
- assert_equal "/twirp/example.Haberdasher", svc.path_prefix
40
+ assert_equal "example.Haberdasher", svc.full_name
39
41
  end
40
42
 
41
43
  def test_init_empty_service
42
44
  empty_svc = EmptyService.new(nil) # an empty service does not need a handler
43
45
  assert empty_svc.respond_to?(:call)
44
- assert_equal "EmptyService", empty_svc.service_full_name
45
- assert_equal "/twirp/EmptyService", empty_svc.path_prefix
46
- end
47
-
48
- def test_init_failures
49
- assert_raises ArgumentError do
50
- Example::Haberdasher.new() # handler is mandatory
51
- end
52
-
53
- err = assert_raises ArgumentError do
54
- Example::Haberdasher.new("fake handler")
55
- end
56
- assert_equal "Handler must respond to .make_hat(input) in order to handle the message MakeHat.", err.message
46
+ assert_equal "EmptyService", empty_svc.full_name
57
47
  end
58
48
 
59
49
  def test_successful_json_request
60
- env = json_req "/twirp/example.Haberdasher/MakeHat", inches: 10
61
- status, headers, body = haberdasher_service.call(env)
50
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
51
+ status, headers, body = haberdasher_service.call(rack_env)
62
52
 
63
53
  assert_equal 200, status
64
54
  assert_equal 'application/json', headers['Content-Type']
@@ -66,8 +56,8 @@ class ServiceTest < Minitest::Test
66
56
  end
67
57
 
68
58
  def test_successful_proto_request
69
- env = proto_req "/twirp/example.Haberdasher/MakeHat", Example::Size.new(inches: 10)
70
- status, headers, body = haberdasher_service.call(env)
59
+ rack_env = proto_req "/example.Haberdasher/MakeHat", Example::Size.new(inches: 10)
60
+ status, headers, body = haberdasher_service.call(rack_env)
71
61
 
72
62
  assert_equal 200, status
73
63
  assert_equal 'application/protobuf', headers['Content-Type']
@@ -75,80 +65,142 @@ class ServiceTest < Minitest::Test
75
65
  end
76
66
 
77
67
  def test_bad_route_with_wrong_rpc_method
78
- env = json_req "/twirp/example.Haberdasher/MakeUnicorns", and_rainbows: true
79
- status, headers, body = haberdasher_service.call(env)
68
+ rack_env = json_req "/twirp/example.Haberdasher/MakeUnicorns", and_rainbows: true
69
+ status, headers, body = haberdasher_service.call(rack_env)
80
70
 
81
71
  assert_equal 404, status
82
72
  assert_equal 'application/json', headers['Content-Type']
83
73
  assert_equal({
84
74
  "code" => 'bad_route',
85
- "msg" => 'rpc method not found: "MakeUnicorns"',
86
- "meta"=> {"twirp_invalid_route" => "POST /twirp/example.Haberdasher/MakeUnicorns"},
75
+ "msg" => 'Invalid rpc method "MakeUnicorns"',
76
+ "meta" => {"twirp_invalid_route" => "POST /twirp/example.Haberdasher/MakeUnicorns"},
87
77
  }, JSON.parse(body[0]))
88
78
  end
89
79
 
90
80
  def test_bad_route_with_wrong_http_method
91
- env = Rack::MockRequest.env_for "/twirp/example.Haberdasher/MakeHat",
81
+ rack_env = Rack::MockRequest.env_for "example.Haberdasher/MakeHat",
92
82
  method: "GET", input: '{"inches": 10}', "CONTENT_TYPE" => "application/json"
93
- status, headers, body = haberdasher_service.call(env)
83
+ status, headers, body = haberdasher_service.call(rack_env)
94
84
 
95
85
  assert_equal 404, status
96
86
  assert_equal 'application/json', headers['Content-Type']
97
87
  assert_equal({
98
88
  "code" => 'bad_route',
99
- "msg" => 'HTTP request method must be POST',
100
- "meta"=> {"twirp_invalid_route" => "GET /twirp/example.Haberdasher/MakeHat"},
89
+ "msg" => 'HTTP request method must be POST',
90
+ "meta" => {"twirp_invalid_route" => "GET /example.Haberdasher/MakeHat"},
101
91
  }, JSON.parse(body[0]))
102
92
  end
103
93
 
104
94
  def test_bad_route_with_wrong_content_type
105
- env = Rack::MockRequest.env_for "/twirp/example.Haberdasher/MakeHat",
95
+ rack_env = Rack::MockRequest.env_for "example.Haberdasher/MakeHat",
106
96
  method: "POST", input: 'free text', "CONTENT_TYPE" => "text/plain"
107
- status, headers, body = haberdasher_service.call(env)
97
+ status, headers, body = haberdasher_service.call(rack_env)
108
98
 
109
99
  assert_equal 404, status
110
100
  assert_equal 'application/json', headers['Content-Type']
111
101
  assert_equal({
112
102
  "code" => 'bad_route',
113
- "msg" => 'unexpected Content-Type: "text/plain". Content-Type header must be one of "application/json" or "application/protobuf"',
114
- "meta"=> {"twirp_invalid_route" => "POST /twirp/example.Haberdasher/MakeHat"},
103
+ "msg" => 'unexpected Content-Type: "text/plain". Content-Type header must be one of application/json or application/protobuf',
104
+ "meta" => {"twirp_invalid_route" => "POST /example.Haberdasher/MakeHat"},
115
105
  }, JSON.parse(body[0]))
116
106
  end
117
107
 
118
108
  def test_bad_route_with_wrong_path_json
119
- env = json_req "/wrongpath", {}
120
- status, headers, body = haberdasher_service.call(env)
109
+ rack_env = json_req "/wrongpath", {}
110
+ status, headers, body = haberdasher_service.call(rack_env)
121
111
 
122
112
  assert_equal 404, status
123
113
  assert_equal 'application/json', headers['Content-Type']
124
114
  assert_equal({
125
115
  "code" => 'bad_route',
126
- "msg" => 'Invalid route. Expected format: POST {BaseURL}/twirp/(package.)?{Service}/{Method}',
127
- "meta"=> {"twirp_invalid_route" => "POST /wrongpath"},
116
+ "msg" => 'Invalid route. Expected format: POST {BaseURL}/example.Haberdasher/{Method}',
117
+ "meta" => {"twirp_invalid_route" => "POST /wrongpath"},
128
118
  }, JSON.parse(body[0]))
129
119
  end
130
120
 
131
121
  def test_bad_route_with_wrong_path_protobuf
132
- env = proto_req "/another/wrong.Path/MakeHat", Example::Empty.new()
133
- status, headers, body = haberdasher_service.call(env)
122
+ rack_env = proto_req "/another/wrong.Path/MakeHat", Example::Empty.new()
123
+ status, headers, body = haberdasher_service.call(rack_env)
134
124
 
135
125
  assert_equal 404, status
136
126
  assert_equal 'application/json', headers['Content-Type'] # error responses are always JSON, even for Protobuf requests
137
127
  assert_equal({
138
128
  "code" => 'bad_route',
139
- "msg" => 'Invalid route. Expected format: POST {BaseURL}/twirp/(package.)?{Service}/{Method}',
140
- "meta"=> {"twirp_invalid_route" => "POST /another/wrong.Path/MakeHat"},
129
+ "msg" => 'Invalid route. Expected format: POST {BaseURL}/example.Haberdasher/{Method}',
130
+ "meta" => {"twirp_invalid_route" => "POST /another/wrong.Path/MakeHat"},
131
+ }, JSON.parse(body[0]))
132
+ end
133
+
134
+ def test_bad_route_with_wrong_json_body
135
+ rack_env = Rack::MockRequest.env_for "example.Haberdasher/MakeHat",
136
+ method: "POST", input: 'bad json', "CONTENT_TYPE" => "application/json"
137
+ status, headers, body = haberdasher_service.call(rack_env)
138
+
139
+ assert_equal 404, status
140
+ assert_equal 'application/json', headers['Content-Type']
141
+ assert_equal({
142
+ "code" => 'bad_route',
143
+ "msg" => 'Invalid request body for rpc method "MakeHat" with Content-Type=application/json',
144
+ "meta" => {"twirp_invalid_route" => "POST /example.Haberdasher/MakeHat"},
141
145
  }, JSON.parse(body[0]))
142
146
  end
143
147
 
148
+ def test_bad_route_with_wrong_protobuf_body
149
+ rack_env = Rack::MockRequest.env_for "example.Haberdasher/MakeHat",
150
+ method: "POST", input: 'bad protobuf', "CONTENT_TYPE" => "application/protobuf"
151
+ status, headers, body = haberdasher_service.call(rack_env)
152
+
153
+ assert_equal 404, status
154
+ assert_equal 'application/json', headers['Content-Type']
155
+ assert_equal({
156
+ "code" => 'bad_route',
157
+ "msg" => 'Invalid request body for rpc method "MakeHat" with Content-Type=application/protobuf',
158
+ "meta" => {"twirp_invalid_route" => "POST /example.Haberdasher/MakeHat"},
159
+ }, JSON.parse(body[0]))
160
+ end
161
+
162
+ def test_bad_route_triggers_on_error_hooks
163
+ svc = haberdasher_service
164
+
165
+ before_called = false
166
+ svc.before do |rack_env, env|
167
+ before_called = true
168
+ end
169
+
170
+ success_called = false
171
+ svc.on_success do |env|
172
+ success_called = true
173
+ end
174
+
175
+ error_called = false
176
+ svc.on_error do |twerr, env|
177
+ error_called = true
178
+ end
179
+
180
+ rack_env = json_req "/example.Haberdasher/BadRouteMethod", inches: 10
181
+ status, headers, body = svc.call(rack_env)
182
+
183
+ assert_equal 404, status
184
+ refute before_called
185
+ refute success_called
186
+ assert error_called # << on_error called
187
+ end
188
+
189
+ def test_long_base_url
190
+ rack_env = json_req "long-ass/base/url/twirp/example.Haberdasher/MakeHat", {inches: 10}
191
+ status, headers, body = haberdasher_service.call(rack_env)
192
+
193
+ assert_equal 200, status
194
+ end
195
+
144
196
  # Handler should be able to return an instance of the proto message
145
197
  def test_handler_returns_a_proto_message
146
- svc = Example::Haberdasher.new(HaberdasherHandler.new do |size|
198
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
147
199
  Example::Hat.new(inches: 11)
148
200
  end)
149
201
 
150
- env = proto_req "/twirp/example.Haberdasher/MakeHat", Example::Size.new
151
- status, headers, body = svc.call(env)
202
+ rack_env = proto_req "/example.Haberdasher/MakeHat", Example::Size.new
203
+ status, headers, body = svc.call(rack_env)
152
204
 
153
205
  assert_equal 200, status
154
206
  assert_equal Example::Hat.new(inches: 11, color: ""), Example::Hat.decode(body[0])
@@ -156,62 +208,569 @@ class ServiceTest < Minitest::Test
156
208
 
157
209
  # Handler should be able to return a hash with attributes
158
210
  def test_handler_returns_hash_attributes
159
- svc = Example::Haberdasher.new(HaberdasherHandler.new do |size|
211
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
160
212
  {inches: 11}
161
213
  end)
162
214
 
163
- env = proto_req "/twirp/example.Haberdasher/MakeHat", Example::Size.new
164
- status, headers, body = svc.call(env)
215
+ rack_env = proto_req "/example.Haberdasher/MakeHat", Example::Size.new
216
+ status, headers, body = svc.call(rack_env)
165
217
 
166
218
  assert_equal 200, status
167
219
  assert_equal Example::Hat.new(inches: 11, color: ""), Example::Hat.decode(body[0])
168
220
  end
169
221
 
170
- # Handler should be able to return nil, as a message with all zero-values
171
- def test_handler_returns_nil
172
- svc = Example::Haberdasher.new(HaberdasherHandler.new do |size|
222
+ # Handler should be able to return Twirp::Error values, that will trigger error responses
223
+ def test_handler_returns_twirp_error
224
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
225
+ return Twirp::Error.invalid_argument "I don't like that size"
226
+ end)
227
+
228
+ rack_env = proto_req "/example.Haberdasher/MakeHat", Example::Size.new(inches: 666)
229
+ status, headers, body = svc.call(rack_env)
230
+ assert_equal 400, status
231
+ assert_equal 'application/json', headers['Content-Type'] # error responses are always JSON, even for Protobuf requests
232
+ assert_equal({
233
+ "code" => 'invalid_argument',
234
+ "msg" => "I don't like that size",
235
+ }, JSON.parse(body[0]))
236
+ end
237
+
238
+ def test_handler_method_can_set_response_headers_through_the_env
239
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
240
+ env[:http_response_headers]["Cache-Control"] = "public, max-age=60"
241
+ {}
242
+ end)
243
+
244
+ rack_env = proto_req "/example.Haberdasher/MakeHat", Example::Size.new
245
+ status, headers, body = svc.call(rack_env)
246
+
247
+ assert_equal 200, status
248
+ assert_equal "public, max-age=60", headers["Cache-Control"] # set by the handler
249
+ assert_equal "application/protobuf", headers["Content-Type"] # set by Twirp::Service
250
+ end
251
+
252
+ def test_handler_returns_invalid_type_nil
253
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
173
254
  nil
174
255
  end)
175
256
 
176
- env = proto_req "/twirp/example.Haberdasher/MakeHat", Example::Size.new
177
- status, headers, body = svc.call(env)
257
+ rack_env = proto_req "/example.Haberdasher/MakeHat", Example::Size.new
258
+ status, headers, body = svc.call(rack_env)
259
+
260
+ assert_equal 500, status
261
+ assert_equal({
262
+ "code" => 'internal',
263
+ "msg" => "Handler method make_hat expected to return one of Example::Hat, Hash or Twirp::Error, but returned NilClass.",
264
+ }, JSON.parse(body[0]))
265
+ end
266
+
267
+ def test_handler_returns_invalid_type_string
268
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
269
+ "bad type"
270
+ end)
271
+
272
+ rack_env = proto_req "/example.Haberdasher/MakeHat", Example::Size.new
273
+ status, headers, body = svc.call(rack_env)
274
+
275
+ assert_equal 500, status
276
+ assert_equal({
277
+ "code" => 'internal',
278
+ "msg" => "Handler method make_hat expected to return one of Example::Hat, Hash or Twirp::Error, but returned String.",
279
+ }, JSON.parse(body[0]))
280
+ end
281
+
282
+ def test_handler_not_implementing_method
283
+ svc = Example::Haberdasher.new("foo")
284
+
285
+ rack_env = proto_req "/example.Haberdasher/MakeHat", Example::Size.new
286
+ status, headers, body = svc.call(rack_env)
287
+
288
+ assert_equal 501, status
289
+ assert_equal({
290
+ "code" => 'unimplemented',
291
+ "msg" => "Handler method make_hat is not implemented.",
292
+ }, JSON.parse(body[0]))
293
+ end
294
+
295
+ def test_handler_returns_invalid_attributes
296
+ Example::Haberdasher.raise_exceptions = false
297
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
298
+ {bad_attribute: "bad value", lol: 666}
299
+ end)
300
+
301
+ rack_env = proto_req "/example.Haberdasher/MakeHat", Example::Size.new
302
+ status, headers, body = svc.call(rack_env)
303
+
304
+ assert_equal 500, status
305
+ assert_equal({
306
+ "code" => 'internal',
307
+ "msg" => "Unknown field name 'bad_attribute' in initialization map entry.",
308
+ "meta" => {"cause"=>"ArgumentError"},
309
+ }, JSON.parse(body[0]))
310
+ end
311
+
312
+ def test_handler_raises_standard_error
313
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
314
+ 10 / 0 # divided by 0
315
+ end)
316
+
317
+ # check that tests can see raised errors from the handler with raise_exceptions = true
318
+ assert_raises ZeroDivisionError do
319
+ rack_env = proto_req "/example.Haberdasher/MakeHat", Example::Size.new
320
+ svc.call(rack_env)
321
+ end
322
+
323
+ # check that the error is properly wrapped with raise_exceptions = false
324
+ Example::Haberdasher.raise_exceptions = false
325
+ rack_env = proto_req "/example.Haberdasher/MakeHat", Example::Size.new
326
+ status, headers, body = svc.call(rack_env)
327
+
328
+ assert_equal 500, status
329
+ assert_equal({
330
+ "code" => 'internal',
331
+ "msg" => "divided by 0",
332
+ "meta" => {"cause"=>"ZeroDivisionError"},
333
+ }, JSON.parse(body[0]))
334
+ end
335
+
336
+ def test_before_simple
337
+ handler_method_called = false
338
+ handler = HaberdasherHandler.new do |size, env|
339
+ handler_method_called = true
340
+ {}
341
+ end
342
+
343
+ called_with_env = nil
344
+ called_with_rack_env = nil
345
+ svc = Example::Haberdasher.new(handler)
346
+ svc.before do |rack_env, env|
347
+ env[:raw_contet_type] = rack_env["CONTENT_TYPE"]
348
+ called_with_env = env
349
+ called_with_rack_env = rack_env
350
+ end
351
+
352
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
353
+ status, headers, body = svc.call(rack_env)
354
+
355
+ refute_nil called_with_env, "the before hook was called with a Twirp env"
356
+ assert_equal :MakeHat, called_with_env[:rpc_method]
357
+ assert_equal Example::Size.new(inches: 10), called_with_env[:input]
358
+ assert_equal "application/json", called_with_env[:raw_contet_type]
359
+
360
+ refute_nil called_with_rack_env, "the before hook was called with a Rack env"
361
+ assert_equal "POST", called_with_rack_env["REQUEST_METHOD"]
362
+
363
+ assert handler_method_called, "the handler method was called"
364
+ assert_equal 200, status, "response is successful"
365
+ end
366
+
367
+ def test_before_add_data_in_env_for_the_handler_method
368
+ val = nil
369
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
370
+ val = env[:from_the_hook]
371
+ {}
372
+ end)
373
+ svc.before do |rack_env, env|
374
+ env[:from_the_hook] = "hello handler"
375
+ end
376
+
377
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
378
+ status, headers, body = svc.call(rack_env)
178
379
 
179
380
  assert_equal 200, status
180
- assert_equal Example::Hat.new(inches: 0, color: ""), Example::Hat.decode(body[0])
381
+ assert_equal "hello handler", val
181
382
  end
182
383
 
183
- # Handler should be able to return Twirp::Error values, that will trigger error responses
184
- def test_handler_returns_twirp_error
185
- svc = Example::Haberdasher.new(HaberdasherHandler.new do |size|
186
- return Twirp::Error.new(:invalid_argument, "I don't like that size")
384
+ def test_before_returning_twirp_error_cancels_request
385
+ handler_called = false
386
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
387
+ handler_called = true
388
+ {}
187
389
  end)
390
+ svc.before do |rack_env, env|
391
+ return Twirp::Error.internal "error from before hook"
392
+ end
188
393
 
189
- env = proto_req "/twirp/example.Haberdasher/MakeHat", Example::Size.new(inches: 666)
190
- status, headers, body = svc.call(env)
191
- assert_equal 400, status
192
- assert_equal 'application/json', headers['Content-Type'] # error responses are always JSON, even for Protobuf requests
394
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
395
+ status, headers, body = svc.call(rack_env)
396
+
397
+ assert_equal 500, status
398
+ refute handler_called
193
399
  assert_equal({
194
- "code" => 'invalid_argument',
195
- "msg" => "I don't like that size",
400
+ "code" => 'intenal',
401
+ "msg" => 'error from before hook',
196
402
  }, JSON.parse(body[0]))
197
403
  end
198
404
 
199
- # Handler should be able to raise a Twirp::Exception, that will trigger error responses
200
- def test_handler_raises_twirp_exception
201
- svc = Example::Haberdasher.new(HaberdasherHandler.new do |size|
202
- raise Twirp::Exception.new(:invalid_argument, "I don't like that size")
405
+ def test_before_raising_exception_cancels_request
406
+ handler_called = false
407
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
408
+ handler_called = true
409
+ {}
203
410
  end)
411
+ svc.before do |rack_env, env|
412
+ 1 / 0 # divided by 0.
413
+ end
204
414
 
205
- env = proto_req "/twirp/example.Haberdasher/MakeHat", Example::Size.new(inches: 666)
206
- status, headers, body = svc.call(env)
207
- assert_equal 400, status
208
- assert_equal 'application/json', headers['Content-Type'] # error responses are always JSON, even for Protobuf requests
415
+ Example::Haberdasher.raise_exceptions = false
416
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
417
+ status, headers, body = svc.call(rack_env)
418
+
419
+ assert_equal 500, status
420
+ refute handler_called
209
421
  assert_equal({
210
- "code" => 'invalid_argument',
211
- "msg" => "I don't like that size",
422
+ "code" => 'internal',
423
+ "msg" => "divided by 0",
424
+ "meta" => {"cause"=>"ZeroDivisionError"},
425
+ }, JSON.parse(body[0]))
426
+ end
427
+
428
+ def test_before_multiple_run_in_order
429
+ handler_called = false
430
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
431
+ handler_called = true
432
+ refute_nil env[:from_hook_1]
433
+ refute_nil env[:from_hook_2]
434
+ {}
435
+ end)
436
+ svc.before do |rack_env, env|
437
+ assert_nil env[:from_hook_1]
438
+ assert_nil env[:from_hook_2]
439
+ env[:from_hook_1] = true
440
+ end
441
+ svc.before do |rack_env, env|
442
+ refute_nil env[:from_hook_1]
443
+ assert_nil env[:from_hook_2]
444
+ env[:from_hook_2] = true
445
+ end
446
+
447
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
448
+ status, headers, body = svc.call(rack_env)
449
+
450
+ assert_equal 200, status
451
+ assert_equal true, handler_called
452
+ end
453
+
454
+ def test_before_multiple_first_returning_error_halts_chain
455
+ hook1_called = false
456
+ hook2_called = false
457
+ handler_called = false
458
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
459
+ handler_called = true
460
+ {}
461
+ end)
462
+ svc.before do |rack_env, env|
463
+ hook1_called = true
464
+ return Twirp::Error.internal "hook1 failed"
465
+ end
466
+ svc.before do |rack_env, env|
467
+ hook2_called = true
468
+ return Twirp::Error.internal "hook2 failed"
469
+ end
470
+
471
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
472
+ status, headers, body = svc.call(rack_env)
473
+
474
+ assert_equal 500, status
475
+ assert_equal 'application/json', headers['Content-Type']
476
+ assert_equal({
477
+ "code" => 'intenal',
478
+ "msg" => 'hook1 failed',
479
+ }, JSON.parse(body[0]))
480
+
481
+ assert hook1_called
482
+ refute hook2_called
483
+ refute handler_called
484
+ end
485
+
486
+ def test_on_success_has_env_output
487
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |input, env|
488
+ env[:handler_called] = true
489
+ {inches: 88, color: "blue"}
490
+ end)
491
+ success_called = false
492
+ svc.on_success do |env|
493
+ success_called = true
494
+ assert env[:handler_called]
495
+ assert_equal Example::Hat.new(inches: 88, color: "blue"), env[:output] # output from hadnler
496
+ end
497
+
498
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
499
+ status, headers, body = svc.call(rack_env)
500
+
501
+ assert_equal 200, status
502
+ assert success_called
503
+ end
504
+
505
+ def test_on_success_does_not_trigger_error
506
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |input, env|
507
+ return Twirp::Error.internal "error from handler"
508
+ end)
509
+ success_called = false
510
+ svc.on_success do |env|
511
+ success_called = true
512
+ end
513
+
514
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
515
+ status, headers, body = svc.call(rack_env)
516
+
517
+ assert_equal 500, status
518
+ refute success_called # after hook not called
519
+ assert_equal({
520
+ "code" => 'intenal',
521
+ "msg" => 'error from handler',
212
522
  }, JSON.parse(body[0]))
213
523
  end
214
524
 
525
+ def test_on_success_multiple_run_in_order
526
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
527
+ env[:from_handler] = true
528
+ {}
529
+ end)
530
+ svc.on_success do |env|
531
+ refute_nil env[:from_handler]
532
+ assert_nil env[:from_hook_1]
533
+ assert_nil env[:from_hook_2]
534
+ env[:from_hook_1] = true
535
+ end
536
+ hook2_called = false
537
+ svc.on_success do |env|
538
+ refute_nil env[:from_handler]
539
+ refute_nil env[:from_hook_1]
540
+ assert_nil env[:from_hook_2]
541
+ env[:from_hook_2] = true
542
+ hook2_called = true
543
+ end
544
+
545
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
546
+ status, headers, body = svc.call(rack_env)
547
+
548
+ assert_equal 200, status
549
+ assert hook2_called
550
+ end
551
+
552
+ def test_on_success_raising_exception_is_handled_as_internal
553
+ handler_called = false
554
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
555
+ handler_called = true
556
+ {}
557
+ end)
558
+ svc.on_success do |env|
559
+ 1 / 0 # divided by 0
560
+ end
561
+
562
+ Example::Haberdasher.raise_exceptions = false
563
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
564
+ status, headers, body = svc.call(rack_env)
565
+
566
+ assert_equal 500, status
567
+ assert handler_called
568
+ assert_equal({
569
+ "code" => 'internal',
570
+ "msg" => "divided by 0",
571
+ "meta" => {"cause"=>"ZeroDivisionError"},
572
+ }, JSON.parse(body[0]))
573
+ end
574
+
575
+ def test_on_error_after_before_fails
576
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
577
+ {}
578
+ end)
579
+ svc.before do |rack_env, env|
580
+ return Twirp::Error.internal "before failed"
581
+ end
582
+ error_called = false
583
+ svc.on_error do |twerr, env|
584
+ error_called = true
585
+ assert_equal :internal, twerr.code
586
+ assert_equal "before failed", twerr.msg
587
+ end
588
+
589
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
590
+ status, headers, body = svc.call(rack_env)
591
+
592
+ assert_equal 500, status
593
+ assert_equal({
594
+ "code" => 'intenal',
595
+ "msg" => 'before failed',
596
+ }, JSON.parse(body[0]))
597
+ assert error_called
598
+ end
599
+
600
+ def test_on_error_after_before_raises_exception
601
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
602
+ {}
603
+ end)
604
+ svc.before do |rack_env, env|
605
+ 1 / 0 # divided by 0
606
+ end
607
+ Example::Haberdasher.raise_exceptions = false
608
+
609
+ error_called = false
610
+ svc.on_error do |twerr, env|
611
+ error_called = true
612
+ end
613
+
614
+ exception_raised_called = false
615
+ svc.exception_raised do |e, env|
616
+ exception_raised_called = true
617
+ assert_equal "divided by 0", e.message
618
+ end
619
+
620
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
621
+ status, headers, body = svc.call(rack_env)
622
+
623
+ assert_equal 500, status
624
+ assert_equal({
625
+ "code" => 'internal',
626
+ "msg" => "divided by 0",
627
+ "meta" => {"cause"=>"ZeroDivisionError"},
628
+ }, JSON.parse(body[0]))
629
+ assert error_called
630
+ assert exception_raised_called
631
+ end
632
+
633
+ def test_on_error_after_handler_returns_twirp_error
634
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
635
+ return Twirp::Error.internal "handler error"
636
+ end)
637
+
638
+ error_called = false
639
+ svc.on_error do |twerr, env|
640
+ error_called = true
641
+ assert_equal :internal, twerr.code
642
+ assert_equal "handler error", twerr.msg
643
+ end
644
+
645
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
646
+ status, headers, body = svc.call(rack_env)
647
+
648
+ assert_equal 500, status
649
+ assert_equal({
650
+ "code" => 'intenal',
651
+ "msg" => 'handler error',
652
+ }, JSON.parse(body[0]))
653
+ assert error_called
654
+ end
655
+
656
+ def test_on_error_after_handler_raises_exception
657
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
658
+ 1 / 0 # divided by 0
659
+ end)
660
+ Example::Haberdasher.raise_exceptions = false
661
+
662
+ error_called = false
663
+ svc.on_error do |twerr, env|
664
+ error_called = true
665
+ end
666
+
667
+ exception_raised_called = false
668
+ svc.exception_raised do |e, env|
669
+ exception_raised_called = true
670
+ assert_equal "divided by 0", e.message
671
+ end
672
+
673
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
674
+ status, headers, body = svc.call(rack_env)
675
+
676
+ assert_equal 500, status
677
+ assert_equal({
678
+ "code" => 'internal',
679
+ "msg" => "divided by 0",
680
+ "meta" => {"cause"=>"ZeroDivisionError"},
681
+ }, JSON.parse(body[0]))
682
+ assert error_called
683
+ assert exception_raised_called
684
+ end
685
+
686
+ def test_on_error_is_not_called_after_success_raises_exception
687
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
688
+ {}
689
+ end)
690
+ svc.on_success do |env|
691
+ 1 / 0 # divided by 0
692
+ end
693
+ Example::Haberdasher.raise_exceptions = false
694
+
695
+ error_called = false
696
+ svc.on_error do |twerr, env|
697
+ error_called = true
698
+ end
699
+
700
+ exception_raised_called = false
701
+ svc.exception_raised do |e, env|
702
+ exception_raised_called = true
703
+ assert_equal "divided by 0", e.message
704
+ end
705
+
706
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
707
+ status, headers, body = svc.call(rack_env)
708
+
709
+ assert_equal 500, status
710
+ assert_equal({
711
+ "code" => 'internal',
712
+ "msg" => "divided by 0",
713
+ "meta" => {"cause"=>"ZeroDivisionError"},
714
+ }, JSON.parse(body[0]))
715
+ refute error_called # << NOT CALLED
716
+ assert exception_raised_called
717
+ end
718
+
719
+ def test_on_error_multiple_run_in_order
720
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
721
+ env[:from_handler] = true
722
+ Twirp::Error.not_found "my friend"
723
+ end)
724
+ svc.on_error do |twerr, env|
725
+ refute_nil env[:from_handler]
726
+ assert_nil env[:from_hook_1]
727
+ assert_nil env[:from_hook_2]
728
+ env[:from_hook_1] = true
729
+ end
730
+ hook2_called = false
731
+ svc.on_error do |twerr, env|
732
+ refute_nil env[:from_handler]
733
+ refute_nil env[:from_hook_1]
734
+ assert_nil env[:from_hook_2]
735
+ env[:from_hook_2] = true
736
+ hook2_called = true
737
+ end
738
+
739
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
740
+ status, headers, body = svc.call(rack_env)
741
+
742
+ assert_equal 404, status
743
+ assert hook2_called
744
+ end
745
+
746
+ def test_on_error_raising_exception_is_handled_as_internal
747
+ svc = Example::Haberdasher.new(HaberdasherHandler.new do |size, env|
748
+ Twirp::Error.permission_denied "blah blah"
749
+ end)
750
+ svc.on_error do |twerr, env|
751
+ 1 / 0 # divided by 0
752
+ end
753
+ Example::Haberdasher.raise_exceptions = false
754
+
755
+ exception_raised_called = false
756
+ svc.exception_raised do |e, env|
757
+ exception_raised_called = true
758
+ end
759
+
760
+ rack_env = json_req "/example.Haberdasher/MakeHat", inches: 10
761
+ status, headers, body = svc.call(rack_env)
762
+
763
+ assert_equal 500, status
764
+ assert_equal({
765
+ "code" => 'internal',
766
+ "msg" => "divided by 0",
767
+ "meta" => {"cause"=>"ZeroDivisionError"},
768
+ }, JSON.parse(body[0]))
769
+ assert exception_raised_called
770
+ end
771
+
772
+
773
+
215
774
 
216
775
  # Test Helpers
217
776
  # ------------
@@ -229,7 +788,9 @@ class ServiceTest < Minitest::Test
229
788
  end
230
789
 
231
790
  def haberdasher_service
232
- Example::Haberdasher.new(HaberdasherHandler.new)
791
+ Example::Haberdasher.new(HaberdasherHandler.new do |size, _|
792
+ {inches: size.inches, color: "white"}
793
+ end)
233
794
  end
234
795
  end
235
796