mirror of
https://github.com/github/rails.git
synced 2026-04-26 03:00:59 -04:00
Merge commit 'mainstream/master'
This commit is contained in:
@@ -33,6 +33,7 @@ end
|
||||
|
||||
gem 'rack', '>= 0.9.0'
|
||||
require 'rack'
|
||||
require 'action_controller/rack_ext'
|
||||
|
||||
module ActionController
|
||||
# TODO: Review explicit to see if they will automatically be handled by
|
||||
@@ -59,16 +60,14 @@ module ActionController
|
||||
autoload :MiddlewareStack, 'action_controller/middleware_stack'
|
||||
autoload :MimeResponds, 'action_controller/mime_responds'
|
||||
autoload :PolymorphicRoutes, 'action_controller/polymorphic_routes'
|
||||
autoload :Request, 'action_controller/request'
|
||||
autoload :RequestParser, 'action_controller/request_parser'
|
||||
autoload :UrlEncodedPairParser, 'action_controller/url_encoded_pair_parser'
|
||||
autoload :UploadedStringIO, 'action_controller/uploaded_file'
|
||||
autoload :UploadedTempfile, 'action_controller/uploaded_file'
|
||||
autoload :RecordIdentifier, 'action_controller/record_identifier'
|
||||
autoload :Response, 'action_controller/response'
|
||||
autoload :Request, 'action_controller/request'
|
||||
autoload :RequestForgeryProtection, 'action_controller/request_forgery_protection'
|
||||
autoload :RequestParser, 'action_controller/request_parser'
|
||||
autoload :Rescue, 'action_controller/rescue'
|
||||
autoload :Resources, 'action_controller/resources'
|
||||
autoload :Response, 'action_controller/response'
|
||||
autoload :RewindableInput, 'action_controller/rewindable_input'
|
||||
autoload :Routing, 'action_controller/routing'
|
||||
autoload :SessionManagement, 'action_controller/session_management'
|
||||
autoload :StatusCodes, 'action_controller/status_codes'
|
||||
@@ -76,9 +75,11 @@ module ActionController
|
||||
autoload :TestCase, 'action_controller/test_case'
|
||||
autoload :TestProcess, 'action_controller/test_process'
|
||||
autoload :Translation, 'action_controller/translation'
|
||||
autoload :UploadedStringIO, 'action_controller/uploaded_file'
|
||||
autoload :UploadedTempfile, 'action_controller/uploaded_file'
|
||||
autoload :UrlEncodedPairParser, 'action_controller/url_encoded_pair_parser'
|
||||
autoload :UrlRewriter, 'action_controller/url_rewriter'
|
||||
autoload :UrlWriter, 'action_controller/url_rewriter'
|
||||
autoload :VerbPiggybacking, 'action_controller/verb_piggybacking'
|
||||
autoload :Verification, 'action_controller/verification'
|
||||
|
||||
module Assertions
|
||||
|
||||
@@ -55,31 +55,7 @@ module ActionController
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Simple Digest example. Note the block must return the user's password so the framework
|
||||
# can appropriately hash it to check the user's credentials. Returning nil will cause authentication to fail.
|
||||
#
|
||||
# class PostsController < ApplicationController
|
||||
# Users = {"dhh" => "secret"}
|
||||
#
|
||||
# before_filter :authenticate, :except => [ :index ]
|
||||
#
|
||||
# def index
|
||||
# render :text => "Everyone can see me!"
|
||||
# end
|
||||
#
|
||||
# def edit
|
||||
# render :text => "I'm only accessible if you know the password"
|
||||
# end
|
||||
#
|
||||
# private
|
||||
# def authenticate
|
||||
# authenticate_or_request_with_http_digest(realm) do |user_name|
|
||||
# Users[user_name]
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
#
|
||||
#
|
||||
# In your integration tests, you can do something like this:
|
||||
#
|
||||
# def test_access_granted_from_xml
|
||||
@@ -132,10 +108,7 @@ module ActionController
|
||||
end
|
||||
|
||||
def decode_credentials(request)
|
||||
# Properly decode credentials spanning a new-line
|
||||
auth = authorization(request)
|
||||
auth.slice!('Basic ')
|
||||
ActiveSupport::Base64.decode64(auth || '')
|
||||
ActiveSupport::Base64.decode64(authorization(request).split.last || '')
|
||||
end
|
||||
|
||||
def encode_credentials(user_name, password)
|
||||
@@ -147,165 +120,5 @@ module ActionController
|
||||
controller.__send__ :render, :text => "HTTP Basic: Access denied.\n", :status => :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
module Digest
|
||||
extend self
|
||||
|
||||
module ControllerMethods
|
||||
def authenticate_or_request_with_http_digest(realm = "Application", &password_procedure)
|
||||
begin
|
||||
authenticate_with_http_digest!(realm, &password_procedure)
|
||||
rescue ActionController::HttpAuthentication::Error => e
|
||||
msg = e.message
|
||||
msg = "#{msg} expected '#{e.expected}' was '#{e.was}'" unless e.expected.nil?
|
||||
raise msg if e.fatal?
|
||||
request_http_digest_authentication(realm, msg)
|
||||
end
|
||||
end
|
||||
|
||||
# Authenticate using HTTP Digest, throwing ActionController::HttpAuthentication::Error on failure.
|
||||
# This allows more detailed analysis of authentication failures
|
||||
# to be relayed to the client.
|
||||
def authenticate_with_http_digest!(realm = "Application", &login_procedure)
|
||||
HttpAuthentication::Digest.authenticate(self, realm, &login_procedure)
|
||||
end
|
||||
|
||||
# Authenticate with HTTP Digest, returns true or false
|
||||
def authenticate_with_http_digest(realm = "Application", &login_procedure)
|
||||
HttpAuthentication::Digest.authenticate(self, realm, &login_procedure) rescue false
|
||||
end
|
||||
|
||||
# Render output including the HTTP Digest authentication header
|
||||
def request_http_digest_authentication(realm = "Application", message = nil)
|
||||
HttpAuthentication::Digest.authentication_request(self, realm, message)
|
||||
end
|
||||
|
||||
# Add HTTP Digest authentication header to result headers
|
||||
def http_digest_authentication_header(realm = "Application")
|
||||
HttpAuthentication::Digest.authentication_header(self, realm)
|
||||
end
|
||||
end
|
||||
|
||||
# Raises error unless authentictaion succeeds, returns true otherwise
|
||||
def authenticate(controller, realm, &password_procedure)
|
||||
raise Error.new(false), "No authorization header found" unless authorization(controller.request)
|
||||
validate_digest_response(controller, realm, &password_procedure)
|
||||
true
|
||||
end
|
||||
|
||||
def authorization(request)
|
||||
request.env['HTTP_AUTHORIZATION'] ||
|
||||
request.env['X-HTTP_AUTHORIZATION'] ||
|
||||
request.env['X_HTTP_AUTHORIZATION'] ||
|
||||
request.env['REDIRECT_X_HTTP_AUTHORIZATION']
|
||||
end
|
||||
|
||||
# Raises error unless the request credentials response value matches the expected value.
|
||||
def validate_digest_response(controller, realm, &password_procedure)
|
||||
credentials = decode_credentials(controller.request)
|
||||
|
||||
# Check the nonce, opaque and realm.
|
||||
# Ignore nc, as we have no way to validate the number of times this nonce has been used
|
||||
validate_nonce(controller.request, credentials[:nonce])
|
||||
raise Error.new(false, realm, credentials[:realm]), "Realm doesn't match" unless realm == credentials[:realm]
|
||||
raise Error.new(true, opaque(controller.request), credentials[:opaque]),"Opaque doesn't match" unless opaque(controller.request) == credentials[:opaque]
|
||||
|
||||
password = password_procedure.call(credentials[:username])
|
||||
raise Error.new(false), "No password" if password.nil?
|
||||
expected = expected_response(controller.request.env['REQUEST_METHOD'], controller.request.url, credentials, password)
|
||||
raise Error.new(false, expected, credentials[:response]), "Invalid response" unless expected == credentials[:response]
|
||||
end
|
||||
|
||||
# Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
|
||||
def expected_response(http_method, uri, credentials, password)
|
||||
ha1 = ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':'))
|
||||
ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase,uri].join(':'))
|
||||
::Digest::MD5.hexdigest([ha1,credentials[:nonce], credentials[:nc], credentials[:cnonce],credentials[:qop],ha2].join(':'))
|
||||
end
|
||||
|
||||
def encode_credentials(http_method, credentials, password)
|
||||
credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password)
|
||||
"Digest " + credentials.sort_by {|x| x[0].to_s }.inject([]) {|a, v| a << "#{v[0]}='#{v[1]}'" }.join(', ')
|
||||
end
|
||||
|
||||
def decode_credentials(request)
|
||||
authorization(request).to_s.gsub(/^Digest\s+/,'').split(',').inject({}) do |hash, pair|
|
||||
key, value = pair.split('=', 2)
|
||||
hash[key.strip.to_sym] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')
|
||||
hash
|
||||
end
|
||||
end
|
||||
|
||||
def authentication_header(controller, realm)
|
||||
controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce(controller.request)}", opaque="#{opaque(controller.request)}")
|
||||
end
|
||||
|
||||
def authentication_request(controller, realm, message = "HTTP Digest: Access denied")
|
||||
authentication_header(controller, realm)
|
||||
controller.send! :render, :text => message, :status => :unauthorized
|
||||
end
|
||||
|
||||
# Uses an MD5 digest based on time to generate a value to be used only once.
|
||||
#
|
||||
# A server-specified data string which should be uniquely generated each time a 401 response is made.
|
||||
# It is recommended that this string be base64 or hexadecimal data.
|
||||
# Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed.
|
||||
#
|
||||
# The contents of the nonce are implementation dependent.
|
||||
# The quality of the implementation depends on a good choice.
|
||||
# A nonce might, for example, be constructed as the base 64 encoding of
|
||||
#
|
||||
# => time-stamp H(time-stamp ":" ETag ":" private-key)
|
||||
#
|
||||
# where time-stamp is a server-generated time or other non-repeating value,
|
||||
# ETag is the value of the HTTP ETag header associated with the requested entity,
|
||||
# and private-key is data known only to the server.
|
||||
# With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and
|
||||
# reject the request if it did not match the nonce from that header or
|
||||
# if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity.
|
||||
# The inclusion of the ETag prevents a replay request for an updated version of the resource.
|
||||
# (Note: including the IP address of the client in the nonce would appear to offer the server the ability
|
||||
# to limit the reuse of the nonce to the same client that originally got it.
|
||||
# However, that would break proxy farms, where requests from a single user often go through different proxies in the farm.
|
||||
# Also, IP address spoofing is not that hard.)
|
||||
#
|
||||
# An implementation might choose not to accept a previously used nonce or a previously used digest, in order to
|
||||
# protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for
|
||||
# POST or PUT requests and a time-stamp for GET requests. For more details on the issues involved see Section 4
|
||||
# of this document.
|
||||
#
|
||||
# The nonce is opaque to the client.
|
||||
def nonce(request, time = Time.now)
|
||||
session_id = request.is_a?(String) ? request : request.session.session_id
|
||||
t = time.to_i
|
||||
hashed = [t, session_id]
|
||||
digest = ::Digest::MD5.hexdigest(hashed.join(":"))
|
||||
Base64.encode64("#{t}:#{digest}").gsub("\n", '')
|
||||
end
|
||||
|
||||
def validate_nonce(request, value)
|
||||
t = Base64.decode64(value).split(":").first.to_i
|
||||
raise Error.new(true), "Stale Nonce" if (t - Time.now.to_i).abs > 10 * 60
|
||||
n = nonce(request, t)
|
||||
raise Error.new(true, value, n), "Bad Nonce" unless n == value
|
||||
end
|
||||
|
||||
# Opaque based on digest of session_id
|
||||
def opaque(request)
|
||||
session_id = request.is_a?(String) ? request : request.session.session_id
|
||||
@opaque ||= Base64.encode64(::Digest::MD5::hexdigest(session_id)).gsub("\n", '')
|
||||
end
|
||||
end
|
||||
|
||||
class Error < RuntimeError
|
||||
attr_accessor :expected, :was
|
||||
def initialize(fatal = false, expected = nil, was = nil)
|
||||
@fatal = fatal
|
||||
@expected = expected
|
||||
@was = was
|
||||
end
|
||||
|
||||
def fatal?; @fatal; end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,17 +2,6 @@ require 'stringio'
|
||||
require 'uri'
|
||||
require 'active_support/test_case'
|
||||
|
||||
# Monkey patch Rack::Lint to support rewind
|
||||
module Rack
|
||||
class Lint
|
||||
class InputWrapper
|
||||
def rewind
|
||||
@input.rewind
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ActionController
|
||||
module Integration #:nodoc:
|
||||
# An integration Session instance represents a set of requests and responses
|
||||
@@ -68,15 +57,6 @@ module ActionController
|
||||
# A running counter of the number of requests processed.
|
||||
attr_accessor :request_count
|
||||
|
||||
# Nonce value for Digest Authentication, implicitly set on response with WWW-Authentication
|
||||
attr_accessor :nonce
|
||||
|
||||
# Opaque value for Digest Authentication, implicitly set on response with WWW-Authentication
|
||||
attr_accessor :opaque
|
||||
|
||||
# Opaque value for Authentication, implicitly set on response with WWW-Authentication
|
||||
attr_accessor :realm
|
||||
|
||||
class MultiPartNeededException < Exception
|
||||
end
|
||||
|
||||
@@ -252,53 +232,6 @@ module ActionController
|
||||
end
|
||||
alias xhr :xml_http_request
|
||||
|
||||
def request_with_noauth(http_method, uri, parameters, headers)
|
||||
process_with_auth http_method, uri, parameters, headers
|
||||
end
|
||||
|
||||
# Performs a request with the given http_method and parameters, including HTTP Basic authorization headers.
|
||||
# See get() for more details on paramters and headers.
|
||||
#
|
||||
# You can perform GET, POST, PUT, DELETE, and HEAD requests with #get_with_basic, #post_with_basic,
|
||||
# #put_with_basic, #delete_with_basic, and #head_with_basic.
|
||||
def request_with_basic(http_method, uri, parameters, headers, user_name, password)
|
||||
process_with_auth http_method, uri, parameters, headers.merge(:authorization => ActionController::HttpAuthentication::Basic.encode_credentials(user_name, password))
|
||||
end
|
||||
|
||||
# Performs a request with the given http_method and parameters, including HTTP Digest authorization headers.
|
||||
# See get() for more details on paramters and headers.
|
||||
#
|
||||
# You can perform GET, POST, PUT, DELETE, and HEAD requests with #get_with_digest, #post_with_digest,
|
||||
# #put_with_digest, #delete_with_digest, and #head_with_digest.
|
||||
def request_with_digest(http_method, uri, parameters, headers, user_name, password)
|
||||
# Realm, Nonce, and Opaque taken from previoius 401 response
|
||||
|
||||
credentials = {
|
||||
:username => user_name,
|
||||
:realm => @realm,
|
||||
:nonce => @nonce,
|
||||
:qop => "auth",
|
||||
:nc => "00000001",
|
||||
:cnonce => "0a4f113b",
|
||||
:opaque => @opaque,
|
||||
:uri => uri
|
||||
}
|
||||
|
||||
raise "Digest request without previous 401 response" if @opaque.nil?
|
||||
|
||||
process_with_auth http_method, uri, parameters, headers.merge(:authorization => ActionController::HttpAuthentication::Digest.encode_credentials(http_method, credentials, password))
|
||||
end
|
||||
|
||||
# def get_with_basic, def post_with_basic, def put_with_basic, def delete_with_basic, def head_with_basic
|
||||
# def get_with_digest, def post_with_digest, def put_with_digest, def delete_with_digest, def head_with_digest
|
||||
[:get, :post, :put, :delete, :head].each do |method|
|
||||
[:noauth, :basic, :digest].each do |auth_type|
|
||||
define_method("#{method}_with_#{auth_type}") do |uri, parameters, headers, *auth|
|
||||
send("request_with_#{auth_type}", method, uri, parameters, headers, *auth)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the URL for the given options, according to the rules specified
|
||||
# in the application's routes.
|
||||
def url_for(options)
|
||||
@@ -423,32 +356,6 @@ module ActionController
|
||||
return status
|
||||
end
|
||||
|
||||
# Same as process, but handles authentication returns to perform
|
||||
# Basic or Digest authentication
|
||||
def process_with_auth(method, path, parameters = nil, headers = nil)
|
||||
status = process(method, path, parameters, headers)
|
||||
|
||||
if status == 401
|
||||
# Extract authentication information from response
|
||||
auth_data = @response.headers['WWW-Authenticate']
|
||||
if /^Basic /.match(auth_data)
|
||||
# extract realm, to be used in subsequent request
|
||||
@realm = auth_header.split(' ')[1]
|
||||
elsif /^Digest/.match(auth_data)
|
||||
creds = auth_data.to_s.gsub(/^Digest\s+/,'').split(',').inject({}) do |hash, pair|
|
||||
key, value = pair.split('=', 2)
|
||||
hash[key.strip.to_sym] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')
|
||||
hash
|
||||
end
|
||||
@realm = creds[:realm]
|
||||
@nonce = creds[:nonce]
|
||||
@opaque = creds[:opaque]
|
||||
end
|
||||
end
|
||||
|
||||
return status
|
||||
end
|
||||
|
||||
# Encode the cookies hash in a format suitable for passing to a
|
||||
# request.
|
||||
def encode_cookies
|
||||
@@ -513,7 +420,7 @@ module ActionController
|
||||
def multipart_body(params, boundary)
|
||||
multipart_requestify(params).map do |key, value|
|
||||
if value.respond_to?(:original_filename)
|
||||
File.open(value.path) do |f|
|
||||
File.open(value.path, "rb") do |f|
|
||||
f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
|
||||
|
||||
<<-EOF
|
||||
|
||||
@@ -32,6 +32,8 @@ module ActionController
|
||||
else
|
||||
@klass.to_s.constantize
|
||||
end
|
||||
rescue NameError
|
||||
@klass
|
||||
end
|
||||
|
||||
def active?
|
||||
|
||||
@@ -18,4 +18,5 @@ use "ActiveRecord::QueryCache", :if => lambda { defined?(ActiveRecord) }
|
||||
)
|
||||
end
|
||||
|
||||
use ActionController::VerbPiggybacking
|
||||
use ActionController::RewindableInput
|
||||
use Rack::MethodOverride
|
||||
|
||||
22
actionpack/lib/action_controller/rack_ext.rb
Normal file
22
actionpack/lib/action_controller/rack_ext.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
module Rack
|
||||
module Utils
|
||||
module Multipart
|
||||
class << self
|
||||
def parse_multipart_with_rewind(env)
|
||||
result = parse_multipart_without_rewind(env)
|
||||
|
||||
begin
|
||||
env['rack.input'].rewind if env['rack.input'].respond_to?(:rewind)
|
||||
rescue Errno::ESPIPE
|
||||
# Handles exceptions raised by input streams that cannot be rewound
|
||||
# such as when using plain CGI under Apache
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
alias_method_chain :parse_multipart, :rewind
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
44
actionpack/lib/action_controller/rewindable_input.rb
Normal file
44
actionpack/lib/action_controller/rewindable_input.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
module ActionController
|
||||
class RewindableInput
|
||||
class RewindableIO < ActiveSupport::BasicObject
|
||||
def initialize(io)
|
||||
@io = io
|
||||
end
|
||||
|
||||
def read(*args)
|
||||
read_original_io
|
||||
@io.read(*args)
|
||||
end
|
||||
|
||||
def rewind
|
||||
read_original_io
|
||||
@io.rewind
|
||||
end
|
||||
|
||||
def string
|
||||
@string
|
||||
end
|
||||
|
||||
def method_missing(method, *args, &block)
|
||||
@io.send(method, *args, &block)
|
||||
end
|
||||
|
||||
private
|
||||
def read_original_io
|
||||
unless @string
|
||||
@string = @io.read
|
||||
@io = StringIO.new(@string)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
env['rack.input'] = RewindableIO.new(env['rack.input'])
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -163,9 +163,9 @@ module ActionController
|
||||
|
||||
def ensure_session_key(key)
|
||||
if key.blank?
|
||||
raise ArgumentError, 'A session_key is required to write a ' +
|
||||
raise ArgumentError, 'A key is required to write a ' +
|
||||
'cookie containing the session data. Use ' +
|
||||
'config.action_controller.session = { :session_key => ' +
|
||||
'config.action_controller.session = { :key => ' +
|
||||
'"_myapp_session", :secret => "some secret phrase" } in ' +
|
||||
'config/environment.rb'
|
||||
end
|
||||
@@ -181,7 +181,7 @@ module ActionController
|
||||
if secret.blank?
|
||||
raise ArgumentError, "A secret is required to generate an " +
|
||||
"integrity hash for cookie session data. Use " +
|
||||
"config.action_controller.session = { :session_key => " +
|
||||
"config.action_controller.session = { :key => " +
|
||||
"\"_myapp_session\", :secret => \"some secret phrase of at " +
|
||||
"least #{SECRET_MIN_LENGTH} characters\" } " +
|
||||
"in config/environment.rb"
|
||||
|
||||
@@ -484,7 +484,8 @@ module ActionController #:nodoc:
|
||||
#
|
||||
# post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png', :binary)
|
||||
def fixture_file_upload(path, mime_type = nil, binary = false)
|
||||
ActionController::TestUploadedFile.new("#{ActionController::TestCase.try(:fixture_path)}#{path}", mime_type, binary)
|
||||
fixture_path = ActionController::TestCase.send(:fixture_path) if ActionController::TestCase.respond_to?(:fixture_path)
|
||||
ActionController::TestUploadedFile.new("#{fixture_path}#{path}", mime_type, binary)
|
||||
end
|
||||
|
||||
# A helper to make it easier to test different route configurations.
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
module ActionController
|
||||
# TODO: Use Rack::MethodOverride when it is released
|
||||
class VerbPiggybacking
|
||||
HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS)
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
if env["REQUEST_METHOD"] == "POST"
|
||||
req = Request.new(env)
|
||||
if method = (req.parameters[:_method] || env["HTTP_X_HTTP_METHOD_OVERRIDE"])
|
||||
method = method.to_s.upcase
|
||||
if HTTP_METHODS.include?(method)
|
||||
env["REQUEST_METHOD"] = method
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,73 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
|
||||
class HttpDigestAuthenticationTest < Test::Unit::TestCase
|
||||
include ActionController::HttpAuthentication::Digest
|
||||
|
||||
class DummyController
|
||||
attr_accessor :headers, :renders, :request, :response
|
||||
|
||||
def initialize
|
||||
@headers, @renders = {}, []
|
||||
@request = ActionController::TestRequest.new
|
||||
@response = ActionController::TestResponse.new
|
||||
request.session.session_id = "test_session"
|
||||
end
|
||||
|
||||
def render(options)
|
||||
self.renderers << options
|
||||
end
|
||||
end
|
||||
|
||||
def setup
|
||||
@controller = DummyController.new
|
||||
@credentials = {
|
||||
:username => "dhh",
|
||||
:realm => "testrealm@host.com",
|
||||
:nonce => ActionController::HttpAuthentication::Digest.nonce(@controller.request),
|
||||
:qop => "auth",
|
||||
:nc => "00000001",
|
||||
:cnonce => "0a4f113b",
|
||||
:opaque => ActionController::HttpAuthentication::Digest.opaque(@controller.request),
|
||||
:uri => "http://test.host/"
|
||||
}
|
||||
@encoded_credentials = ActionController::HttpAuthentication::Digest.encode_credentials("GET", @credentials, "secret")
|
||||
end
|
||||
|
||||
def test_decode_credentials
|
||||
set_headers
|
||||
assert_equal @credentials, decode_credentials(@controller.request)
|
||||
end
|
||||
|
||||
def test_nonce_format
|
||||
assert_nothing_thrown do
|
||||
validate_nonce(@controller.request, nonce(@controller.request))
|
||||
end
|
||||
end
|
||||
|
||||
def test_authenticate_should_raise_for_nil_password
|
||||
set_headers ActionController::HttpAuthentication::Digest.encode_credentials(:get, @credentials, nil)
|
||||
assert_raise ActionController::HttpAuthentication::Error do
|
||||
authenticate(@controller, @credentials[:realm]) { |user| user == "dhh" && "secret" }
|
||||
end
|
||||
end
|
||||
|
||||
def test_authenticate_should_raise_for_incorrect_password
|
||||
set_headers
|
||||
assert_raise ActionController::HttpAuthentication::Error do
|
||||
authenticate(@controller, @credentials[:realm]) { |user| user == "dhh" && "bad password" }
|
||||
end
|
||||
end
|
||||
|
||||
def test_authenticate_should_not_raise_for_correct_password
|
||||
set_headers
|
||||
assert_nothing_thrown do
|
||||
authenticate(@controller, @credentials[:realm]) { |user| user == "dhh" && "secret" }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def set_headers(value = @encoded_credentials, name = 'HTTP_AUTHORIZATION', method = "GET")
|
||||
@controller.request.env[name] = value
|
||||
@controller.request.env["REQUEST_METHOD"] = method
|
||||
end
|
||||
end
|
||||
@@ -8,25 +8,7 @@ class SessionTest < Test::Unit::TestCase
|
||||
}
|
||||
|
||||
def setup
|
||||
@credentials = {
|
||||
:username => "username",
|
||||
:realm => "MyApp",
|
||||
:nonce => ActionController::HttpAuthentication::Digest.nonce("session_id"),
|
||||
:qop => "auth",
|
||||
:nc => "00000001",
|
||||
:cnonce => "0a4f113b",
|
||||
:opaque => ActionController::HttpAuthentication::Digest.opaque("session_id"),
|
||||
:uri => "/index"
|
||||
}
|
||||
|
||||
@session = ActionController::Integration::Session.new(StubApp)
|
||||
@session.nonce = @credentials[:nonce]
|
||||
@session.opaque = @credentials[:opaque]
|
||||
@session.realm = @credentials[:realm]
|
||||
end
|
||||
|
||||
def encoded_credentials(method)
|
||||
ActionController::HttpAuthentication::Digest.encode_credentials(method, @credentials, "password")
|
||||
end
|
||||
|
||||
def test_https_bang_works_and_sets_truth_by_default
|
||||
@@ -150,76 +132,6 @@ class SessionTest < Test::Unit::TestCase
|
||||
@session.head(path,params,headers)
|
||||
end
|
||||
|
||||
def test_get_with_basic
|
||||
path = "/index"; params = "blah"; headers = {:location => 'blah'}
|
||||
expected_headers = headers.merge(:authorization => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=\n")
|
||||
@session.expects(:process).with(:get,path,params,expected_headers)
|
||||
@session.get_with_basic(path,params,headers,'username','password')
|
||||
end
|
||||
|
||||
def test_post_with_basic
|
||||
path = "/index"; params = "blah"; headers = {:location => 'blah'}
|
||||
expected_headers = headers.merge(:authorization => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=\n")
|
||||
@session.expects(:process).with(:post,path,params,expected_headers)
|
||||
@session.post_with_basic(path,params,headers,'username','password')
|
||||
end
|
||||
|
||||
def test_put_with_basic
|
||||
path = "/index"; params = "blah"; headers = {:location => 'blah'}
|
||||
expected_headers = headers.merge(:authorization => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=\n")
|
||||
@session.expects(:process).with(:put,path,params,expected_headers)
|
||||
@session.put_with_basic(path,params,headers,'username','password')
|
||||
end
|
||||
|
||||
def test_delete_with_basic
|
||||
path = "/index"; params = "blah"; headers = {:location => 'blah'}
|
||||
expected_headers = headers.merge(:authorization => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=\n")
|
||||
@session.expects(:process).with(:delete,path,params,expected_headers)
|
||||
@session.delete_with_basic(path,params,headers,'username','password')
|
||||
end
|
||||
|
||||
def test_head_with_basic
|
||||
path = "/index"; params = "blah"; headers = {:location => 'blah'}
|
||||
expected_headers = headers.merge(:authorization => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=\n")
|
||||
@session.expects(:process).with(:head,path,params,expected_headers)
|
||||
@session.head_with_basic(path,params,headers,'username','password')
|
||||
end
|
||||
|
||||
def test_get_with_digest
|
||||
path = "/index"; params = "blah"; headers = {:location => 'blah'}
|
||||
expected_headers = headers.merge(:authorization => encoded_credentials(:get))
|
||||
@session.expects(:process).with(:get,path,params,expected_headers)
|
||||
@session.get_with_digest(path,params,headers,'username','password')
|
||||
end
|
||||
|
||||
def test_post_with_digest
|
||||
path = "/index"; params = "blah"; headers = {:location => 'blah'}
|
||||
expected_headers = headers.merge(:authorization => encoded_credentials(:post))
|
||||
@session.expects(:process).with(:post,path,params,expected_headers)
|
||||
@session.post_with_digest(path,params,headers,'username','password')
|
||||
end
|
||||
|
||||
def test_put_with_digest
|
||||
path = "/index"; params = "blah"; headers = {:location => 'blah'}
|
||||
expected_headers = headers.merge(:authorization => encoded_credentials(:put))
|
||||
@session.expects(:process).with(:put,path,params,expected_headers)
|
||||
@session.put_with_digest(path,params,headers,'username','password')
|
||||
end
|
||||
|
||||
def test_delete_with_digest
|
||||
path = "/index"; params = "blah"; headers = {:location => 'blah'}
|
||||
expected_headers = headers.merge(:authorization => encoded_credentials(:delete))
|
||||
@session.expects(:process).with(:delete,path,params,expected_headers)
|
||||
@session.delete_with_digest(path,params,headers,'username','password')
|
||||
end
|
||||
|
||||
def test_head_with_digest
|
||||
path = "/index"; params = "blah"; headers = {:location => 'blah'}
|
||||
expected_headers = headers.merge(:authorization => encoded_credentials(:head))
|
||||
@session.expects(:process).with(:head,path,params,expected_headers)
|
||||
@session.head_with_digest(path,params,headers,'username','password')
|
||||
end
|
||||
|
||||
def test_xml_http_request_get
|
||||
path = "/index"; params = "blah"; headers = {:location => 'blah'}
|
||||
headers_after_xhr = headers.merge(
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
|
||||
unless defined? ApplicationController
|
||||
class ApplicationController < ActionController::Base
|
||||
end
|
||||
end
|
||||
|
||||
class UploadTestController < ActionController::Base
|
||||
def update
|
||||
SessionUploadTest.last_request_type = ActionController::Base.param_parsers[request.content_type]
|
||||
render :text => "got here"
|
||||
end
|
||||
|
||||
def read
|
||||
render :text => "File: #{params[:uploaded_data].read}"
|
||||
end
|
||||
end
|
||||
|
||||
class SessionUploadTest < ActionController::IntegrationTest
|
||||
FILES_DIR = File.dirname(__FILE__) + '/../fixtures/multipart'
|
||||
|
||||
class << self
|
||||
attr_accessor :last_request_type
|
||||
end
|
||||
|
||||
def test_upload_and_read_file
|
||||
with_test_routing do
|
||||
post '/read', :uploaded_data => fixture_file_upload(FILES_DIR + "/hello.txt", "text/plain")
|
||||
assert_equal "File: Hello", response.body
|
||||
end
|
||||
end
|
||||
|
||||
# The lint wrapper is used in integration tests
|
||||
# instead of a normal StringIO class
|
||||
InputWrapper = Rack::Lint::InputWrapper
|
||||
|
||||
def test_post_with_upload_with_unrewindable_input
|
||||
InputWrapper.any_instance.expects(:rewind).raises(Errno::ESPIPE)
|
||||
|
||||
with_test_routing do
|
||||
post '/read', :uploaded_data => fixture_file_upload(FILES_DIR + "/hello.txt", "text/plain")
|
||||
assert_equal "File: Hello", response.body
|
||||
end
|
||||
end
|
||||
|
||||
def test_post_with_upload_with_params_parsing
|
||||
with_test_routing do
|
||||
params = { :uploaded_data => fixture_file_upload(FILES_DIR + "/mona_lisa.jpg", "image/jpg") }
|
||||
post '/update', params, :location => 'blah'
|
||||
assert_equal(:multipart_form, SessionUploadTest.last_request_type)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def with_test_routing
|
||||
with_routing do |set|
|
||||
set.draw do |map|
|
||||
map.update 'update', :controller => "upload_test", :action => "update", :method => :post
|
||||
map.read 'read', :controller => "upload_test", :action => "read", :method => :post
|
||||
end
|
||||
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,208 @@
|
||||
require 'abstract_unit'
|
||||
|
||||
class MultipartParamsParsingTest < ActionController::IntegrationTest
|
||||
class TestController < ActionController::Base
|
||||
class << self
|
||||
attr_accessor :last_request_parameters
|
||||
end
|
||||
|
||||
def parse
|
||||
self.class.last_request_parameters = request.request_parameters
|
||||
head :ok
|
||||
end
|
||||
|
||||
def read
|
||||
render :text => "File: #{params[:uploaded_data].read}"
|
||||
end
|
||||
end
|
||||
|
||||
FIXTURE_PATH = File.dirname(__FILE__) + '/../../fixtures/multipart'
|
||||
|
||||
def teardown
|
||||
TestController.last_request_parameters = nil
|
||||
end
|
||||
|
||||
test "parses single parameter" do
|
||||
assert_equal({ 'foo' => 'bar' }, parse_multipart('single_parameter'))
|
||||
end
|
||||
|
||||
test "parses bracketed parameters" do
|
||||
assert_equal({ 'foo' => { 'baz' => 'bar'}}, parse_multipart('bracketed_param'))
|
||||
end
|
||||
|
||||
test "parses text file" do
|
||||
params = parse_multipart('text_file')
|
||||
assert_equal %w(file foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
|
||||
file = params['file']
|
||||
assert_kind_of StringIO, file
|
||||
assert_equal 'file.txt', file.original_filename
|
||||
assert_equal "text/plain", file.content_type
|
||||
assert_equal 'contents', file.read
|
||||
end
|
||||
|
||||
test "parses boundary problem file" do
|
||||
params = parse_multipart('boundary_problem_file')
|
||||
assert_equal %w(file foo), params.keys.sort
|
||||
|
||||
file = params['file']
|
||||
foo = params['foo']
|
||||
|
||||
assert_kind_of Tempfile, file
|
||||
|
||||
assert_equal 'file.txt', file.original_filename
|
||||
assert_equal "text/plain", file.content_type
|
||||
|
||||
assert_equal 'bar', foo
|
||||
end
|
||||
|
||||
test "parses large text file" do
|
||||
params = parse_multipart('large_text_file')
|
||||
assert_equal %w(file foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
|
||||
file = params['file']
|
||||
|
||||
assert_kind_of Tempfile, file
|
||||
|
||||
assert_equal 'file.txt', file.original_filename
|
||||
assert_equal "text/plain", file.content_type
|
||||
assert ('a' * 20480) == file.read
|
||||
end
|
||||
|
||||
test "parses binary file" do
|
||||
params = parse_multipart('binary_file')
|
||||
assert_equal %w(file flowers foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
|
||||
file = params['file']
|
||||
assert_kind_of StringIO, file
|
||||
assert_equal 'file.csv', file.original_filename
|
||||
assert_nil file.content_type
|
||||
assert_equal 'contents', file.read
|
||||
|
||||
file = params['flowers']
|
||||
assert_kind_of StringIO, file
|
||||
assert_equal 'flowers.jpg', file.original_filename
|
||||
assert_equal "image/jpeg", file.content_type
|
||||
assert_equal 19512, file.size
|
||||
end
|
||||
|
||||
test "parses mixed files" do
|
||||
params = parse_multipart('mixed_files')
|
||||
assert_equal %w(files foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
|
||||
# Ruby CGI doesn't handle multipart/mixed for us.
|
||||
files = params['files']
|
||||
assert_kind_of String, files
|
||||
files.force_encoding('ASCII-8BIT') if files.respond_to?(:force_encoding)
|
||||
assert_equal 19756, files.size
|
||||
end
|
||||
|
||||
test "uploads and reads binary file" do
|
||||
with_test_routing do
|
||||
fixture = FIXTURE_PATH + "/mona_lisa.jpg"
|
||||
params = { :uploaded_data => fixture_file_upload(fixture, "image/jpg") }
|
||||
post '/read', params
|
||||
expected_length = 'File: '.length + File.size(fixture)
|
||||
assert_equal expected_length, response.content_length
|
||||
end
|
||||
end
|
||||
|
||||
test "uploads and reads file" do
|
||||
with_test_routing do
|
||||
post '/read', :uploaded_data => fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain")
|
||||
assert_equal "File: Hello", response.body
|
||||
end
|
||||
end
|
||||
|
||||
# The lint wrapper is used in integration tests
|
||||
# instead of a normal StringIO class
|
||||
InputWrapper = Rack::Lint::InputWrapper
|
||||
|
||||
test "parses unwindable stream" do
|
||||
InputWrapper.any_instance.stubs(:rewind).raises(Errno::ESPIPE)
|
||||
params = parse_multipart('large_text_file')
|
||||
assert_equal %w(file foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
end
|
||||
|
||||
test "uploads and reads file with unwindable input" do
|
||||
InputWrapper.any_instance.stubs(:rewind).raises(Errno::ESPIPE)
|
||||
|
||||
with_test_routing do
|
||||
post '/read', :uploaded_data => fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain")
|
||||
assert_equal "File: Hello", response.body
|
||||
end
|
||||
end
|
||||
|
||||
test "passes through rack middleware and uploads file" do
|
||||
with_muck_middleware do
|
||||
with_test_routing do
|
||||
post '/read', :uploaded_data => fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain")
|
||||
assert_equal "File: Hello", response.body
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "passes through rack middleware and uploads file with unwindable input" do
|
||||
InputWrapper.any_instance.stubs(:rewind).raises(Errno::ESPIPE)
|
||||
|
||||
with_muck_middleware do
|
||||
with_test_routing do
|
||||
post '/read', :uploaded_data => fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain")
|
||||
assert_equal "File: Hello", response.body
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def fixture(name)
|
||||
File.open(File.join(FIXTURE_PATH, name), 'rb') do |file|
|
||||
{ "rack.input" => file.read,
|
||||
"CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
|
||||
"CONTENT_LENGTH" => file.stat.size.to_s }
|
||||
end
|
||||
end
|
||||
|
||||
def parse_multipart(name)
|
||||
with_test_routing do
|
||||
headers = fixture(name)
|
||||
post "/parse", headers.delete("rack.input"), headers
|
||||
assert_response :ok
|
||||
TestController.last_request_parameters
|
||||
end
|
||||
end
|
||||
|
||||
def with_test_routing
|
||||
with_routing do |set|
|
||||
set.draw do |map|
|
||||
map.connect ':action', :controller => "multipart_params_parsing_test/test"
|
||||
end
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
class MuckMiddleware
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
req = Rack::Request.new(env)
|
||||
req.params # Parse params
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
|
||||
def with_muck_middleware
|
||||
original_middleware = ActionController::Dispatcher.middleware
|
||||
middleware = original_middleware.dup
|
||||
middleware.insert_after ActionController::RewindableInput, MuckMiddleware
|
||||
ActionController::Dispatcher.middleware = middleware
|
||||
yield
|
||||
ActionController::Dispatcher.middleware = original_middleware
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,208 @@
|
||||
require 'abstract_unit'
|
||||
|
||||
class UrlEncodedParamsParsingTest < ActionController::IntegrationTest
|
||||
class TestController < ActionController::Base
|
||||
class << self
|
||||
attr_accessor :last_request_parameters, :last_request_type
|
||||
end
|
||||
|
||||
def parse
|
||||
self.class.last_request_parameters = request.request_parameters
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
|
||||
def teardown
|
||||
TestController.last_request_parameters = nil
|
||||
end
|
||||
|
||||
test "parses unbalanced query string with array" do
|
||||
assert_parses(
|
||||
{'location' => ["1", "2"], 'age_group' => ["2"]},
|
||||
"location[]=1&location[]=2&age_group[]=2"
|
||||
)
|
||||
end
|
||||
|
||||
test "parses nested hash" do
|
||||
query = [
|
||||
"note[viewers][viewer][][type]=User",
|
||||
"note[viewers][viewer][][id]=1",
|
||||
"note[viewers][viewer][][type]=Group",
|
||||
"note[viewers][viewer][][id]=2"
|
||||
].join("&")
|
||||
|
||||
expected = { "note" => { "viewers"=>{"viewer"=>[{ "id"=>"1", "type"=>"User"}, {"type"=>"Group", "id"=>"2"} ]} } }
|
||||
assert_parses(expected, query)
|
||||
end
|
||||
|
||||
test "parses more complex nesting" do
|
||||
query = [
|
||||
"customers[boston][first][name]=David",
|
||||
"customers[boston][first][url]=http://David",
|
||||
"customers[boston][second][name]=Allan",
|
||||
"customers[boston][second][url]=http://Allan",
|
||||
"something_else=blah",
|
||||
"something_nil=",
|
||||
"something_empty=",
|
||||
"products[first]=Apple Computer",
|
||||
"products[second]=Pc",
|
||||
"=Save"
|
||||
].join("&")
|
||||
|
||||
expected = {
|
||||
"customers" => {
|
||||
"boston" => {
|
||||
"first" => {
|
||||
"name" => "David",
|
||||
"url" => "http://David"
|
||||
},
|
||||
"second" => {
|
||||
"name" => "Allan",
|
||||
"url" => "http://Allan"
|
||||
}
|
||||
}
|
||||
},
|
||||
"something_else" => "blah",
|
||||
"something_empty" => "",
|
||||
"something_nil" => "",
|
||||
"products" => {
|
||||
"first" => "Apple Computer",
|
||||
"second" => "Pc"
|
||||
}
|
||||
}
|
||||
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with array" do
|
||||
query = "selected[]=1&selected[]=2&selected[]=3"
|
||||
expected = { "selected" => [ "1", "2", "3" ] }
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with non alphanumeric name" do
|
||||
query = "a/b[c]=d"
|
||||
expected = { "a/b" => { "c" => "d" }}
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with single brackets in the middle" do
|
||||
query = "a/b[c]d=e"
|
||||
expected = { "a/b" => {} }
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with separated brackets" do
|
||||
query = "a/b@[c]d[e]=f"
|
||||
expected = { "a/b@" => { }}
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with separated brackets and array" do
|
||||
query = "a/b@[c]d[e][]=f"
|
||||
expected = { "a/b@" => { }}
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with unmatched brackets and array" do
|
||||
query = "a/b@[c][d[e][]=f"
|
||||
expected = { "a/b@" => { "c" => { }}}
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with nil key" do
|
||||
query = "=&test2=value1"
|
||||
expected = { "test2" => "value1" }
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with array prefix and hashes" do
|
||||
query = "a[][b][c]=d"
|
||||
expected = {"a" => [{"b" => {"c" => "d"}}]}
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with complex nesting" do
|
||||
query = "a[][b][c][][d][]=e"
|
||||
expected = {"a" => [{"b" => {"c" => [{"d" => ["e"]}]}}]}
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with file path" do
|
||||
query = [
|
||||
"customers[boston][first][name]=David",
|
||||
"something_else=blah",
|
||||
"logo=#{File.expand_path(__FILE__)}"
|
||||
].join("&")
|
||||
|
||||
expected = {
|
||||
"customers" => {
|
||||
"boston" => {
|
||||
"first" => {
|
||||
"name" => "David"
|
||||
}
|
||||
}
|
||||
},
|
||||
"something_else" => "blah",
|
||||
"logo" => File.expand_path(__FILE__),
|
||||
}
|
||||
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "passes through rack middleware and parses params" do
|
||||
with_muck_middleware do
|
||||
assert_parses({ "a" => { "b" => "c" } }, "a[b]=c")
|
||||
end
|
||||
end
|
||||
|
||||
# The lint wrapper is used in integration tests
|
||||
# instead of a normal StringIO class
|
||||
InputWrapper = Rack::Lint::InputWrapper
|
||||
|
||||
test "passes through rack middleware and parses params with unwindable input" do
|
||||
InputWrapper.any_instance.stubs(:rewind).raises(Errno::ESPIPE)
|
||||
with_muck_middleware do
|
||||
assert_parses({ "a" => { "b" => "c" } }, "a[b]=c")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
class MuckMiddleware
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
req = Rack::Request.new(env)
|
||||
req.params # Parse params
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
|
||||
def with_muck_middleware
|
||||
original_middleware = ActionController::Dispatcher.middleware
|
||||
middleware = original_middleware.dup
|
||||
middleware.insert_after ActionController::RewindableInput, MuckMiddleware
|
||||
ActionController::Dispatcher.middleware = middleware
|
||||
yield
|
||||
ActionController::Dispatcher.middleware = original_middleware
|
||||
end
|
||||
|
||||
def with_test_routing
|
||||
with_routing do |set|
|
||||
set.draw do |map|
|
||||
map.connect ':action', :controller => "url_encoded_params_parsing_test/test"
|
||||
end
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def assert_parses(expected, actual)
|
||||
with_test_routing do
|
||||
post "/parse", actual
|
||||
assert_response :ok
|
||||
assert_equal(expected, TestController.last_request_parameters)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -405,308 +405,3 @@ class RequestTest < ActiveSupport::TestCase
|
||||
@request.request_method(true)
|
||||
end
|
||||
end
|
||||
|
||||
class UrlEncodedRequestParameterParsingTest < ActiveSupport::TestCase
|
||||
def test_unbalanced_query_string_with_array
|
||||
assert_equal(
|
||||
{'location' => ["1", "2"], 'age_group' => ["2"]},
|
||||
ActionController::RequestParser.parse_request_parameters({'location[]' => ["1", "2"], 'age_group[]' => ["2"]})
|
||||
)
|
||||
end
|
||||
|
||||
def test_request_hash_parsing
|
||||
query = {
|
||||
"note[viewers][viewer][][type]" => ["User", "Group"],
|
||||
"note[viewers][viewer][][id]" => ["1", "2"]
|
||||
}
|
||||
|
||||
expected = { "note" => { "viewers"=>{"viewer"=>[{ "id"=>"1", "type"=>"User"}, {"type"=>"Group", "id"=>"2"} ]} } }
|
||||
|
||||
assert_equal(expected, ActionController::RequestParser.parse_request_parameters(query))
|
||||
end
|
||||
|
||||
def test_parse_params
|
||||
input = {
|
||||
"customers[boston][first][name]" => [ "David" ],
|
||||
"customers[boston][first][url]" => [ "http://David" ],
|
||||
"customers[boston][second][name]" => [ "Allan" ],
|
||||
"customers[boston][second][url]" => [ "http://Allan" ],
|
||||
"something_else" => [ "blah" ],
|
||||
"something_nil" => [ nil ],
|
||||
"something_empty" => [ "" ],
|
||||
"products[first]" => [ "Apple Computer" ],
|
||||
"products[second]" => [ "Pc" ],
|
||||
"" => [ 'Save' ]
|
||||
}
|
||||
|
||||
expected_output = {
|
||||
"customers" => {
|
||||
"boston" => {
|
||||
"first" => {
|
||||
"name" => "David",
|
||||
"url" => "http://David"
|
||||
},
|
||||
"second" => {
|
||||
"name" => "Allan",
|
||||
"url" => "http://Allan"
|
||||
}
|
||||
}
|
||||
},
|
||||
"something_else" => "blah",
|
||||
"something_empty" => "",
|
||||
"something_nil" => "",
|
||||
"products" => {
|
||||
"first" => "Apple Computer",
|
||||
"second" => "Pc"
|
||||
}
|
||||
}
|
||||
|
||||
assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
UploadedStringIO = ActionController::UploadedStringIO
|
||||
class MockUpload < UploadedStringIO
|
||||
def initialize(content_type, original_path, *args)
|
||||
self.content_type = content_type
|
||||
self.original_path = original_path
|
||||
super *args
|
||||
end
|
||||
end
|
||||
|
||||
def test_parse_params_from_multipart_upload
|
||||
file = MockUpload.new('img/jpeg', 'foo.jpg')
|
||||
ie_file = MockUpload.new('img/jpeg', 'c:\\Documents and Settings\\foo\\Desktop\\bar.jpg')
|
||||
non_file_text_part = MockUpload.new('text/plain', '', 'abc')
|
||||
|
||||
input = {
|
||||
"something" => [ UploadedStringIO.new("") ],
|
||||
"array_of_stringios" => [[ UploadedStringIO.new("One"), UploadedStringIO.new("Two") ]],
|
||||
"mixed_types_array" => [[ UploadedStringIO.new("Three"), "NotStringIO" ]],
|
||||
"mixed_types_as_checkboxes[strings][nested]" => [[ file, "String", UploadedStringIO.new("StringIO")]],
|
||||
"ie_mixed_types_as_checkboxes[strings][nested]" => [[ ie_file, "String", UploadedStringIO.new("StringIO")]],
|
||||
"products[string]" => [ UploadedStringIO.new("Apple Computer") ],
|
||||
"products[file]" => [ file ],
|
||||
"ie_products[string]" => [ UploadedStringIO.new("Microsoft") ],
|
||||
"ie_products[file]" => [ ie_file ],
|
||||
"text_part" => [non_file_text_part]
|
||||
}
|
||||
|
||||
expected_output = {
|
||||
"something" => "",
|
||||
"array_of_stringios" => ["One", "Two"],
|
||||
"mixed_types_array" => [ "Three", "NotStringIO" ],
|
||||
"mixed_types_as_checkboxes" => {
|
||||
"strings" => {
|
||||
"nested" => [ file, "String", "StringIO" ]
|
||||
},
|
||||
},
|
||||
"ie_mixed_types_as_checkboxes" => {
|
||||
"strings" => {
|
||||
"nested" => [ ie_file, "String", "StringIO" ]
|
||||
},
|
||||
},
|
||||
"products" => {
|
||||
"string" => "Apple Computer",
|
||||
"file" => file
|
||||
},
|
||||
"ie_products" => {
|
||||
"string" => "Microsoft",
|
||||
"file" => ie_file
|
||||
},
|
||||
"text_part" => "abc"
|
||||
}
|
||||
|
||||
params = ActionController::RequestParser.parse_request_parameters(input)
|
||||
assert_equal expected_output, params
|
||||
|
||||
# Lone filenames are preserved.
|
||||
assert_equal 'foo.jpg', params['mixed_types_as_checkboxes']['strings']['nested'].first.original_filename
|
||||
assert_equal 'foo.jpg', params['products']['file'].original_filename
|
||||
|
||||
# But full Windows paths are reduced to their basename.
|
||||
assert_equal 'bar.jpg', params['ie_mixed_types_as_checkboxes']['strings']['nested'].first.original_filename
|
||||
assert_equal 'bar.jpg', params['ie_products']['file'].original_filename
|
||||
end
|
||||
|
||||
def test_parse_params_with_file
|
||||
input = {
|
||||
"customers[boston][first][name]" => [ "David" ],
|
||||
"something_else" => [ "blah" ],
|
||||
"logo" => [ File.new(File.dirname(__FILE__) + "/rack_test.rb").path ]
|
||||
}
|
||||
|
||||
expected_output = {
|
||||
"customers" => {
|
||||
"boston" => {
|
||||
"first" => {
|
||||
"name" => "David"
|
||||
}
|
||||
}
|
||||
},
|
||||
"something_else" => "blah",
|
||||
"logo" => File.new(File.dirname(__FILE__) + "/rack_test.rb").path,
|
||||
}
|
||||
|
||||
assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_array
|
||||
input = { "selected[]" => [ "1", "2", "3" ] }
|
||||
|
||||
expected_output = { "selected" => [ "1", "2", "3" ] }
|
||||
|
||||
assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_non_alphanumeric_name
|
||||
input = { "a/b[c]" => %w(d) }
|
||||
expected = { "a/b" => { "c" => "d" }}
|
||||
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_single_brackets_in_middle
|
||||
input = { "a/b[c]d" => %w(e) }
|
||||
expected = { "a/b" => {} }
|
||||
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_separated_brackets
|
||||
input = { "a/b@[c]d[e]" => %w(f) }
|
||||
expected = { "a/b@" => { }}
|
||||
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_separated_brackets_and_array
|
||||
input = { "a/b@[c]d[e][]" => %w(f) }
|
||||
expected = { "a/b@" => { }}
|
||||
assert_equal expected , ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_unmatched_brackets_and_array
|
||||
input = { "a/b@[c][d[e][]" => %w(f) }
|
||||
expected = { "a/b@" => { "c" => { }}}
|
||||
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_nil_key
|
||||
input = { nil => nil, "test2" => %w(value1) }
|
||||
expected = { "test2" => "value1" }
|
||||
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_array_prefix_and_hashes
|
||||
input = { "a[][b][c]" => %w(d) }
|
||||
expected = {"a" => [{"b" => {"c" => "d"}}]}
|
||||
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_complex_nesting
|
||||
input = { "a[][b][c][][d][]" => %w(e) }
|
||||
expected = {"a" => [{"b" => {"c" => [{"d" => ["e"]}]}}]}
|
||||
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
end
|
||||
|
||||
class MultipartRequestParameterParsingTest < ActiveSupport::TestCase
|
||||
FIXTURE_PATH = File.dirname(__FILE__) + '/../fixtures/multipart'
|
||||
|
||||
def test_single_parameter
|
||||
params = parse_multipart('single_parameter')
|
||||
assert_equal({ 'foo' => 'bar' }, params)
|
||||
end
|
||||
|
||||
def test_bracketed_param
|
||||
assert_equal({ 'foo' => { 'baz' => 'bar'}}, parse_multipart('bracketed_param'))
|
||||
end
|
||||
|
||||
def test_text_file
|
||||
params = parse_multipart('text_file')
|
||||
assert_equal %w(file foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
|
||||
file = params['file']
|
||||
assert_kind_of StringIO, file
|
||||
assert_equal 'file.txt', file.original_filename
|
||||
assert_equal "text/plain", file.content_type
|
||||
assert_equal 'contents', file.read
|
||||
end
|
||||
|
||||
def test_boundary_problem_file
|
||||
params = parse_multipart('boundary_problem_file')
|
||||
assert_equal %w(file foo), params.keys.sort
|
||||
|
||||
file = params['file']
|
||||
foo = params['foo']
|
||||
|
||||
assert_kind_of Tempfile, file
|
||||
|
||||
assert_equal 'file.txt', file.original_filename
|
||||
assert_equal "text/plain", file.content_type
|
||||
|
||||
assert_equal 'bar', foo
|
||||
end
|
||||
|
||||
def test_large_text_file
|
||||
params = parse_multipart('large_text_file')
|
||||
assert_equal %w(file foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
|
||||
file = params['file']
|
||||
|
||||
assert_kind_of Tempfile, file
|
||||
|
||||
assert_equal 'file.txt', file.original_filename
|
||||
assert_equal "text/plain", file.content_type
|
||||
assert ('a' * 20480) == file.read
|
||||
end
|
||||
|
||||
uses_mocha "test_no_rewind_stream" do
|
||||
def test_no_rewind_stream
|
||||
# Ensures that parse_multipart_form_parameters works with streams that cannot be rewound
|
||||
file = File.open(File.join(FIXTURE_PATH, 'large_text_file'), 'rb')
|
||||
file.expects(:rewind).raises(Errno::ESPIPE)
|
||||
params = ActionController::RequestParser.parse_multipart_form_parameters(file, 'AaB03x', file.stat.size, {})
|
||||
assert_not_equal 0, file.pos # file was not rewound after reading
|
||||
end
|
||||
end
|
||||
|
||||
def test_binary_file
|
||||
params = parse_multipart('binary_file')
|
||||
assert_equal %w(file flowers foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
|
||||
file = params['file']
|
||||
assert_kind_of StringIO, file
|
||||
assert_equal 'file.csv', file.original_filename
|
||||
assert_nil file.content_type
|
||||
assert_equal 'contents', file.read
|
||||
|
||||
file = params['flowers']
|
||||
assert_kind_of StringIO, file
|
||||
assert_equal 'flowers.jpg', file.original_filename
|
||||
assert_equal "image/jpeg", file.content_type
|
||||
assert_equal 19512, file.size
|
||||
#assert_equal File.read(File.dirname(__FILE__) + '/../../../activerecord/test/fixtures/flowers.jpg'), file.read
|
||||
end
|
||||
|
||||
def test_mixed_files
|
||||
params = parse_multipart('mixed_files')
|
||||
assert_equal %w(files foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
|
||||
# Ruby CGI doesn't handle multipart/mixed for us.
|
||||
files = params['files']
|
||||
assert_kind_of String, files
|
||||
files.force_encoding('ASCII-8BIT') if files.respond_to?(:force_encoding)
|
||||
assert_equal 19756, files.size
|
||||
end
|
||||
|
||||
private
|
||||
def parse_multipart(name)
|
||||
File.open(File.join(FIXTURE_PATH, name), 'rb') do |file|
|
||||
params = ActionController::RequestParser.parse_multipart_form_parameters(file, 'AaB03x', file.stat.size, {})
|
||||
assert_equal 0, file.pos # file was rewound after reading
|
||||
params
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
*2.3.0/3.0*
|
||||
|
||||
* Support nested transactions using database savepoints. #383 [Jonathan Viney, Hongli Lai]
|
||||
|
||||
* Added dynamic scopes ala dynamic finders #1648 [Yaroslav Markin]
|
||||
|
||||
* Fixed that ActiveRecord::Base#new_record? should return false (not nil) for existing records #1219 [Yaroslav Markin]
|
||||
|
||||
@@ -53,36 +53,124 @@ module ActiveRecord
|
||||
def delete(sql, name = nil)
|
||||
delete_sql(sql, name)
|
||||
end
|
||||
|
||||
# Checks whether there is currently no transaction active. This is done
|
||||
# by querying the database driver, and does not use the transaction
|
||||
# house-keeping information recorded by #increment_open_transactions and
|
||||
# friends.
|
||||
#
|
||||
# Returns true if there is no transaction active, false if there is a
|
||||
# transaction active, and nil if this information is unknown.
|
||||
#
|
||||
# Not all adapters supports transaction state introspection. Currently,
|
||||
# only the PostgreSQL adapter supports this.
|
||||
def outside_transaction?
|
||||
nil
|
||||
end
|
||||
|
||||
# Runs the given block in a database transaction, and returns the result
|
||||
# of the block.
|
||||
#
|
||||
# == Nested transactions support
|
||||
#
|
||||
# Most databases don't support true nested transactions. At the time of
|
||||
# writing, the only database that supports true nested transactions that
|
||||
# we're aware of, is MS-SQL.
|
||||
#
|
||||
# In order to get around this problem, #transaction will emulate the effect
|
||||
# of nested transactions, by using savepoints:
|
||||
# http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
|
||||
# Savepoints are supported by MySQL and PostgreSQL, but not SQLite3.
|
||||
#
|
||||
# It is safe to call this method if a database transaction is already open,
|
||||
# i.e. if #transaction is called within another #transaction block. In case
|
||||
# of a nested call, #transaction will behave as follows:
|
||||
#
|
||||
# - The block will be run without doing anything. All database statements
|
||||
# that happen within the block are effectively appended to the already
|
||||
# open database transaction.
|
||||
# - However, if +:requires_new+ is set, the block will be wrapped in a
|
||||
# database savepoint acting as a sub-transaction.
|
||||
#
|
||||
# === Caveats
|
||||
#
|
||||
# MySQL doesn't support DDL transactions. If you perform a DDL operation,
|
||||
# then any created savepoints will be automatically released. For example,
|
||||
# if you've created a savepoint, then you execute a CREATE TABLE statement,
|
||||
# then the savepoint that was created will be automatically released.
|
||||
#
|
||||
# This means that, on MySQL, you shouldn't execute DDL operations inside
|
||||
# a #transaction call that you know might create a savepoint. Otherwise,
|
||||
# #transaction will raise exceptions when it tries to release the
|
||||
# already-automatically-released savepoints:
|
||||
#
|
||||
# Model.connection.transaction do # BEGIN
|
||||
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
|
||||
# Model.connection.create_table(...)
|
||||
# # active_record_1 now automatically released
|
||||
# end # RELEASE SAVEPOINT active_record_1 <--- BOOM! database error!
|
||||
# end
|
||||
def transaction(options = {})
|
||||
options.assert_valid_keys :requires_new, :joinable
|
||||
|
||||
last_transaction_joinable = @transaction_joinable
|
||||
if options.has_key?(:joinable)
|
||||
@transaction_joinable = options[:joinable]
|
||||
else
|
||||
@transaction_joinable = true
|
||||
end
|
||||
requires_new = options[:requires_new] || !last_transaction_joinable
|
||||
|
||||
# Wrap a block in a transaction. Returns result of block.
|
||||
def transaction(start_db_transaction = true)
|
||||
transaction_open = false
|
||||
begin
|
||||
if block_given?
|
||||
if start_db_transaction
|
||||
begin_db_transaction
|
||||
if requires_new || open_transactions == 0
|
||||
if open_transactions == 0
|
||||
begin_db_transaction
|
||||
elsif requires_new
|
||||
create_savepoint
|
||||
end
|
||||
increment_open_transactions
|
||||
transaction_open = true
|
||||
end
|
||||
yield
|
||||
end
|
||||
rescue Exception => database_transaction_rollback
|
||||
if transaction_open
|
||||
if transaction_open && !outside_transaction?
|
||||
transaction_open = false
|
||||
rollback_db_transaction
|
||||
decrement_open_transactions
|
||||
if open_transactions == 0
|
||||
rollback_db_transaction
|
||||
else
|
||||
rollback_to_savepoint
|
||||
end
|
||||
end
|
||||
raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
|
||||
raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback)
|
||||
end
|
||||
ensure
|
||||
if transaction_open
|
||||
@transaction_joinable = last_transaction_joinable
|
||||
|
||||
if outside_transaction?
|
||||
@open_transactions = 0
|
||||
elsif transaction_open
|
||||
decrement_open_transactions
|
||||
begin
|
||||
commit_db_transaction
|
||||
if open_transactions == 0
|
||||
commit_db_transaction
|
||||
else
|
||||
release_savepoint
|
||||
end
|
||||
rescue Exception => database_transaction_rollback
|
||||
rollback_db_transaction
|
||||
if open_transactions == 0
|
||||
rollback_db_transaction
|
||||
else
|
||||
rollback_to_savepoint
|
||||
end
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Begins the transaction (and turns off auto-committing).
|
||||
def begin_db_transaction() end
|
||||
|
||||
|
||||
@@ -66,6 +66,12 @@ module ActiveRecord
|
||||
def supports_ddl_transactions?
|
||||
false
|
||||
end
|
||||
|
||||
# Does this adapter support savepoints? PostgreSQL and MySQL do, SQLite
|
||||
# does not.
|
||||
def supports_savepoints?
|
||||
false
|
||||
end
|
||||
|
||||
# Should primary key values be selected from their corresponding
|
||||
# sequence before the insert statement? If true, next_sequence_value
|
||||
@@ -160,6 +166,23 @@ module ActiveRecord
|
||||
@open_transactions -= 1
|
||||
end
|
||||
|
||||
def transaction_joinable=(joinable)
|
||||
@transaction_joinable = joinable
|
||||
end
|
||||
|
||||
def create_savepoint
|
||||
end
|
||||
|
||||
def rollback_to_savepoint
|
||||
end
|
||||
|
||||
def release_savepoint
|
||||
end
|
||||
|
||||
def current_savepoint_name
|
||||
"active_record_#{open_transactions}"
|
||||
end
|
||||
|
||||
def log_info(sql, name, ms)
|
||||
if @logger && @logger.debug?
|
||||
name = '%s (%.1fms)' % [name || 'SQL', ms]
|
||||
|
||||
@@ -210,6 +210,10 @@ module ActiveRecord
|
||||
def supports_migrations? #:nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
def supports_savepoints? #:nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
def native_database_types #:nodoc:
|
||||
NATIVE_DATABASE_TYPES
|
||||
@@ -349,6 +353,17 @@ module ActiveRecord
|
||||
# Transactions aren't supported
|
||||
end
|
||||
|
||||
def create_savepoint
|
||||
execute("SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def rollback_to_savepoint
|
||||
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def release_savepoint
|
||||
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def add_limit_offset!(sql, options) #:nodoc:
|
||||
if limit = options[:limit]
|
||||
|
||||
@@ -272,6 +272,10 @@ module ActiveRecord
|
||||
def supports_ddl_transactions?
|
||||
true
|
||||
end
|
||||
|
||||
def supports_savepoints?
|
||||
true
|
||||
end
|
||||
|
||||
# Returns the configured supported identifier length supported by PostgreSQL,
|
||||
# or report the default of 63 on PostgreSQL 7.x.
|
||||
@@ -528,45 +532,26 @@ module ActiveRecord
|
||||
def rollback_db_transaction
|
||||
execute "ROLLBACK"
|
||||
end
|
||||
|
||||
# ruby-pg defines Ruby constants for transaction status,
|
||||
# ruby-postgres does not.
|
||||
PQTRANS_IDLE = defined?(PGconn::PQTRANS_IDLE) ? PGconn::PQTRANS_IDLE : 0
|
||||
|
||||
# Check whether a transaction is active.
|
||||
def transaction_active?
|
||||
@connection.transaction_status != PQTRANS_IDLE
|
||||
end
|
||||
|
||||
# Wrap a block in a transaction. Returns result of block.
|
||||
def transaction(start_db_transaction = true)
|
||||
transaction_open = false
|
||||
begin
|
||||
if block_given?
|
||||
if start_db_transaction
|
||||
begin_db_transaction
|
||||
transaction_open = true
|
||||
end
|
||||
yield
|
||||
end
|
||||
rescue Exception => database_transaction_rollback
|
||||
if transaction_open && transaction_active?
|
||||
transaction_open = false
|
||||
rollback_db_transaction
|
||||
end
|
||||
raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
|
||||
end
|
||||
ensure
|
||||
if transaction_open && transaction_active?
|
||||
begin
|
||||
commit_db_transaction
|
||||
rescue Exception => database_transaction_rollback
|
||||
rollback_db_transaction
|
||||
raise
|
||||
end
|
||||
|
||||
if defined?(PGconn::PQTRANS_IDLE)
|
||||
# The ruby-pg driver supports inspecting the transaction status,
|
||||
# while the ruby-postgres driver does not.
|
||||
def outside_transaction?
|
||||
@connection.transaction_status == PGconn::PQTRANS_IDLE
|
||||
end
|
||||
end
|
||||
|
||||
def create_savepoint
|
||||
execute("SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def rollback_to_savepoint
|
||||
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def release_savepoint
|
||||
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
# SCHEMA STATEMENTS ========================================
|
||||
|
||||
|
||||
@@ -516,7 +516,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
|
||||
|
||||
all_loaded_fixtures.update(fixtures_map)
|
||||
|
||||
connection.transaction(connection.open_transactions.zero?) do
|
||||
connection.transaction(:requires_new => true) do
|
||||
fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
|
||||
fixtures.each { |fixture| fixture.insert_fixtures }
|
||||
|
||||
@@ -937,6 +937,7 @@ module ActiveRecord
|
||||
@@already_loaded_fixtures[self.class] = @loaded_fixtures
|
||||
end
|
||||
ActiveRecord::Base.connection.increment_open_transactions
|
||||
ActiveRecord::Base.connection.transaction_joinable = false
|
||||
ActiveRecord::Base.connection.begin_db_transaction
|
||||
# Load fixtures for every test.
|
||||
else
|
||||
|
||||
@@ -53,11 +53,6 @@ module ActiveRecord
|
||||
before_save :raise_on_session_data_overflow!
|
||||
|
||||
class << self
|
||||
# Don't try to reload ARStore::Session in dev mode.
|
||||
def reloadable? #:nodoc:
|
||||
false
|
||||
end
|
||||
|
||||
def data_column_size_limit
|
||||
@data_column_size_limit ||= columns_hash[@@data_column_name].limit
|
||||
end
|
||||
|
||||
@@ -120,16 +120,66 @@ module ActiveRecord
|
||||
# end
|
||||
#
|
||||
# One should restart the entire transaction if a StatementError occurred.
|
||||
#
|
||||
# == Nested transactions
|
||||
#
|
||||
# #transaction calls can be nested. By default, this makes all database
|
||||
# statements in the nested transaction block become part of the parent
|
||||
# transaction. For example:
|
||||
#
|
||||
# User.transaction do
|
||||
# User.create(:username => 'Kotori')
|
||||
# User.transaction do
|
||||
# User.create(:username => 'Nemu')
|
||||
# raise ActiveRecord::Rollback
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# User.find(:all) # => empty
|
||||
#
|
||||
# It is also possible to requires a sub-transaction by passing
|
||||
# <tt>:requires_new => true</tt>. If anything goes wrong, the
|
||||
# database rolls back to the beginning of the sub-transaction
|
||||
# without rolling back the parent transaction. For example:
|
||||
#
|
||||
# User.transaction do
|
||||
# User.create(:username => 'Kotori')
|
||||
# User.transaction(:requires_new => true) do
|
||||
# User.create(:username => 'Nemu')
|
||||
# raise ActiveRecord::Rollback
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# User.find(:all) # => Returns only Kotori
|
||||
#
|
||||
# Most databases don't support true nested transactions. At the time of
|
||||
# writing, the only database that we're aware of that supports true nested
|
||||
# transactions, is MS-SQL. Because of this, Active Record emulates nested
|
||||
# transactions by using savepoints. See
|
||||
# http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
|
||||
# for more information about savepoints.
|
||||
#
|
||||
# === Caveats
|
||||
#
|
||||
# If you're on MySQL, then do not use DDL operations in nested transactions
|
||||
# blocks that are emulated with savepoints. That is, do not execute statements
|
||||
# like 'CREATE TABLE' inside such blocks. This is because MySQL automatically
|
||||
# releases all savepoints upon executing a DDL operation. When #transaction
|
||||
# is finished and tries to release the savepoint it created earlier, a
|
||||
# database error will occur because the savepoint has already been
|
||||
# automatically released. The following example demonstrates the problem:
|
||||
#
|
||||
# Model.connection.transaction do # BEGIN
|
||||
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
|
||||
# Model.connection.create_table(...) # active_record_1 now automatically released
|
||||
# end # RELEASE savepoint active_record_1
|
||||
# # ^^^^ BOOM! database error!
|
||||
# end
|
||||
module ClassMethods
|
||||
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
|
||||
def transaction(&block)
|
||||
connection.increment_open_transactions
|
||||
|
||||
begin
|
||||
connection.transaction(connection.open_transactions == 1, &block)
|
||||
ensure
|
||||
connection.decrement_open_transactions
|
||||
end
|
||||
def transaction(options = {}, &block)
|
||||
# See the ConnectionAdapters::DatabaseStatements#transaction API docs.
|
||||
connection.transaction(options, &block)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -18,11 +18,43 @@ class DefaultTest < ActiveRecord::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
if current_adapter?(:MysqlAdapter)
|
||||
if current_adapter?(:PostgreSQLAdapter, :FirebirdAdapter, :OpenBaseAdapter, :OracleAdapter)
|
||||
def test_default_integers
|
||||
default = Default.new
|
||||
assert_instance_of Fixnum, default.positive_integer
|
||||
assert_equal 1, default.positive_integer
|
||||
assert_instance_of Fixnum, default.negative_integer
|
||||
assert_equal -1, default.negative_integer
|
||||
assert_instance_of BigDecimal, default.decimal_number
|
||||
assert_equal BigDecimal.new("2.78"), default.decimal_number
|
||||
end
|
||||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter)
|
||||
def test_multiline_default_text
|
||||
# older postgres versions represent the default with escapes ("\\012" for a newline)
|
||||
assert ( "--- []\n\n" == Default.columns_hash['multiline_default'].default ||
|
||||
"--- []\\012\\012" == Default.columns_hash['multiline_default'].default)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#MySQL 5 and higher is quirky with not null text/blob columns.
|
||||
#With MySQL Text/blob columns cannot have defaults. If the column is not null MySQL will report that the column has a null default
|
||||
#but it behaves as though the column had a default of ''
|
||||
if current_adapter?(:MysqlAdapter)
|
||||
class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase
|
||||
# ActiveRecord::Base#create! (and #save and other related methods) will
|
||||
# open a new transaction. When in transactional fixtures mode, this will
|
||||
# cause ActiveRecord to create a new savepoint. However, since MySQL doesn't
|
||||
# support DDL transactions, creating a table will result in any created
|
||||
# savepoints to be automatically released. This in turn causes the savepoint
|
||||
# release code in AbstractAdapter#transaction to fail.
|
||||
#
|
||||
# We don't want that to happen, so we disable transactional fixtures here.
|
||||
self.use_transactional_fixtures = false
|
||||
|
||||
# MySQL 5 and higher is quirky with not null text/blob columns.
|
||||
# With MySQL Text/blob columns cannot have defaults. If the column is not
|
||||
# null MySQL will report that the column has a null default
|
||||
# but it behaves as though the column had a default of ''
|
||||
def test_mysql_text_not_null_defaults
|
||||
klass = Class.new(ActiveRecord::Base)
|
||||
klass.table_name = 'test_mysql_text_not_null_defaults'
|
||||
@@ -48,8 +80,7 @@ class DefaultTest < ActiveRecord::TestCase
|
||||
ensure
|
||||
klass.connection.drop_table(klass.table_name) rescue nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
# MySQL uses an implicit default 0 rather than NULL unless in strict mode.
|
||||
# We use an implicit NULL so schema.rb is compatible with other databases.
|
||||
def test_mysql_integer_not_null_defaults
|
||||
@@ -77,24 +108,4 @@ class DefaultTest < ActiveRecord::TestCase
|
||||
klass.connection.drop_table(klass.table_name) rescue nil
|
||||
end
|
||||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter, :FirebirdAdapter, :OpenBaseAdapter, :OracleAdapter)
|
||||
def test_default_integers
|
||||
default = Default.new
|
||||
assert_instance_of Fixnum, default.positive_integer
|
||||
assert_equal 1, default.positive_integer
|
||||
assert_instance_of Fixnum, default.negative_integer
|
||||
assert_equal -1, default.negative_integer
|
||||
assert_instance_of BigDecimal, default.decimal_number
|
||||
assert_equal BigDecimal.new("2.78"), default.decimal_number
|
||||
end
|
||||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter)
|
||||
def test_multiline_default_text
|
||||
# older postgres versions represent the default with escapes ("\\012" for a newline)
|
||||
assert ( "--- []\n\n" == Default.columns_hash['multiline_default'].default ||
|
||||
"--- []\\012\\012" == Default.columns_hash['multiline_default'].default)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,7 +34,7 @@ rescue LoadError
|
||||
end
|
||||
|
||||
ActiveRecord::Base.connection.class.class_eval do
|
||||
IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/]
|
||||
IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/]
|
||||
|
||||
def execute_with_query_record(sql, name = nil, &block)
|
||||
$queries_executed ||= []
|
||||
|
||||
@@ -213,11 +213,104 @@ class TransactionTest < ActiveRecord::TestCase
|
||||
assert Topic.find(2).approved?, "Second should still be approved"
|
||||
end
|
||||
|
||||
def test_invalid_keys_for_transaction
|
||||
assert_raises ArgumentError do
|
||||
Topic.transaction :nested => true do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_force_savepoint_in_nested_transaction
|
||||
Topic.transaction do
|
||||
@first.approved = true
|
||||
@second.approved = false
|
||||
@first.save!
|
||||
@second.save!
|
||||
|
||||
begin
|
||||
Topic.transaction :requires_new => true do
|
||||
@first.happy = false
|
||||
@first.save!
|
||||
raise
|
||||
end
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
||||
assert @first.reload.approved?
|
||||
assert !@second.reload.approved?
|
||||
end if Topic.connection.supports_savepoints?
|
||||
|
||||
def test_no_savepoint_in_nested_transaction_without_force
|
||||
Topic.transaction do
|
||||
@first.approved = true
|
||||
@second.approved = false
|
||||
@first.save!
|
||||
@second.save!
|
||||
|
||||
begin
|
||||
Topic.transaction do
|
||||
@first.approved = false
|
||||
@first.save!
|
||||
raise
|
||||
end
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
||||
assert !@first.reload.approved?
|
||||
assert !@second.reload.approved?
|
||||
end if Topic.connection.supports_savepoints?
|
||||
|
||||
def test_many_savepoints
|
||||
Topic.transaction do
|
||||
@first.content = "One"
|
||||
@first.save!
|
||||
|
||||
begin
|
||||
Topic.transaction :requires_new => true do
|
||||
@first.content = "Two"
|
||||
@first.save!
|
||||
|
||||
begin
|
||||
Topic.transaction :requires_new => true do
|
||||
@first.content = "Three"
|
||||
@first.save!
|
||||
|
||||
begin
|
||||
Topic.transaction :requires_new => true do
|
||||
@first.content = "Four"
|
||||
@first.save!
|
||||
raise
|
||||
end
|
||||
rescue
|
||||
end
|
||||
|
||||
@three = @first.reload.content
|
||||
raise
|
||||
end
|
||||
rescue
|
||||
end
|
||||
|
||||
@two = @first.reload.content
|
||||
raise
|
||||
end
|
||||
rescue
|
||||
end
|
||||
|
||||
@one = @first.reload.content
|
||||
end
|
||||
|
||||
assert_equal "One", @one
|
||||
assert_equal "Two", @two
|
||||
assert_equal "Three", @three
|
||||
end if Topic.connection.supports_savepoints?
|
||||
|
||||
uses_mocha 'mocking connection.commit_db_transaction' do
|
||||
def test_rollback_when_commit_raises
|
||||
Topic.connection.expects(:begin_db_transaction)
|
||||
Topic.connection.expects(:transaction_active?).returns(true) if current_adapter?(:PostgreSQLAdapter)
|
||||
Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
|
||||
Topic.connection.expects(:outside_transaction?).returns(false)
|
||||
Topic.connection.expects(:rollback_db_transaction)
|
||||
|
||||
assert_raise RuntimeError do
|
||||
@@ -227,6 +320,38 @@ class TransactionTest < ActiveRecord::TestCase
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE)
|
||||
def test_outside_transaction_works
|
||||
assert Topic.connection.outside_transaction?
|
||||
Topic.connection.begin_db_transaction
|
||||
assert !Topic.connection.outside_transaction?
|
||||
Topic.connection.rollback_db_transaction
|
||||
assert Topic.connection.outside_transaction?
|
||||
end
|
||||
|
||||
uses_mocha 'mocking connection.rollback_db_transaction' do
|
||||
def test_rollback_wont_be_executed_if_no_transaction_active
|
||||
assert_raise RuntimeError do
|
||||
Topic.transaction do
|
||||
Topic.connection.rollback_db_transaction
|
||||
Topic.connection.expects(:rollback_db_transaction).never
|
||||
raise "Rails doesn't scale!"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_open_transactions_count_is_reset_to_zero_if_no_transaction_active
|
||||
Topic.transaction do
|
||||
Topic.transaction do
|
||||
Topic.connection.rollback_db_transaction
|
||||
end
|
||||
assert_equal 0, Topic.connection.open_transactions
|
||||
end
|
||||
assert_equal 0, Topic.connection.open_transactions
|
||||
end
|
||||
end
|
||||
|
||||
def test_sqlite_add_column_in_transaction_raises_statement_invalid
|
||||
return true unless current_adapter?(:SQLite3Adapter, :SQLiteAdapter)
|
||||
@@ -282,6 +407,45 @@ class TransactionTest < ActiveRecord::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
|
||||
self.use_transactional_fixtures = true
|
||||
fixtures :topics
|
||||
|
||||
def test_automatic_savepoint_in_outer_transaction
|
||||
@first = Topic.find(1)
|
||||
|
||||
begin
|
||||
Topic.transaction do
|
||||
@first.approved = true
|
||||
@first.save!
|
||||
raise
|
||||
end
|
||||
rescue
|
||||
assert !@first.reload.approved?
|
||||
end
|
||||
end
|
||||
|
||||
def test_no_automatic_savepoint_for_inner_transaction
|
||||
@first = Topic.find(1)
|
||||
|
||||
Topic.transaction do
|
||||
@first.approved = true
|
||||
@first.save!
|
||||
|
||||
begin
|
||||
Topic.transaction do
|
||||
@first.approved = false
|
||||
@first.save!
|
||||
raise
|
||||
end
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
||||
assert !@first.reload.approved?
|
||||
end
|
||||
end if Topic.connection.supports_savepoints?
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter)
|
||||
class ConcurrentTransactionTest < TransactionTest
|
||||
use_concurrent_connections
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
*2.3.0 [Edge]*
|
||||
|
||||
* TimeWithZone#xmlschema accepts optional fraction_digits argument [#1725 state:resolved] [Nicholas Dainty]
|
||||
|
||||
* Object#tap shim for Ruby < 1.8.7. Similar to Object#returning, tap yields self then returns self. [Jeremy Kemper]
|
||||
array.select { ... }.tap(&:inspect).map { ... }
|
||||
|
||||
|
||||
@@ -102,6 +102,6 @@ class Object
|
||||
# Person.try(:find, 1)
|
||||
# @people.try(:map) {|p| p.name}
|
||||
def try(method, *args, &block)
|
||||
send(method, *args, &block) if respond_to?(method, true)
|
||||
send(method, *args, &block) unless self.nil?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -99,8 +99,12 @@ module ActiveSupport
|
||||
"#{time.strftime('%a, %d %b %Y %H:%M:%S')} #{zone} #{formatted_offset}"
|
||||
end
|
||||
|
||||
def xmlschema
|
||||
"#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{formatted_offset(true, 'Z')}"
|
||||
def xmlschema(fraction_digits = 0)
|
||||
fraction = if fraction_digits > 0
|
||||
".%i" % time.usec.to_s[0, fraction_digits]
|
||||
end
|
||||
|
||||
"#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(true, 'Z')}"
|
||||
end
|
||||
alias_method :iso8601, :xmlschema
|
||||
|
||||
|
||||
@@ -256,21 +256,13 @@ class ObjectTryTest < Test::Unit::TestCase
|
||||
def test_nonexisting_method
|
||||
method = :undefined_method
|
||||
assert !@string.respond_to?(method)
|
||||
assert_nil @string.try(method)
|
||||
assert_raises(NoMethodError) { @string.try(method) }
|
||||
end
|
||||
|
||||
def test_valid_method
|
||||
assert_equal 5, @string.try(:size)
|
||||
end
|
||||
|
||||
def test_valid_private_method
|
||||
class << @string
|
||||
private :size
|
||||
end
|
||||
|
||||
assert_equal 5, @string.try(:size)
|
||||
end
|
||||
|
||||
def test_argument_forwarding
|
||||
assert_equal 'Hey', @string.try(:sub, 'llo', 'y')
|
||||
end
|
||||
@@ -278,4 +270,13 @@ class ObjectTryTest < Test::Unit::TestCase
|
||||
def test_block_forwarding
|
||||
assert_equal 'Hey', @string.try(:sub, 'llo') { |match| 'y' }
|
||||
end
|
||||
|
||||
def test_nil_to_type
|
||||
assert_nil nil.try(:to_s)
|
||||
assert_nil nil.try(:to_i)
|
||||
end
|
||||
|
||||
def test_false_try
|
||||
assert_equal 'false', false.try(:to_s)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -105,6 +105,15 @@ class TimeWithZoneTest < Test::Unit::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
def test_xmlschema_with_fractional_seconds
|
||||
silence_warnings do # silence warnings raised by tzinfo gem
|
||||
@twz += 0.123456 # advance the time by a fraction of a second
|
||||
assert_equal "1999-12-31T19:00:00.123-05:00", @twz.xmlschema(3)
|
||||
assert_equal "1999-12-31T19:00:00.123456-05:00", @twz.xmlschema(6)
|
||||
assert_equal "1999-12-31T19:00:00.123456-05:00", @twz.xmlschema(12)
|
||||
end
|
||||
end
|
||||
|
||||
def test_to_yaml
|
||||
silence_warnings do # silence warnings raised by tzinfo gem
|
||||
assert_equal "--- 1999-12-31 19:00:00 -05:00\n", @twz.to_yaml
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# Make sure the secret is at least 30 characters and all random,
|
||||
# no regular words or you'll be exposed to dictionary attacks.
|
||||
ActionController::Base.session = {
|
||||
:session_key => '_<%= app_name %>_session',
|
||||
:key => '_<%= app_name %>_session',
|
||||
:secret => '<%= app_secret %>'
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ That means the security of this storage depends on this secret (and of the diges
|
||||
|
||||
....................................
|
||||
config.action_controller.session = {
|
||||
:session_key => ‘_app_session’,
|
||||
:key => ‘_app_session’,
|
||||
:secret => ‘0x0dkfj3927dkc7djdh36rkckdfzsg...’
|
||||
}
|
||||
....................................
|
||||
|
||||
@@ -537,7 +537,7 @@ Run `rake gems:install` to install the missing gems.
|
||||
end
|
||||
|
||||
def initialize_metal
|
||||
configuration.middleware.insert_before(:"ActionController::VerbPiggybacking", Rails::Rack::Metal)
|
||||
configuration.middleware.insert_before(:"ActionController::RewindableInput", Rails::Rack::Metal)
|
||||
end
|
||||
|
||||
# Initializes framework-specific settings for each of the loaded frameworks
|
||||
|
||||
Reference in New Issue
Block a user