PageRenderTime 29ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/test/helpers_test.rb

https://gitlab.com/OBSERVER-DLL/sinatra
Ruby | 1751 lines | 1593 code | 157 blank | 1 comment | 24 complexity | 61334640402f399c2a655df69d9ff889 MD5 | raw file
  1. require File.expand_path('../helper', __FILE__)
  2. require 'date'
  3. class HelpersTest < Test::Unit::TestCase
  4. def test_default
  5. assert true
  6. end
  7. def status_app(code, &block)
  8. code += 2 if [204, 205, 304].include? code
  9. block ||= proc { }
  10. mock_app do
  11. get('/') do
  12. status code
  13. instance_eval(&block).inspect
  14. end
  15. end
  16. get '/'
  17. end
  18. describe 'status' do
  19. it 'sets the response status code' do
  20. status_app 207
  21. assert_equal 207, response.status
  22. end
  23. end
  24. describe 'not_found?' do
  25. it 'is true for status == 404' do
  26. status_app(404) { not_found? }
  27. assert_body 'true'
  28. end
  29. it 'is false for status gt 404' do
  30. status_app(405) { not_found? }
  31. assert_body 'false'
  32. end
  33. it 'is false for status lt 404' do
  34. status_app(403) { not_found? }
  35. assert_body 'false'
  36. end
  37. end
  38. describe 'informational?' do
  39. it 'is true for 1xx status' do
  40. status_app(100 + rand(100)) { informational? }
  41. assert_body 'true'
  42. end
  43. it 'is false for status > 199' do
  44. status_app(200 + rand(400)) { informational? }
  45. assert_body 'false'
  46. end
  47. end
  48. describe 'success?' do
  49. it 'is true for 2xx status' do
  50. status_app(200 + rand(100)) { success? }
  51. assert_body 'true'
  52. end
  53. it 'is false for status < 200' do
  54. status_app(100 + rand(100)) { success? }
  55. assert_body 'false'
  56. end
  57. it 'is false for status > 299' do
  58. status_app(300 + rand(300)) { success? }
  59. assert_body 'false'
  60. end
  61. end
  62. describe 'redirect?' do
  63. it 'is true for 3xx status' do
  64. status_app(300 + rand(100)) { redirect? }
  65. assert_body 'true'
  66. end
  67. it 'is false for status < 300' do
  68. status_app(200 + rand(100)) { redirect? }
  69. assert_body 'false'
  70. end
  71. it 'is false for status > 399' do
  72. status_app(400 + rand(200)) { redirect? }
  73. assert_body 'false'
  74. end
  75. end
  76. describe 'client_error?' do
  77. it 'is true for 4xx status' do
  78. status_app(400 + rand(100)) { client_error? }
  79. assert_body 'true'
  80. end
  81. it 'is false for status < 400' do
  82. status_app(200 + rand(200)) { client_error? }
  83. assert_body 'false'
  84. end
  85. it 'is false for status > 499' do
  86. status_app(500 + rand(100)) { client_error? }
  87. assert_body 'false'
  88. end
  89. end
  90. describe 'server_error?' do
  91. it 'is true for 5xx status' do
  92. status_app(500 + rand(100)) { server_error? }
  93. assert_body 'true'
  94. end
  95. it 'is false for status < 500' do
  96. status_app(200 + rand(300)) { server_error? }
  97. assert_body 'false'
  98. end
  99. end
  100. describe 'body' do
  101. it 'takes a block for defered body generation' do
  102. mock_app do
  103. get('/') { body { 'Hello World' } }
  104. end
  105. get '/'
  106. assert_equal 'Hello World', body
  107. end
  108. it 'takes a String, Array, or other object responding to #each' do
  109. mock_app { get('/') { body 'Hello World' } }
  110. get '/'
  111. assert_equal 'Hello World', body
  112. end
  113. end
  114. describe 'redirect' do
  115. it 'uses a 302 when only a path is given' do
  116. mock_app do
  117. get('/') do
  118. redirect '/foo'
  119. fail 'redirect should halt'
  120. end
  121. end
  122. get '/'
  123. assert_equal 302, status
  124. assert_equal '', body
  125. assert_equal 'http://example.org/foo', response['Location']
  126. end
  127. it 'uses the code given when specified' do
  128. mock_app do
  129. get('/') do
  130. redirect '/foo', 301
  131. fail 'redirect should halt'
  132. end
  133. end
  134. get '/'
  135. assert_equal 301, status
  136. assert_equal '', body
  137. assert_equal 'http://example.org/foo', response['Location']
  138. end
  139. it 'redirects back to request.referer when passed back' do
  140. mock_app { get('/try_redirect') { redirect back } }
  141. request = Rack::MockRequest.new(@app)
  142. response = request.get('/try_redirect', 'HTTP_REFERER' => '/foo')
  143. assert_equal 302, response.status
  144. assert_equal 'http://example.org/foo', response['Location']
  145. end
  146. it 'redirects using a non-standard HTTP port' do
  147. mock_app { get('/') { redirect '/foo' } }
  148. request = Rack::MockRequest.new(@app)
  149. response = request.get('/', 'SERVER_PORT' => '81')
  150. assert_equal 'http://example.org:81/foo', response['Location']
  151. end
  152. it 'redirects using a non-standard HTTPS port' do
  153. mock_app { get('/') { redirect '/foo' } }
  154. request = Rack::MockRequest.new(@app)
  155. response = request.get('/', 'SERVER_PORT' => '444')
  156. assert_equal 'http://example.org:444/foo', response['Location']
  157. end
  158. it 'uses 303 for post requests if request is HTTP 1.1' do
  159. mock_app { post('/') { redirect '/'} }
  160. post('/', {}, 'HTTP_VERSION' => 'HTTP/1.1')
  161. assert_equal 303, status
  162. assert_equal '', body
  163. assert_equal 'http://example.org/', response['Location']
  164. end
  165. it 'uses 302 for post requests if request is HTTP 1.0' do
  166. mock_app { post('/') { redirect '/'} }
  167. post('/', {}, 'HTTP_VERSION' => 'HTTP/1.0')
  168. assert_equal 302, status
  169. assert_equal '', body
  170. assert_equal 'http://example.org/', response['Location']
  171. end
  172. it 'works behind a reverse proxy' do
  173. mock_app { get('/') { redirect '/foo' } }
  174. request = Rack::MockRequest.new(@app)
  175. response = request.get('/', 'HTTP_X_FORWARDED_HOST' => 'example.com', 'SERVER_PORT' => '8080')
  176. assert_equal 'http://example.com/foo', response['Location']
  177. end
  178. it 'accepts absolute URIs' do
  179. mock_app do
  180. get('/') do
  181. redirect 'http://google.com'
  182. fail 'redirect should halt'
  183. end
  184. end
  185. get '/'
  186. assert_equal 302, status
  187. assert_equal '', body
  188. assert_equal 'http://google.com', response['Location']
  189. end
  190. it 'accepts absolute URIs with a different schema' do
  191. mock_app do
  192. get('/') do
  193. redirect 'mailto:jsmith@example.com'
  194. fail 'redirect should halt'
  195. end
  196. end
  197. get '/'
  198. assert_equal 302, status
  199. assert_equal '', body
  200. assert_equal 'mailto:jsmith@example.com', response['Location']
  201. end
  202. end
  203. describe 'error' do
  204. it 'sets a status code and halts' do
  205. mock_app do
  206. get('/') do
  207. error 501
  208. fail 'error should halt'
  209. end
  210. end
  211. get '/'
  212. assert_equal 501, status
  213. assert_equal '', body
  214. end
  215. it 'takes an optional body' do
  216. mock_app do
  217. get('/') do
  218. error 501, 'FAIL'
  219. fail 'error should halt'
  220. end
  221. end
  222. get '/'
  223. assert_equal 501, status
  224. assert_equal 'FAIL', body
  225. end
  226. it 'uses a 500 status code when first argument is a body' do
  227. mock_app do
  228. get('/') do
  229. error 'FAIL'
  230. fail 'error should halt'
  231. end
  232. end
  233. get '/'
  234. assert_equal 500, status
  235. assert_equal 'FAIL', body
  236. end
  237. end
  238. describe 'not_found' do
  239. it 'halts with a 404 status' do
  240. mock_app do
  241. get('/') do
  242. not_found
  243. fail 'not_found should halt'
  244. end
  245. end
  246. get '/'
  247. assert_equal 404, status
  248. assert_equal '', body
  249. end
  250. it 'does not set a X-Cascade header' do
  251. mock_app do
  252. get('/') do
  253. not_found
  254. fail 'not_found should halt'
  255. end
  256. end
  257. get '/'
  258. assert_equal 404, status
  259. assert_equal nil, response.headers['X-Cascade']
  260. end
  261. end
  262. describe 'headers' do
  263. it 'sets headers on the response object when given a Hash' do
  264. mock_app do
  265. get('/') do
  266. headers 'X-Foo' => 'bar', 'X-Baz' => 'bling'
  267. 'kthx'
  268. end
  269. end
  270. get '/'
  271. assert ok?
  272. assert_equal 'bar', response['X-Foo']
  273. assert_equal 'bling', response['X-Baz']
  274. assert_equal 'kthx', body
  275. end
  276. it 'returns the response headers hash when no hash provided' do
  277. mock_app do
  278. get('/') do
  279. headers['X-Foo'] = 'bar'
  280. 'kthx'
  281. end
  282. end
  283. get '/'
  284. assert ok?
  285. assert_equal 'bar', response['X-Foo']
  286. end
  287. end
  288. describe 'session' do
  289. it 'uses the existing rack.session' do
  290. mock_app do
  291. get('/') do
  292. session[:foo]
  293. end
  294. end
  295. get('/', {}, { 'rack.session' => { :foo => 'bar' } })
  296. assert_equal 'bar', body
  297. end
  298. it 'creates a new session when none provided' do
  299. mock_app do
  300. enable :sessions
  301. get('/') do
  302. assert session[:foo].nil?
  303. session[:foo] = 'bar'
  304. redirect '/hi'
  305. end
  306. get('/hi') do
  307. "hi #{session[:foo]}"
  308. end
  309. end
  310. get '/'
  311. follow_redirect!
  312. assert_equal 'hi bar', body
  313. end
  314. it 'inserts session middleware' do
  315. mock_app do
  316. enable :sessions
  317. get('/') do
  318. assert env['rack.session']
  319. assert env['rack.session.options']
  320. 'ok'
  321. end
  322. end
  323. get '/'
  324. assert_body 'ok'
  325. end
  326. it 'sets a default session secret' do
  327. mock_app do
  328. enable :sessions
  329. get('/') do
  330. secret = env['rack.session.options'][:secret]
  331. assert secret
  332. assert_equal secret, settings.session_secret
  333. 'ok'
  334. end
  335. end
  336. get '/'
  337. assert_body 'ok'
  338. end
  339. it 'allows disabling session secret' do
  340. mock_app do
  341. enable :sessions
  342. disable :session_secret
  343. get('/') do
  344. assert !env['rack.session.options'].include?(:session_secret)
  345. 'ok'
  346. end
  347. end
  348. get '/'
  349. assert_body 'ok'
  350. end
  351. it 'accepts an options hash' do
  352. mock_app do
  353. set :sessions, :foo => :bar
  354. get('/') do
  355. assert_equal env['rack.session.options'][:foo], :bar
  356. 'ok'
  357. end
  358. end
  359. get '/'
  360. assert_body 'ok'
  361. end
  362. end
  363. describe 'mime_type' do
  364. include Sinatra::Helpers
  365. it "looks up mime types in Rack's MIME registry" do
  366. Rack::Mime::MIME_TYPES['.foo'] = 'application/foo'
  367. assert_equal 'application/foo', mime_type('foo')
  368. assert_equal 'application/foo', mime_type('.foo')
  369. assert_equal 'application/foo', mime_type(:foo)
  370. end
  371. it 'returns nil when given nil' do
  372. assert mime_type(nil).nil?
  373. end
  374. it 'returns nil when media type not registered' do
  375. assert mime_type(:bizzle).nil?
  376. end
  377. it 'returns the argument when given a media type string' do
  378. assert_equal 'text/plain', mime_type('text/plain')
  379. end
  380. end
  381. test 'Base.mime_type registers mime type' do
  382. mock_app do
  383. mime_type :foo, 'application/foo'
  384. get('/') do
  385. "foo is #{mime_type(:foo)}"
  386. end
  387. end
  388. get '/'
  389. assert_equal 'foo is application/foo', body
  390. end
  391. describe 'content_type' do
  392. it 'sets the Content-Type header' do
  393. mock_app do
  394. get('/') do
  395. content_type 'text/plain'
  396. 'Hello World'
  397. end
  398. end
  399. get '/'
  400. assert_equal 'text/plain;charset=utf-8', response['Content-Type']
  401. assert_equal 'Hello World', body
  402. end
  403. it 'takes media type parameters (like charset=)' do
  404. mock_app do
  405. get('/') do
  406. content_type 'text/html', :charset => 'latin1'
  407. "<h1>Hello, World</h1>"
  408. end
  409. end
  410. get '/'
  411. assert ok?
  412. assert_equal 'text/html;charset=latin1', response['Content-Type']
  413. assert_equal "<h1>Hello, World</h1>", body
  414. end
  415. it "looks up symbols in Rack's mime types dictionary" do
  416. Rack::Mime::MIME_TYPES['.foo'] = 'application/foo'
  417. mock_app do
  418. get('/foo.xml') do
  419. content_type :foo
  420. "I AM FOO"
  421. end
  422. end
  423. get '/foo.xml'
  424. assert ok?
  425. assert_equal 'application/foo', response['Content-Type']
  426. assert_equal 'I AM FOO', body
  427. end
  428. it 'fails when no mime type is registered for the argument provided' do
  429. mock_app do
  430. get('/foo.xml') do
  431. content_type :bizzle
  432. "I AM FOO"
  433. end
  434. end
  435. assert_raise(RuntimeError) { get '/foo.xml' }
  436. end
  437. it 'only sets default charset for specific mime types' do
  438. tests_ran = false
  439. mock_app do
  440. mime_type :foo, 'text/foo'
  441. mime_type :bar, 'application/bar'
  442. mime_type :baz, 'application/baz'
  443. add_charset << mime_type(:baz)
  444. get('/') do
  445. assert_equal content_type(:txt), 'text/plain;charset=utf-8'
  446. assert_equal content_type(:css), 'text/css;charset=utf-8'
  447. assert_equal content_type(:html), 'text/html;charset=utf-8'
  448. assert_equal content_type(:foo), 'text/foo;charset=utf-8'
  449. assert_equal content_type(:xml), 'application/xml;charset=utf-8'
  450. assert_equal content_type(:xhtml), 'application/xhtml+xml;charset=utf-8'
  451. assert_equal content_type(:js), 'application/javascript;charset=utf-8'
  452. assert_equal content_type(:json), 'application/json;charset=utf-8'
  453. assert_equal content_type(:bar), 'application/bar'
  454. assert_equal content_type(:png), 'image/png'
  455. assert_equal content_type(:baz), 'application/baz;charset=utf-8'
  456. tests_ran = true
  457. "done"
  458. end
  459. end
  460. get '/'
  461. assert tests_ran
  462. end
  463. it 'handles already present params' do
  464. mock_app do
  465. get('/') do
  466. content_type 'foo/bar;level=1', :charset => 'utf-8'
  467. 'ok'
  468. end
  469. end
  470. get '/'
  471. assert_equal 'foo/bar;level=1, charset=utf-8', response['Content-Type']
  472. end
  473. it 'does not add charset if present' do
  474. mock_app do
  475. get('/') do
  476. content_type 'text/plain;charset=utf-16'
  477. 'ok'
  478. end
  479. end
  480. get '/'
  481. assert_equal 'text/plain;charset=utf-16', response['Content-Type']
  482. end
  483. end
  484. describe 'attachment' do
  485. def attachment_app(filename=nil)
  486. mock_app do
  487. get('/attachment') do
  488. attachment filename
  489. response.write("<sinatra></sinatra>")
  490. end
  491. end
  492. end
  493. it 'sets the Content-Type response header' do
  494. attachment_app('test.xml')
  495. get '/attachment'
  496. assert_equal 'application/xml;charset=utf-8', response['Content-Type']
  497. assert_equal '<sinatra></sinatra>', body
  498. end
  499. it 'sets the Content-Type response header without extname' do
  500. attachment_app('test')
  501. get '/attachment'
  502. assert_equal 'text/html;charset=utf-8', response['Content-Type']
  503. assert_equal '<sinatra></sinatra>', body
  504. end
  505. it 'sets the Content-Type response header with extname' do
  506. mock_app do
  507. get('/attachment') do
  508. content_type :atom
  509. attachment 'test.xml'
  510. response.write("<sinatra></sinatra>")
  511. end
  512. end
  513. get '/attachment'
  514. assert_equal 'application/atom+xml', response['Content-Type']
  515. assert_equal '<sinatra></sinatra>', body
  516. end
  517. end
  518. describe 'send_file' do
  519. setup do
  520. @file = File.dirname(__FILE__) + '/file.txt'
  521. File.open(@file, 'wb') { |io| io.write('Hello World') }
  522. end
  523. def teardown
  524. File.unlink @file
  525. @file = nil
  526. end
  527. def send_file_app(opts={})
  528. path = @file
  529. mock_app {
  530. get '/file.txt' do
  531. send_file path, opts
  532. end
  533. }
  534. end
  535. it "sends the contents of the file" do
  536. send_file_app
  537. get '/file.txt'
  538. assert ok?
  539. assert_equal 'Hello World', body
  540. end
  541. it 'sets the Content-Type response header if a mime-type can be located' do
  542. send_file_app
  543. get '/file.txt'
  544. assert_equal 'text/plain;charset=utf-8', response['Content-Type']
  545. end
  546. it 'sets the Content-Type response header if type option is set to a file extesion' do
  547. send_file_app :type => 'html'
  548. get '/file.txt'
  549. assert_equal 'text/html;charset=utf-8', response['Content-Type']
  550. end
  551. it 'sets the Content-Type response header if type option is set to a mime type' do
  552. send_file_app :type => 'application/octet-stream'
  553. get '/file.txt'
  554. assert_equal 'application/octet-stream', response['Content-Type']
  555. end
  556. it 'sets the Content-Length response header' do
  557. send_file_app
  558. get '/file.txt'
  559. assert_equal 'Hello World'.length.to_s, response['Content-Length']
  560. end
  561. it 'sets the Last-Modified response header' do
  562. send_file_app
  563. get '/file.txt'
  564. assert_equal File.mtime(@file).httpdate, response['Last-Modified']
  565. end
  566. it 'allows passing in a differen Last-Modified response header with :last_modified' do
  567. time = Time.now
  568. send_file_app :last_modified => time
  569. get '/file.txt'
  570. assert_equal time.httpdate, response['Last-Modified']
  571. end
  572. it "returns a 404 when not found" do
  573. mock_app {
  574. get('/') { send_file 'this-file-does-not-exist.txt' }
  575. }
  576. get '/'
  577. assert not_found?
  578. end
  579. it "does not set the Content-Disposition header by default" do
  580. send_file_app
  581. get '/file.txt'
  582. assert_nil response['Content-Disposition']
  583. end
  584. it "sets the Content-Disposition header when :disposition set to 'attachment'" do
  585. send_file_app :disposition => 'attachment'
  586. get '/file.txt'
  587. assert_equal 'attachment; filename="file.txt"', response['Content-Disposition']
  588. end
  589. it "sets the Content-Disposition header when :disposition set to 'inline'" do
  590. send_file_app :disposition => 'inline'
  591. get '/file.txt'
  592. assert_equal 'inline', response['Content-Disposition']
  593. end
  594. it "sets the Content-Disposition header when :filename provided" do
  595. send_file_app :filename => 'foo.txt'
  596. get '/file.txt'
  597. assert_equal 'attachment; filename="foo.txt"', response['Content-Disposition']
  598. end
  599. it 'allows setting a custom status code' do
  600. send_file_app :status => 201
  601. get '/file.txt'
  602. assert_status 201
  603. end
  604. it "is able to send files with unkown mime type" do
  605. @file = File.dirname(__FILE__) + '/file.foobar'
  606. File.open(@file, 'wb') { |io| io.write('Hello World') }
  607. send_file_app
  608. get '/file.txt'
  609. assert_equal 'application/octet-stream', response['Content-Type']
  610. end
  611. it "does not override Content-Type if already set and no explicit type is given" do
  612. path = @file
  613. mock_app do
  614. get('/') do
  615. content_type :png
  616. send_file path
  617. end
  618. end
  619. get '/'
  620. assert_equal 'image/png', response['Content-Type']
  621. end
  622. it "does override Content-Type even if already set, if explicit type is given" do
  623. path = @file
  624. mock_app do
  625. get('/') do
  626. content_type :png
  627. send_file path, :type => :gif
  628. end
  629. end
  630. get '/'
  631. assert_equal 'image/gif', response['Content-Type']
  632. end
  633. end
  634. describe 'cache_control' do
  635. setup do
  636. mock_app do
  637. get('/foo') do
  638. cache_control :public, :no_cache, :max_age => 60.0
  639. 'Hello World'
  640. end
  641. get('/bar') do
  642. cache_control :public, :no_cache
  643. 'Hello World'
  644. end
  645. end
  646. end
  647. it 'sets the Cache-Control header' do
  648. get '/foo'
  649. assert_equal ['public', 'no-cache', 'max-age=60'], response['Cache-Control'].split(', ')
  650. end
  651. it 'last argument does not have to be a hash' do
  652. get '/bar'
  653. assert_equal ['public', 'no-cache'], response['Cache-Control'].split(', ')
  654. end
  655. end
  656. describe 'expires' do
  657. setup do
  658. mock_app do
  659. get('/foo') do
  660. expires 60, :public, :no_cache
  661. 'Hello World'
  662. end
  663. get('/bar') { expires Time.now }
  664. get('/baz') { expires Time.at(0) }
  665. get('/blah') do
  666. obj = Object.new
  667. def obj.method_missing(*a, &b) 60.send(*a, &b) end
  668. def obj.is_a?(thing) 60.is_a?(thing) end
  669. expires obj, :public, :no_cache
  670. 'Hello World'
  671. end
  672. get('/boom') { expires '9999' }
  673. end
  674. end
  675. it 'sets the Cache-Control header' do
  676. get '/foo'
  677. assert_equal ['public', 'no-cache', 'max-age=60'], response['Cache-Control'].split(', ')
  678. end
  679. it 'sets the Expires header' do
  680. get '/foo'
  681. assert_not_nil response['Expires']
  682. end
  683. it 'allows passing Time.now objects' do
  684. get '/bar'
  685. assert_not_nil response['Expires']
  686. end
  687. it 'allows passing Time.at objects' do
  688. get '/baz'
  689. assert_equal 'Thu, 01 Jan 1970 00:00:00 GMT', response['Expires']
  690. end
  691. it 'accepts values pretending to be a Numeric (like ActiveSupport::Duration)' do
  692. get '/blah'
  693. assert_equal ['public', 'no-cache', 'max-age=60'], response['Cache-Control'].split(', ')
  694. end
  695. it 'fails when Time.parse raises an ArgumentError' do
  696. assert_raise(ArgumentError) { get '/boom' }
  697. end
  698. end
  699. describe 'last_modified' do
  700. it 'ignores nil' do
  701. mock_app { get('/') { last_modified nil; 200; } }
  702. get '/'
  703. assert ! response['Last-Modified']
  704. end
  705. it 'does not change a status other than 200' do
  706. mock_app do
  707. get('/') do
  708. status 299
  709. last_modified Time.at(0)
  710. 'ok'
  711. end
  712. end
  713. get('/', {}, 'HTTP_IF_MODIFIED_SINCE' => 'Sun, 26 Sep 2030 23:43:52 GMT')
  714. assert_status 299
  715. assert_body 'ok'
  716. end
  717. [Time.now, DateTime.now, Date.today, Time.now.to_i,
  718. Struct.new(:to_time).new(Time.now) ].each do |last_modified_time|
  719. describe "with #{last_modified_time.class.name}" do
  720. setup do
  721. mock_app do
  722. get('/') do
  723. last_modified last_modified_time
  724. 'Boo!'
  725. end
  726. end
  727. wrapper = Object.new.extend Sinatra::Helpers
  728. @last_modified_time = wrapper.time_for last_modified_time
  729. end
  730. # fixes strange missing test error when running complete test suite.
  731. it("does not complain about missing tests") { }
  732. context "when there's no If-Modified-Since header" do
  733. it 'sets the Last-Modified header to a valid RFC 2616 date value' do
  734. get '/'
  735. assert_equal @last_modified_time.httpdate, response['Last-Modified']
  736. end
  737. it 'conditional GET misses and returns a body' do
  738. get '/'
  739. assert_equal 200, status
  740. assert_equal 'Boo!', body
  741. end
  742. end
  743. context "when there's an invalid If-Modified-Since header" do
  744. it 'sets the Last-Modified header to a valid RFC 2616 date value' do
  745. get('/', {}, { 'HTTP_IF_MODIFIED_SINCE' => 'a really weird date' })
  746. assert_equal @last_modified_time.httpdate, response['Last-Modified']
  747. end
  748. it 'conditional GET misses and returns a body' do
  749. get('/', {}, { 'HTTP_IF_MODIFIED_SINCE' => 'a really weird date' })
  750. assert_equal 200, status
  751. assert_equal 'Boo!', body
  752. end
  753. end
  754. context "when the resource has been modified since the If-Modified-Since header date" do
  755. it 'sets the Last-Modified header to a valid RFC 2616 date value' do
  756. get('/', {}, { 'HTTP_IF_MODIFIED_SINCE' => (@last_modified_time - 1).httpdate })
  757. assert_equal @last_modified_time.httpdate, response['Last-Modified']
  758. end
  759. it 'conditional GET misses and returns a body' do
  760. get('/', {}, { 'HTTP_IF_MODIFIED_SINCE' => (@last_modified_time - 1).httpdate })
  761. assert_equal 200, status
  762. assert_equal 'Boo!', body
  763. end
  764. it 'does not rely on string comparison' do
  765. mock_app do
  766. get('/compare') do
  767. last_modified "Mon, 18 Oct 2010 20:57:11 GMT"
  768. "foo"
  769. end
  770. end
  771. get('/compare', {}, { 'HTTP_IF_MODIFIED_SINCE' => 'Sun, 26 Sep 2010 23:43:52 GMT' })
  772. assert_equal 200, status
  773. assert_equal 'foo', body
  774. get('/compare', {}, { 'HTTP_IF_MODIFIED_SINCE' => 'Sun, 26 Sep 2030 23:43:52 GMT' })
  775. assert_equal 304, status
  776. assert_equal '', body
  777. end
  778. end
  779. context "when the resource has been modified on the exact If-Modified-Since header date" do
  780. it 'sets the Last-Modified header to a valid RFC 2616 date value' do
  781. get('/', {}, { 'HTTP_IF_MODIFIED_SINCE' => @last_modified_time.httpdate })
  782. assert_equal @last_modified_time.httpdate, response['Last-Modified']
  783. end
  784. it 'conditional GET matches and halts' do
  785. get( '/', {}, { 'HTTP_IF_MODIFIED_SINCE' => @last_modified_time.httpdate })
  786. assert_equal 304, status
  787. assert_equal '', body
  788. end
  789. end
  790. context "when the resource hasn't been modified since the If-Modified-Since header date" do
  791. it 'sets the Last-Modified header to a valid RFC 2616 date value' do
  792. get('/', {}, { 'HTTP_IF_MODIFIED_SINCE' => (@last_modified_time + 1).httpdate })
  793. assert_equal @last_modified_time.httpdate, response['Last-Modified']
  794. end
  795. it 'conditional GET matches and halts' do
  796. get('/', {}, { 'HTTP_IF_MODIFIED_SINCE' => (@last_modified_time + 1).httpdate })
  797. assert_equal 304, status
  798. assert_equal '', body
  799. end
  800. end
  801. context "If-Unmodified-Since" do
  802. it 'results in 200 if resource has not been modified' do
  803. get('/', {}, { 'HTTP_IF_UNMODIFIED_SINCE' => 'Sun, 26 Sep 2030 23:43:52 GMT' })
  804. assert_equal 200, status
  805. assert_equal 'Boo!', body
  806. end
  807. it 'results in 412 if resource has been modified' do
  808. get('/', {}, { 'HTTP_IF_UNMODIFIED_SINCE' => Time.at(0).httpdate })
  809. assert_equal 412, status
  810. assert_equal '', body
  811. end
  812. end
  813. end
  814. end
  815. end
  816. describe 'etag' do
  817. context "safe requests" do
  818. it 'returns 200 for normal requests' do
  819. mock_app do
  820. get('/') do
  821. etag 'foo'
  822. 'ok'
  823. end
  824. end
  825. get '/'
  826. assert_status 200
  827. assert_body 'ok'
  828. end
  829. context "If-None-Match" do
  830. it 'returns 304 when If-None-Match is *' do
  831. mock_app do
  832. get('/') do
  833. etag 'foo'
  834. 'ok'
  835. end
  836. end
  837. get('/', {}, 'HTTP_IF_NONE_MATCH' => '*')
  838. assert_status 304
  839. assert_body ''
  840. end
  841. it 'returns 200 when If-None-Match is * for new resources' do
  842. mock_app do
  843. get('/') do
  844. etag 'foo', :new_resource => true
  845. 'ok'
  846. end
  847. end
  848. get('/', {}, 'HTTP_IF_NONE_MATCH' => '*')
  849. assert_status 200
  850. assert_body 'ok'
  851. end
  852. it 'returns 304 when If-None-Match is * for existing resources' do
  853. mock_app do
  854. get('/') do
  855. etag 'foo', :new_resource => false
  856. 'ok'
  857. end
  858. end
  859. get('/', {}, 'HTTP_IF_NONE_MATCH' => '*')
  860. assert_status 304
  861. assert_body ''
  862. end
  863. it 'returns 304 when If-None-Match is the etag' do
  864. mock_app do
  865. get('/') do
  866. etag 'foo'
  867. 'ok'
  868. end
  869. end
  870. get('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"')
  871. assert_status 304
  872. assert_body ''
  873. end
  874. it 'returns 304 when If-None-Match includes the etag' do
  875. mock_app do
  876. get('/') do
  877. etag 'foo'
  878. 'ok'
  879. end
  880. end
  881. get('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar", "foo"')
  882. assert_status 304
  883. assert_body ''
  884. end
  885. it 'returns 200 when If-None-Match does not include the etag' do
  886. mock_app do
  887. get('/') do
  888. etag 'foo'
  889. 'ok'
  890. end
  891. end
  892. get('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"')
  893. assert_status 200
  894. assert_body 'ok'
  895. end
  896. it 'ignores If-Modified-Since if If-None-Match does not match' do
  897. mock_app do
  898. get('/') do
  899. etag 'foo'
  900. last_modified Time.at(0)
  901. 'ok'
  902. end
  903. end
  904. get('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"')
  905. assert_status 200
  906. assert_body 'ok'
  907. end
  908. it 'does not change a status code other than 2xx or 304' do
  909. mock_app do
  910. get('/') do
  911. status 499
  912. etag 'foo'
  913. 'ok'
  914. end
  915. end
  916. get('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"')
  917. assert_status 499
  918. assert_body 'ok'
  919. end
  920. it 'does change 2xx status codes' do
  921. mock_app do
  922. get('/') do
  923. status 299
  924. etag 'foo'
  925. 'ok'
  926. end
  927. end
  928. get('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"')
  929. assert_status 304
  930. assert_body ''
  931. end
  932. it 'does not send a body on 304 status codes' do
  933. mock_app do
  934. get('/') do
  935. status 304
  936. etag 'foo'
  937. 'ok'
  938. end
  939. end
  940. get('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"')
  941. assert_status 304
  942. assert_body ''
  943. end
  944. end
  945. context "If-Match" do
  946. it 'returns 200 when If-Match is the etag' do
  947. mock_app do
  948. get('/') do
  949. etag 'foo'
  950. 'ok'
  951. end
  952. end
  953. get('/', {}, 'HTTP_IF_MATCH' => '"foo"')
  954. assert_status 200
  955. assert_body 'ok'
  956. end
  957. it 'returns 200 when If-Match includes the etag' do
  958. mock_app do
  959. get('/') do
  960. etag 'foo'
  961. 'ok'
  962. end
  963. end
  964. get('/', {}, 'HTTP_IF_MATCH' => '"foo", "bar"')
  965. assert_status 200
  966. assert_body 'ok'
  967. end
  968. it 'returns 200 when If-Match is *' do
  969. mock_app do
  970. get('/') do
  971. etag 'foo'
  972. 'ok'
  973. end
  974. end
  975. get('/', {}, 'HTTP_IF_MATCH' => '*')
  976. assert_status 200
  977. assert_body 'ok'
  978. end
  979. it 'returns 412 when If-Match is * for new resources' do
  980. mock_app do
  981. get('/') do
  982. etag 'foo', :new_resource => true
  983. 'ok'
  984. end
  985. end
  986. get('/', {}, 'HTTP_IF_MATCH' => '*')
  987. assert_status 412
  988. assert_body ''
  989. end
  990. it 'returns 200 when If-Match is * for existing resources' do
  991. mock_app do
  992. get('/') do
  993. etag 'foo', :new_resource => false
  994. 'ok'
  995. end
  996. end
  997. get('/', {}, 'HTTP_IF_MATCH' => '*')
  998. assert_status 200
  999. assert_body 'ok'
  1000. end
  1001. it 'returns 412 when If-Match does not include the etag' do
  1002. mock_app do
  1003. get('/') do
  1004. etag 'foo'
  1005. 'ok'
  1006. end
  1007. end
  1008. get('/', {}, 'HTTP_IF_MATCH' => '"bar"')
  1009. assert_status 412
  1010. assert_body ''
  1011. end
  1012. end
  1013. end
  1014. context "idempotent requests" do
  1015. it 'returns 200 for normal requests' do
  1016. mock_app do
  1017. put('/') do
  1018. etag 'foo'
  1019. 'ok'
  1020. end
  1021. end
  1022. put '/'
  1023. assert_status 200
  1024. assert_body 'ok'
  1025. end
  1026. context "If-None-Match" do
  1027. it 'returns 412 when If-None-Match is *' do
  1028. mock_app do
  1029. put('/') do
  1030. etag 'foo'
  1031. 'ok'
  1032. end
  1033. end
  1034. put('/', {}, 'HTTP_IF_NONE_MATCH' => '*')
  1035. assert_status 412
  1036. assert_body ''
  1037. end
  1038. it 'returns 200 when If-None-Match is * for new resources' do
  1039. mock_app do
  1040. put('/') do
  1041. etag 'foo', :new_resource => true
  1042. 'ok'
  1043. end
  1044. end
  1045. put('/', {}, 'HTTP_IF_NONE_MATCH' => '*')
  1046. assert_status 200
  1047. assert_body 'ok'
  1048. end
  1049. it 'returns 412 when If-None-Match is * for existing resources' do
  1050. mock_app do
  1051. put('/') do
  1052. etag 'foo', :new_resource => false
  1053. 'ok'
  1054. end
  1055. end
  1056. put('/', {}, 'HTTP_IF_NONE_MATCH' => '*')
  1057. assert_status 412
  1058. assert_body ''
  1059. end
  1060. it 'returns 412 when If-None-Match is the etag' do
  1061. mock_app do
  1062. put '/' do
  1063. etag 'foo'
  1064. 'ok'
  1065. end
  1066. end
  1067. put('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"')
  1068. assert_status 412
  1069. assert_body ''
  1070. end
  1071. it 'returns 412 when If-None-Match includes the etag' do
  1072. mock_app do
  1073. put('/') do
  1074. etag 'foo'
  1075. 'ok'
  1076. end
  1077. end
  1078. put('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar", "foo"')
  1079. assert_status 412
  1080. assert_body ''
  1081. end
  1082. it 'returns 200 when If-None-Match does not include the etag' do
  1083. mock_app do
  1084. put('/') do
  1085. etag 'foo'
  1086. 'ok'
  1087. end
  1088. end
  1089. put('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"')
  1090. assert_status 200
  1091. assert_body 'ok'
  1092. end
  1093. it 'ignores If-Modified-Since if If-None-Match does not match' do
  1094. mock_app do
  1095. put('/') do
  1096. etag 'foo'
  1097. last_modified Time.at(0)
  1098. 'ok'
  1099. end
  1100. end
  1101. put('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"')
  1102. assert_status 200
  1103. assert_body 'ok'
  1104. end
  1105. end
  1106. context "If-Match" do
  1107. it 'returns 200 when If-Match is the etag' do
  1108. mock_app do
  1109. put('/') do
  1110. etag 'foo'
  1111. 'ok'
  1112. end
  1113. end
  1114. put('/', {}, 'HTTP_IF_MATCH' => '"foo"')
  1115. assert_status 200
  1116. assert_body 'ok'
  1117. end
  1118. it 'returns 200 when If-Match includes the etag' do
  1119. mock_app do
  1120. put('/') do
  1121. etag 'foo'
  1122. 'ok'
  1123. end
  1124. end
  1125. put('/', {}, 'HTTP_IF_MATCH' => '"foo", "bar"')
  1126. assert_status 200
  1127. assert_body 'ok'
  1128. end
  1129. it 'returns 200 when If-Match is *' do
  1130. mock_app do
  1131. put('/') do
  1132. etag 'foo'
  1133. 'ok'
  1134. end
  1135. end
  1136. put('/', {}, 'HTTP_IF_MATCH' => '*')
  1137. assert_status 200
  1138. assert_body 'ok'
  1139. end
  1140. it 'returns 412 when If-Match is * for new resources' do
  1141. mock_app do
  1142. put('/') do
  1143. etag 'foo', :new_resource => true
  1144. 'ok'
  1145. end
  1146. end
  1147. put('/', {}, 'HTTP_IF_MATCH' => '*')
  1148. assert_status 412
  1149. assert_body ''
  1150. end
  1151. it 'returns 200 when If-Match is * for existing resources' do
  1152. mock_app do
  1153. put('/') do
  1154. etag 'foo', :new_resource => false
  1155. 'ok'
  1156. end
  1157. end
  1158. put('/', {}, 'HTTP_IF_MATCH' => '*')
  1159. assert_status 200
  1160. assert_body 'ok'
  1161. end
  1162. it 'returns 412 when If-Match does not include the etag' do
  1163. mock_app do
  1164. put('/') do
  1165. etag 'foo'
  1166. 'ok'
  1167. end
  1168. end
  1169. put('/', {}, 'HTTP_IF_MATCH' => '"bar"')
  1170. assert_status 412
  1171. assert_body ''
  1172. end
  1173. end
  1174. end
  1175. context "post requests" do
  1176. it 'returns 200 for normal requests' do
  1177. mock_app do
  1178. post('/') do
  1179. etag 'foo'
  1180. 'ok'
  1181. end
  1182. end
  1183. post('/')
  1184. assert_status 200
  1185. assert_body 'ok'
  1186. end
  1187. context "If-None-Match" do
  1188. it 'returns 200 when If-None-Match is *' do
  1189. mock_app do
  1190. post('/') do
  1191. etag 'foo'
  1192. 'ok'
  1193. end
  1194. end
  1195. post('/', {}, 'HTTP_IF_NONE_MATCH' => '*')
  1196. assert_status 200
  1197. assert_body 'ok'
  1198. end
  1199. it 'returns 200 when If-None-Match is * for new resources' do
  1200. mock_app do
  1201. post('/') do
  1202. etag 'foo', :new_resource => true
  1203. 'ok'
  1204. end
  1205. end
  1206. post('/', {}, 'HTTP_IF_NONE_MATCH' => '*')
  1207. assert_status 200
  1208. assert_body 'ok'
  1209. end
  1210. it 'returns 412 when If-None-Match is * for existing resources' do
  1211. mock_app do
  1212. post('/') do
  1213. etag 'foo', :new_resource => false
  1214. 'ok'
  1215. end
  1216. end
  1217. post('/', {}, 'HTTP_IF_NONE_MATCH' => '*')
  1218. assert_status 412
  1219. assert_body ''
  1220. end
  1221. it 'returns 412 when If-None-Match is the etag' do
  1222. mock_app do
  1223. post('/') do
  1224. etag 'foo'
  1225. 'ok'
  1226. end
  1227. end
  1228. post('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"')
  1229. assert_status 412
  1230. assert_body ''
  1231. end
  1232. it 'returns 412 when If-None-Match includes the etag' do
  1233. mock_app do
  1234. post('/') do
  1235. etag 'foo'
  1236. 'ok'
  1237. end
  1238. end
  1239. post('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar", "foo"')
  1240. assert_status 412
  1241. assert_body ''
  1242. end
  1243. it 'returns 200 when If-None-Match does not include the etag' do
  1244. mock_app do
  1245. post('/') do
  1246. etag 'foo'
  1247. 'ok'
  1248. end
  1249. end
  1250. post('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"')
  1251. assert_status 200
  1252. assert_body 'ok'
  1253. end
  1254. it 'ignores If-Modified-Since if If-None-Match does not match' do
  1255. mock_app do
  1256. post('/') do
  1257. etag 'foo'
  1258. last_modified Time.at(0)
  1259. 'ok'
  1260. end
  1261. end
  1262. post('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"')
  1263. assert_status 200
  1264. assert_body 'ok'
  1265. end
  1266. end
  1267. context "If-Match" do
  1268. it 'returns 200 when If-Match is the etag' do
  1269. mock_app do
  1270. post('/') do
  1271. etag 'foo'
  1272. 'ok'
  1273. end
  1274. end
  1275. post('/', {}, 'HTTP_IF_MATCH' => '"foo"')
  1276. assert_status 200
  1277. assert_body 'ok'
  1278. end
  1279. it 'returns 200 when If-Match includes the etag' do
  1280. mock_app do
  1281. post('/') do
  1282. etag 'foo'
  1283. 'ok'
  1284. end
  1285. end
  1286. post('/', {}, 'HTTP_IF_MATCH' => '"foo", "bar"')
  1287. assert_status 200
  1288. assert_body 'ok'
  1289. end
  1290. it 'returns 412 when If-Match is *' do
  1291. mock_app do
  1292. post('/') do
  1293. etag 'foo'
  1294. 'ok'
  1295. end
  1296. end
  1297. post('/', {}, 'HTTP_IF_MATCH' => '*')
  1298. assert_status 412
  1299. assert_body ''
  1300. end
  1301. it 'returns 412 when If-Match is * for new resources' do
  1302. mock_app do
  1303. post('/') do
  1304. etag 'foo', :new_resource => true
  1305. 'ok'
  1306. end
  1307. end
  1308. post('/', {}, 'HTTP_IF_MATCH' => '*')
  1309. assert_status 412
  1310. assert_body ''
  1311. end
  1312. it 'returns 200 when If-Match is * for existing resources' do
  1313. mock_app do
  1314. post('/') do
  1315. etag 'foo', :new_resource => false
  1316. 'ok'
  1317. end
  1318. end
  1319. post('/', {}, 'HTTP_IF_MATCH' => '*')
  1320. assert_status 200
  1321. assert_body 'ok'
  1322. end
  1323. it 'returns 412 when If-Match does not include the etag' do
  1324. mock_app do
  1325. post('/') do
  1326. etag 'foo'
  1327. 'ok'
  1328. end
  1329. end
  1330. post('/', {}, 'HTTP_IF_MATCH' => '"bar"')
  1331. assert_status 412
  1332. assert_body ''
  1333. end
  1334. end
  1335. end
  1336. it 'uses a weak etag with the :weak option' do
  1337. mock_app do
  1338. get('/') do
  1339. etag 'FOO', :weak
  1340. "that's weak, dude."
  1341. end
  1342. end
  1343. get '/'
  1344. assert_equal 'W/"FOO"', response['ETag']
  1345. end
  1346. it 'raises an ArgumentError for an invalid strength' do
  1347. mock_app do
  1348. get('/') do
  1349. etag 'FOO', :w00t
  1350. "that's weak, dude."
  1351. end
  1352. end
  1353. assert_raise(ArgumentError) { get('/') }
  1354. end
  1355. end
  1356. describe 'back' do
  1357. it "makes redirecting back pretty" do
  1358. mock_app { get('/foo') { redirect back } }
  1359. get('/foo', {}, 'HTTP_REFERER' => 'http://github.com')
  1360. assert redirect?
  1361. assert_equal "http://github.com", response.location
  1362. end
  1363. end
  1364. describe 'uri' do
  1365. it 'generates absolute urls' do
  1366. mock_app { get('/') { uri }}
  1367. get '/'
  1368. assert_equal 'http://example.org/', body
  1369. end
  1370. it 'includes path_info' do
  1371. mock_app { get('/:name') { uri }}
  1372. get '/foo'
  1373. assert_equal 'http://example.org/foo', body
  1374. end
  1375. it 'allows passing an alternative to path_info' do
  1376. mock_app { get('/:name') { uri '/bar' }}
  1377. get '/foo'
  1378. assert_equal 'http://example.org/bar', body
  1379. end
  1380. it 'includes script_name' do
  1381. mock_app { get('/:name') { uri '/bar' }}
  1382. get '/foo', {}, { "SCRIPT_NAME" => '/foo' }
  1383. assert_equal 'http://example.org/foo/bar', body
  1384. end
  1385. it 'handles absolute URIs' do
  1386. mock_app { get('/') { uri 'http://google.com' }}
  1387. get '/'
  1388. assert_equal 'http://google.com', body
  1389. end
  1390. it 'handles different protocols' do
  1391. mock_app { get('/') { uri 'mailto:jsmith@example.com' }}
  1392. get '/'
  1393. assert_equal 'mailto:jsmith@example.com', body
  1394. end
  1395. it 'is aliased to #url' do
  1396. mock_app { get('/') { url }}
  1397. get '/'
  1398. assert_equal 'http://example.org/', body
  1399. end
  1400. it 'is aliased to #to' do
  1401. mock_app { get('/') { to }}
  1402. get '/'
  1403. assert_equal 'http://example.org/', body
  1404. end
  1405. end
  1406. describe 'logger' do
  1407. it 'logging works when logging is enabled' do
  1408. mock_app do
  1409. enable :logging
  1410. get('/') do
  1411. logger.info "Program started"
  1412. logger.warn "Nothing to do!"
  1413. end
  1414. end
  1415. io = StringIO.new
  1416. get '/', {}, 'rack.errors' => io
  1417. assert io.string.include?("INFO -- : Program started")
  1418. assert io.string.include?("WARN -- : Nothing to do")
  1419. end
  1420. it 'logging works when logging is disable, but no output is produced' do
  1421. mock_app do
  1422. disable :logging
  1423. get('/') do
  1424. logger.info "Program started"
  1425. logger.warn "Nothing to do!"
  1426. end
  1427. end
  1428. io = StringIO.new
  1429. get '/', {}, 'rack.errors' => io
  1430. assert !io.string.include?("INFO -- : Program started")
  1431. assert !io.string.include?("WARN -- : Nothing to do")
  1432. end
  1433. it 'does not create a logger when logging is set to nil' do
  1434. mock_app do
  1435. set :logging, nil
  1436. get('/') { logger.inspect }
  1437. end
  1438. get '/'
  1439. assert_body 'nil'
  1440. end
  1441. end
  1442. module ::HelperOne; def one; '1'; end; end
  1443. module ::HelperTwo; def two; '2'; end; end
  1444. describe 'Adding new helpers' do
  1445. it 'takes a list of modules to mix into the app' do
  1446. mock_app do
  1447. helpers ::HelperOne, ::HelperTwo
  1448. get('/one') { one }
  1449. get('/two') { two }
  1450. end
  1451. get '/one'
  1452. assert_equal '1', body
  1453. get '/two'
  1454. assert_equal '2', body
  1455. end
  1456. it 'takes a block to mix into the app' do
  1457. mock_app do
  1458. helpers do
  1459. def foo
  1460. 'foo'
  1461. end
  1462. end
  1463. get('/') { foo }
  1464. end
  1465. get '/'
  1466. assert_equal 'foo', body
  1467. end
  1468. it 'evaluates the block in class context so that methods can be aliased' do
  1469. mock_app do
  1470. helpers { alias_method :h, :escape_html }
  1471. get('/') { h('42 < 43') }
  1472. end
  1473. get '/'
  1474. assert ok?
  1475. assert_equal '42 &lt; 43', body
  1476. end
  1477. end
  1478. end