mirror of
https://github.com/thoughtbot/shoulda-matchers.git
synced 2022-11-09 12:01:38 -05:00
b7e02184d9
- bump rubocop to v1.0 - Fix Layout/MultilineAssignmentLayout and other remaining offences - Exculde appraisal generated gemfiles in rubocop - Replace NON_NUMERIC_VALUE constant with instance method against failing test case in rails <= 5.1 and postgres adapter. This is a defect in rails where a frozen string is modified in https://github.com/rails/rails/blob/v5.1.7/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb#L25
450 lines
13 KiB
Ruby
450 lines
13 KiB
Ruby
require 'delegate'
|
|
|
|
begin
|
|
require 'strong_parameters'
|
|
rescue LoadError
|
|
end
|
|
|
|
require 'active_support/hash_with_indifferent_access'
|
|
|
|
module Shoulda
|
|
module Matchers
|
|
module ActionController
|
|
# The `permit` matcher tests that an action in your controller receives a
|
|
# whitelist of parameters using Rails' Strong Parameters feature
|
|
# (specifically that `permit` was called with the correct arguments).
|
|
#
|
|
# Here's an example:
|
|
#
|
|
# class UsersController < ApplicationController
|
|
# def create
|
|
# user = User.create(user_params)
|
|
# # ...
|
|
# end
|
|
#
|
|
# private
|
|
#
|
|
# def user_params
|
|
# params.require(:user).permit(
|
|
# :first_name,
|
|
# :last_name,
|
|
# :email,
|
|
# :password
|
|
# )
|
|
# end
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe UsersController, type: :controller do
|
|
# it do
|
|
# params = {
|
|
# user: {
|
|
# first_name: 'John',
|
|
# last_name: 'Doe',
|
|
# email: 'johndoe@example.com',
|
|
# password: 'password'
|
|
# }
|
|
# }
|
|
# should permit(:first_name, :last_name, :email, :password).
|
|
# for(:create, params: params).
|
|
# on(:user)
|
|
# end
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class UsersControllerTest < ActionController::TestCase
|
|
# should "(for POST #create) restrict parameters on :user to first_name, last_name, email, and password" do
|
|
# params = {
|
|
# user: {
|
|
# first_name: 'John',
|
|
# last_name: 'Doe',
|
|
# email: 'johndoe@example.com',
|
|
# password: 'password'
|
|
# }
|
|
# }
|
|
# matcher = permit(:first_name, :last_name, :email, :password).
|
|
# for(:create, params: params).
|
|
# on(:user)
|
|
# assert_accepts matcher, subject
|
|
# end
|
|
# end
|
|
#
|
|
# If your action requires query parameters in order to work, then you'll
|
|
# need to supply them:
|
|
#
|
|
# class UsersController < ApplicationController
|
|
# def update
|
|
# user = User.find(params[:id])
|
|
#
|
|
# if user.update_attributes(user_params)
|
|
# # ...
|
|
# else
|
|
# # ...
|
|
# end
|
|
# end
|
|
#
|
|
# private
|
|
#
|
|
# def user_params
|
|
# params.require(:user).permit(
|
|
# :first_name,
|
|
# :last_name,
|
|
# :email,
|
|
# :password
|
|
# )
|
|
# end
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe UsersController, type: :controller do
|
|
# before do
|
|
# create(:user, id: 1)
|
|
# end
|
|
#
|
|
# it do
|
|
# params = {
|
|
# id: 1,
|
|
# user: {
|
|
# first_name: 'Jon',
|
|
# last_name: 'Doe',
|
|
# email: 'jondoe@example.com',
|
|
# password: 'password'
|
|
# }
|
|
# }
|
|
# should permit(:first_name, :last_name, :email, :password).
|
|
# for(:update, params: params).
|
|
# on(:user)
|
|
# end
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class UsersControllerTest < ActionController::TestCase
|
|
# setup do
|
|
# create(:user, id: 1)
|
|
# end
|
|
#
|
|
# should "(for PATCH #update) restrict parameters on :user to :first_name, :last_name, :email, and :password" do
|
|
# params = {
|
|
# id: 1,
|
|
# user: {
|
|
# first_name: 'Jon',
|
|
# last_name: 'Doe',
|
|
# email: 'jondoe@example.com',
|
|
# password: 'password'
|
|
# }
|
|
# }
|
|
# matcher = permit(:first_name, :last_name, :email, :password).
|
|
# for(:update, params: params).
|
|
# on(:user)
|
|
# assert_accepts matcher, subject
|
|
# end
|
|
# end
|
|
#
|
|
# Finally, if you have an action that isn't one of the seven resourceful
|
|
# actions, then you'll need to provide the HTTP verb that it responds to:
|
|
#
|
|
# Rails.application.routes.draw do
|
|
# resources :users do
|
|
# member do
|
|
# put :toggle
|
|
# end
|
|
# end
|
|
# end
|
|
#
|
|
# class UsersController < ApplicationController
|
|
# def toggle
|
|
# user = User.find(params[:id])
|
|
#
|
|
# if user.update_attributes(user_params)
|
|
# # ...
|
|
# else
|
|
# # ...
|
|
# end
|
|
# end
|
|
#
|
|
# private
|
|
#
|
|
# def user_params
|
|
# params.require(:user).permit(:activated)
|
|
# end
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe UsersController, type: :controller do
|
|
# before do
|
|
# create(:user, id: 1)
|
|
# end
|
|
#
|
|
# it do
|
|
# params = { id: 1, user: { activated: true } }
|
|
# should permit(:activated).
|
|
# for(:toggle, params: params, verb: :put).
|
|
# on(:user)
|
|
# end
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class UsersControllerTest < ActionController::TestCase
|
|
# setup do
|
|
# create(:user, id: 1)
|
|
# end
|
|
#
|
|
# should "(for PUT #toggle) restrict parameters on :user to :activated" do
|
|
# params = { id: 1, user: { activated: true } }
|
|
# matcher = permit(:activated).
|
|
# for(:toggle, params: params, verb: :put).
|
|
# on(:user)
|
|
# assert_accepts matcher, subject
|
|
# end
|
|
# end
|
|
#
|
|
# @return [PermitMatcher]
|
|
#
|
|
def permit(*params)
|
|
PermitMatcher.new(params).in_context(self)
|
|
end
|
|
|
|
# @private
|
|
class PermitMatcher
|
|
attr_writer :stubbed_params
|
|
|
|
def initialize(expected_permitted_parameter_names)
|
|
@expected_permitted_parameter_names =
|
|
expected_permitted_parameter_names
|
|
@action = nil
|
|
@verb = nil
|
|
@request_params = {}
|
|
@subparameter_name = nil
|
|
@parameters_double_registry = CompositeParametersDoubleRegistry.new
|
|
end
|
|
|
|
def for(action, options = {})
|
|
@action = action
|
|
@verb = options.fetch(:verb, default_verb)
|
|
@request_params = options.fetch(:params, {})
|
|
self
|
|
end
|
|
|
|
def add_params(params)
|
|
request_params.merge!(params)
|
|
self
|
|
end
|
|
|
|
def on(subparameter_name)
|
|
@subparameter_name = subparameter_name
|
|
self
|
|
end
|
|
|
|
def in_context(context)
|
|
@context = context
|
|
self
|
|
end
|
|
|
|
def description
|
|
"(for #{verb.upcase} ##{action}) " + expectation
|
|
end
|
|
|
|
def matches?(controller)
|
|
@controller = controller
|
|
ensure_action_and_verb_present!
|
|
|
|
parameters_double_registry.register
|
|
|
|
Doublespeak.with_doubles_activated do
|
|
Shoulda::Matchers::RailsShim.make_controller_request(
|
|
context,
|
|
verb,
|
|
action,
|
|
request_params,
|
|
)
|
|
end
|
|
|
|
unpermitted_parameter_names.empty?
|
|
end
|
|
|
|
def failure_message
|
|
"Expected #{verb.upcase} ##{action} to #{expectation},"\
|
|
"\nbut #{reality}."
|
|
end
|
|
|
|
def failure_message_when_negated
|
|
"Expected #{verb.upcase} ##{action} not to #{expectation},"\
|
|
"\nbut it did."
|
|
end
|
|
|
|
protected
|
|
|
|
attr_reader :controller, :double_collections_by_parameter_name, :action,
|
|
:verb, :request_params, :expected_permitted_parameter_names,
|
|
:context, :subparameter_name, :parameters_double_registry
|
|
|
|
def expectation
|
|
message = 'restrict parameters '
|
|
|
|
if subparameter_name
|
|
message << "on #{subparameter_name.inspect} "
|
|
end
|
|
|
|
message << 'to '\
|
|
"#{format_parameter_names(expected_permitted_parameter_names)}"
|
|
|
|
message
|
|
end
|
|
|
|
def reality
|
|
if actual_permitted_parameter_names.empty?
|
|
'it did not restrict any parameters'
|
|
else
|
|
'the restricted parameters were '\
|
|
"#{format_parameter_names(actual_permitted_parameter_names)}"\
|
|
' instead'
|
|
end
|
|
end
|
|
|
|
def format_parameter_names(parameter_names)
|
|
parameter_names.map(&:inspect).to_sentence
|
|
end
|
|
|
|
def actual_permitted_parameter_names
|
|
@_actual_permitted_parameter_names ||= begin
|
|
options =
|
|
if subparameter_name
|
|
{ for: subparameter_name }
|
|
else
|
|
{}
|
|
end
|
|
parameters_double_registry.permitted_parameter_names(options)
|
|
end
|
|
end
|
|
|
|
def unpermitted_parameter_names
|
|
expected_permitted_parameter_names - actual_permitted_parameter_names
|
|
end
|
|
|
|
def ensure_action_and_verb_present!
|
|
if action.blank?
|
|
raise ActionNotDefinedError
|
|
end
|
|
|
|
if verb.blank?
|
|
raise VerbNotDefinedError
|
|
end
|
|
end
|
|
|
|
def default_verb
|
|
case action
|
|
when :create then :post
|
|
when :update then RailsShim.verb_for_update
|
|
end
|
|
end
|
|
|
|
def parameter_names_as_sentence
|
|
expected_permitted_parameter_names.map(&:inspect).to_sentence
|
|
end
|
|
|
|
# @private
|
|
class CompositeParametersDoubleRegistry
|
|
def initialize
|
|
@parameters_double_registries = []
|
|
end
|
|
|
|
def register
|
|
double_collection = Doublespeak.double_collection_for(
|
|
::ActionController::Parameters.singleton_class,
|
|
)
|
|
double_collection.register_proxy(:new).to_return do |call|
|
|
params = call.return_value
|
|
parameters_double_registry = ParametersDoubleRegistry.new(params)
|
|
parameters_double_registry.register
|
|
parameters_double_registries << parameters_double_registry
|
|
end
|
|
end
|
|
|
|
def permitted_parameter_names(options = {})
|
|
parameters_double_registries.flat_map do |double_registry|
|
|
double_registry.permitted_parameter_names(options)
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
attr_reader :parameters_double_registries
|
|
end
|
|
|
|
# @private
|
|
class ParametersDoubleRegistry
|
|
TOP_LEVEL = Object.new
|
|
|
|
def self.permitted_parameter_names_within(double_collection)
|
|
double_collection.calls_to(:permit).map(&:args).flatten
|
|
end
|
|
|
|
def initialize(params)
|
|
@params = params
|
|
@double_collections_by_parameter_name = {}
|
|
end
|
|
|
|
def register
|
|
register_double_for_permit_against(params, TOP_LEVEL)
|
|
end
|
|
|
|
def permitted_parameter_names(args = {})
|
|
subparameter_name = args.fetch(:for, TOP_LEVEL)
|
|
|
|
if double_collections_by_parameter_name.key?(subparameter_name)
|
|
self.class.permitted_parameter_names_within(
|
|
double_collections_by_parameter_name[subparameter_name],
|
|
)
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
attr_reader :params, :double_collections_by_parameter_name
|
|
|
|
private
|
|
|
|
def register_double_for_permit_against(params, subparameter_name)
|
|
klass = params.singleton_class
|
|
|
|
double_collection = Doublespeak.double_collection_for(klass)
|
|
register_double_for_permit_on(double_collection)
|
|
register_double_for_require_on(double_collection)
|
|
|
|
double_collections_by_parameter_name[subparameter_name] =
|
|
double_collection
|
|
end
|
|
|
|
def register_double_for_permit_on(double_collection)
|
|
double_collection.register_proxy(:permit)
|
|
end
|
|
|
|
def register_double_for_require_on(double_collection)
|
|
double_collection.register_proxy(:require).to_return do |call|
|
|
params = call.return_value
|
|
subparameter_name = call.args.first
|
|
register_double_for_permit_against(params, subparameter_name)
|
|
end
|
|
end
|
|
end
|
|
|
|
# @private
|
|
class ActionNotDefinedError < StandardError
|
|
def message
|
|
'You must specify the controller action using the #for method.'
|
|
end
|
|
end
|
|
|
|
# @private
|
|
class VerbNotDefinedError < StandardError
|
|
def message
|
|
'You must specify an HTTP verb when using a non-RESTful action.'\
|
|
' For example: for(:authorize, verb: :post)'
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|