2015-11-23 8 views
5

Ho questo esempio di esempio di controller che passa nella mia applicazione solo API basata su Rails 4.2.0 e Ruby 2.2.1Rails 5 - Esempio di specifiche controller - i parametri passati a zero ottengono il loro valore impostato su stringa vuota

let!(:params) { { user_token: user_token } } 

    context "- and optional address and contact details params value are received as a nil values -" do 
    it "doesn't set the address and contact details and responds with 201 success", check: true do 
     params.merge!(
     address_street: nil, address_other: nil, city: nil, state: nil, 
     zip_code: nil, phone: nil) 

     post :create, params 

     expect(response).to have_http_status(201) 

     saved_client_id = json_response["id"] 
     saved_client = Client.find_by(id: saved_client_id) 
     expect(saved_client.address_street).to be_nil 
     expect(saved_client.address_other).to be_nil 
     expect(saved_client.city).to be_nil 
     expect(saved_client.state).to be_nil 
     expect(saved_client.zip_code).to be_nil 
     expect(saved_client.phone).to be_nil 
    end 
    end 

Tuttavia valutare la mia applicazione Rails contro 5 (versione bordo) e Ruby 2.2.3 la stessa spec esito negativo con l'errore seguente:

1) Api::V1::ClientsController POST #create when receives valid client details - and optional address and contact details params value are received as nil values - doesn't set the address and contact details and responds with 201 success 
    Failure/Error: expect(saved_client.address_street).to be_nil 
     expected: nil 
      got: "" 
    # ./spec/controllers/api/v1/clients_controller_spec.rb:352:in `block (5 levels) in <top (required)>' 
    # ./spec/rails_helper.rb:61:in `block (3 levels) in <top (required)>' 
    # /home/jignesh/.rvm/gems/[email protected]/gems/database_cleaner-1.5.1/lib/database_cleaner/generic/base.rb:16:in `cleaning' 
    # /home/jignesh/.rvm/gems/[email protected]/gems/database_cleaner-1.5.1/lib/database_cleaner/base.rb:92:in `cleaning' 
    # /home/jignesh/.rvm/gems/[email protected]/gems/database_cleaner-1.5.1/lib/database_cleaner/configuration.rb:86:in `block (2 levels) in cleaning' 
    # /home/jignesh/.rvm/gems/[email protected]/gems/database_cleaner-1.5.1/lib/database_cleaner/configuration.rb:87:in `call' 
    # /home/jignesh/.rvm/gems/[email protected]/gems/database_cleaner-1.5.1/lib/database_cleaner/configuration.rb:87:in `cleaning' 
# ./spec/rails_helper.rb:60:in `block (2 levels) in <top (required)>' 

ho ispezionato la ferrovia s codice sorgente in pochi punti e ha scoperto che i valori nil vengono trasformati in valori vuoti prima di raggiungere la logica di azione mirata del controllore.

Questo comportamento modificato sta impostando gli attributi su stringhe vuote, quando dovrebbero essere nulle.

In Gemfile di mia app (per usare Rails 5) Ho specificato Rails utilizzando seguente codice:

gem 'rails', git: 'https://github.com/rails/rails.git' 

gem 'rack', :git => 'https://github.com/rack/rack.git' 
gem 'arel', :git => 'https://github.com/rails/arel.git' 

e in seguito Gemfile.lock può essere visto (porzioni Gem e dipendenze troncato per farla breve) :

GIT 
    remote: git://github.com/capistrano/rbenv.git 
    revision: 6f1216cfe0a6b4ac23ca4eaf8acf012e8165d247 
    specs: 
    capistrano-rbenv (2.0.3) 
     capistrano (~> 3.1) 
     sshkit (~> 1.3) 

GIT 
    remote: https://github.com/rack/rack.git 
    revision: c393176b0edf3e5d06cabbb6eb9d9c7a07b2afa7 
    specs: 
    rack (2.0.0.alpha) 
     json 

GIT 
    remote: https://github.com/rails/arel.git 
    revision: 3c429c5d86e9e2201c2a35d934ca6a8911c18e69 
    specs: 
    arel (7.0.0.alpha) 

