nano-sharp-gem 0.0.1

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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/cuba-4.0.3/CHANGELOG +71 -0
  3. data/cuba-4.0.3/CONTRIBUTING +19 -0
  4. data/cuba-4.0.3/LICENSE +23 -0
  5. data/cuba-4.0.3/README.md +781 -0
  6. data/cuba-4.0.3/cuba.gemspec +18 -0
  7. data/cuba-4.0.3/examples/config.ru +18 -0
  8. data/cuba-4.0.3/examples/measure.rb +17 -0
  9. data/cuba-4.0.3/examples/rack-response.ru +21 -0
  10. data/cuba-4.0.3/examples/views/home.mote +7 -0
  11. data/cuba-4.0.3/examples/views/layout.mote +11 -0
  12. data/cuba-4.0.3/lib/cuba/capybara.rb +13 -0
  13. data/cuba-4.0.3/lib/cuba/render.rb +63 -0
  14. data/cuba-4.0.3/lib/cuba/safe/csrf.rb +47 -0
  15. data/cuba-4.0.3/lib/cuba/safe/secure_headers.rb +43 -0
  16. data/cuba-4.0.3/lib/cuba/safe.rb +23 -0
  17. data/cuba-4.0.3/lib/cuba/test.rb +11 -0
  18. data/cuba-4.0.3/lib/cuba.rb +436 -0
  19. data/cuba-4.0.3/makefile +4 -0
  20. data/cuba-4.0.3/test/accept.rb +77 -0
  21. data/cuba-4.0.3/test/captures.rb +162 -0
  22. data/cuba-4.0.3/test/composition.rb +103 -0
  23. data/cuba-4.0.3/test/cookie.rb +34 -0
  24. data/cuba-4.0.3/test/csrf.rb +140 -0
  25. data/cuba-4.0.3/test/extension.rb +21 -0
  26. data/cuba-4.0.3/test/helper.rb +11 -0
  27. data/cuba-4.0.3/test/host.rb +29 -0
  28. data/cuba-4.0.3/test/integration.rb +114 -0
  29. data/cuba-4.0.3/test/match.rb +86 -0
  30. data/cuba-4.0.3/test/middleware.rb +47 -0
  31. data/cuba-4.0.3/test/number.rb +36 -0
  32. data/cuba-4.0.3/test/on.rb +157 -0
  33. data/cuba-4.0.3/test/param.rb +66 -0
  34. data/cuba-4.0.3/test/path.rb +86 -0
  35. data/cuba-4.0.3/test/plugin.rb +68 -0
  36. data/cuba-4.0.3/test/rack.rb +22 -0
  37. data/cuba-4.0.3/test/redirect.rb +21 -0
  38. data/cuba-4.0.3/test/render.rb +128 -0
  39. data/cuba-4.0.3/test/root.rb +83 -0
  40. data/cuba-4.0.3/test/run.rb +23 -0
  41. data/cuba-4.0.3/test/safe.rb +74 -0
  42. data/cuba-4.0.3/test/segment.rb +45 -0
  43. data/cuba-4.0.3/test/session.rb +21 -0
  44. data/cuba-4.0.3/test/settings.rb +52 -0
  45. data/cuba-4.0.3/test/views/about.erb +1 -0
  46. data/cuba-4.0.3/test/views/about.str +1 -0
  47. data/cuba-4.0.3/test/views/content-yield.erb +1 -0
  48. data/cuba-4.0.3/test/views/custom/abs_path.mote +1 -0
  49. data/cuba-4.0.3/test/views/frag.mote +1 -0
  50. data/cuba-4.0.3/test/views/home.erb +2 -0
  51. data/cuba-4.0.3/test/views/home.mote +1 -0
  52. data/cuba-4.0.3/test/views/home.str +2 -0
  53. data/cuba-4.0.3/test/views/layout-alternative.erb +2 -0
  54. data/cuba-4.0.3/test/views/layout-yield.erb +3 -0
  55. data/cuba-4.0.3/test/views/layout.erb +2 -0
  56. data/cuba-4.0.3/test/views/layout.mote +2 -0
  57. data/cuba-4.0.3/test/views/layout.str +2 -0
  58. data/cuba-4.0.3/test/views/test.erb +1 -0
  59. data/cuba-4.0.3/test/with.rb +42 -0
  60. data/nano-sharp-gem.gemspec +11 -0
  61. metadata +99 -0
