SuperPumpup (dot com)
General awesomeness may be found here.

01 February 2013

Testing Rails 4 Strong Parameters

I'm currently implementing Strong Parameters into a Rails application I'm building (3.2.11), and ran into a snag - well, honestly I've been battling it a fair amount for different reasons, but this is a new one, and in the vein of the Destroy All Software: Debugging With Tests, I'm getting this behavior to emerge at the lowest level I can.

That means actually testing the PermittedParameters system that I set up (a la Railscast 371). Oops, I probably should have done it as I built it but since it wasn't 100% clear what was needed to do the testing, I just skipped it and made sure that my "integration" tests - the controllers and acceptance tests - were behaving properly. I hereby deduct from myself 10 TDD points.

So here's the structure I set up to pull my first bug up to the surface:

require 'spec_helper'

describe PermittedParams do
  context "timesheet" do
    let(:admin) {create(:admin_user)}
    it "allows user to set task_item_attributes like start_time" do
      params = ActionController::Parameters.new(timesheet: {task_items_attributes:{"0"=>{"title"=>"Load Material", "equipment_item_id"=>nil, "start_time(1i)"=>"2013", "start_time(2i)"=>"1", "start_time(3i)"=>"14", "start_time(4i)"=>"05", "start_time(5i)"=>"00", "end_time(1i)"=>"2013", "end_time(2i)"=>"1", "end_time(3i)"=>"14", "end_time(4i)"=>"07", "end_time(5i)"=>"00", "rate_multiplier"=>"1.00"}}})
      permitted_params = PermittedParams.new(params, admin)
      permitted_params.timesheet["task_items_attributes"].first[1].keys.should include("start_time(3i)")
    end

    it "allows user to set task_item_attributes: id" do
      params = ActionController::Parameters.new(timesheet: {task_items_attributes:{"0"=>{"title"=>"Load Material", "equipment_item_id"=>nil, "start_time(1i)"=>"2013", "start_time(2i)"=>"1", "start_time(3i)"=>"14", "start_time(4i)"=>"05", "start_time(5i)"=>"00", "end_time(1i)"=>"2013", "end_time(2i)"=>"1", "end_time(3i)"=>"14", "end_time(4i)"=>"07", "end_time(5i)"=>"00", "rate_multiplier"=>"1.00"}}})
      permitted_params = PermittedParams.new(params, admin)
      permitted_params.timesheet["task_items_attributes"].first[1].keys.should include("id")
    end
  end
end

This gets me one passing and one failing test.

% rspec spec/models/permitted_params_spec.rb
.F

Failures:

  1) PermittedParams timesheet allows user to set task_item_attributes: id
     Failure/Error: permitted_params.timesheet["task_items_attributes"].first[1].keys.should include("id")
       expected ["title", "equipment_item_id", "start_time(1i)", "start_time(2i)", "start_time(3i)", "start_time(4i)", "start_time(5i)", "end_time(1i)", "end_time(2i)", "end_time(3i)", "end_time(4i)", "end_time(5i)", "rate_multiplier"] to include "id"
     # ./spec/models/permitted_params_spec.rb:15:in `block (3 levels) in <top (required)>'

Finished in 0.49697 seconds
2 examples, 1 failure

Failed examples:

rspec ./spec/models/permitted_params_spec.rb:12 # PermittedParams timesheet allows user to set task_item_attributes: id

Perfect, I've flushed it out.

A couple notes about the above snippet though. I do require 'spec_helper' since this needs Rails (the ActionController::Parameters) to work. Thus, it's a bit slower than I'd prefer, but it's ok.

Here's the relevant section of my PermittedParams class:

def timesheet
  params.require(:timesheet).permit(*timesheet_attributes)
end

def timesheet_attributes
  return unless user
  if user.has_role?(:superuser) || user.has_role?(:admin)
    [:job_token, :date, :employee_id, :per_diem, :drive_time,
      task_items_attributes: [:title, :equipment_item_id, :start_time, :end_time, :rate_multiplier]]
  elsif user.has_role?(:supervisor)
  elsif user.has_role?(:dispatcher)
    [:job_token, :date, :employee_id, :per_diem, :drive_time,
      task_items_attributes: [:title, :equipment_item_id, :start_time, :end_time, :rate_multiplier]]
  elsif user.has_role?(:office)
    [:job_token, :date, :employee_id, :per_diem, :drive_time,
      task_items_attributes: [:title, :equipment_item_id, :start_time, :end_time, :rate_multiplier]]
  end
end

So strange... Popping into pry to debug:

[1] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> params
=> {"timesheet"=>
  {"task_items_attributes"=>
    {"0"=>
      {"id"=>"12",
       "title"=>"Load Material",
       "equipment_item_id"=>nil,
       "start_time(1i)"=>"2013",
       "start_time(2i)"=>"1",
       "start_time(3i)"=>"14",
       "start_time(4i)"=>"05",
       "start_time(5i)"=>"00",
       "end_time(1i)"=>"2013",
       "end_time(2i)"=>"1",
       "end_time(3i)"=>"14",
       "end_time(4i)"=>"07",
       "end_time(5i)"=>"00",
       "rate_multiplier"=>"1.00"}}}}