GIT 
    remote: https://github.com/rails/rails.git 
    revision: 58df2f4b4abcce0b698c2540da215a565c24cbc9 
    specs: 
    actionmailer (5.0.0.alpha) 
     actionpack (= 5.0.0.alpha) 
     actionview (= 5.0.0.alpha) 
     activejob (= 5.0.0.alpha) 
     mail (~> 2.5, >= 2.5.4) 
     rails-dom-testing (~> 1.0, >= 1.0.5) 
    actionpack (5.0.0.alpha) 
     actionview (= 5.0.0.alpha) 
     activesupport (= 5.0.0.alpha) 
     rack (~> 2.x) 
     rack-test (~> 0.6.3) 
     rails-dom-testing (~> 1.0, >= 1.0.5) 
     rails-html-sanitizer (~> 1.0, >= 1.0.2) 
    actionview (5.0.0.alpha) 
     activesupport (= 5.0.0.alpha) 
     builder (~> 3.1) 
     erubis (~> 2.7.0) 
     rails-dom-testing (~> 1.0, >= 1.0.5) 
     rails-html-sanitizer (~> 1.0, >= 1.0.2) 
    activejob (5.0.0.alpha) 
     activesupport (= 5.0.0.alpha) 
     globalid (>= 0.3.0) 
    activemodel (5.0.0.alpha) 
     activesupport (= 5.0.0.alpha) 
     builder (~> 3.1) 
    activerecord (5.0.0.alpha) 
     activemodel (= 5.0.0.alpha) 
     activesupport (= 5.0.0.alpha) 
     arel (= 7.0.0.alpha) 
    activesupport (5.0.0.alpha) 
     concurrent-ruby (~> 1.0) 
     i18n (~> 0.7) 
     json (~> 1.7, >= 1.7.7) 
     method_source 
     minitest (~> 5.1) 
     tzinfo (~> 1.1) 
    rails (5.0.0.alpha) 
     actionmailer (= 5.0.0.alpha) 
     actionpack (= 5.0.0.alpha) 
     actionview (= 5.0.0.alpha) 
     activejob (= 5.0.0.alpha) 
     activemodel (= 5.0.0.alpha) 
     activerecord (= 5.0.0.alpha) 
     activesupport (= 5.0.0.alpha) 
     bundler (>= 1.3.0, < 2.0) 
     railties (= 5.0.0.alpha) 
     sprockets-rails (>= 2.0.0) 
    railties (5.0.0.alpha) 
     actionpack (= 5.0.0.alpha) 
     activesupport (= 5.0.0.alpha) 
     method_source 
     rake (>= 0.8.7) 
     thor (>= 0.18.1, < 2.0) 
... 
.... 

Qualcuno può, per favore, farmi sapere che cosa ha causato questo cambiamento? Immagino che abbia qualcosa a che fare con il cambiamento in Rails 5 o l'ultimo Rack. È questo un tipo di bug che deve essere corretto nella versione finale o questo è un cambiamento intenzionale.

risposta

8

ho trovato la causa principale del comportamento indicato in: In Rails 5 è causato dal difetto CONTENT_TYPE intestazione essendo impostato 'application/x-www-form-urlencoded' da ActionController :: TestRequest metodo #assign_parameters, tuttavia in Rails 4.2.0 questo non è il Astuccio.

Risultati dettagliati sono riportati di seguito su come sono giunto alla conclusione:

Nel contesto delle params vengono passati nell'esempio spec (illustrato nel mio post questione) l'esecuzione scorre in Rotaie 5 (e la sua versione rack) e Rotaie 4.2.0 (e la sua versione rack) va in modo dettagliato qui di seguito:

Rotaie 5

actionpack/lib azione/_dispatch/http_request.rb # form_data? restituisce true

actionpack/lib/action_dispatch/http_request.rb # metodo POST si presenta come segue:

# Override Rack's POST method to support indifferent access 
def POST 
    fetch_header("action_dispatch.request.request_parameters") do 
    pr = parse_formatted_parameters(params_parsers) do |params| 
     super || {} 
    end 
    self.request_parameters = Request::Utils.normalize_encode_params(pr) 
    end 
rescue ParamsParser::ParseError # one of the parse strategies blew up 
    self.request_parameters = Request::Utils.normalize_encode_params(super || {}) 
    raise 
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e 
    raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}") 
end 
alias :request_parameters :POST 

che, cercando di valutare fetch_header("action_dispatch.request.request_parameters") corre il blocco valore di default che invoca super che rende la chiamata va a rack di Richiesta (/rack-c393176b0edf/lib/rack/request.rb) metodo POST. Ho mostrato il seguente codice di questo metodo con alcune affermazioni di debug che ho inserito:

rack/lib/rack/richiesta.rb # POST

