diff --git a/.travis.yml b/.travis.yml index 75b4d43c..016ebf16 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,9 +7,9 @@ rvm: - jruby - ruby-head env: - - "rack=1.3.0" + - "rack=1.3.4" - "rack=master" - - "tilt=1.3.2" + - "tilt=1.3.3" - "tilt=master" notifications: recipients: diff --git a/CHANGES b/CHANGES index dc0ef77c..c90653c5 100644 --- a/CHANGES +++ b/CHANGES @@ -1,4 +1,23 @@ -= 1.3.0 / Not Yet Released += 1.3.2 / Not Yet Released + + * Don't automatically add `Rack::CommonLogger` if `Rack::Server` is adding it, + too. (Konstantin Haase) + + * Setting `logging` to `nil` will avoid setting up `Rack::NullLogger`. + (Konstantin Haase) + + * Fix bug where rendering a second template in the same request after the + first one raised an exception skipped the default layout (Nathan Baum) + += 1.3.1 / 2011-10-05 + + * Support adding more than one callback to the stream object. (Konstantin + Haase) + + * Fix for infinite loop when streaming on 1.9.2 with Thin from a modular + application (Konstantin Haase) + += 1.3.0 / 2011-09-30 * Added `stream` helper method for easily creating streaming APIs, Server Sent Events or even WebSockets. See README for more on that topic. @@ -12,7 +31,7 @@ * Added support for HTTP PATCH requests. (Konstantin Haase) * Use rack-protection to defend against common opportunistic attacks. - (Konstantin Haase) + (Josh Lane, Jacob Burkhart, Konstantin Haase) * Support for Creole templates, Creole is a standardized wiki markup, supported by many wiki implementations. (Konstanin Haase) @@ -82,6 +101,11 @@ * Conditional requests on `etag` helper now work properly for unsafe HTTP methods. (Matthew Schinckel, Konstantin Haase) + * The `last_modified` helper does not stop execution and change the status code + if the status code is something different than 200. (Konstantin Haase) + + * Added support for If-Unmodified-Since header. (Konstantin Haase) + * `Sinatra::Base.run!` now prints to stderr rather than stdout. (Andrew Armenia) @@ -120,7 +144,14 @@ * Fix handling of broken query params when displaying exceptions. (Luke Jahnke) -= 1.2.7 (backports release) / Not Yet Released += 1.2.8 / Not Yet Released + +Backported from 1.3.2: + +* Fix bug where rendering a second template in the same request after the + first one raised an exception skipped the default layout (Nathan Baum) + += 1.2.7 (backports release) / 2011-09-30 Custom changes: diff --git a/Gemfile b/Gemfile index 37d30ab0..e1e2ba5f 100644 --- a/Gemfile +++ b/Gemfile @@ -19,9 +19,14 @@ gem 'ci_reporter', :group => :ci github = "git://github.com/%s.git" repos = { 'tilt' => github % "rtomayko/tilt", 'rack' => github % "rack/rack" } %w[tilt rack].each do |lib| - dep = (ENV[lib] || 'stable').sub "#{lib}-", '' - dep = nil if dep == 'stable' - dep = {:git => repos[lib], :branch => dep} if dep and dep !~ /(\d+\.)+\d+/ + dep = case ENV[lib] || 'stable' + when 'stable' + nil + when /(\d+\.)+\d+/ + "~> " + ENV[lib].sub("#{lib}-", '') + else + {:git => repos[lib], :branch => dep} + end gem lib, dep end @@ -29,17 +34,10 @@ gem 'haml', '>= 3.0' gem 'sass' gem 'builder' gem 'erubis' -gem 'less', '~> 1.0' - -if RUBY_ENGINE == "maglev" - gem 'liquid', :git => "https://github.com/Shopify/liquid.git" -else - gem 'liquid' -end - +gem 'liquid' gem 'slim', '~> 1.0' gem 'temple', '!= 0.3.3' -gem 'RedCloth' if RUBY_VERSION < "1.9.3" and not RUBY_ENGINE.start_with? 'ma' +gem 'RedCloth' if RUBY_VERSION < "1.9.3" and not RUBY_ENGINE == "macruby" gem 'coffee-script', '>= 2.0' gem 'rdoc' gem 'kramdown' @@ -49,10 +47,16 @@ gem 'creole' if RUBY_ENGINE == 'jruby' gem 'nokogiri', '!= 1.5.0' gem 'jruby-openssl' -elsif RUBY_ENGINE != 'maglev' +else gem 'nokogiri' end +if RUBY_ENGINE == "ruby" + gem 'less', '~> 2.0' +else + gem 'less', '~> 1.0' +end + unless RUBY_ENGINE == 'jruby' && JRUBY_VERSION < "1.6.1" && !ENV['TRAVIS'] # C extensions gem 'rdiscount' @@ -62,16 +66,10 @@ unless RUBY_ENGINE == 'jruby' && JRUBY_VERSION < "1.6.1" && !ENV['TRAVIS'] #gem 'bluecloth' end -if RUBY_ENGINE == 'maglev' - gem 'json', :git => "https://github.com/MagLev/json.git" +platforms :ruby_18, :jruby do + gem 'json' gem 'markaby' gem 'radius' -else - platforms :ruby_18, :jruby do - gem 'json' - gem 'markaby' - gem 'radius' - end end platforms :mri_18 do diff --git a/README.de.rdoc b/README.de.rdoc index 6b62f582..a1a78a09 100644 --- a/README.de.rdoc +++ b/README.de.rdoc @@ -1953,9 +1953,9 @@ SemVer und SemVerTag. * {Mailing-Liste}[http://groups.google.com/group/sinatrarb] * {IRC: #sinatra}[irc://chat.freenode.net/#sinatra] auf http://freenode.net * {Sinatra Book}[http://sinatra-book.gittr.com] Kochbuch Tutorial -* {Sinatra Book Contrib}[http://sinatra-book-contrib.com/] Sinatra-Rezepte aus +* {Sinatra Recipes}[http://recipes.sinatrarb.com/] Sinatra-Rezepte aus der Community * API Dokumentation für die {aktuelle Version}[http://rubydoc.info/gems/sinatra] oder für {HEAD}[http://rubydoc.info/github/sinatra/sinatra] auf http://rubydoc.info -* {CI Server}[http://ci.rkh.im/view/Sinatra/] \ No newline at end of file +* {CI Server}[http://ci.rkh.im/view/Sinatra/] diff --git a/README.es.rdoc b/README.es.rdoc index 5fc21915..292783c9 100644 --- a/README.es.rdoc +++ b/README.es.rdoc @@ -916,11 +916,16 @@ para Sinatra::Application. Si heredaste de Sinatra::Base, probablemente quieras habilitarlo manualmente: class MiApp < Sinatra::Base - configure(:production, :development) do + configure :production, :development do enable :logging end end +Para evitar que se inicialice cualquier middleware de logging, configurá ++logging+ a +nil+. Tené en cuenta que, cuando hagas esto, +logger+ va a +devolver +nil+. Un caso común es cuando querés usar tu propio logger. Sinatra +va a usar lo que encuentre en env['rack.logger']. + === Tipos Mime Cuando usás send_file o archivos estáticos tal vez tengas tipos mime @@ -1055,6 +1060,23 @@ Usá la configuración :static_cache_control para agregar el encabezado Cache-Control a archivos estáticos (ver la sección de configuración para más detalles). +De acuerdo con la RFC 2616 tu aplicación debería comportarse diferente si a las +cabeceras If-Match o If-None-Match se le asigna el valor * cuando el +recurso solicitado ya existe. Sinatra asume para peticiones seguras (como get) +e idempotentes (como put) que el recurso existe, mientras que para el resto +(como post), que no. Podes cambiar este comportamiento con la opción +:new_resource: + + get '/crear' do + etag '', :new_resource => true + Articulo.create + erb :nuevo_articulo + end + +Si querés seguir usando una weak ETag, indicalo con la opción :kind: + + etag '', :new_resource => true, :kind => :weak + === Enviando Archivos Para enviar archivos, podés usar el método send_file: @@ -1283,7 +1305,7 @@ Podés acceder a estas opciones utilizando el método settings: ... end -==== Configurando la Protección de Ataques +=== Configurando la Protección de Ataques Sinatra usa {Rack::Protection}[https://github.com/rkh/rack-protection#readme] para defender a tu aplicación de los ataques más comunes. Tenés que tener en @@ -1987,7 +2009,7 @@ siguiendo las especificaciones SemVer y SemVerTag. * {Lista de Correo}[http://groups.google.com/group/sinatrarb/topics] * {IRC: #sinatra}[irc://chat.freenode.net/#sinatra] en http://freenode.net * {Sinatra Book}[http://sinatra-book.gittr.com] Tutorial (en inglés). -* {Sinatra Book Contrib}[http://sinatra-book-contrib.com/] Recetas contribuidas +* {Sinatra Recipes}[http://recipes.sinatrarb.com/] Recetas contribuidas por la comunidad (en inglés). * Documentación de la API para la {última versión liberada}[http://rubydoc.info/gems/sinatra] o para la diff --git a/README.fr.rdoc b/README.fr.rdoc index 112ed1fa..fc4fbd5f 100644 --- a/README.fr.rdoc +++ b/README.fr.rdoc @@ -1988,9 +1988,9 @@ SemVer que SemVerTag. * {IRC : #sinatra}[irc://chat.freenode.net/#sinatra] sur http://freenode.net * {IRC : #sinatra}[irc://chat.freenode.net/#sinatra] on http://freenode.net * {Sinatra Book}[http://sinatra-book.gittr.com] Tutoriels et recettes -* {Sinatra Book Contrib}[http://sinatra-book-contrib.com/] Recettes contribuées +* {Sinatra Recipes}[http://recipes.sinatrarb.com/] Recettes contribuées par la communauté * Documentation API de la {dernière version}[http://rubydoc.info/gems/sinatra] ou du {HEAD courant}[http://rubydoc.info/github/sinatra/sinatra] sur http://rubydoc.info -* {CI server}[http://ci.rkh.im/view/Sinatra/] \ No newline at end of file +* {CI server}[http://ci.rkh.im/view/Sinatra/] diff --git a/README.jp.rdoc b/README.jp.rdoc index 1562aa41..1765bb97 100644 --- a/README.jp.rdoc +++ b/README.jp.rdoc @@ -376,6 +376,7 @@ textileからメソッドを呼び出すことも、localsに変数を渡すこ RDocテンプレートを使うにはRDocライブラリが必要です: # rdoc/markup/to_htmlを読み込みます + require "rdoc" require "rdoc/markup/to_html" get '/' do diff --git a/README.rdoc b/README.rdoc index bbbfe927..ae51bd50 100644 --- a/README.rdoc +++ b/README.rdoc @@ -885,11 +885,16 @@ default, so if you inherit from Sinatra::Base, you probably want to enable it yourself: class MyApp < Sinatra::Base - configure(:production, :development) do + configure :production, :development do enable :logging end end +To avoid any logging middleware to be set up, set the +logging+ setting to ++nil+. However, keep in mind that +logger+ will in that case return +nil+. A +common use case is when you want to set your own logger. Sinatra will use +whatever it will find in env['rack.logger']. + === Mime Types When using send_file or static files you may have mime types Sinatra @@ -1018,6 +1023,23 @@ try {rack-cache}[http://rtomayko.github.com/rack-cache/]: Use the :static_cache_control setting (see below) to add Cache-Control header info to static files. +According to RFC 2616 your application should behave differently if the If-Match +or If-None-Match header is set to * depending on whether the resource +requested is already in existence. Sinatra assumes resources for safe (like get) +and idempotent (like put) requests are already in existence, whereas other +resources (for instance for post requests), are treated as new resources. You +can change this behavior by passing in a :new_resource option: + + get '/create' do + etag '', :new_resource => true + Article.create + erb :new_article + end + +If you still want to use a weak ETag, pass in a :kind option: + + etag '', :new_resource => true, :kind => :weak + === Sending Files For sending files, you can use the send_file helper method: @@ -1925,7 +1947,7 @@ SemVerTag. * {Mailing List}[http://groups.google.com/group/sinatrarb/topics] * {IRC: #sinatra}[irc://chat.freenode.net/#sinatra] on http://freenode.net * {Sinatra Book}[http://sinatra-book.gittr.com] Cookbook Tutorial -* {Sinatra Book Contrib}[http://sinatra-book-contrib.com/] Community +* {Sinatra Recipes}[http://recipes.sinatrarb.com/] Community contributed recipes * API documentation for the {latest release}[http://rubydoc.info/gems/sinatra] or the {current HEAD}[http://rubydoc.info/github/sinatra/sinatra] on diff --git a/README.ru.rdoc b/README.ru.rdoc index 63def1e7..bffba5e9 100644 --- a/README.ru.rdoc +++ b/README.ru.rdoc @@ -1779,7 +1779,7 @@ SemVerTag. * {Группы рассылки}[http://groups.google.com/group/sinatrarb/topics] * {IRC: #sinatra}[irc://chat.freenode.net/#sinatra] на http://freenode.net * {Sinatra Book}[http://sinatra-book.gittr.com] учебник и сборник рецептов -* {Sinatra Book Contrib}[http://sinatra-book-contrib.com/] сборник рецептов +* {Sinatra Recipes}[http://recipes.sinatrarb.com/] сборник рецептов * API документация к {последнему релизу}[http://rubydoc.info/gems/sinatra] или {текущему HEAD}[http://rubydoc.info/github/sinatra/sinatra] на http://rubydoc.info diff --git a/README.zh.rdoc b/README.zh.rdoc index 64b562ec..4b6fc373 100644 --- a/README.zh.rdoc +++ b/README.zh.rdoc @@ -492,6 +492,7 @@ Rack body对象或者HTTP状态码: 需要引入 RDoc gem/library 以渲染RDoc模板: # 需要在你的应用中引入rdoc/markup/to_html + require "rdoc" require "rdoc/markup/to_html" get '/' do diff --git a/Rakefile b/Rakefile index a315079f..d7e0d8a9 100644 --- a/Rakefile +++ b/Rakefile @@ -164,6 +164,10 @@ if defined?(Gem) end task 'release' => ['test', package('.gem')] do + if File.read("CHANGES") =~ /= \d\.\d\.\d . not yet released$/i + fail 'please update changes first' + end + sh <<-SH gem install #{package('.gem')} --local && gem push #{package('.gem')} && diff --git a/lib/sinatra/base.rb b/lib/sinatra/base.rb index df51d3d3..81fbfd57 100644 --- a/lib/sinatra/base.rb +++ b/lib/sinatra/base.rb @@ -51,7 +51,7 @@ module Sinatra private def accept_entry(entry) - type, *options = entry.gsub(/\s/, '').split(';') + type, *options = entry.delete(' ').split(';') quality = 0 # we sort smalles first options.delete_if { |e| quality = 1 - e[2..-1].to_f if e.start_with? 'q=' } [type, [quality, type.count('*'), 1 - options.size]] @@ -247,11 +247,14 @@ module Sinatra def self.defer(*) yield end def initialize(scheduler = self.class, keep_open = false, &back) - @back, @scheduler, @callback, @keep_open = back.to_proc, scheduler, nil, keep_open + @back, @scheduler, @keep_open = back.to_proc, scheduler, keep_open + @callbacks, @closed = [], false end def close - @scheduler.schedule { @callback.call if @callback } + return if @closed + @closed = true + @scheduler.schedule { @callbacks.each { |c| c.call }} end def each(&front) @@ -272,7 +275,7 @@ module Sinatra end def callback(&block) - @callback = block + @callbacks << block end alias errback callback @@ -308,7 +311,7 @@ module Sinatra hash = {} end - values = values.map { |value| value.to_s.tr('_','-') } + values.map! { |value| value.to_s.tr('_','-') } hash.each do |key, value| key = key.to_s.tr('_', '-') value = value.to_i if key == "max-age" @@ -355,8 +358,19 @@ module Sinatra return unless time time = time_for time response['Last-Modified'] = time.httpdate - # compare based on seconds since epoch - halt 304 if Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i >= time.to_i + return if env['HTTP_IF_NONE_MATCH'] + + if status == 200 and env['HTTP_IF_MODIFIED_SINCE'] + # compare based on seconds since epoch + since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i + halt 304 if since >= time.to_i + end + + if (success? or status == 412) and env['HTTP_IF_UNMODIFIED_SINCE'] + # compare based on seconds since epoch + since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i + halt 412 if since < time.to_i + end rescue ArgumentError end @@ -369,18 +383,27 @@ module Sinatra # When the current request includes an 'If-None-Match' header with a # matching etag, execution is immediately halted. If the request method is # GET or HEAD, a '304 Not Modified' response is sent. - def etag(value, kind = :strong) - raise ArgumentError, ":strong or :weak expected" unless [:strong,:weak].include?(kind) + def etag(value, options = {}) + # Before touching this code, please double check RFC 2616 14.24 and 14.26. + options = {:kind => options} unless Hash === options + kind = options[:kind] || :strong + new_resource = options.fetch(:new_resource) { request.post? } + + unless [:strong, :weak].include?(kind) + raise ArgumentError, ":strong or :weak expected" + end + value = '"%s"' % value value = 'W/' + value if kind == :weak response['ETag'] = value - if etags = env['HTTP_IF_NONE_MATCH'] - etags = etags.split(/\s*,\s*/) - if etags.include?(value) or etags.include?('*') - halt 304 if request.safe? - else - halt 412 unless request.safe? + if success? or status == 304 + if etag_matches? env['HTTP_IF_NONE_MATCH'], new_resource + halt(request.safe? ? 304 : 412) + end + + if env['HTTP_IF_MATCH'] + halt 412 unless etag_matches? env['HTTP_IF_MATCH'], new_resource end end end @@ -445,6 +468,14 @@ module Sinatra rescue Exception raise ArgumentError, "unable to convert #{value.inspect} to a Time object" end + + private + + # Helper method checking if a ETag value list includes the current ETag. + def etag_matches?(list, new_resource = request.post?) + return !new_resource if list == '*' + list.to_s.split(/\s*,\s*/).include? response['ETag'] + end end private @@ -588,11 +619,14 @@ module Sinatra scope = options.delete(:scope) || self # compile and render template - layout_was = @default_layout - @default_layout = false - template = compile_template(engine, data, options, views) - output = template.render(scope, locals, &block) - @default_layout = layout_was + begin + layout_was = @default_layout + @default_layout = false + template = compile_template(engine, data, options, views) + output = template.render(scope, locals, &block) + ensure + @default_layout = layout_was + end # render layout if layout @@ -817,7 +851,7 @@ module Sinatra return unless path.start_with?(public_dir) and File.file?(path) env['sinatra.static_file'] = path - cache_control *settings.static_cache_control if settings.static_cache_control? + cache_control(*settings.static_cache_control) if settings.static_cache_control? send_file path, :disposition => nil end @@ -1184,9 +1218,8 @@ module Sinatra def compile(path) keys = [] if path.respond_to? :to_str - special_chars = %w{. + ( ) $} pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c) } - pattern.gsub! /((:\w+)|\*)/ do |match| + pattern.gsub!(/((:\w+)|\*)/) do |match| if match == "*" keys << 'splat' "(.*?)" @@ -1320,21 +1353,34 @@ module Sinatra def setup_logging(builder) if logging? - builder.use Rack::CommonLogger - if logging.respond_to? :to_int - builder.use Rack::Logger, logging - else - builder.use Rack::Logger - end + setup_common_logger(builder) + setup_custom_logger(builder) + elsif logging == false + setup_null_logger(builder) + end + end + + def setup_null_logger(builder) + builder.use Rack::NullLogger + end + + def setup_common_logger(builder) + return if ["development", "deployment", nil].include? ENV["RACK_ENV"] + builder.use Rack::CommonLogger + end + + def setup_custom_logger(builder) + if logging.respond_to? :to_int + builder.use Rack::Logger, logging else - builder.use Rack::NullLogger + builder.use Rack::Logger end end def setup_protection(builder) return unless protection? options = Hash === protection ? protection.dup : {} - options[:except] = Array options[:except] + options[:except] = Array(options[:except] || :escaped_params) options[:except] += [:session_hijacking, :remote_token] unless sessions? builder.use Rack::Protection, options end diff --git a/lib/sinatra/main.rb b/lib/sinatra/main.rb index 3f644aaa..fc6ac70d 100644 --- a/lib/sinatra/main.rb +++ b/lib/sinatra/main.rb @@ -8,7 +8,7 @@ module Sinatra # on this path by default. set :app_file, caller_files.first || $0 - set :run, Proc.new { $0 == app_file } + set :run, Proc.new { File.expand_path($0) == File.expand_path(app_file) } if run? && ARGV.any? require 'optparse' diff --git a/lib/sinatra/version.rb b/lib/sinatra/version.rb index a5f10d33..f6e4da3b 100644 --- a/lib/sinatra/version.rb +++ b/lib/sinatra/version.rb @@ -1,3 +1,3 @@ module Sinatra - VERSION = '1.3.0' + VERSION = '1.3.1' end diff --git a/sinatra.gemspec b/sinatra.gemspec index 9548a6bc..1b3bccd5 100644 --- a/sinatra.gemspec +++ b/sinatra.gemspec @@ -7,12 +7,12 @@ Gem::Specification.new 'sinatra', Sinatra::VERSION do |s| s.authors = ["Blake Mizerany", "Ryan Tomayko", "Simon Rozet", "Konstantin Haase"] s.email = "sinatrarb@googlegroups.com" s.homepage = "http://www.sinatrarb.com/" - s.files = `git ls-files`.split("\n") + s.files = `git ls-files`.split("\n") - %w[.gitignore .travis.yml] s.test_files = s.files.select { |p| p =~ /^test\/.*_test.rb/ } s.extra_rdoc_files = s.files.select { |p| p =~ /^README/ } << 'LICENSE' s.rdoc_options = %w[--line-numbers --inline-source --title Sinatra --main README.rdoc] - s.add_dependency 'rack', '~> 1.3' - s.add_dependency 'rack-protection', '~> 1.1' - s.add_dependency 'tilt', '~> 1.3' + s.add_dependency 'rack', '~> 1.3', '>= 1.3.4' + s.add_dependency 'rack-protection', '~> 1.1', '>= 1.1.2' + s.add_dependency 'tilt', '~> 1.3', '>= 1.3.3' end diff --git a/test/filter_test.rb b/test/filter_test.rb index 031af9ef..57f4d01c 100644 --- a/test/filter_test.rb +++ b/test/filter_test.rb @@ -97,6 +97,17 @@ class BeforeFilterTest < Test::Unit::TestCase assert_equal 'cool', body end + it "properly unescapes parameters" do + mock_app { + before { @foo = params['foo'] } + get('/foo') { @foo } + } + + get '/foo?foo=bar%3Abaz%2Fbend' + assert ok? + assert_equal 'bar:baz/bend', body + end + it "runs filters defined in superclasses" do base = Class.new(Sinatra::Base) base.before { @foo = 'hello from superclass' } diff --git a/test/helper.rb b/test/helper.rb index 972b416b..dc8fd405 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -69,7 +69,15 @@ class Test::Unit::TestCase end def assert_body(value) - assert_equal value.lstrip.gsub(/\s*\n\s*/, ""), body.lstrip.gsub(/\s*\n\s*/, "") + if value.respond_to? :to_str + assert_equal value.lstrip.gsub(/\s*\n\s*/, ""), body.lstrip.gsub(/\s*\n\s*/, "") + else + assert_match value, body + end + end + + def assert_status(expected) + assert_equal Integer(expected), Integer(status) end def assert_like(a,b) diff --git a/test/helpers_test.rb b/test/helpers_test.rb index ca6d1c82..42d1f9a1 100644 --- a/test/helpers_test.rb +++ b/test/helpers_test.rb @@ -858,6 +858,20 @@ class HelpersTest < Test::Unit::TestCase assert ! response['Last-Modified'] end + it 'does not change a status other than 200' do + mock_app do + get '/' do + status 299 + last_modified Time.at(0) + 'ok' + end + end + + get('/', {}, 'HTTP_IF_MODIFIED_SINCE' => 'Sun, 26 Sep 2030 23:43:52 GMT') + assert_status 299 + assert_body 'ok' + end + [Time.now, DateTime.now, Date.today, Time.now.to_i, Struct.new(:to_time).new(Time.now) ].each do |last_modified_time| describe "with #{last_modified_time.class.name}" do @@ -955,74 +969,641 @@ class HelpersTest < Test::Unit::TestCase assert_equal '', body end end + + context "If-Unmodified-Since" do + it 'results in 200 if resource has not been modified' do + get '/', {}, { 'HTTP_IF_UNMODIFIED_SINCE' => 'Sun, 26 Sep 2030 23:43:52 GMT' } + assert_equal 200, status + assert_equal 'Boo!', body + end + + it 'results in 412 if resource has been modified' do + get '/', {}, { 'HTTP_IF_UNMODIFIED_SINCE' => Time.at(0).httpdate } + assert_equal 412, status + assert_equal '', body + end + end end end end describe 'etag' do - setup do - mock_app { - get '/' do - body { 'Hello World' } - etag 'FOO' - 'Boo!' + context "safe requests" do + it 'returns 200 for normal requests' do + mock_app do + get '/' do + etag 'foo' + 'ok' + end end - post '/' do - etag 'FOO' - 'Matches!' + get('/') + assert_status 200 + assert_body 'ok' + end + + context "If-None-Match" do + it 'returns 304 when If-None-Match is *' do + mock_app do + get '/' do + etag 'foo' + 'ok' + end + end + + get('/', {}, 'HTTP_IF_NONE_MATCH' => '*') + assert_status 304 + assert_body '' end - } + + it 'returns 200 when If-None-Match is * for new resources' do + mock_app do + get '/' do + etag 'foo', :new_resource => true + 'ok' + end + end + + get('/', {}, 'HTTP_IF_NONE_MATCH' => '*') + assert_status 200 + assert_body 'ok' + end + + it 'returns 304 when If-None-Match is * for existing resources' do + mock_app do + get '/' do + etag 'foo', :new_resource => false + 'ok' + end + end + + get('/', {}, 'HTTP_IF_NONE_MATCH' => '*') + assert_status 304 + assert_body '' + end + + it 'returns 304 when If-None-Match is the etag' do + mock_app do + get '/' do + etag 'foo' + 'ok' + end + end + + get('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"') + assert_status 304 + assert_body '' + end + + it 'returns 304 when If-None-Match includes the etag' do + mock_app do + get '/' do + etag 'foo' + 'ok' + end + end + + get('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar", "foo"') + assert_status 304 + assert_body '' + end + + it 'returns 200 when If-None-Match does not include the etag' do + mock_app do + get '/' do + etag 'foo' + 'ok' + end + end + + get('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"') + assert_status 200 + assert_body 'ok' + end + + it 'ignores If-Modified-Since if If-None-Match does not match' do + mock_app do + get '/' do + etag 'foo' + last_modified Time.at(0) + 'ok' + end + end + + get('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"') + assert_status 200 + assert_body 'ok' + end + + it 'does not change a status code other than 2xx or 304' do + mock_app do + get '/' do + status 499 + etag 'foo' + 'ok' + end + end + + get('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"') + assert_status 499 + assert_body 'ok' + end + + it 'does change 2xx status codes' do + mock_app do + get '/' do + status 299 + etag 'foo' + 'ok' + end + end + + get('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"') + assert_status 304 + assert_body '' + end + + it 'does not send a body on 304 status codes' do + mock_app do + get '/' do + status 304 + etag 'foo' + 'ok' + end + end + + get('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"') + assert_status 304 + assert_body '' + end + end + + context "If-Match" do + it 'returns 200 when If-Match is the etag' do + mock_app do + get '/' do + etag 'foo' + 'ok' + end + end + + get('/', {}, 'HTTP_IF_MATCH' => '"foo"') + assert_status 200 + assert_body 'ok' + end + + it 'returns 200 when If-Match includes the etag' do + mock_app do + get '/' do + etag 'foo' + 'ok' + end + end + + get('/', {}, 'HTTP_IF_MATCH' => '"foo", "bar"') + assert_status 200 + assert_body 'ok' + end + + it 'returns 200 when If-Match is *' do + mock_app do + get '/' do + etag 'foo' + 'ok' + end + end + + get('/', {}, 'HTTP_IF_MATCH' => '*') + assert_status 200 + assert_body 'ok' + end + + it 'returns 412 when If-Match is * for new resources' do + mock_app do + get '/' do + etag 'foo', :new_resource => true + 'ok' + end + end + + get('/', {}, 'HTTP_IF_MATCH' => '*') + assert_status 412 + assert_body '' + end + + it 'returns 200 when If-Match is * for existing resources' do + mock_app do + get '/' do + etag 'foo', :new_resource => false + 'ok' + end + end + + get('/', {}, 'HTTP_IF_MATCH' => '*') + assert_status 200 + assert_body 'ok' + end + + it 'returns 412 when If-Match does not include the etag' do + mock_app do + get '/' do + etag 'foo' + 'ok' + end + end + + get('/', {}, 'HTTP_IF_MATCH' => '"bar"') + assert_status 412 + assert_body '' + end + end end - it 'sets the ETag header' do - get '/' - assert_equal '"FOO"', response['ETag'] + context "idempotent requests" do + it 'returns 200 for normal requests' do + mock_app do + put '/' do + etag 'foo' + 'ok' + end + end + + put('/') + assert_status 200 + assert_body 'ok' + end + + context "If-None-Match" do + it 'returns 412 when If-None-Match is *' do + mock_app do + put '/' do + etag 'foo' + 'ok' + end + end + + put('/', {}, 'HTTP_IF_NONE_MATCH' => '*') + assert_status 412 + assert_body '' + end + + it 'returns 200 when If-None-Match is * for new resources' do + mock_app do + put '/' do + etag 'foo', :new_resource => true + 'ok' + end + end + + put('/', {}, 'HTTP_IF_NONE_MATCH' => '*') + assert_status 200 + assert_body 'ok' + end + + it 'returns 412 when If-None-Match is * for existing resources' do + mock_app do + put '/' do + etag 'foo', :new_resource => false + 'ok' + end + end + + put('/', {}, 'HTTP_IF_NONE_MATCH' => '*') + assert_status 412 + assert_body '' + end + + it 'returns 412 when If-None-Match is the etag' do + mock_app do + put '/' do + etag 'foo' + 'ok' + end + end + + put('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"') + assert_status 412 + assert_body '' + end + + it 'returns 412 when If-None-Match includes the etag' do + mock_app do + put '/' do + etag 'foo' + 'ok' + end + end + + put('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar", "foo"') + assert_status 412 + assert_body '' + end + + it 'returns 200 when If-None-Match does not include the etag' do + mock_app do + put '/' do + etag 'foo' + 'ok' + end + end + + put('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"') + assert_status 200 + assert_body 'ok' + end + + it 'ignores If-Modified-Since if If-None-Match does not match' do + mock_app do + put '/' do + etag 'foo' + last_modified Time.at(0) + 'ok' + end + end + + put('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"') + assert_status 200 + assert_body 'ok' + end + end + + context "If-Match" do + it 'returns 200 when If-Match is the etag' do + mock_app do + put '/' do + etag 'foo' + 'ok' + end + end + + put('/', {}, 'HTTP_IF_MATCH' => '"foo"') + assert_status 200 + assert_body 'ok' + end + + it 'returns 200 when If-Match includes the etag' do + mock_app do + put '/' do + etag 'foo' + 'ok' + end + end + + put('/', {}, 'HTTP_IF_MATCH' => '"foo", "bar"') + assert_status 200 + assert_body 'ok' + end + + it 'returns 200 when If-Match is *' do + mock_app do + put '/' do + etag 'foo' + 'ok' + end + end + + put('/', {}, 'HTTP_IF_MATCH' => '*') + assert_status 200 + assert_body 'ok' + end + + it 'returns 412 when If-Match is * for new resources' do + mock_app do + put '/' do + etag 'foo', :new_resource => true + 'ok' + end + end + + put('/', {}, 'HTTP_IF_MATCH' => '*') + assert_status 412 + assert_body '' + end + + it 'returns 200 when If-Match is * for existing resources' do + mock_app do + put '/' do + etag 'foo', :new_resource => false + 'ok' + end + end + + put('/', {}, 'HTTP_IF_MATCH' => '*') + assert_status 200 + assert_body 'ok' + end + + it 'returns 412 when If-Match does not include the etag' do + mock_app do + put '/' do + etag 'foo' + 'ok' + end + end + + put('/', {}, 'HTTP_IF_MATCH' => '"bar"') + assert_status 412 + assert_body '' + end + end end - it 'returns a body when conditional get misses' do - get '/' - assert_equal 200, status - assert_equal 'Boo!', body - end + context "post requests" do + it 'returns 200 for normal requests' do + mock_app do + post '/' do + etag 'foo' + 'ok' + end + end - it 'returns a body when posting with no If-None-Match header' do - post '/' - assert_equal 200, status - assert_equal 'Matches!', body - end + post('/') + assert_status 200 + assert_body 'ok' + end - it 'returns a body when conditional post matches' do - post '/', {}, { 'HTTP_IF_NONE_MATCH' => '"FOO"' } - assert_equal 200, status - assert_equal 'Matches!', body - end + context "If-None-Match" do + it 'returns 200 when If-None-Match is *' do + mock_app do + post '/' do + etag 'foo' + 'ok' + end + end - it 'halts with 412 when conditional post misses' do - post '/', {}, { 'HTTP_IF_NONE_MATCH' => '"BAR"' } - assert_equal 412, status - assert_equal '', body - end + post('/', {}, 'HTTP_IF_NONE_MATCH' => '*') + assert_status 200 + assert_body 'ok' + end - it 'halts when a conditional GET matches' do - get '/', {}, { 'HTTP_IF_NONE_MATCH' => '"FOO"' } - assert_equal 304, status - assert_equal '', body - end + it 'returns 200 when If-None-Match is * for new resources' do + mock_app do + post '/' do + etag 'foo', :new_resource => true + 'ok' + end + end - it 'should handle multiple ETag values in If-None-Match header' do - get '/', {}, { 'HTTP_IF_NONE_MATCH' => '"BAR", *' } - assert_equal 304, status - assert_equal '', body + post('/', {}, 'HTTP_IF_NONE_MATCH' => '*') + assert_status 200 + assert_body 'ok' + end + + it 'returns 412 when If-None-Match is * for existing resources' do + mock_app do + post '/' do + etag 'foo', :new_resource => false + 'ok' + end + end + + post('/', {}, 'HTTP_IF_NONE_MATCH' => '*') + assert_status 412 + assert_body '' + end + + it 'returns 412 when If-None-Match is the etag' do + mock_app do + post '/' do + etag 'foo' + 'ok' + end + end + + post('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"') + assert_status 412 + assert_body '' + end + + it 'returns 412 when If-None-Match includes the etag' do + mock_app do + post '/' do + etag 'foo' + 'ok' + end + end + + post('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar", "foo"') + assert_status 412 + assert_body '' + end + + it 'returns 200 when If-None-Match does not include the etag' do + mock_app do + post '/' do + etag 'foo' + 'ok' + end + end + + post('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"') + assert_status 200 + assert_body 'ok' + end + + it 'ignores If-Modified-Since if If-None-Match does not match' do + mock_app do + post '/' do + etag 'foo' + last_modified Time.at(0) + 'ok' + end + end + + post('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"') + assert_status 200 + assert_body 'ok' + end + end + + context "If-Match" do + it 'returns 200 when If-Match is the etag' do + mock_app do + post '/' do + etag 'foo' + 'ok' + end + end + + post('/', {}, 'HTTP_IF_MATCH' => '"foo"') + assert_status 200 + assert_body 'ok' + end + + it 'returns 200 when If-Match includes the etag' do + mock_app do + post '/' do + etag 'foo' + 'ok' + end + end + + post('/', {}, 'HTTP_IF_MATCH' => '"foo", "bar"') + assert_status 200 + assert_body 'ok' + end + + it 'returns 412 when If-Match is *' do + mock_app do + post '/' do + etag 'foo' + 'ok' + end + end + + post('/', {}, 'HTTP_IF_MATCH' => '*') + assert_status 412 + assert_body '' + end + + it 'returns 412 when If-Match is * for new resources' do + mock_app do + post '/' do + etag 'foo', :new_resource => true + 'ok' + end + end + + post('/', {}, 'HTTP_IF_MATCH' => '*') + assert_status 412 + assert_body '' + end + + it 'returns 200 when If-Match is * for existing resources' do + mock_app do + post '/' do + etag 'foo', :new_resource => false + 'ok' + end + end + + post('/', {}, 'HTTP_IF_MATCH' => '*') + assert_status 200 + assert_body 'ok' + end + + it 'returns 412 when If-Match does not include the etag' do + mock_app do + post '/' do + etag 'foo' + 'ok' + end + end + + post('/', {}, 'HTTP_IF_MATCH' => '"bar"') + assert_status 412 + assert_body '' + end + end end it 'uses a weak etag with the :weak option' do - mock_app { + mock_app do get '/' do etag 'FOO', :weak "that's weak, dude." end - } + end get '/' assert_equal 'W/"FOO"', response['ETag'] end @@ -1130,6 +1711,16 @@ class HelpersTest < Test::Unit::TestCase assert !io.string.include?("INFO -- : Program started") assert !io.string.include?("WARN -- : Nothing to do") end + + it 'does not create a logger when logging is set to nil' do + mock_app do + set :logging, nil + get('/') { logger.inspect } + end + + get '/' + assert_body 'nil' + end end module ::HelperOne; def one; '1'; end; end diff --git a/test/less_test.rb b/test/less_test.rb index e79b8359..979f201c 100644 --- a/test/less_test.rb +++ b/test/less_test.rb @@ -16,7 +16,7 @@ class LessTest < Test::Unit::TestCase it 'renders inline Less strings' do less_app { less "@white_color: #fff; #main { background-color: @white_color }" } assert ok? - assert_equal "#main { background-color: #ffffff; }\n", body + assert_equal "#main{background-color:#ffffff;}", body.gsub(/\s/, "") end it 'defaults content type to css' do @@ -45,13 +45,13 @@ class LessTest < Test::Unit::TestCase it 'renders .less files in views path' do less_app { less :hello } assert ok? - assert_equal "#main { background-color: #ffffff; }\n", body + assert_equal "#main{background-color:#ffffff;}", body.gsub(/\s/, "") end it 'ignores the layout option' do less_app { less :hello, :layout => :layout2 } assert ok? - assert_equal "#main { background-color: #ffffff; }\n", body + assert_equal "#main{background-color:#ffffff;}", body.gsub(/\s/, "") end it "raises error if template not found" do diff --git a/test/rdoc_test.rb b/test/rdoc_test.rb index ff547bc4..0d588563 100644 --- a/test/rdoc_test.rb +++ b/test/rdoc_test.rb @@ -1,6 +1,7 @@ require File.expand_path('../helper', __FILE__) begin +require 'rdoc' require 'rdoc/markup/to_html' class RdocTest < Test::Unit::TestCase @@ -15,13 +16,13 @@ class RdocTest < Test::Unit::TestCase it 'renders inline rdoc strings' do rdoc_app { rdoc '= Hiya' } assert ok? - assert_body "