[2] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> permitted_params.timesheet
=> {"task_items_attributes"=>
  {"0"=>
    {"title"=>"Load Material",
     "equipment_item_id"=>nil,
     "start_time(1i)"=>"2013",
     "start_time(2i)"=>"1",
     "start_time(3i)"=>"14",
     "start_time(4i)"=>"05",
     "start_time(5i)"=>"00",
     "end_time(1i)"=>"2013",
     "end_time(2i)"=>"1",
     "end_time(3i)"=>"14",
     "end_time(4i)"=>"07",
     "end_time(5i)"=>"00",
     "rate_multiplier"=>"1.00"}}}
[3] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)>
[4] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> params.require(:timesheet).permit(task_items_attributes: :id)
=> {"task_items_attributes"=>{"0"=>{"id"=>"12"}}}
...typos ommitted so I look smarter...
[8] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> params.require(:timesheet).permit(:job_token, :date, :employee_id, :per_diem, :drive_time, task_items_attributes: [:title, :equipment_item_id, :start_time, :end_time, :rate_multiplier])
=> {"task_items_attributes"=>
  {"0"=>
    {"title"=>"Load Material",
     "equipment_item_id"=>nil,
     "start_time(1i)"=>"2013",
     "start_time(2i)"=>"1",
     "start_time(3i)"=>"14",
     "start_time(4i)"=>"05",
     "start_time(5i)"=>"00",
     "end_time(1i)"=>"2013",
     "end_time(2i)"=>"1",
     "end_time(3i)"=>"14",
     "end_time(4i)"=>"07",
     "end_time(5i)"=>"00",
     "rate_multiplier"=>"1.00"}}}
#Aha!  Looks like "id" is not there now...
[9] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> params.require(:timesheet).permit(:job_token, :date, :employee_id, :per_diem, :drive_time, task_items_attributes: [:title, :equipment_item_id, :start_time, :end_time, :rate_multiplier, :id])
=> {"task_items_attributes"=>
  {"0"=>
    {"title"=>"Load Material",
     "equipment_item_id"=>nil,
     "start_time(1i)"=>"2013",
     "start_time(2i)"=>"1",
     "start_time(3i)"=>"14",
     "start_time(4i)"=>"05",
     "start_time(5i)"=>"00",
     "end_time(1i)"=>"2013",
     "end_time(2i)"=>"1",
     "end_time(3i)"=>"14",
     "end_time(4i)"=>"07",
     "end_time(5i)"=>"00",
     "rate_multiplier"=>"1.00",
     "id"=>"12"}}}
[10] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> pp = _
=> {"task_items_attributes"=>
  {"0"=>
    {"title"=>"Load Material",
     "equipment_item_id"=>nil,
     "start_time(1i)"=>"2013",
     "start_time(2i)"=>"1",
     "start_time(3i)"=>"14",
     "start_time(4i)"=>"05",
     "start_time(5i)"=>"00",
     "end_time(1i)"=>"2013",
     "end_time(2i)"=>"1",
     "end_time(3i)"=>"14",
     "end_time(4i)"=>"07",
     "end_time(5i)"=>"00",
     "rate_multiplier"=>"1.00",
     "id"=>"12"}}}
