Add support for specifying options to builder and thus `config.ru`. (#2094)
[rack.git] / test / spec_builder.rb
blob72a1dcf5fcf317ef37673919455c2f79d9b2e9eb
1 # frozen_string_literal: true
3 require_relative 'helper'
5 separate_testing do
6   require_relative '../lib/rack/builder'
7   require_relative '../lib/rack/lint'
8   require_relative '../lib/rack/mock_request'
9   require_relative '../lib/rack/content_length'
10   require_relative '../lib/rack/show_exceptions'
11   require_relative '../lib/rack/auth/basic'
12 end
14 class NothingMiddleware
15   def initialize(app, **)
16     @app = app
17   end
18   def call(env)
19     @@env = env
20     response = @app.call(env)
21     response
22   end
23   def self.env
24     @@env
25   end
26 end
28 describe Rack::Builder do
29   def builder(&block)
30     Rack::Lint.new Rack::Builder.new(&block)
31   end
33   def builder_to_app(&block)
34     Rack::Lint.new Rack::Builder.new(&block).to_app
35   end
37   it "can provide options" do
38     builder = Rack::Builder.new(foo: :bar)
39     builder.options[:foo].must_equal :bar
40   end
42   it "supports run with block" do
43     app = builder_to_app do
44       run {|env| [200, { "content-type" => "text/plain" }, ["OK"]]}
45     end
46     Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK'
47   end
49   it "supports mapping" do
50     app = builder_to_app do
51       map '/' do |outer_env|
52         run lambda { |inner_env| [200, { "content-type" => "text/plain" }, ['root']] }
53       end
54       map '/sub' do
55         run lambda { |inner_env| [200, { "content-type" => "text/plain" }, ['sub']] }
56       end
57     end
58     Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'root'
59     Rack::MockRequest.new(app).get("/sub").body.to_s.must_equal 'sub'
60   end
62   it "supports use when mapping" do
63     app = builder_to_app do
64       map '/sub' do
65         use Rack::ContentLength
66         run lambda { |inner_env| [200, { "content-type" => "text/plain" }, ['sub']] }
67       end
68       use Rack::ContentLength
69       run lambda { |inner_env| [200, { "content-type" => "text/plain" }, ['root']] }
70     end
71     Rack::MockRequest.new(app).get("/").headers['content-length'].must_equal '4'
72     Rack::MockRequest.new(app).get("/sub").headers['content-length'].must_equal '3'
73   end
75   it "doesn't dupe env even when mapping" do
76     app = builder_to_app do
77       use NothingMiddleware, noop: :noop
78       map '/' do |outer_env|
79         run lambda { |inner_env|
80           inner_env['new_key'] = 'new_value'
81           [200, { "content-type" => "text/plain" }, ['root']]
82         }
83       end
84     end
85     Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'root'
86     NothingMiddleware.env['new_key'].must_equal 'new_value'
87   end
89   it "dupe #to_app when mapping so Rack::Reloader can reload the application on each request" do
90     app = builder do
91       map '/' do |outer_env|
92         run lambda { |env|  [200, { "content-type" => "text/plain" }, [object_id.to_s]] }
93       end
94     end
96     builder_app1_id = Rack::MockRequest.new(app).get("/").body.to_s
97     builder_app2_id = Rack::MockRequest.new(app).get("/").body.to_s
99     builder_app2_id.wont_equal builder_app1_id
100   end
102   it "chains apps by default" do
103     app = builder_to_app do
104       use Rack::ShowExceptions
105       run lambda { |env| raise "bzzzt" }
106     end
108     Rack::MockRequest.new(app).get("/").must_be :server_error?
109     Rack::MockRequest.new(app).get("/").must_be :server_error?
110     Rack::MockRequest.new(app).get("/").must_be :server_error?
111   end
113   it "has implicit #to_app" do
114     app = builder do
115       use Rack::ShowExceptions
116       run lambda { |env| raise "bzzzt" }
117     end
119     Rack::MockRequest.new(app).get("/").must_be :server_error?
120     Rack::MockRequest.new(app).get("/").must_be :server_error?
121     Rack::MockRequest.new(app).get("/").must_be :server_error?
122   end
124   it "supports blocks on use" do
125     app = builder do
126       use Rack::ShowExceptions
127       use Rack::Auth::Basic do |username, password|
128         'secret' == password
129       end
131       run lambda { |env| [200, { "content-type" => "text/plain" }, ['Hi Boss']] }
132     end
134     response = Rack::MockRequest.new(app).get("/")
135     response.must_be :client_error?
136     response.status.must_equal 401
138     # with auth...
139     response = Rack::MockRequest.new(app).get("/",
140         'HTTP_AUTHORIZATION' => 'Basic ' + ["joe:secret"].pack("m*"))
141     response.status.must_equal 200
142     response.body.to_s.must_equal 'Hi Boss'
143   end
145   it "has explicit #to_app" do
146     app = builder do
147       use Rack::ShowExceptions
148       run lambda { |env| raise "bzzzt" }
149     end
151     Rack::MockRequest.new(app).get("/").must_be :server_error?
152     Rack::MockRequest.new(app).get("/").must_be :server_error?
153     Rack::MockRequest.new(app).get("/").must_be :server_error?
154   end
156   it "can mix map and run for endpoints" do
157     app = builder do
158       map '/sub' do
159         run lambda { |inner_env| [200, { "content-type" => "text/plain" }, ['sub']] }
160       end
161       run lambda { |inner_env| [200, { "content-type" => "text/plain" }, ['root']] }
162     end
164     Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'root'
165     Rack::MockRequest.new(app).get("/sub").body.to_s.must_equal 'sub'
166   end
168   it "accepts middleware-only map blocks" do
169     app = builder do
170       map('/foo') { use Rack::ShowExceptions }
171       run lambda { |env| raise "bzzzt" }
172     end
174     proc { Rack::MockRequest.new(app).get("/") }.must_raise(RuntimeError)
175     Rack::MockRequest.new(app).get("/foo").must_be :server_error?
176   end
178   it "yields the generated app to a block for warmup" do
179     warmed_up_app = nil
181     app = Rack::Builder.new do
182       warmup { |a| warmed_up_app = a }
183       run lambda { |env| [200, {}, []] }
184     end.to_app
186     warmed_up_app.must_equal app
187   end
189   it "initialize apps once" do
190     app = builder do
191       class AppClass
192         def initialize
193           @called = 0
194         end
195         def call(env)
196           raise "bzzzt"  if @called > 0
197         @called += 1
198           [200, { 'content-type' => 'text/plain' }, ['OK']]
199         end
200       end
202       use Rack::ShowExceptions
203       run AppClass.new
204     end
206     Rack::MockRequest.new(app).get("/").status.must_equal 200
207     Rack::MockRequest.new(app).get("/").must_be :server_error?
208   end
210   it "allows use after run" do
211     app = builder do
212       run lambda { |env| raise "bzzzt" }
213       use Rack::ShowExceptions
214     end
216     Rack::MockRequest.new(app).get("/").must_be :server_error?
217     Rack::MockRequest.new(app).get("/").must_be :server_error?
218     Rack::MockRequest.new(app).get("/").must_be :server_error?
219   end
221   it "supports #freeze_app for freezing app and middleware" do
222     app = builder do
223       freeze_app
224       use Rack::ShowExceptions
225       use(Class.new do
226         def initialize(app) @app = app end
227         def call(env) @a = 1 if env['PATH_INFO'] == '/a'; @app.call(env) end
228       end)
229       o = Object.new
230       def o.call(env)
231         @a = 1 if env['PATH_INFO'] == '/b';
232         [200, {}, []]
233       end
234       run o
235     end
237     Rack::MockRequest.new(app).get("/a").must_be :server_error?
238     Rack::MockRequest.new(app).get("/b").must_be :server_error?
239     Rack::MockRequest.new(app).get("/c").status.must_equal 200
240   end
242   it 'complains about a missing run' do
243     proc do
244       Rack::Lint.new Rack::Builder.app { use Rack::ShowExceptions }
245     end.must_raise(RuntimeError)
246   end
248   describe "parse_file" do
249     def config_file(name)
250       File.join(File.dirname(__FILE__), 'builder', name)
251     end
253     it "raises if parses commented options" do
254       proc do
255         Rack::Builder.parse_file config_file('options.ru')
256       end.must_raise(RuntimeError).
257        message.must_include('Parsing options from the first comment line is no longer supported')
258     end
260     it "removes __END__ before evaluating app" do
261       app, _ = Rack::Builder.parse_file config_file('end.ru')
262       Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK'
263     end
265     it "supports multi-line comments" do
266       app = Rack::Builder.parse_file(config_file('comment.ru'))
267       app.must_be_kind_of(Proc)
268     end
270     it 'requires an_underscore_app not ending in .ru' do
271       $: << File.dirname(__FILE__)
272       app, * = Rack::Builder.parse_file 'builder/an_underscore_app'
273       Rack::MockRequest.new(app).get('/').body.to_s.must_equal 'OK'
274       $:.pop
275     end
277     it "sets __LINE__ correctly" do
278       app, _ = Rack::Builder.parse_file config_file('line.ru')
279       Rack::MockRequest.new(app).get("/").body.to_s.must_equal '3'
280     end
282     it "strips leading unicode byte order mark when present" do
283       enc = Encoding.default_external
284       begin
285         verbose, $VERBOSE = $VERBOSE, nil
286         Encoding.default_external = 'UTF-8'
287         app, _ = Rack::Builder.parse_file config_file('bom.ru')
288         Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK'
289       ensure
290         Encoding.default_external = enc
291         $VERBOSE = verbose
292       end
293     end
295     it "respects the frozen_string_literal magic comment" do
296       app, _ = Rack::Builder.parse_file(config_file('frozen.ru'))
297       response = Rack::MockRequest.new(app).get('/')
298       response.body.must_equal 'frozen'
299       body = response.instance_variable_get(:@body)
300       body.must_equal(['frozen'])
301       body[0].frozen?.must_equal true
302     end
303   end
305   describe 'new_from_string' do
306     it "builds a rack app from string" do
307       app, = Rack::Builder.new_from_string "run lambda{|env| [200, {'content-type' => 'text/plane'}, ['OK']] }"
308       Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK'
309     end
310   end