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)}endspec_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 endendIn 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.