[11] pry(#<RSpec::Core::ExampleGroup::Nested_1::Nested_1>)> pp
=> {"task_items_attributes"=>
  {"0"=>
    {"title"=>"Load Material",
     "equipment_item_id"=>nil,
     "start_time(1i)"=>"2013",
     "start_time(2i)"=>"1",
     "start_time(3i)"=>"14",
     "start_time(4i)"=>"05",
     "start_time(5i)"=>"00",
     "end_time(1i)"=>"2013",
     "end_time(2i)"=>"1",
     "end_time(3i)"=>"14",
     "end_time(4i)"=>"07",
     "end_time(5i)"=>"00",
     "rate_multiplier"=>"1.00",
     "id"=>"12"}}}

Odd, I would think that :id should be allowed to bubble through all the time as long as its value is an allowed primitive, but I guess I have explicitly allow it. So I went back and added ":id" to the allowed parameters in the permitted_params, and passed. Cool.

% rspec spec/models/permitted_params_spec.rb
No DRb server is running. Running in local process instead ...
Rack::File headers parameter replaces cache_control after Rack 1.5.
..

Finished in 0.49389 seconds
2 examples, 0 failures

Now, to go out a level to the controller. This is the test that was failing:

it "does not duplicate task items" do
  job = create(:job)
  employee = create(:employee)
  put :update, id: @timesheet, timesheet: {task_items_attributes:{"0"=>{"title"=>"Load Material", "equipment_item_id"=>nil, "start_time(1i)"=>"2013", "start_time(2i)"=>"1", "start_time(3i)"=>"14", "start_time(4i)"=>"05", "start_time(5i)"=>"00", "end_time(1i)"=>"2013", "end_time(2i)"=>"1", "end_time(3i)"=>"14", "end_time(4i)"=>"07", "end_time(5i)"=>"00", "rate_multiplier"=>"1.00"}, "1"=>{"title"=>"Load Material", "equipment_item_id"=>nil, "start_time(1i)"=>"2013", "start_time(2i)"=>"1", "start_time(3i)"=>"14", "start_time(4i)"=>"05", "start_time(5i)"=>"00", "end_time(1i)"=>"2013", "end_time(2i)"=>"1", "end_time(3i)"=>"14", "end_time(4i)"=>"07", "end_time(5i)"=>"00", "rate_multiplier"=>"1.00"}, "2"=>{"title"=>"Load Material", "equipment_item_id"=>nil, "start_time(1i)"=>"2013", "start_time(2i)"=>"1", "start_time(3i)"=>"14", "start_time(4i)"=>"05", "start_time(5i)"=>"00", "end_time(1i)"=>"2013", "end_time(2i)"=>"1", "end_time(3i)"=>"14", "end_time(4i)"=>"07", "end_time(5i)"=>"00", "rate_multiplier"=>"1.00"}, "3"=>{"title"=>"Load Material", "equipment_item_id"=>nil, "start_time(1i)"=>"2013", "start_time(2i)"=>"1", "start_time(3i)"=>"14", "start_time(4i)"=>"05", "start_time(5i)"=>"00", "end_time(1i)"=>"2013", "end_time(2i)"=>"1", "end_time(3i)"=>"14", "end_time(4i)"=>"07", "end_time(5i)"=>"00", "rate_multiplier"=>"1.00"}}}
  @timesheet.reload
  expect{
    put :update, id: @timesheet, timesheet: {task_items_attributes:{"0"=>{"id"=>@timesheet.task_items[0].id, "title"=>"Load Material", "equipment_item_id"=>nil, "start_time(1i)"=>"2013", "start_time(2i)"=>"1", "start_time(3i)"=>"14", "start_time(4i)"=>"05", "start_time(5i)"=>"00", "end_time(1i)"=>"2013", "end_time(2i)"=>"1", "end_time(3i)"=>"14", "end_time(4i)"=>"07", "end_time(5i)"=>"00", "rate_multiplier"=>"1.00"}, "1"=>{"title"=>"Load Material", "equipment_item_id"=>nil, "start_time(1i)"=>"2013", "start_time(2i)"=>"1", "start_time(3i)"=>"14", "start_time(4i)"=>"05", "start_time(5i)"=>"00", "end_time(1i)"=>"2013", "end_time(2i)"=>"1", "end_time(3i)"=>"14", "end_time(4i)"=>"07", "end_time(5i)"=>"00", "rate_multiplier"=>"1.00", "id"=>@timesheet.task_items[1].id}, "2"=>{"title"=>"Load Material", "equipment_item_id"=>nil, "start_time(1i)"=>"2013", "start_time(2i)"=>"1", "start_time(3i)"=>"14", "start_time(4i)"=>"05", "start_time(5i)"=>"00", "end_time(1i)"=>"2013", "end_time(2i)"=>"1", "end_time(3i)"=>"14", "end_time(4i)"=>"07", "end_time(5i)"=>"00", "rate_multiplier"=>"1.00", "id"=>@timesheet.task_items[2].id}, "3"=>{"title"=>"Load Material", "equipment_item_id"=>nil, "start_time(1i)"=>"2013", "start_time(2i)"=>"1", "start_time(3i)"=>"14", "start_time(4i)"=>"05", "start_time(5i)"=>"00", "end_time(1i)"=>"2013", "end_time(2i)"=>"1", "end_time(3i)"=>"14", "end_time(4i)"=>"07", "end_time(5i)"=>"00", "rate_multiplier"=>"1.00", "id"=>@timesheet.task_items[3].id}}}
  }.to_not change(@timesheet.task_items, :count)
end

Sadly, this was still failing:

% rspec spec/controllers/timesheets_controller_spec.rb
No DRb server is running. Running in local process instead ...
Rack::File headers parameter replaces cache_control after Rack 1.5.
..F.......

Failures:

  1) TimesheetsController PUT #update does not duplicate task items
     Failure/Error: expect{
       count should not have changed, but did change from 4 to 8
     # ./spec/controllers/timesheets_controller_spec.rb:54:in `block (3 levels) in <top (required)>'

Finished in 1.72 seconds
10 examples, 1 failure

Failed examples:

rspec ./spec/controllers/timesheets_controller_spec.rb:49 # TimesheetsController PUT #update does not duplicate task items

I spent a long time digging around in pry, and was always able to manually build a permitted_params hash that had inte info that I wanted with the command PermittedParams.new(params, current_user) but for whatever reason the permitted_params that had made its way through CanCan was 'tainted'.

So, my solution was simply to go back and recreate the permitted_params hash each time it's called. I did that by changing this code in application_controller.rb from:

def permitted_params
  @permitted_params ||= PermittedParams.new(params, current_user)
end
helper_method :permitted_params

To this:

def permitted_params
  PermittedParams.new(params, current_user)
end
helper_method :permitted_params

I wasn't able to actually isolate where the permitted_params hash was getting screwed up, but I've solved my problem. And now I have a structure to explicitly test my strong_parameters with RSpec! Super Pumpup!