Using RSpec to Test attr_accessible

We all know about why you really, really should be using attr_accessible right? Well, if not read this write-up on mass assignment a few times.

That's all well and good, but @r38y and I have been wondering what's the best way to test this lately and here's what I've come up with:

user.rb

class User < ActiveRecord::Base  has_many :orders  attr_accessible :email, :password, :password_confirmationend

user_spec.rb

describe User do  it {should only_mass_assign_accessible_attributes(:email, :password, :password_confirmation)}end

spec_helper.rb

# Finds all setter methods for the object, # builds a new object with these methods set to an arbitrary value, # checks if the given attrs are able to be mass assigned and all others are not,# and finally returns true if there are no failures.Spec::Matchers.define :only_mass_assign_accessible_attributes do |*attrs|  match do |object|    setters = object.methods.map{|m| m if m.match(/[a-z].*\=$/)}.compact    getters = setters.map{|s| s.gsub('=', '').to_sym}        params = {}    getters.each do |getter|      params[getter] = 'test'    end    record = object.class.new(params)        @shouldnt, @should = [], []    getters.each do |getter|      value = record.send(getter)      if value == 'test' && !attrs.include?(getter)        @shouldnt << getter.to_s      elsif value != 'test' && attrs.include?(getter)        @should << getter.to_s      end    end        @shouldnt.length > 0 && @should.length > 0 ? false : true  end    failure_message_for_should do |actual|    str = ""    str += "The following attributes were mass assigned even though they shouldn't have been: #{@shouldnt.to_yaml}" unless @shouldnt.empty?    str += "The following attributes were not mass assigned even though they should have been: #{@should.to_yaml}" unless @should.empty?    str  endend

In addition to your attr_accessors created from your database columns, rails is going to add additional setters such as #orders= and #order_ids= from relationship definitions like above. This list of setter methods grabbed in the above custom matcher will look something like this: 

address=, address_id=, attributes=, avatar=, avatar_content_type=, avatar_file_name=, avatar_file_size=, avatar_updated_at=, created_at=, crypted_password=, email=, id=, last_login_at=, last_login_ip=, last_request_at=, login_count=, order_ids=, orders=, password=, password_confirmation=, password_salt=, persistence_token=, updated_at=

So in the above example, the custom matcher will loop through each of these setter methods and make sure that only email, password, and password_confirmation can be mass assigned.