Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge remote-tracking branch 'upstream/master'

  • Loading branch information...
commit cf27ad4196a230b9babf14f0b819ee351aba6f7b 2 parents 393640e + a27a95d
@michelc michelc authored
View
4 .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:
View
37 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:
View
40 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
View
4 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/]
+* {CI Server}[http://ci.rkh.im/view/Sinatra/]
View
28 README.es.rdoc
@@ -916,11 +916,16 @@ para <tt>Sinatra::Application</tt>. Si heredaste de
<tt>Sinatra::Base</tt>, 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 <tt>env['rack.logger']</tt>.
+
=== Tipos Mime
Cuando usás <tt>send_file</tt> o archivos estáticos tal vez tengas tipos mime
@@ -1055,6 +1060,23 @@ Usá la configuración <tt>:static_cache_control</tt> para agregar el encabezado
<tt>Cache-Control</tt> 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 <tt>*</tt> 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
+<tt>:new_resource</tt>:
+
+ 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 <tt>:kind</tt>:
+
+ etag '', :new_resource => true, :kind => :weak
+
=== Enviando Archivos
Para enviar archivos, podés usar el método <tt>send_file</tt>:
@@ -1283,7 +1305,7 @@ Podés acceder a estas opciones utilizando el método <tt>settings</tt>:
...
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
View
4 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/]
+* {CI server}[http://ci.rkh.im/view/Sinatra/]
View
1  README.jp.rdoc
@@ -376,6 +376,7 @@ textileからメソッドを呼び出すことも、localsに変数を渡すこ
RDocテンプレートを使うにはRDocライブラリが必要です:
# rdoc/markup/to_htmlを読み込みます
+ require "rdoc"
require "rdoc/markup/to_html"
get '/' do
View
26 README.rdoc
@@ -885,11 +885,16 @@ default, so if you inherit from <tt>Sinatra::Base</tt>, 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 <tt>env['rack.logger']</tt>.
+
=== Mime Types
When using <tt>send_file</tt> or static files you may have mime types Sinatra
@@ -1018,6 +1023,23 @@ try {rack-cache}[http://rtomayko.github.com/rack-cache/]:
Use the <tt>:static_cache_control</tt> setting (see below) to add
<tt>Cache-Control</tt> 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 <tt>*</tt> 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 <tt>:new_resource</tt> 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 <tt>:kind</tt> option:
+
+ etag '', :new_resource => true, :kind => :weak
+
=== Sending Files
For sending files, you can use the <tt>send_file</tt> 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
View
2  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
View
1  README.zh.rdoc
@@ -492,6 +492,7 @@ Rack body对象或者HTTP状态码:
需要引入 <tt>RDoc</tt> gem/library 以渲染RDoc模板:
# 需要在你的应用中引入rdoc/markup/to_html
+ require "rdoc"
require "rdoc/markup/to_html"
get '/' do
View
4 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')} &&
View
108 lib/sinatra/base.rb
@@ -51,7 +51,7 @@ def idempotent?
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 @@ def self.schedule(*) yield end
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 @@ def <<(data)
end
def callback(&block)
- @callback = block
+ @callbacks << block
end
alias errback callback
@@ -308,7 +311,7 @@ def cache_control(*values)
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 @@ def last_modified(time)
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 @@ def last_modified(time)
# 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 @@ def time_for(value)
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 @@ def render(engine, data, options={}, locals={}, &block)
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 @@ def static!
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 @@ def compile!(verb, path, block, options = {})
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 @@ def setup_middleware(builder)
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
View
2  lib/sinatra/main.rb
@@ -8,7 +8,7 @@ class Application < Base
# 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'
View
2  lib/sinatra/version.rb
@@ -1,3 +1,3 @@
module Sinatra
- VERSION = '1.3.0'
+ VERSION = '1.3.1'
end
View
8 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
View
11 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' }
View
10 test/helper.rb
@@ -69,7 +69,15 @@ def body
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)
View
679 test/helpers_test.rb
@@ -858,6 +858,20 @@ def obj.is_a?(thing) 60.is_a?(thing) end
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 @@ def obj.is_a?(thing) 60.is_a?(thing) end
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
- }
- end
- it 'sets the ETag header' do
- get '/'
- assert_equal '"FOO"', response['ETag']
- 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
- it 'returns a body when conditional get misses' do
- get '/'
- assert_equal 200, status
- assert_equal 'Boo!', body
- end
+ get('/', {}, 'HTTP_IF_NONE_MATCH' => '*')
+ assert_status 200
+ assert_body 'ok'
+ end
- it 'returns a body when posting with no If-None-Match header' do
- post '/'
- assert_equal 200, status
- assert_equal 'Matches!', body
- 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
- it 'returns a body when conditional post matches' do
- post '/', {}, { 'HTTP_IF_NONE_MATCH' => '"FOO"' }
- assert_equal 200, status
- assert_equal 'Matches!', body
- end
+ get('/', {}, 'HTTP_IF_NONE_MATCH' => '*')
+ assert_status 304
+ assert_body ''
+ end
- it 'halts with 412 when conditional post misses' do
- post '/', {}, { 'HTTP_IF_NONE_MATCH' => '"BAR"' }
- assert_equal 412, status
- assert_equal '', body
+ 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 'halts when a conditional GET matches' do
- get '/', {}, { 'HTTP_IF_NONE_MATCH' => '"FOO"' }
- assert_equal 304, status
- assert_equal '', body
+ 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 'should handle multiple ETag values in If-None-Match header' do
- get '/', {}, { 'HTTP_IF_NONE_MATCH' => '"BAR", *' }
- assert_equal 304, status
- assert_equal '', body
+ context "post requests" do
+ it 'returns 200 for normal requests' do
+ mock_app do
+ post '/' do
+ etag 'foo'
+ 'ok'
+ end
+ end
+
+ post('/')
+ assert_status 200
+ assert_body 'ok'
+ 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
+
+ post('/', {}, 'HTTP_IF_NONE_MATCH' => '*')
+ assert_status 200
+ assert_body 'ok'
+ 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
+
+ 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 @@ def obj.is_a?(thing) 60.is_a?(thing) end
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
View
6 test/less_test.rb
@@ -16,7 +16,7 @@ def less_app(options = {}, &block)
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 @@ def less_app(options = {}, &block)
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
View
5 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 @@ def rdoc_app(&block)
it 'renders inline rdoc strings' do
rdoc_app { rdoc '= Hiya' }
assert ok?
- assert_body "<h1>Hiya</h1>"
+ assert_body /<h1[^>]*>Hiya<\/h1>/
end
it 'renders .rdoc files in views path' do
rdoc_app { rdoc :hello }
assert ok?
- assert_body "<h1>Hello From RDoc</h1>"
+ assert_body /<h1[^>]*>Hello From RDoc<\/h1>/
end
it "raises error if template not found" do
View
2  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
View
15 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 @@ def defer!(*) @defer.pop.call until @defer.empty? end
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
Please sign in to comment.
Something went wrong with that request. Please try again.