46

In order to ensure that my application is not vulnerable to this exploit, I am trying to create a controller test in RSpec to cover it. In order to do so, I need to be able to post raw JSON, but I haven't seemed to find a way to do that. In doing some research, I've determined that there at least used to be a way to do so using the RAW_POST_DATA header, but this doesn't seem to work anymore:

it "should not be exploitable by using an integer token value" do
  request.env["CONTENT_TYPE"] = "application/json"
  request.env["RAW_POST_DATA"]  = { token: 0 }.to_json
  post :reset_password
end

When I look at the params hash, token is not set at all, and it just contains { "controller" => "user", "action" => "reset_password" }. I get the same results when trying to use XML, or even when trying to just use regular post data, in all cases, it seems to not set it period.

I know that with the recent Rails vulnerabilities, the way parameters are hashed was changed, but is there still a way to post raw data through RSpec? Can I somehow directly use Rack::Test::Methods?

Daniel Vandersluis
  • 91,582
  • 23
  • 169
  • 153

7 Answers7

84

As far as I have been able to tell, sending raw POST data is no longer possible within a controller spec. However, it can be done pretty easily in a request spec:

describe "Example", :type => :request do
  params = { token: 0 }
  post "/user/reset_password", params.to_json, { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
  #=> params contains { "controller" => "user", "action" => "reset_password", "token" => 0 }
end
Daniel Vandersluis
  • 91,582
  • 23
  • 169
  • 153
  • 1
    This is the cleanest way I've found to test controllers that expect raw json POST requests. Thanks. – sockmonk Apr 04 '13 at 14:00
  • 1
    'CONTENT_TYPE' header is enough – Sergii Mostovyi May 18 '13 at 11:58
  • 15
    this solution is not working for me in Rails 3.2.13. My workaround was writing `params = { token: 0, format: :json }`. Also remove .to_json and the hash following it in the example. Also you might want to have `config.include Rails.application.routes.url_helpers` in `spec_helper.rb`. Verify json response with `response.header['Content-Type'].should include 'application/json'` – davidtingsu Jun 28 '13 at 20:43
  • 3
    I did like this: post "/user/reset_password", params.merge(format: 'json') with Rails 3.2.14 – untidyhair Dec 31 '13 at 17:57
  • @untidyhair This approach worked for me, while the one in the above answer did not. – Automatico Feb 13 '15 at 11:39
  • @IanVaughan HTTP_ACCEPT tells that the return will be accept as JSON, the CONTENT_TYPE is saying that you are sending JSON – Cassio Cabral Feb 19 '16 at 19:58
  • @untidyhair It worked as charm! Can you add your comments as answer ? – Atul Yadav Aug 29 '16 at 09:36
  • Thank you @davidtingsu You saved my time! – arthur bryant Feb 23 '17 at 09:11
  • 1
    In Rails 5, try `as: :json` instead of `format: :json` to convert the payload. – nicb Aug 31 '18 at 02:17
  • Thank you so much, this ended hours of search. This solution is pretty much the only one working with rspec 2.12 and rails 3.0.6. – Nico Brenner Dec 19 '21 at 04:47
11

This is the way to send raw JSON to a controller action (Rails 3+):

Let's say we have a route like this:

post "/users/:username/posts" => "posts#create"

And let's say you expect the body to be a json that you read by doing:

JSON.parse(request.body.read)

Then your test will look like this:

it "should create a post from a json body" do
  json_payload = '{"message": "My opinion is very important"}'
  post :create, json_payload, {format: 'json', username: "larry" }
end

{format: 'json'} is the magic that makes it happen. Additionally, if we look at the source for TestCase#post http://api.rubyonrails.org/classes/ActionController/TestCase/Behavior.html#method-i-process you can see that it takes the first argument after the action (json_payload) and if it is a string it sets that as raw post body, and parses the rest of the args as normal.

It's also important to point out that rspec is simply a DSL on top of the Rails testing architecture. The post method above is the ActionController::TestCase#post and not some rspec invention.

mastaBlasta
  • 5,700
  • 1
  • 24
  • 26
11

What we've done in our controller tests is explicitly set the RAW_POST_DATA:

before do
  @request.env['RAW_POST_DATA'] = payload.to_json
  post :my_action
end
andrewpurcell
  • 309
  • 2
  • 5
  • If you're building a library that needs this kind of setup, this is the only way to get this behavior with a Rails 3, 4, and 5 compliant syntax. – Ryan McGeary Dec 05 '17 at 15:31
10

Rails 5 example:

RSpec.describe "Sessions responds to JSON", :type => :request do

  scenario 'with correct authentication' do
    params = {id: 1, format: :json}
    post "/users/sign_in", params: params.to_json, headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
    expect(response.header['Content-Type']).to include 'application/json'
  end
end
Jeremy Lynch
  • 6,780
  • 3
  • 52
  • 63
6

Here is a full working example of a controller test sending raw json data:

describe UsersController, :type => :controller do

  describe "#update" do
    context 'when resource is found' do
      before(:each) do
        @user = FactoryGirl.create(:user)
      end

      it 'updates the resource with valid data' do
        @request.headers['Content-Type'] = 'application/vnd.api+json'
        old_email = @user.email
        new_email = Faker::Internet.email
        jsondata = 
        {
          "data" => {
            "type" => "users",
            "id" => @user.id,
            "attributes" => {
              "email" => new_email
            }
          }
        }

        patch :update, jsondata.to_json, jsondata.merge({:id => old_id})

        expect(response.status).to eq(200)
        json_response = JSON.parse(response.body)
        expect(json_response['data']['id']).to eq(@user.id)
        expect(json_response['data']['attributes']['email']).to eq(new_email)
      end
    end
  end
end

The important parts are:

@request.headers['Content-Type'] = 'application/vnd.api+json'

and

patch :update, jsondata.to_json, jsondata.merge({:id => old_id})

The first makes sure that the content type is correctly set for your request, this is pretty straightforward. The second part was giving me headaches for a few hours, my initial approach was quite a bit different, but it turned out that there is a Rails bug, which prevents us from sending raw post data in functional tests (but allows us in integration tests), and this is an ugly workaround, but it works (on rails 4.1.8 and rspec-rails 3.0.0).

qqbenq
  • 10,220
  • 4
  • 40
  • 45
0

On Rails 4:

params = { shop: { shop_id: new_subscrip.shop.id } }
post api_v1_shop_stats_path, params.to_json, { 'CONTENT_TYPE' => 'application/json',
                                                     'ACCEPT' => 'application/json' }
aarkerio
  • 2,183
  • 2
  • 20
  • 34
0

A slight alternative to @daniel-vandersluis answer, on rails 3.0.6, with rspec 2.99 and rspec-rails 2.99:

describe "Example", :type => :request do
  params = { token: 0 }
  post "/user/reset_password", params.merge({format: 'json'}).to_json, { 'CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json' }
end

The HTTP_ACCEPT header didn't make much difference, (it can be either HTTP_ACCEPT or just ACCEPT). But in my case, for it to work, the params had to: have the .merge({format: 'json'}) and .to_json

Another variation:

describe "Example", :type => :request do
  params = { token: 0 }
  post "/user/reset_password", params.merge({format: 'json'}).to_json, { 'CONTENT_TYPE' => Mime::JSON.to_s, 'HTTP_ACCEPT' => Mime::JSON }
end

It uses Mime::JSON and Mime::JSON.to_s instead of application/json for the header values.

Nico Brenner
  • 690
  • 7
  • 11