How to send raw post data in a Rails functional test? - ruby-on-rails

I'm looking to send raw post data (e.g. unparamaterized JSON) to one of my controllers for testing:
class LegacyOrderUpdateControllerTest < ActionController::TestCase
test "sending json" do
post :index, '{"foo":"bar", "bool":true}'
end
end
but this gives me a NoMethodError: undefined method `symbolize_keys' for #<String:0x00000102cb6080> error.
What is the correct way to send raw post data in ActionController::TestCase?
Here is some controller code:
def index
post_data = request.body.read
req = JSON.parse(post_data)
end

I ran across the same issue today and found a solution.
In your test_helper.rb define the following method inside of ActiveSupport::TestCase:
def raw_post(action, params, body)
#request.env['RAW_POST_DATA'] = body
response = post(action, params)
#request.env.delete('RAW_POST_DATA')
response
end
In your functional test, use it just like the post method but pass the raw post body as the third argument.
class LegacyOrderUpdateControllerTest < ActionController::TestCase
test "sending json" do
raw_post :index, {}, {:foo => "bar", :bool => true}.to_json
end
end
I tested this on Rails 2.3.4 when reading the raw post body using
request.raw_post
instead of
request.body.read
If you look at the source code you'll see that raw_post just wraps request.body.read with a check for this RAW_POST_DATA in the request env hash.

Version for Rails 5:
post :create, body: '{"foo": "bar", "bool": true}'
See here - body string parameter is treated as raw request body.

I actually solved the same issues just adding one line
before simulating the rspec post request. What you do
is to populate the "RAW_POST_DATA". I tried to remove
the attributes var on the post :create, but if I do so,
it do not find the action.
Here my solution.
def do_create(attributes)
request.env['RAW_POST_DATA'] = attributes.to_json
post :create, attributes
end
In the controller the code you need to read the JSON is
something similar to this
#property = Property.new(JSON.parse(request.body.read))

Looking at stack trace running a test you can acquire more control on request preparation:
ActionDispatch::Integration::RequestHelpers.post => ActionDispatch::Integration::Session.process =>
Rack::Test::Session.env_for
You can pass json string as :params AND specify a content type "application/json". In other case content type will be set to "application/x-www-form-urlencoded" and your json will be parsed properly.
So all you need is to specify "CONTENT_TYPE":
post :index, '{"foo":"bar", "bool":true}', "CONTENT_TYPE" => 'application/json'

For those using Rails5+ integration tests, the (undocumented) way to do this is to pass a string in the params argument, so:
post '/path', params: raw_body, headers: { 'Content-Type' => 'application/json' }

I was searching very long for how to post raw JSON content in a integration test (Rails 5.1). I guess my solution could also help in this case.
I looked up the documentation and source code for the post method: https://api.rubyonrails.org/v5.1/classes/ActionDispatch/Integration/RequestHelpers.html#method-i-post
This directed me to the process method for more details: https://api.rubyonrails.org/v5.1/classes/ActionDispatch/Integration/Session.html#method-i-process
Thanks to this, I finally found out what parameters are accepted by the process and thus post method.
Here's what my final solution looked like:
post my_url, params: nil, headers: nil, env: {'RAW_POST_DATA' => my_body_content}, as: :json

If you are using RSpec (>= 2.12.0) and writing Request specs, the module that is included is ActionDispatch::Integration::Runner. If you take a look at the source code you can notice that the post method calls process which accepts a rack_env parameter.
All this means that you can simply do the following in your spec:
#spec/requests/articles_spec.rb
post '/articles', {}, {'RAW_POST_DATA' => 'something'}
And in the controller:
#app/controllers/articles_controller.rb
def create
puts request.body.read
end

Using Rails 4, I was looking to do this to test the processing of raw xml that was being posted to the controller. I was able to do it by just providing the string to the post:
raw_xml = File.read("my_raw.xml")
post :message, raw_xml, format: :xml
I believe if the parameter provided is a string, it just gets passed along to the controller as the body.

In rails, 5.1 the following work for me when doing a delete request that needed data in the body:
delete your_app_url, as: :json, env: {
"RAW_POST_DATA" => {"a_key" => "a_value"}.to_json
}
NOTE: This only works when doing an Integration test.

The post method expects a hash of name-value pairs, so you'll need to do something like this:
post :index, :data => '{"foo":"bar", "bool":true}'
Then, in your controller, get the data to be parsed like this:
post_data = params[:data]

As of Rails 4.1.5, this was the only thing that worked for me:
class LegacyOrderUpdateControllerTest < ActionController::TestCase
def setup
#request.headers["Content-Type"] = 'application/json'
end
test "sending json" do
post :index, '{"foo":"bar", "bool":true}'.to_json, { account_id: 5, order_id: 10 }
end
end
for a url at /accounts/5/orders/10/items. This gets the url params conveyed as well as the JSON body. Of course, if orders is not embedded then you can leave off the params hash.
class LegacyOrderUpdateControllerTest < ActionController::TestCase
def setup
#request.headers["Content-Type"] = 'application/json'
end
test "sending json" do
post :index, '{"foo":"bar", "bool":true}'.to_json
end
end

In Rails 4 (at least in 4.2.11.3) there's no easy way to test your controllers that consume json (functional tests). For parsing json in a running server the ActionDispatch::ParamsParser middleware is responsible. Controller tests though rely on Rack, which can't parse json to this day (not that it should).
You can do:
post :create, body_params.to_json
or:
post :update, body_parmas.to_json, url_params
But body_params won't be accessible in the controller via params. You've got to do JSON.parse(request.body.read). So the only thing that comes to mind is:
post :update, url_params.merge(body_params)
That is, in tests pass everything via parameters (application/x-www-form-urlencoded). In production the body will be parsed by ActionDispatch::ParamsParser to the same effect. Except that your numbers become strings (and possibly more):
# test/controllers/post_controller_test.rb
post :update, {id: 1, n: 2}
# app/controller/posts_controller.rb
def update
p params # tests:
# {"id"=>"1", "n" => "2", "controller"=>"posts", "action"=>"update"}
# production
# {"id"=>"1", "n" => 2, "controller"=>"posts", "action"=>"update"}
end
If you're willing to parse json in controllers yourself though you can do:
# test/controllers/post_controller_test.rb
post_json :update, {n: 2}.to_json, {id: 1}
# app/controller/posts_controller.rb
def update
p JSON.parse(request.body.read) # {"id"=>"1", "n" => 2, "controller"=>"posts", "action"=>"update"}
end

post :index, {:foo=> 'bar', :bool => 'true'}

Related

Test case with enum requires key after upgrade Rails from v4 to v5

I am in the process of upgrading my Rails app from v4 to v5. When I run the tests, some of them fail which used to pass before.
For instance,
(enum)
enum session_type: {regular: 0, demo: 1, promotional: 2}
(usage)
session = {
:uuid => SecureRandom.uuid,
:session_type => 0,
}
post :create, :format => :json, params: { :session => session }
The parameter goes through a params.require(:session).permit(...)
'0' is not a valid session_type
/home/anz/.rvm/gems/ruby-2.2.3/gems/activerecord-5.0.0/lib/active_record/enum.rb:137:in `assert_valid_value'
/home/anz/.rvm/gems/ruby-2.2.3/gems/activerecord-5.0.0/lib/active_record/attribute.rb:67:in `with_value_from_user'
/home/anz/.rvm/gems/ruby-2.2.3/gems/activerecord-5.0.0/lib/active_record/attribute_set.rb:51:in `write_from_user'
/home/anz/.rvm/gems/ruby-2.2.3/gems/activerecord-5.0.0/lib/active_record/attribute_methods/write.rb:50:in `write_attribute_with_type_cast'
/home/anz/.rvm/gems/ruby-2.2.3/gems/activerecord-5.0.0/lib/active_record/attribute_methods/write.rb:32:in `write_attribute'
/home/anz/.rvm/gems/ruby-2.2.3/gems/activerecord-5.0.0/lib/active_record/attribute_methods/write.rb:20:in `__temp__3756373796f6e6f547970756='
When I use regular instead of 0, it works. What's going on?
UPDATE 1:
Doing so correctly parses the parameters
#request.env['CONTENT_TYPE'] = 'application/json'
post :create, :format => :json, params: { :session => session }
I tried as: :json, but it failed to work. I wonder why it is not working, looks way better than adding #request.env.
post :create, as: :json, params: { :session => session }
UPDATE 2:
After updating rails from 5.0.0 to 5.0.7.2, as: :json is working.
post :create, as: :json, params: { :session => session }
Look carefully at the error message, it is complaining about '0', not 0.
If you try to set session_type to a string, then it is expecting 'regular', 'demo', or 'promotional'. If you use a number then it expects 0, 1, or 2. But ActiveRecord isn't clever enough to try converting a string to a number before seeing if you're passing a valid value so it just sees '0' is something that isn't in %w[regular demo promotional] and tells you that you're passing an invalid value.
So either fix up the clients to send in strings or adjust your argument parsing to map strings to numbers with something like this:
def model_params
permitted = params.require(:session).permit(...)
if(permitted[:session_type].in?(Model.session_types.values.map(&:to_s))
permitted[:session_type] = permitted[:session_type].to_i
end
permitted
end

Test a controller with distinct body and querystring params

I'd like to test with RSpec a controller receiving this kind of request:
curl -X POST \
--data "{\"same_key\": \"value_in_body\"}" \
--header "same_key: value_in_header" \
"http://localhost:5000/articles/?same_key=value_in_querystring"
having:
same_key in the body
same_key in the header
same_key in the querystring
and where:
request.request_parameters["same_key"]: "value_in_body"
request.headers["same_key"]: "value_in_header"
request.query_parameters["same_key"]: "value_in_querystring"
I wrote this test:
RSpec.describe ArticlesController, type: :controller do
describe '#create' do
it 'creates an article' do
post :post,
as: :json,
params: { same_key: 'value_in_body' },
headers: { same_key: 'value_in_header' }
expect(response).to have_http_status(:created)
end
end
end
So far, it would be good for the body param and the header param.
But how should we do to also send the querystring param?
If you really need this kind of scenario, you have to use the Rails URI pattern instead of only specifying action name in the post statement,
post '/documents/create_new_doc', params: {same_key: 'value_in_body'}
Note: Get the exact URI pattern from rake routes
You actually can't have both request.query_parameters and request.request_parameters at the same time from either post or get functions in RSpec Controller tests or Request tests. This other Stack Overflow answer explains that RSpec only ever sets one or the other: https://stackoverflow.com/a/36715875/2479282
The solution (posed by that other answer) is to use IntegrationTests instead, a'la:
class CollectionsTest < ActionDispatch::IntegrationTest
test 'foo' do
post collections_path, { collection: { name: 'New Collection' } },
{ "QUERY_STRING" => "api_key=my_api_key" }
# this proves that the parameters are recognized separately in the controller
# (you can test this in you controller as well as here in the test):
puts request.POST.inspect
# => {"collection"=>{"name"=>"New Collection"}}
puts request.GET.inspect
# => {"api_key"=>"my_api_key"}
end
end

rails request spec PUT method with json doesn't go through

In one of my tests I have to verify that a certain offer returns 404 response if the available limit (10 seats for example with that offer) are all sold, or it has expired which ever comes first.
I just cannot get the PUT request to work in Request Spec, here's my code
RSpec.describe "Offers", type: :request do
describe "gives not found response" do
it "when available limit exhausts before date till available" do
offer = Offer.new
Timecop.freeze(Date.today - 12.days) do
offer = FactoryGirl.create(:offer)
end
payload = FactoryGirl.attributes_for(:offer, :available_limit => 0, :discount_id => offer.discount.id, :coupon_id => offer.coupon.id)
sign_in
put '/offers/'<<offer.id, params: { id: offer.id, offer: payload }, as: :json
get "/vouchers/"<<offer.coupon.voucher_code
expect(response.status).to eq 404
end
end
end
needles to say that I have tried many hacks including
put '/offers/'<<offer.id.to_s<<".json", params: { offer: payload }
or even
put '/offers/'<<offer.id.to_s<<".json", payload
What I also noticed was that in one of the combinations the request did go through but it responds with both HTML and JSON format which lead to error in the spec as I am not running them under capybara (and I do not want to either)
Don't use << to build those paths. Using << with an id will insert a character code equivalent to the integer value of the id, e.g.
'XYZ' << 123 << 'ABC'
=> "XYZ{ABC"
Just use normal string interpolation, e.g.
put "/offers/#{offer.id}", params: { id: offer.id, offer: payload }, format: :json
get "/vouchers/#{offer.coupon.voucher_code}"
Credit to #house9 for noticing the format: :json part as well.
I think you want to use format: :json not as: :json
Try:
params = { id: offer.id, offer: payload }
put :offers, params: params, format: :json

Get HTTP POST Response in Rails Controler

I have two rails apps. App #1 sends a post request from a controller action to another controller action in App #2. I want to be able to read in App #1 the response to that POST.
App #1 Controller:
require 'net/http'
# get the url that we need to post to
url = URI.parse('http://app2.com/sessions/login_request')
# build the params string
post_args1 = {
'username' => 'my#test.com'
}
# send the request
resp, data = Net::HTTP.post_form(url, post_args1)
#HOW do I read :token and :tokenMnemonic here??
App #2 controller:
def login_request
# do some logic here
render :json => {
:result => "OK",
:token => random_token,
:tokenMnemonic => tokenMnemonic
}
end
The question is how can I read :token and :tokenMnemonic from the POST response received at the controller of App #1.
The variable resp represents the response object. You can use the #body method to get the body of the response as String.
If the body is a String serialization of a JSON, simply parse it back to fetch the elements.
hash = JSON.parse(resp.body)
hash['token']
# => ...

HTTParty with Digest Authentication

I would like to connect to an API using Ruby On Rails 2.3.8 and HTTParty gem.
My model is the following:
class Onnion < ActiveRecord::Base
require 'net/http'
include HTTParty
base_uri 'http://myapiurl.com'
digest_auth 'user', 'password'
disable_rails_query_string_format
def self.create_rma(order)
put('/orders/rma', :query => {:action => 'put', :data => {:api_key => 'user', :onnion_order_id => order.id, :customer_rma => order.saving.profile.user.id, :comments => ''}})
end
end
What I would like to do is to call a method of the API called Put, with certain parameters grouped within data parameter.
After executing this method I'm getting a 401 Unauthorized error message.
What am I doing wrong? This is the first time I'm trying to do something like this.
What version of HTTParty are you using, and have you tried using the very latest version from Github? There were some fixes to do with digest auth security a little while ago in version 0.7.3.
If that doesn't work it could be that the server you're attempting to talk to isn't following protocol correctly. I've had this happen before, had to monkey patch HTTParty to get it to login correctly. I'll put the patch I used here in-case it works for you...
module Net
module HTTPHeader
class DigestAuthenticator
# use NC = 1 instead of 0
def authorization_header
#cnonce = md5(random)
header = [%Q(Digest username="#{#username}"),
%Q(realm="#{#response['realm']}"),
%Q(nonce="#{#response['nonce']}"),
%Q(uri="#{#path}"),
%Q(response="#{request_digest}")]
[%Q(cnonce="#{#cnonce}"),
%Q(opaque="#{#response['opaque']}"),
%Q(qop="#{#response['qop']}"),
%Q(nc="1")].each { |field| header << field } if qop_present?
header
end
private
def request_digest
a = [md5(a1), #response['nonce'], md5(a2)]
a.insert(2, "1", #cnonce, #response['qop']) if qop_present?
md5(a.join(":"))
end
end
end
end
module HTTParty
class Request
def setup_digest_auth
# issue a get instead of a head request
res = http.get(uri.request_uri, options[:headers]||{})
if res['www-authenticate'] != nil && res['www-authenticate'].length > 0
#raw_request.digest_auth(username, password, res)
end
end
end
end
The changes made were to send NC 1 and not NC 0, and also to do a GET request, rather than a HEAD request in setup_digest_auth

Resources