# Returns the data received in the request body. 
    # 
    # This method support both application/x-www-form-urlencoded and 
    # multipart/form-data. 
    def POST 
    puts ">>>>>>>>>>> DEBUG 2" 
    if get_header(RACK_INPUT).nil? 
     puts ">>>>>>>>>>> DEBUG 2.1" 
     raise "Missing rack.input" 
    elsif get_header(RACK_REQUEST_FORM_INPUT) == get_header(RACK_INPUT) 
     puts ">>>>>>>>>>> DEBUG 2.2" 
     get_header(RACK_REQUEST_FORM_HASH) 
    elsif form_data? || parseable_data? 
     puts ">>>>>>>>>>> DEBUG 2.3" 
     unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart) 
     form_vars = get_header(RACK_INPUT).read 

     # Fix for Safari Ajax postings that always append \0 
     # form_vars.sub!(/\0\z/, '') # performance replacement: 
     form_vars.slice!(-1) if form_vars[-1] == ?\0 

     set_header RACK_REQUEST_FORM_VARS, form_vars 
     set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&') 
     get_header(RACK_INPUT).rewind 
     end 
     set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT) 
     get_header RACK_REQUEST_FORM_HASH 
    else 
     puts ">>>>>>>>>>> DEBUG 2.4" 
     {} 
    end 

Con queste dichiarazioni di debug del flusso di esecuzione si è conclusa in ">>>>>>>>>>> DEBUG 2.3". Ci ho anche ispezionato get_header RACK_REQUEST_FORM_HASH ed è stampato

>>>>>>>>>>> get_header RACK_REQUEST_FORM_HASH: {"address_other"=>"", "address_street"=>"", "city"=>"", "client_residence_type_id"=>"", "name"=>"Test Client 1", "phone"=>"", "provider_id"=>"64", "state"=>"", "zip_code"=>""} 

Così il suo metodo parse_query(form_vars, '&') che trasforma i valori nulli per svuotare le stringhe.

Rails 4.2.0

actionpack/lib/action_dispatch/http_request.rb # form_data? restituisce false

actionpack/lib/action_dispatch/http_request.rb # metodo POST si presenta come segue:

# Override Rack's POST method to support indifferent access 
def POST 
    @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) 
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e 
    raise ActionController::BadRequest.new(:request, e) 
end 
alias :request_parameters :POST 

che richiama super che effettua la chiamata va a rack Request (rack-1.6.4/lib/cremagliera /request.rb) Metodo POST. Ho mostrato il seguente codice di questo metodo con alcune affermazioni di debug che ho inserito:

rack-1.6.4/lib/rack/request.rb # parseable_data? restituisce false

cremagliera-1.6.4/lib/cremagliera/request.rb # pubblicare il flusso conclusa in ">>>>>>>>>>> DEBUG 2.4"

def POST 
    puts ">>>>>>>>>>> DEBUG 2" 
    if @env["rack.input"].nil? 
    puts ">>>>>>>>>>> DEBUG 2.1" 
    raise "Missing rack.input" 
    elsif @env["rack.request.form_input"].equal? @env["rack.input"] 
    puts ">>>>>>>>>>> DEBUG 2.2" 
    @env["rack.request.form_hash"] 
    elsif form_data? || parseable_data? 
    puts ">>>>>>>>>>> DEBUG 2.3" 
    unless @env["rack.request.form_hash"] = parse_multipart(env) 
     form_vars = @env["rack.input"].read 

     # Fix for Safari Ajax postings that always append \0 
     # form_vars.sub!(/\0\z/, '') # performance replacement: 
     form_vars.slice!(-1) if form_vars[-1] == ?\0 

     @env["rack.request.form_vars"] = form_vars 
     @env["rack.request.form_hash"] = parse_query({ :query => form_vars, :separator => '&' }) 

     @env["rack.input"].rewind 
    end 
    @env["rack.request.form_input"] = @env["rack.input"] 
    @env["rack.request.form_hash"] 
    else 
    puts ">>>>>>>>>>> DEBUG 2.4" 
    {} 
    end 
end 

Ciò mi porta a notare che in Rails 5 content_mime_type che viene utilizzato internamente da form_data? è impostato e quindi i parametri inviati dall'esempio spec vengono analizzati come parametri del modulo.

Tuttavia in Rails 4.2.0 la content_mime_type non viene trovato impostato che non causa i params presentate da analizzare come form_params.

Rotaie 4.2.0

Il metodo è definito nella content_mime_typeActionDispatch::Http::MimeNegotiation modulo

def content_mime_type 
    @env["action_dispatch.request.content_type"] ||= begin 
     if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/ 
     Mime::Type.lookup($1.strip.downcase) 
     else 
     nil 
     end 
    end 
    end 

che restituisce nil

Rotaie 5

Il content_mime_type metodo è definito nella ActionDispatch::Http::MimeNegotiation modulo

def content_mime_type 
    fetch_header("action_dispatch.request.content_type") do |k| 
     v = if get_header('CONTENT_TYPE') =~ /^([^,\;]*)/ 
     Mime::Type.lookup($1.strip.downcase) 
     else 
     nil 
     end 
     set_header k, v 
    end 
    end 