Hiya

" + assert_body /]*>Hiya<\/h1>/ end it 'renders .rdoc files in views path' do rdoc_app { rdoc :hello } assert ok? - assert_body "

Hello From RDoc

" + assert_body /]*>Hello From RDoc<\/h1>/ end it "raises error if template not found" do diff --git a/test/routing_test.rb b/test/routing_test.rb index b637f7ac..e3f4d0be 100644 --- a/test/routing_test.rb +++ b/test/routing_test.rb @@ -114,6 +114,8 @@ class RoutingTest < Test::Unit::TestCase it 'matches empty PATH_INFO to "" if a route is defined for ""' do mock_app do + disable :protection + get '/' do 'did not work' end diff --git a/test/streaming_test.rb b/test/streaming_test.rb index f3dc91d4..9448d358 100644 --- a/test/streaming_test.rb +++ b/test/streaming_test.rb @@ -56,6 +56,16 @@ class StreamingTest < Test::Unit::TestCase assert_equal 0, final end + it 'allows adding more than one callback' do + a = b = false + stream = Stream.new { } + stream.callback { a = true } + stream.callback { b = true } + stream.each { |str| } + assert a, 'should trigger first callback' + assert b, 'should trigger second callback' + end + class MockScheduler def initialize(*) @schedule, @defer = [], [] end def schedule(&block) @schedule << block end @@ -97,4 +107,9 @@ class StreamingTest < Test::Unit::TestCase scheduler.defer! assert_raise(RuntimeError) { scheduler.schedule! } end + + it 'does not trigger an infinite loop if you call close in a callback' do + stream = Stream.new { |out| out.callback { out.close }} + stream.each { |str| } + end end