@@ -0,0 +1,436 @@
1
+ require "delegate"
2
+ require "rack"
3
+ require "rack/session"
4
+
5
+ class Cuba
6
+ SLASH = "/".freeze
7
+ EMPTY = "".freeze
8
+ SEGMENT = "([^\\/]+)".freeze
9
+ DEFAULT = "text/html; charset=utf-8".freeze
10
+ REGEXES = Hash.new { |h, pattern| h[pattern] = /\A\/(#{pattern})(\/|\z)/ }
11
+
12
+ class Response
13
+ LOCATION = "location".freeze
14
+
15
+ module ContentType
16
+ HTML = "text/html".freeze # :nodoc:
17
+ TEXT = "text/plain".freeze # :nodoc:
18
+ JSON = "application/json".freeze # :nodoc:
19
+ end
20
+
21
+ attr_accessor :status
22
+
23
+ attr :body
24
+ attr :headers
25
+
26
+ def initialize(headers = {})
27
+ @status = nil
28
+ @headers = headers
29
+ @body = []
30
+ @length = 0
31
+ end
32
+
33
+ def [](key)
34
+ @headers[key]
35
+ end
36
+
37
+ def []=(key, value)
38
+ @headers[key] = value
39
+ end
40
+
41
+ def write(str)
42
+ s = str.to_s
43
+
44
+ @length += s.bytesize
45
+ @headers[Rack::CONTENT_LENGTH] = @length.to_s
46
+ @body << s
47
+ end
48
+
49
+ # Write response body as text/plain
50
+ def text(str)
51
+ @headers[Rack::CONTENT_TYPE] = ContentType::TEXT
52
+ write(str)
53
+ end
54
+
55
+ # Write response body as text/html
56
+ def html(str)
57
+ @headers[Rack::CONTENT_TYPE] = ContentType::HTML
58
+ write(str)
59
+ end
60
+
61
+ # Write response body as application/json
62
+ def json(str)
63
+ @headers[Rack::CONTENT_TYPE] = ContentType::JSON
64
+ write(str)
65
+ end
66
+
67
+ def redirect(path, status = 302)
68
+ @headers[LOCATION] = path
69
+ @status = status
70
+ end
71
+
72
+ def finish
73
+ [@status, @headers, @body]
74
+ end
75
+
76
+ def set_cookie(key, value)
77
+ Rack::Utils.set_cookie_header!(@headers, key, value)
78
+ end
79
+
80
+ def delete_cookie(key, value = {})
81
+ Rack::Utils.delete_cookie_header!(@headers, key, value)
82
+ end
83
+ end
84
+
85
+ def self.reset!
86
+ @app = nil
87
+ @prototype = nil
88
+ end
89
+
90
+ def self.app
91
+ @app ||= Rack::Builder.new
92
+ end
93
+
94
+ def self.use(middleware, *args, **kwargs, &block)
95
+ app.use(middleware, *args, **kwargs, &block)
96
+ end
97
+
98
+ def self.define(&block)
99
+ app.run new(&block)
100
+ end
101
+
102
+ def self.prototype
103
+ @prototype ||= app.to_app
104
+ end
105
+
106
+ def self.call(env)
107
+ prototype.call(env)
108
+ end
109
+
110
+ def self.plugin(mixin)
111
+ include mixin
112
+ extend mixin::ClassMethods if defined?(mixin::ClassMethods)
113
+
114
+ mixin.setup(self) if mixin.respond_to?(:setup)
115
+ end
116
+
117
+ def self.settings
118
+ @settings ||= {}
119
+ end
120
+
121
+ def self.deepclone(obj)
122
+ Marshal.load(Marshal.dump(obj))
123
+ end
124
+
125
+ def self.inherited(child)
126
+ child.settings.replace(deepclone(settings))
127
+ end
128
+
129
+ attr :env
130
+ attr :req
131
+ attr :res
132
+ attr :captures
133
+
134
+ def initialize(&blk)
135
+ @blk = blk
136
+ @captures = []
137
+ end
138
+
139
+ def settings
140
+ self.class.settings
141
+ end
142
+
143
+ def call(env)
144
+ dup.call!(env)
145
+ end
146
+
147
+ def call!(env)
148
+ @env = env
149
+ @req = settings[:req].new(env)
150
+ @res = settings[:res].new(settings[:default_headers].dup)
151
+
152
+ # This `catch` statement will either receive a
153
+ # rack response tuple via a `halt`, or will
154
+ # fall back to issuing a 404.
155
+ #
156
+ # When it `catch`es a throw, the return value
157
+ # of this whole `call!` method will be the
158
+ # rack response tuple, which is exactly what we want.
159
+ catch(:halt) do
160
+ instance_eval(&@blk)
161
+
162
+ not_found
163
+ res.finish
164
+ end
165
+ end
166
+
167
+ def session
168
+ env["rack.session"] || raise(RuntimeError,
169
+ "You're missing a session handler. You can get started " +
170
+ "by adding Cuba.use Rack::Session::Cookie")
171
+ end
172
+
173
+ # The heart of the path / verb / any condition matching.
174
+ #
175
+ # @example
176
+ #
177
+ # on get do
178
+ # res.write "GET"
179
+ # end
180
+ #
181
+ # on get, "signup" do
182
+ # res.write "Signup"
183
+ # end
184
+ #
185
+ # on "user/:id" do |uid|
186
+ # res.write "User: #{uid}"
187
+ # end
188
+ #
189
+ # on "styles", extension("css") do |file|
190
+ # res.write render("styles/#{file}.sass")
191
+ # end
192
+ #
193
+ def on(*args, &block)
194
+ try do
195
+ # For every block, we make sure to reset captures so that
196
+ # nesting matchers won't mess with each other's captures.
197
+ @captures = []
198
+
199
+ # We stop evaluation of this entire matcher unless
200
+ # each and every `arg` defined for this matcher evaluates
201
+ # to a non-false value.
202
+ #
203
+ # Short circuit examples:
204
+ # on true, false do
205
+ #
206
+ # # PATH_INFO=/user
207
+ # on true, "signup"
208
+ return unless args.all? { |arg| match(arg) }
209
+
210
+ # The captures we yield here were generated and assembled
211
+ # by evaluating each of the `arg`s above. Most of these
212
+ # are carried out by #consume.
213
+ yield(*captures)
214
+
215
+ if res.status.nil?
216
+ if res.body.empty?
217
+ not_found
218
+ else
219
+ res.headers[Rack::CONTENT_TYPE] ||= DEFAULT
220
+ res.status = 200
221
+ end
222
+ end
223
+
224
+ halt(res.finish)
225
+ end
226
+ end
227
+
228
+ # @private Used internally by #on to ensure that SCRIPT_NAME and
229
+ # PATH_INFO are reset to their proper values.
230
+ def try
231
+ script, path = env[Rack::SCRIPT_NAME], env[Rack::PATH_INFO]
232
+
233
+ yield
234
+
235
+ ensure
236
+ env[Rack::SCRIPT_NAME], env[Rack::PATH_INFO] = script, path
237
+ end
238
+ private :try
239
+
240
+ def consume(pattern)
241
+ matchdata = env[Rack::PATH_INFO].match(REGEXES[pattern])
242
+
243
+ return false unless matchdata
244
+
245
+ path, *vars = matchdata.captures
246
+
247
+ env[Rack::SCRIPT_NAME] += "/#{path}"
248
+ env[Rack::PATH_INFO] = "#{vars.pop}#{matchdata.post_match}"
249
+
250
+ captures.push(*vars)
251
+ end
252
+ private :consume
253
+
254
+ def match(matcher, segment = SEGMENT)
255
+ case matcher
256
+ when String then consume(matcher.gsub(/:\w+/, segment))
257
+ when Regexp then consume(matcher)
258
+ when Symbol then consume(segment)
259
+ when Proc then matcher.call
260
+ else
261
+ matcher
262
+ end
263
+ end
264
+
265
+ # A matcher for files with a certain extension.
266
+ #
267
+ # @example
268
+ # # PATH_INFO=/style/app.css
269
+ # on "style", extension("css") do |file|
270
+ # res.write file # writes app
271
+ # end
272
+ def extension(ext = "\\w+")
273
+ lambda { consume("([^\\/]+?)\.#{ext}\\z") }
274
+ end
275
+
276
+ # Ensures that certain request parameters are present. Acts like a
277
+ # precondition / assertion for your route. A default value can be
278
+ # provided as a second argument. In that case, it always matches
279
+ # and the result is either the parameter or the default value.
280
+ #
281
+ # @example
282
+ # # POST with data like user[fname]=John&user[lname]=Doe
283
+ # on "signup", param("user") do |atts|
284
+ # User.create(atts)
285
+ # end
286
+ #
287
+ # on "login", param("username", "guest") do |username|
288
+ # # If not provided, username == "guest"
289
+ # end
290
+ def param(key, default = nil)
291
+ value = req.params[key.to_s] || default
292
+
293
+ lambda { captures << value unless value.to_s.empty? }
294
+ end
295
+
296
+ # Useful for matching against the request host (i.e. HTTP_HOST).
297
+ #
298
+ # @example
299
+ # on host("account1.example.com"), "api" do
300
+ # res.write "You have reached the API of account1."
301
+ # end
302
+ def host(hostname)
303
+ hostname === req.host
304
+ end
305
+
306
+ # If you want to match against the HTTP_ACCEPT value.
307
+ #
308
+ # @example
309
+ # # HTTP_ACCEPT=application/xml
310
+ # on accept("application/xml") do
311
+ # # automatically set to application/xml.
312
+ # res.write res["Content-Type"]
313
+ # end
314
+ def accept(mimetype)
315
+ lambda do
316
+ accept = String(env["HTTP_ACCEPT"]).split(",")
317
+
318
+ if accept.any? { |s| s.strip == mimetype }
319
+ res[Rack::CONTENT_TYPE] = mimetype
320
+ end
321
+ end
322
+ end
323
+
324
+ # Syntactic sugar for providing catch-all matches.
325
+ #
326
+ # @example
327
+ # on default do
328
+ # res.write "404"
329
+ # end
330
+ def default
331
+ true
332
+ end
333
+
334
+ # Access the root of the application.
335
+ #
336
+ # @example
337
+ #
338
+ # # GET /
339
+ # on root do
340
+ # res.write "Home"
341
+ # end
342
+ def root
343
+ env[Rack::PATH_INFO] == SLASH || env[Rack::PATH_INFO] == EMPTY
344
+ end
345
+
346
+ # Syntatic sugar for providing HTTP Verb matching.
347
+ #
348
+ # @example
349
+ # on get, "signup" do
350
+ # end
351
+ #
352
+ # on post, "signup" do
353
+ # end
354
+ def get; req.get? end
355
+ def post; req.post? end
356
+ def put; req.put? end
357
+ def patch; req.patch? end
358
+ def delete; req.delete? end
359
+ def head; req.head? end
360
+ def options; req.options? end
361
+ def link; req.link? end
362
+ def unlink; req.unlink? end
363
+ def trace; req.trace? end
364
+
365
+ # If you want to halt the processing of an existing handler
366
+ # and continue it via a different handler.
367
+ #
368
+ # @example
369
+ # def redirect(*args)
370
+ # run Cuba.new { on(default) { res.redirect(*args) }}
371
+ # end
372
+ #
373
+ # on "account" do
374
+ # redirect "/login" unless session["uid"]
375
+ #
376
+ # res.write "Super secure account info."
377
+ # end
378
+ def run(app)
379
+ halt app.call(req.env)
380
+ end
381
+
382
+ def halt(response)
383
+ throw :halt, response
384
+ end
385
+
386
+ # Adds ability to pass information to a nested Cuba application.
387
+ # It receives two parameters: a hash that represents the passed
388
+ # information and a block. The #vars method is used to retrieve
389
+ # a hash with the passed information.
390
+ #
391
+ # class Platforms < Cuba
392
+ # define do
393
+ # platform = vars[:platform]
394
+ #
395
+ # on default do
396
+ # res.write(platform) # => "heroku" or "salesforce"
397
+ # end
398
+ # end
399
+ # end
400
+ #
401
+ # Cuba.define do
402
+ # on "(heroku|salesforce)" do |platform|
403
+ # with(platform: platform) do
404
+ # run(Platforms)
405
+ # end
406
+ # end
407
+ # end
408
+ #
409
+ def with(dict = {})
410
+ old, env["cuba.vars"] = vars, vars.merge(dict)
411
+ yield
412
+ ensure
413
+ env["cuba.vars"] = old
414
+ end
415
+
416
+ # Returns a hash with the information set by the #with method.
417
+ #
418
+ # with(role: "admin", site: "main") do
419
+ # on default do
420
+ # res.write(vars.inspect)
421
+ # end
422
+ # end
423
+ # # => '{:role=>"admin", :site=>"main"}'
424
+ #
425
+ def vars
426
+ env["cuba.vars"] ||= {}
427
+ end
428
+
429
+ def not_found
430
+ res.status = 404
431
+ end
432
+ end
433
+
434
+ Cuba.settings[:req] = Rack::Request
435
+ Cuba.settings[:res] = Cuba::Response
436
+ Cuba.settings[:default_headers] = {}
@@ -0,0 +1,4 @@
1
+ .PHONY: test
2
+
3
+ test:
4
+ cutest ./test/*.rb
@@ -0,0 +1,77 @@
1
+ require File.expand_path("helper", File.dirname(__FILE__))
2
+
3
+ test "accept mimetypes" do
4
+ Cuba.define do
5
+ on accept("application/xml") do
6
+ res.write res["content-type"]
7
+ end
8
+ end
9
+
10
+ env = { "HTTP_ACCEPT" => "application/xml",
11
+ "SCRIPT_NAME" => "/", "PATH_INFO" => "/post" }
12
+
13
+ _, _, body = Cuba.call(env)
14
+
15
+ assert_response body, ["application/xml"]
16
+ end
17
+
18
+ test "tests don't fail when you don't specify an accept type" do
19
+ Cuba.define do
20
+ on accept("application/xml") do
21
+ res.write res["Content-Type"]
22
+ end
23
+
24
+ on default do
25
+ res.write "Default action"
26
+ end
27
+ end
28
+
29
+ _, _, body = Cuba.call({})
30
+
31
+ assert_response body, ["Default action"]
32
+ end
33
+
34
+ test "accept HTML mimetype" do
35
+ Cuba.define do
36
+ on accept("text/html") do
37
+ res.write Cuba::Response::ContentType::HTML
38
+ end
39
+ end
40
+
41
+ env = { "HTTP_ACCEPT" => "text/html",
42
+ "SCRIPT_NAME" => "/", "PATH_INFO" => "/post" }
43
+
44
+ _, _, body = Cuba.call(env)
45
+
46
+ assert_response body, ["text/html"]
47
+ end
48
+
49
+ test "accept TEXT mimetype" do
50
+ Cuba.define do
51
+ on accept("text/plain") do
52
+ res.write Cuba::Response::ContentType::TEXT
53
+ end
54
+ end
55
+
56
+ env = { "HTTP_ACCEPT" => "text/plain",
57
+ "SCRIPT_NAME" => "/", "PATH_INFO" => "/post" }
58
+
59
+ _, _, body = Cuba.call(env)
60
+
61
+ assert_response body, ["text/plain"]
62
+ end
63
+
64
+ test "accept JSON mimetype" do
65
+ Cuba.define do
66
+ on accept("application/json") do
67
+ res.write Cuba::Response::ContentType::JSON
68
+ end
69
+ end
70
+
71
+ env = { "HTTP_ACCEPT" => "application/json",
72
+ "SCRIPT_NAME" => "/", "PATH_INFO" => "/get" }
73
+
74
+ _, _, body = Cuba.call(env)
75
+
76
+ assert_response body, ["application/json"]
77
+ end
@@ -0,0 +1,162 @@
1
+ require File.expand_path("helper", File.dirname(__FILE__))
2
+
3
+ test "doesn't yield HOST" do
4
+ Cuba.define do
5
+ on host("example.com") do |*args|
6
+ res.write args.size
7
+ end
8
+ end
9
+
10
+ env = { "HTTP_HOST" => "example.com" }
11
+
12
+ _, _, resp = Cuba.call(env)
13
+
14
+ assert_response resp, ["0"]
15
+ end
16
+
17
+ test "doesn't yield the verb" do
18
+ Cuba.define do
19
+ on get do |*args|
20
+ res.write args.size
21
+ end
22
+ end
23
+
24
+ env = { "REQUEST_METHOD" => "GET" }
25
+
26
+ _, _, resp = Cuba.call(env)
27
+
28
+ assert_response resp, ["0"]
29
+ end
30
+
31
+ test "doesn't yield the path" do
32
+ Cuba.define do
33
+ on get, "home" do |*args|
34
+ res.write args.size
35
+ end
36
+ end
37
+
38
+ env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/home",
39
+ "SCRIPT_NAME" => "/" }
40
+
41
+ _, _, resp = Cuba.call(env)
42
+
43
+ assert_response resp, ["0"]
44
+ end
45
+
46
+ test "yields the segment" do
47
+ Cuba.define do
48
+ on get, "user", :id do |id|
49
+ res.write id
50
+ end
51
+ end
52
+
53
+ env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/user/johndoe",
54
+ "SCRIPT_NAME" => "/" }
55
+
56
+ _, _, resp = Cuba.call(env)
57
+
58
+ assert_response resp, ["johndoe"]
59
+ end
60
+
61
+ test "yields a number" do
62
+ Cuba.define do
63
+ on get, "user", :id do |id|
64
+ res.write id
65
+ end
66
+ end
67
+
68
+ env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/user/101",
69
+ "SCRIPT_NAME" => "/" }
70
+
71
+ _, _, resp = Cuba.call(env)
72
+
73
+ assert_response resp, ["101"]
74
+ end
75
+
76
+ test "yield a file name with a matching extension" do
77
+ Cuba.define do
78
+ on get, "css", extension("css") do |file|
79
+ res.write file
80
+ end
81
+ end
82
+
83
+ env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/css/app.css",
84
+ "SCRIPT_NAME" => "/" }
85
+
86
+ _, _, resp = Cuba.call(env)
87
+
88
+ assert_response resp, ["app"]
89
+ end
90
+
91
+ test "yields a segment per nested block" do
92
+ Cuba.define do
93
+ on :one do |one|
94
+ on :two do |two|
95
+ on :three do |three|
96
+ res.write one
97
+ res.write two
98
+ res.write three
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/one/two/three",
105
+ "SCRIPT_NAME" => "/" }
106
+
107
+ _, _, resp = Cuba.call(env)
108
+
109
+ assert_response resp, ["one", "two", "three"]
110
+ end
111
+
112
+ test "consumes a slash if needed" do
113
+ Cuba.define do
114
+ on get, "(.+\\.css)" do |file|
115
+ res.write file
116
+ end
117
+ end
118
+
119
+ env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/foo/bar.css",
120
+ "SCRIPT_NAME" => "/" }
121
+
122
+ _, _, resp = Cuba.call(env)
123
+
124
+ assert_response resp, ["foo/bar.css"]
125
+ end
126
+
127
+ test "regex captures in string format" do
128
+ Cuba.define do
129
+ on get, "posts/(\\d+)-(.*)" do |id, slug|
130
+ res.write id
131
+ res.write slug
132
+ end
133
+ end
134
+
135
+
136
+ env = { "REQUEST_METHOD" => "GET",
137
+ "PATH_INFO" => "/posts/123-postal-service",
138
+ "SCRIPT_NAME" => "/" }
139
+
140
+ _, _, resp = Cuba.call(env)
141
+
142
+
143
+ assert_response resp, ["123", "postal-service"]
144
+ end
145
+
146
+ test "regex captures in regex format" do
147
+ Cuba.define do
148
+ on get, %r{posts/(\d+)-(.*)} do |id, slug|
149
+ res.write id
150
+ res.write slug
151
+ end
152
+ end
153
+
154
+ env = { "REQUEST_METHOD" => "GET",
155
+ "PATH_INFO" => "/posts/123-postal-service",
156
+ "SCRIPT_NAME" => "/" }
157
+
158
+ _, _, resp = Cuba.call(env)
159
+
160
+
161
+ assert_response resp, ["123", "postal-service"]
162
+ end