I’m having an issue rendering XML in the response body of a request in a Rails 4 application. In the example below the response body is blank. I’ve put a debugger in the template so I know it runs through it but doesn’t render anything out.
I created a simple rails app to demonstrate the issue I’m having using builder to return xml. Can anyone point me to the (probably stupid simple) problem with this example?
Here are the controller, template, and test:
controllers/bars_controller.rb
require 'builder'
class BarsController < ApplicationController
before_action :set_bar, only: [:show]
# GET /bars/1
# GET /bars/1.json
def show
#xml = Builder::XmlMarkup.new
render template: 'bars/show.xml.builder', formats: [:xml]
end
private
# Use callbacks to share common setup or constraints between actions.
def set_bar
#bar = Bar.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def bar_params
params.require(:bar).permit(:foo, :bar)
end
end
/views/bars/show.xml.builder
#xml.instruct!
#xml.bar do
#xml.foo(#bar.foo)
#xml.bar(#bar.bar)
end
/test/controllers/bars_controller_test.rb
require 'test_helper'
class BarsControllerTest < ActionController::TestCase
setup do
#bar = bars(:one)
end
test "should show bar" do
get :show, id: #bar
assert_response :success
assert_match "<bar>", response.body
end
end
debugging session
1: #xml.instruct!
2: binding.pry
=> 3: #xml.bar do
4: #xml.foo(#bar.foo)
5: #xml.bar(#bar.bar)
6: end
[2] pry(#<#<Class:0x007fc669e9f610>>)> #xml.bar do
[2] pry(#<#<Class:0x007fc669e9f610>>)* #xml.foo(#bar.foo)
[2] pry(#<#<Class:0x007fc669e9f610>>)* #xml.bar(#bar.bar)
[2] pry(#<#<Class:0x007fc669e9f610>>)* end
=> "<?xml version=\"1.0\" encoding=\"UTF-8\"?><bar><foo>MyString</foo><bar>MyString</bar></bar>"
It looks like creating the instance of the Builder::XmlMarkup.new is your problem. Remove the explicit creation of the builder so your controller looks like this:
def show
# You can also simplify by removing "bars/"
render 'bars/show.xml.builder', formats: [:xml]
end
And your view should look like this:
xml.instruct!
xml.bar do
xml.foo(#bar.foo)
xml.bar(#bar.bar)
end
Related
I'm working on a Rails API and I'm using strong parameters in the controllers. I have a request spec that is failing for one model but works on all other models. The controllers for each model are pretty much all the same.
As you can see below in the spec, the request body SHOULD be { "tag": { "name": "a good name" }}. However, this spec is using { "name": "a good name" } which SHOULD be invalid because it's missing he "tag" key. This same spec for the same controller functionality works fine for plenty of other models.
Another interesting twist is that if I change the controller's strong parameter to params.require(:not_tag).permit(:name) it throws an error for not including the "not_tag" key.
Ruby: 2.6.5p114
Rails: 6.0.1
Expected response status: 422
Received response status: 201
Controller
class TagsController < ApplicationController
before_action :set_tag, only: [:show, :update, :destroy]
# Other methods...
# POST /tags
def create
#tag = Tag.new(tag_params)
if #tag.save
render "tags/show", status: :created
else
render json: #tag.errors, status: :unprocessable_entity
end
end
# Other methods...
private
# Use callbacks to share common setup or constraints between actions.
def set_tag
#tag = Tag.find_by(id: params[:id])
if !#tag
object_not_found
end
end
# Only allow a trusted parameter "white list" through.
def tag_params
params.require(:tag).permit(:name)
end
# render response for objects that aren't found
def object_not_found
render :json => {:error => "404 not found"}.to_json, status: :not_found
end
end
Request Spec
require 'rails_helper'
include AuthHelper
include Requests::JsonHelpers
RSpec.describe "Tags", type: :request do
before(:context) do
#user = create(:admin)
#headers = AuthHelper.authenticated_header(#user)
end
# A bunch of other specs...
describe "POST /api/tags" do
context "while authenticated" do
it "fails to create a tag from malformed body with 422 status" do
malformed_body = { "name": "malformed" }.to_json
post "/api/tags", params: malformed_body, headers: #headers
expect(response).to have_http_status(422)
expect(Tag.all.length).to eq 0
end
end
end
# A bunch of other specs...
after(:context) do
#user.destroy
#headers = nil
end
end
This behaviour is because of the ParamsWrapper functionality which is enabled by default in Rails 6. wrap_parameters wraps the parameters that are received, into a nested hash. Hence, this allows clients to send requests without nesting data in the root elements.
For example, in a model named Tag, it basically converts
{
name: "Some name",
age: "Some age"
}
to
{
tag:
{
name: "Some name",
age: "Some age"
}
}
However, as you see in your test, if you change the required key to not_tag, the wrapping breaks the API call, as expected.
This configuration can be changed using the config/initializers/wrap_parameters.rb file. In that file, you could set wrap_parameters format: [:json] to wrap_parameters format: [] to disallow such wrapping of parameters.
I'm getting something really weird when I execute my tests with rails test.
I'm trying to understand and create my first controller tests and I get something unexpected.
I get this error in the console:
F
Failure:
FeedbacksControllerTest#test_should_create_resource [/Users/Daniel/GitHub/haeapua-rails/test/controllers/feedbacks_controller_test.rb:15]:
Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/>
Response body: <html><body>You are being redirected.</body></html>
here is my test
test "should create resource" do
assert_difference 'Feedback.count', 1 do
post feedbacks_url, params: { feedback: { email: "me#myemail.com", summary: "my feedback", rating: 5 } }
end
assert_response :success
assert_redirected_to root_url
end
here is my controller
class FeedbacksController < ApplicationController
# GET /feedbacks/new
def new
#feedback = Feedback.new
# #feedback.user = current_user if user_signed_in?
end
# POST /feedbacks
def create
#feedback = Feedback.new(feedback_params)
# #feedback.user = current_user if user_signed_in?
if #feedback.save
# flash[:info] = "We got it, thanks. Someone will contact you ASAP once we read it"
redirect_to root_url
else
render :new
end
end
private
def feedback_params
params.require(:feedback).permit(:email, :summary, :rating)
end
end
When I put a "puts root_url" in my feedback controller just before the redirect_to I get the value "http://www.example.com/". I search for root_url in my whole code and the only part I have it is in the Controller and in my routes root to: 'static_pages#concept'
I'm using devise, do you think it could be because of that? I'm a bit lost and don't know where to start looking!
It's a weird quirk but rails has two types of url_helpers for routes. something_url and something_path. The first is an absolute path, http://www.awesome.com/something) the second is the relative path (/someurl).
Unless you have a domain name set explicitly on your test environment, use the _path helpers in your tests. If you want to use the _url helpers then you need to configure a base url in your application's test environment (config/environments/test.rb)
Maybe some default config is setting this value. https://github.com/teamcapybara/capybara/blob/master/lib/capybara.rb#L452
Capybara.configure do |config|
# ...
config.default_host = "http://www.example.com"
# ...
end
You have to override this setting in your project config.
If it's not capybara it could be rails issue ? From the input of this thread https://github.com/rspec/rspec-rails/issues/1275 I would try something like this
# config/environment/test.rb
config.action_controller.default_url_options = {
host: '0.0.0.0:3000' # or whatever your host is
}
In the thread I found:
# putting this into initializer of test framework
[ApplicationController, ActionController::Base].each do |klass|
klass.class_eval do
def default_url_options(options = {})
{ :host => "example.com" }.merge(options) # or localhost:8888 or 0.0.0.0:3000 or whatever your server is
end
end
end
I'm trying to set important headers with ActionDispatch request helper method in my specs:
RSpec.describe API::V1::FoosController, type: :request do
describe 'GET #index' do
context 'common case' do
request.env.merge!({'HTTP_FOO': 'FOO'})
get api_foos_path, {}, {Accept: Mime::JSON}
end
end
end
But this header (and generally any header set with request) disappears when it comes to controller:
class API::V1::FoosController < ApplicationController
respond_to :json, :xml
def index
request.env.has_key? 'HTTP_FOO' # false
respond_with serialize_models(Foo.all)
end
# ...
end
Why does it happen and how do I set it properly?
Setting the header with request.header or #request.header does the same.
P.S.: I know I can set headers as a third parameter of Rack::Test::Methods helpers, but I don't want to violate DRY - I'd like Mime formats only to be defined there.
Please try like this:
request.env['HTTP_FOO_HEADER'] = 'foo header'
Use controller.request.headers:
controller.request.headers['HTTP_FOO'] = 'FOO'
I can verify that this approach works in Rails 4.2.5, as this is lifted directly from real-world code.
Our tests look like this:
describe SomeController, type: :controller do
let(:current_user) { create :user }
before :each do
controller.request.headers['Authorization'] = "APIKey #{current_usser.api_key}"
end
end
And our ApplicationController looks (more or less) like this:
before_action :authenticate_request
def authenticate_request
key = request.headers['Authorization']
user = User.find_by(api_key: key)
# raise AuthenticationError unless user, etc etc
end
We have a custom exception app that has been raising (fail safe) exceptions (the application equivalent of having an exception in a rescue block).
I think I've fixed it, but am finding it hard to test. It's an unrouted controller, so I can't use controller tests (require routing).
i.e. I have Rails.configuration.exceptions_app = ExceptionController.action(:show), not Rails.configuration.exceptions_app = self.routes.
Basically what I think I need to do is
Generate a test request request = ActionDispatch::TestRequest.new
include Rack::Test or maybe mimic behavior in ActiveSupport::IntegrationTest
Set #app = ExceptionsController.action(:show)
Fake an exception request.env.merge! 'action_dispatch.exception' => ActionController::RoutingError.new(:foo)
Test response = #app.call(request.env)
Assert no exception is raised and correct response body and status
Problems:
The env needs
a warden / devise session with current_user request.env['warden'] = spy(Warden) and request.session = ActionDispatch::Integration::Session.new(#app)
to manipulate request formats so that I can check that a request without an accept defaults to json request.any?(:json)? constraints: { default: :json } ? `request.accept = "application/javascript"
work work with the respond_with responder
set action_dispatch.show_exceptions, consider all requests local, etc request.env["action_dispatch.show_detailed_exceptions"] = true
Also, I considered building a ActionDispatch::ShowException.new(app, ExceptionController.new) or a small rack app
But our gem has no tests and I haven't been able to apply anything that I've read in exception handling gems (most work at the rescue_action_in_public level or mix in to ShowException) or in the Rails source code
This is a Rails 4.2 app tested via Rspec and Capybara.
Thoughts, links, halp?
Example code and tests
RSpec.describe 'ExceptionController' do
class ExceptionController < ActionController::Base
use ActionDispatch::ShowExceptions, Rails.configuration.exceptions_app
use ActionDispatch::DebugExceptions
#Response
respond_to :html, :json
#Layout
layout :layout_status
#Dependencies
before_action :status, :app_name, :log_exception
def show
respond_with details, status: #status, location: nil
end
def show_detailed_exceptions?
request.local?
end
protected
####################
# Dependencies #
####################
#Info
def status
#exception = env['action_dispatch.exception']
#status = ActionDispatch::ExceptionWrapper.new(env, #exception).status_code
#response = ActionDispatch::ExceptionWrapper.rescue_responses[#exception.class.name]
end
#Format
def details
#details ||= {}.tap do |h|
I18n.with_options scope: [:exception, :show, #response], exception_name: #exception.class.name, exception_message: #exception.message do |i18n|
h[:name] = i18n.t "#{#exception.class.name.underscore}.title", default: i18n.t(:title, default: #exception.class.name)
h[:message] = i18n.t "#{#exception.class.name.underscore}.description", default: i18n.t(:description, default: #exception.message)
end
end
end
helper_method :details
####################
# Layout #
####################
private
def log_exception
if #status.to_s == '500'
request.env[:exception_details] = details
request.env[:exception_details][:location] = ActionDispatch::ExceptionWrapper.new(env, #exception).application_trace[0]
end
end
#Layout
def layout_status
#status.to_s != '404' ? 'error' : 'application'
end
#App
def app_name
#app_name = Rails.application.class.parent_name
end
end
include Rack::Test::Methods
include ActionDispatch::Integration::Runner
include ActionController::TemplateAssertions
include ActionDispatch::Routing::UrlFor
let(:exception) { ActionController::RoutingError.new(:foo) }
let(:request) { ActionDispatch::TestRequest.new }
def app
# Rails.application.config.exceptions_app
#app ||= ExceptionController.action(:show)
end
it 'logs unknown format errors' do
request.env['action_dispatch.show_exceptions'] = true
request.env['consider_all_requests_local'] = true
request.env['warden'] = spy(Warden)
request.session = ActionDispatch::Integration::Session.new(app)
exception = ActionController::RoutingError.new(:foo)
request.env.merge! 'action_dispatch.exception' => exception
post '/whatever'
expect(response.body).to eq("dunno?")
end
end
refs:
https://github.com/richpeck/exception_handler
https://github.com/plataformatec/responders/blob/8f03848a2f50d4685c15a31254a1f600af947bd7/test/action_controller/respond_with_test.rb#L265-L275
https://github.com/rails/rails/blob/1d43458c148f9532a81b92ee3a247da4f1c0b7ad/actionpack/test/dispatch/show_exceptions_test.rb#L92-L99
https://github.com/rails/rails/blob/3e36db4406beea32772b1db1e9a16cc1e8aea14c/railties/test/application/middleware/exceptions_test.rb#L86-L91
https://github.com/rails/rails/blob/34fa6658dd1b779b21e586f01ee64c6f59ca1537/actionpack/lib/action_dispatch/testing/integration.rb#L647-L674
https://github.com/rails/rails/blob/ef8d09d932e36b0614905ea5bc3fb6af318b6ce2/actionview/test/abstract_unit.rb#L146-L184
https://github.com/plataformatec/responders/blob/8f03848a2f50d4685c15a31254a1f600af947bd7/lib/action_controller/respond_with.rb#L196-L207
http://blog.plataformatec.com.br/2012/01/my-five-favorite-hidden-features-in-rails-3-2/
https://github.com/bugsnag/bugsnag-ruby/blob/master/lib/bugsnag/middleware/warden_user.rb
https://github.com/bugsnag/bugsnag-ruby/blob/master/lib/bugsnag/rails/action_controller_rescue.rb#L3-L33
https://github.com/bugsnag/bugsnag-ruby/blob/master/lib/bugsnag/rails/active_record_rescue.rb
https://github.com/rollbar/rollbar-gem/blob/master/spec/rollbar_spec.rb
http://andre.arko.net/2011/12/10/make-rails-3-stop-trying-to-serve-html/
https://github.com/rails/rails/blob/9503e65b9718e4f01860cf017c1cdcdccdfffde7/actionpack/lib/action_dispatch/middleware/show_exceptions.rb#L46-L49
Update 2015-08-27
It has been suggested that this question may be a duplicate of Testing error pages in Rails with Rspec + Capybara, however, that question addresses testing exception responses when the exceptions_app is set to routes.
As I wrote above, I'm using a Controller as the exceptions_app, so though I could use capybara to visit non-existing pages, I'd like to test Controller's action directly, rather than include the rest of the show exceptions stack. This is important because my problem is when the exceptions app is called with an unhandled content type, which I cannot easily test via capybara.
More generally, what I need to test is when the Exceptions app raises and exception, that I have fixed it.
I'm open to seeing some example code, though.
I'm trying to encapsulate the logic for generating my sitemap in a separate class so I can use Delayed::Job to generate it out of band:
class ViewCacher
include ActionController::UrlWriter
def initialize
#av = ActionView::Base.new(Rails::Configuration.new.view_path)
#av.class_eval do
include ApplicationHelper
end
end
def cache_sitemap
songs = Song.all
sitemap = #av.render 'sitemap/sitemap', :songs => songs
Rails.cache.write('sitemap', sitemap)
end
end
But whenever I try ViewCacher.new.cache_sitemap I get this error:
ActionView::TemplateError:
ActionView::TemplateError (You have a nil object when you didn't expect it!
The error occurred while evaluating nil.url_for) on line #5 of app/views/sitemap/_sitemap.builder:
I assume this means that ActionController::UrlWriter is not included in the right place, but I really don't know
Does this do what you're trying to do? This is untested, just an idea.
in lib/view_cacher.rb
module ViewCacher
def self.included(base)
base.class_eval do
#you probably don't even need to include this
include ActionController::UrlWriter
attr_accessor :sitemap
def initialize
#av = ActionView::Base.new(Rails::Configuration.new.view_path)
#av.class_eval do
include ApplicationHelper
end
cache_sitemap
super
end
def cache_sitemap
songs = Song.all
sitemap = #av.render 'sitemap/sitemap', :songs => songs
Rails.cache.write('sitemap', sitemap)
end
end
end
end
then wherever you want to render (I think your probably in your SitemapController):
in app/controllers/sitemap_controller.rb
class SitemapController < ApplicationController
include ViewCacher
# action to render the cached view
def index
#sitemap is a string containing the rendered text from the partial located on
#the disk at Rails::Configuration.new.view_path
# you really wouldn't want to do this, I'm just demonstrating that the cached
# render and the uncached render will be the same format, but the data could be
# different depending on when the last update to the the Songs table happened
if params[:cached]
#songs = Song.all
# cached render
render :text => sitemap
else
# uncached render
render 'sitemap/sitemap', :songs => #songs
end
end
end