In questo caso if get_header('CONTENT_TYPE') =~ /^([^,\;]*)/ viene valutata true e quindi Mime::Type.lookup($1.strip.downcase) viene restituito

rotaie 4,2.0

L'intestazione CONTENT_TYPE non viene fissato dal

actionpack (= percorsi, controller_path, azione, parametri {}) Metodo/lib/action_controller/test_case.rb # Def assign_parameters

def assign_parameters(routes, controller_path, action, parameters = {}) 
    parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action) 
    extra_keys = routes.extra_keys(parameters) 
    non_path_parameters = get? ? query_parameters : request_parameters 
    parameters.each do |key, value| 
    if value.is_a?(Array) && (value.frozen? || value.any?(&:frozen?)) 
     value = value.map{ |v| v.duplicable? ? v.dup : v } 
    elsif value.is_a?(Hash) && (value.frozen? || value.any?{ |k,v| v.frozen? }) 
     value = Hash[value.map{ |k,v| [k, v.duplicable? ? v.dup : v] }] 
    elsif value.frozen? && value.duplicable? 
     value = value.dup 
    end 

    if extra_keys.include?(key) 
     non_path_parameters[key] = value 
    else 
     if value.is_a?(Array) 
     value = value.map(&:to_param) 
     else 
     value = value.to_param 
     end 

     path_parameters[key] = value 
    end 
    end 

    # Clear the combined params hash in case it was already referenced. 
    @env.delete("action_dispatch.request.parameters") 

    # Clear the filter cache variables so they're not stale 
    @filtered_parameters = @filtered_env = @filtered_path = nil 

    params = self.request_parameters.dup 
    %w(controller action only_path).each do |k| 
    params.delete(k) 
    params.delete(k.to_sym) 
    end 
    data = params.to_query 

    @env['CONTENT_LENGTH'] = data.length.to_s 
    @env['rack.input'] = StringIO.new(data) 
end 

Rotaie 5

Il CONTENT_TYPE intestazione viene impostata da

012.351.641,061 mila

actionpack/lib/action_controller/test_case.rb # assign_parameters (percorsi, controller_path, azione, parametri, generated_path, query_string_keys) Metodo

def assign_parameters(routes, controller_path, action, parameters, generated_path, query_string_keys) 
    non_path_parameters = {} 
    path_parameters = {} 

    parameters.each do |key, value| 
    if query_string_keys.include?(key) 
     non_path_parameters[key] = value 
    else 
     if value.is_a?(Array) 
     value = value.map(&:to_param) 
     else 
     value = value.to_param 
     end 

     path_parameters[key] = value 
    end 
    end 

    if get? 
    if self.query_string.blank? 
     self.query_string = non_path_parameters.to_query 
    end 
    else 
    if ENCODER.should_multipart?(non_path_parameters) 
     self.content_type = ENCODER.content_type 
     data = ENCODER.build_multipart non_path_parameters 
    else 
     fetch_header('CONTENT_TYPE') do |k| 
     set_header k, 'application/x-www-form-urlencoded' 
     end 

     case content_mime_type.to_sym 
     when nil 
     raise "Unknown Content-Type: #{content_type}" 
     when :json 
     data = ActiveSupport::JSON.encode(non_path_parameters) 
     when :xml 
     data = non_path_parameters.to_xml 
     when :url_encoded_form 
     data = non_path_parameters.to_query 
     else 
     @custom_param_parsers[content_mime_type] = ->(_) { non_path_parameters } 
     data = non_path_parameters.to_query 
     end 
    end 

    set_header 'CONTENT_LENGTH', data.length.to_s 
    set_header 'rack.input', StringIO.new(data) 
    end 

    fetch_header("PATH_INFO") do |k| 
    set_header k, generated_path 
    end 
    path_parameters[:controller] = controller_path 
    path_parameters[:action] = action 

    self.path_parameters = path_parameters 
end 

Come visibile POST richiesta seguente codice che viene eseguito imposta l'intestazione CONTENT_TYPE ad un valore predefinito 'application/x-www-form-urlencoded'

 fetch_header('CONTENT_TYPE') do |k| 
     set_header k, 'application/x-www-form-urlencoded' 
     end 

Grazie.

7

Sembra che questo problema sia noto ma non ancora risolto. V'è una soluzione indicata in questo numero: https://github.com/rspec/rspec-rails/issues/1655

ho provato e usato questo nel mio test di controllo RSpec e invia i dati attraverso correttamente:

before { request.env['CONTENT_TYPE'] = 'application/json' }

2

Una variante della risposta di violetaria è quello di aggiungi as: :json alle tue richieste, ad es

post :create, params: {…}, as: :json 
Problemi correlati