twirp 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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