diff --git a/spec/controllers/blackouts_controller_spec.rb b/spec/controllers/blackouts_controller_spec.rb index 4f0a03012..ca51e9146 100644 --- a/spec/controllers/blackouts_controller_spec.rb +++ b/spec/controllers/blackouts_controller_spec.rb @@ -1,323 +1,220 @@ +# frozen_string_literal: true require 'spec_helper' -shared_examples_for 'page success' do - it { is_expected.to respond_with(:success) } - it { is_expected.not_to set_flash } -end - -shared_examples_for 'access denied' do - it { is_expected.to redirect_to(root_url) } - it { is_expected.to set_flash } -end - describe BlackoutsController, type: :controller do before(:each) { mock_app_config } - describe 'with admin' do - before do - sign_in FactoryGirl.create(:admin) - end - context 'GET index' do + context 'with admin' do + before { mock_user_sign_in(UserMock.new(:admin)) } + + describe 'GET index' do before do + allow(Blackout).to receive(:all).and_return(Blackout.none) get :index end - it_behaves_like 'page success' - it { is_expected.to render_template(:index) } - it 'should assign @blackouts to all blackouts' do - expect(assigns(:blackouts)).to eq(Blackout.all) + it_behaves_like 'successful request', :index + it 'gets all blackouts' do + expect(Blackout).to have_received(:all).at_least(:once) end end - context 'GET show' do - before do - get :show, id: FactoryGirl.create(:blackout) - end - it_behaves_like 'page success' - it { is_expected.to render_template(:show) } + + describe 'GET show' do context 'single blackout' do - it 'should not display a set' do - expect(assigns(:blackout_set).nil?) + it 'does not try to get a set' do + blackout = BlackoutMock.new(traits: [:findable], set_id: nil) + allow(Blackout).to receive(:where) + get :show, id: blackout.id + expect(Blackout).not_to have_received(:where) end end - end - context 'GET show' do - before do - @blackout = FactoryGirl.create(:blackout, set_id: 1) - @blackout_set = Blackout.where(set_id: 1) - get :show, id: @blackout - end - it_behaves_like 'page success' context 'recurring blackout' do - it 'should display the correct set' do - expect(assigns(:blackout_set).uniq.sort).to\ - eq(@blackout_set.uniq.sort) + it 'gets the set' do + blackout = BlackoutMock.new(traits: [:findable], set_id: 1) + allow(Blackout).to receive(:where) + get :show, id: blackout.id + expect(Blackout).to have_received(:where) + .with('set_id = ?', blackout.set_id) end - # the above code doesn't work; i'm too much of an rspec newbie end end - context 'GET new' do + + describe 'GET new' do before do + allow(Blackout).to receive(:new) get :new end - it_behaves_like 'page success' - it { is_expected.to render_template(:new) } + it 'uses the appropriate date defaults' do + expect(Blackout).to have_received(:new) + .with(start_date: Time.zone.today, end_date: Time.zone.today + 1.day) + end + it_behaves_like 'successful request', :new end - context 'GET new_recurring' do + + describe 'GET new_recurring' do before do + allow(Blackout).to receive(:new) get :new_recurring end - it_behaves_like 'page success' - it { is_expected.to render_template(:new_recurring) } - end - context 'GET edit' do - before do - get :edit, id: FactoryGirl.create(:blackout) + it 'uses the appropriate date defaults' do + expect(Blackout).to have_received(:new) + .with(start_date: Time.zone.today, end_date: Time.zone.today + 1.day) end - it_behaves_like 'page success' - it { is_expected.to render_template(:edit) } + it_behaves_like 'successful request', :new_recurring end - context 'POST create_recurring' do + + describe 'POST create_recurring' do context 'with correct params' do + let!(:blackout) { BlackoutMock.new(valid?: true) } before do - @new_set_id = Blackout.last ? Blackout.last.id + 1 : 0 - @attributes = FactoryGirl.attributes_for(:blackout, days: ['1', '']) - post :create_recurring, blackout: @attributes + allow(Blackout).to receive(:new).and_return(blackout) + allow(Blackout).to receive(:create_blackout_set) + post :create_recurring, blackout: { days: ['1', ''] } end - it 'should create a set' do - expect(Blackout.where(set_id: @new_set_id)).not_to be_empty + it 'creates a set' do + expect(Blackout).to have_received(:create_blackout_set) end it { is_expected.to redirect_to(blackouts_path) } - it { is_expected.to set_flash } + it { is_expected.to set_flash[:notice] } end context 'with incorrect params' do before do request.env['HTTP_REFERER'] = 'where_i_came_from' - @attributes = FactoryGirl.attributes_for(:blackout, days: ['']) - post :create_recurring, blackout: @attributes + post :create_recurring, blackout: { days: [''] } end - it { is_expected.to set_flash } + it { is_expected.to set_flash[:error] } it { is_expected.to render_template('new_recurring') } end - context 'with conflicting reservation' do + context 'with error during creation' do + let!(:blackout) { BlackoutMock.new(valid?: true) } before do - @res = FactoryGirl.create(:valid_reservation, - due_date: Time.zone.today + 1.day) - @attributes = FactoryGirl.attributes_for( - :blackout, days: [(Time.zone.today + 1.day).wday.to_s] - ) - post :create_recurring, blackout: @attributes + allow(Blackout).to receive(:new).and_return(blackout) + allow(Blackout).to receive(:create_blackout_set).and_return('ERROR') + post :create_recurring, blackout: { days: ['1', ''] } end - - it { is_expected.to set_flash } + it { is_expected.to set_flash[:error] } it { is_expected.to render_template('new_recurring') } - it 'should not save the blackouts' do - expect { post :create_recurring, blackout: @attributes }.not_to \ - change { Blackout.all.count } - end end end context 'POST create' do - shared_examples_for 'creates blackout' do |attributes| - before { post :create, blackout: attributes } - - it 'should create the new blackout' do - expect(Blackout.find(assigns(:blackout).id)).not_to be_nil - end - it 'should pass the correct params' do - expect(assigns(:blackout)[:notice]).to eq(attributes[:notice]) - expect(assigns(:blackout)[:start_date]).to\ - eq(attributes[:start_date]) - expect(assigns(:blackout)[:end_date]).to eq(attributes[:end_date]) - expect(assigns(:blackout)[:blackout_type]).to\ - eq(attributes[:blackout_type]) - end - it { is_expected.to redirect_to(blackout_path(assigns(:blackout))) } - it { is_expected.to set_flash } - end - - shared_examples_for 'does not create blackout' do |attributes| - before { post :create, blackout: attributes } - - it { is_expected.to set_flash } - it { is_expected.to render_template(:new) } - it 'should not save the blackout' do - expect { post :create, blackout: attributes }.not_to\ - change { Blackout.all.count } - end - end - - context 'with correct params' do - attributes = FactoryGirl.attributes_for(:blackout) - - it_behaves_like 'creates blackout', attributes - end - - context 'with overlapping archived reservation' do - before do - FactoryGirl.create(:archived_reservation, - start_date: Time.zone.today + 1.day, - due_date: Time.zone.today + 3.days) - end - - attributes = - FactoryGirl.attributes_for(:blackout, - start_date: Time.zone.today, - end_date: Time.zone.today + 2.days) - - it_behaves_like 'creates blackout', attributes - end - - context 'with overlapping missed reservation' do - before do - FactoryGirl.create(:missed_reservation, - start_date: Time.zone.today + 1.day, - due_date: Time.zone.today + 3.days) - end - - attributes = - FactoryGirl.attributes_for(:blackout, - start_date: Time.zone.today, - end_date: Time.zone.today + 2.days) - - it_behaves_like 'creates blackout', attributes - end - - context 'with incorrect params' do - attributes = - FactoryGirl.attributes_for(:blackout, - end_date: Time.zone.today - 1.day) - - it_behaves_like 'does not create blackout', attributes - end - - context 'with conflicting reservation start date' do + context 'successful creation' do + let!(:blackout) { FactoryGirl.build_stubbed(:blackout) } before do - FactoryGirl.create(:valid_reservation, - start_date: Time.zone.today + 1.day, - due_date: Time.zone.today + 3.days) - end - - attributes = - FactoryGirl.attributes_for(:blackout, - start_date: Time.zone.today, - end_date: Time.zone.today + 2.days) - - it_behaves_like 'does not create blackout', attributes - end - - context 'with conflicting reservation due date' do + allow(Blackout).to receive(:new).and_return(blackout) + allow(blackout).to receive(:save).and_return(true) + post :create, blackout: { id: 1 } + end + it { is_expected.to redirect_to(blackout) } + it { is_expected.to set_flash[:notice] } + end + context 'failed creation' do + context 'failed save' do + let!(:blackout) { BlackoutMock.new(save: false) } + before do + allow(Blackout).to receive(:new).and_return(blackout) + post :create, blackout: { id: 1 } + end + it { is_expected.to render_template(:new) } + it { is_expected.to set_flash[:error] } + end + context 'overlapping reservations' do + let!(:blackout) { BlackoutMock.new(save: true) } + before do + allow(Blackout).to receive(:new).and_return(blackout) + allow(Reservation).to \ + receive_message_chain(:overlaps_with_date_range, :active) + .and_return(instance_spy('Array', empty?: false)) + post :create, blackout: { id: 1 } + end + it { is_expected.to render_template(:new) } + it { is_expected.to set_flash[:error] } + end + end + end + + describe 'PUT update' do + context 'successful update' do + let!(:blackout) { FactoryGirl.build_stubbed(:blackout) } before do - FactoryGirl.create(:valid_reservation, - start_date: Time.zone.today, - due_date: Time.zone.today + 2.days) + allow(Blackout).to receive(:find) + allow(Blackout).to receive(:find) + .with(blackout.id.to_s).and_return(blackout) + allow(blackout).to receive(:update_attributes).and_return(true) + allow(blackout).to receive(:set_id=) + put :update, id: blackout.id, blackout: { id: 1 } end - - attributes = - FactoryGirl.attributes_for(:blackout, - start_date: Time.zone.today + 1.day, - end_date: Time.zone.today + 3.days) - - it_behaves_like 'does not create blackout', attributes - end - end - - context 'PUT update' do - context 'single blackout' do - before do - @new_attributes = FactoryGirl.attributes_for(:blackout) - @new_attributes[:notice] = 'New Message!!' - put :update, id: FactoryGirl.create(:blackout), - blackout: @new_attributes - end - it 'updates the blackout' do - expect(assigns(:blackout)[:notice]).to eq(@new_attributes[:notice]) + it { is_expected.to redirect_to(blackout) } + it { is_expected.to set_flash[:notice] } + it 'deletes the set_id' do + expect(blackout).to have_received(:set_id=).with(nil) end end - context 'recurring blackout' do + context 'unsuccessful update' do + let!(:blackout) { BlackoutMock.new(traits: [:findable]) } before do - @new_attributes = FactoryGirl.attributes_for(:blackout) - @new_attributes[:notice] = 'New Message!!' - put :update, id: FactoryGirl.create(:blackout, set_id: 1), - blackout: @new_attributes + allow(blackout).to receive(:update_attributes).and_return(false) + put :update, id: blackout.id, blackout: { id: 1 } end - it 'updates the blackout' do - expect(assigns(:blackout)[:notice]).to eq(@new_attributes[:notice]) - end - it 'sets the set_id to nil' do - expect(assigns(:blackout)[:set_id]).to be_nil + it { is_expected.to render_template(:edit) } + it 'deletes the set_id' do + expect(blackout).to have_received(:set_id=).with(nil) end end end - context 'DELETE destroy' do - before do - delete :destroy, id: FactoryGirl.create(:blackout) - end - it 'should delete the blackout' do - expect(Blackout.where(id: assigns(:blackout)[:id])).to be_empty + + describe 'DELETE destroy' do + let!(:blackout) { BlackoutMock.new(traits: [:findable]) } + before { delete :destroy, id: blackout.id } + it 'deletes the blackout' do + expect(blackout).to have_received(:destroy).with(:force) end it { is_expected.to redirect_to(blackouts_path) } end - context 'DELETE destroy recurring' do + + describe 'DELETE destroy recurring' do + let!(:blackout) { BlackoutMock.new(traits: [:findable], set_id: 1) } before do - # create an extra instance to test that the whole set was deleted - @extra = FactoryGirl.create(:blackout, set_id: 1) - delete :destroy_recurring, id: FactoryGirl.create(:blackout, set_id: 1) + allow(Blackout).to receive(:where) + .with('set_id = ?', 1).and_return([blackout]) + delete :destroy_recurring, id: blackout.id end - it 'should delete the whole set' do - expect(Blackout.where(set_id: @extra[:set_id])).to be_empty + it 'deletes the whole set' do + expect(blackout).to have_received(:destroy).with(:force) end - it { is_expected.to set_flash } + it { is_expected.to set_flash[:notice] } it { is_expected.to redirect_to(blackouts_path) } end end - context 'is not admin' do - before do - sign_in FactoryGirl.create(:user) - @blackout = FactoryGirl.create(:blackout) - @attributes = FactoryGirl.attributes_for(:blackout) - end + context 'is not admin' do + before { mock_user_sign_in } context 'GET index' do - before do - get :index - end - it_behaves_like 'access denied' + before { get :index } + it_behaves_like 'redirected request' end context 'GET show' do - before do - get :show, id: @blackout - end - it_behaves_like 'access denied' + before { get :show, id: 1 } + it_behaves_like 'redirected request' end context 'POST create' do - before do - post :create, blackout: @attributes - end - it_behaves_like 'access denied' + before { post :create, blackout: { id: 1 } } + it_behaves_like 'redirected request' end context 'PUT update' do - before do - put :update, id: @blackout - end - it_behaves_like 'access denied' + before { put :update, id: 1 } + it_behaves_like 'redirected request' end context 'POST create recurring' do - before do - post :create_recurring, blackout: @attributes - end - it_behaves_like 'access denied' + before { post :create_recurring, blackout: { id: 1 } } + it_behaves_like 'redirected request' end context 'DELETE destroy' do - before do - delete :destroy, id: @blackout - end - it_behaves_like 'access denied' + before { delete :destroy, id: 1 } + it_behaves_like 'redirected request' end context 'DELETE destroy recurring' do - before do - delete :destroy_recurring, id: @blackout - end - it_behaves_like 'access denied' + before { delete :destroy_recurring, id: 1 } + it_behaves_like 'redirected request' end end end diff --git a/spec/support/controller_helpers.rb b/spec/support/controller_helpers.rb index 87e36a0f3..a90cf501c 100644 --- a/spec/support/controller_helpers.rb +++ b/spec/support/controller_helpers.rb @@ -1,14 +1,21 @@ -# some basic helpers to simulate devise controller methods in specs +# frozen_string_literal: true +require Rails.root.join('spec/support/mockers/user.rb') + module ControllerHelpers - def current_user - user_session_info = - response.request.env['rack.session']['warden.user.user.key'] - return unless user_session_info - user_id = user_session_info[0][0] - User.find(user_id) + def mock_user_sign_in(user = UserMock.new(traits: [:findable])) + pass_app_setup_check + allow(request.env['warden']).to receive(:authenticate!).and_return(user) + # necessary for permissions to work + allow(ApplicationController).to receive(:current_user).and_return(user) + allow(Ability).to receive(:new).and_return(Ability.new(user)) + allow_any_instance_of(described_class).to \ + receive(:current_user).and_return(user) end - def user_signed_in? - !current_user.nil? + private + + def pass_app_setup_check + allow(AppConfig).to receive(:first).and_return(true) unless AppConfig.first + allow(User).to receive(:count).and_return(1) unless User.first end end diff --git a/spec/support/mockers/blackout.rb b/spec/support/mockers/blackout.rb new file mode 100644 index 000000000..5c043de59 --- /dev/null +++ b/spec/support/mockers/blackout.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +require Rails.root.join('spec/support/mockers/mocker.rb') + +class BlackoutMock < Mocker + def self.klass + Blackout + end + + def self.klass_name + 'Blackout' + end +end diff --git a/spec/support/mockers/category.rb b/spec/support/mockers/category.rb new file mode 100644 index 000000000..9c4cc7521 --- /dev/null +++ b/spec/support/mockers/category.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true +require Rails.root.join('spec/support/mockers/mocker.rb') +require Rails.root.join('spec/support/mockers/equipment_model.rb') + +class CategoryMock < Mocker + def self.klass + Category + end + + def self.klass_name + 'Category' + end + + private + + def with_equipment_models(models: nil, count: 1) + models ||= Array.new(count) { EquipmentModelMock.new } + parent_has_many(mocked_children: models, parent_sym: :category, + child_sym: :equipment_models) + end +end diff --git a/spec/support/mockers/equipment_item.rb b/spec/support/mockers/equipment_item.rb new file mode 100644 index 000000000..87f69f37b --- /dev/null +++ b/spec/support/mockers/equipment_item.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true +require Rails.root.join('spec/support/mockers/mocker.rb') +require Rails.root.join('spec/support/mockers/equipment_model.rb') + +class EquipmentItemMock < Mocker + def self.klass + EquipmentItem + end + + def self.klass_name + 'EquipmentItem' + end + + private + + def with_model(model: nil) + model ||= EquipmentModelMock.new + child_of_has_many(mocked_parent: model, parent_sym: :equipment_model, + child_sym: :equipment_items) + end +end diff --git a/spec/support/mockers/equipment_model.rb b/spec/support/mockers/equipment_model.rb new file mode 100644 index 000000000..398137a2d --- /dev/null +++ b/spec/support/mockers/equipment_model.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true +require Rails.root.join('spec/support/mockers/mocker.rb') +require Rails.root.join('spec/support/mockers/category.rb') +require Rails.root.join('spec/support/mockers/equipment_item.rb') + +class EquipmentModelMock < Mocker + def self.klass + EquipmentModel + end + + def self.klass_name + 'EquipmentModel' + end + + private + + def with_item(item:) + with_items(items: [item]) + end + + def with_items(items: nil, count: 1) + items ||= Array.new(count) { EquipmentItemMock.new } + parent_has_many(mocked_children: items, parent_sym: :equipment_model, + child_sym: :equipment_items) + end + + def with_category(cat: nil) + cat ||= CategoryMock.new + child_of_has_many(mocked_parent: cat, parent_sym: :category, + child_sym: :equipment_models) + end +end diff --git a/spec/support/mockers/mocker.rb b/spec/support/mockers/mocker.rb new file mode 100644 index 000000000..8b95b22a8 --- /dev/null +++ b/spec/support/mockers/mocker.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true +require 'rspec/mocks/standalone' + +# This class behaves as an extension of rspec-mocks' instance_spy. +# It is intended to be extended and used to make mocking models much simpler! +# +# To create a new subclass, the following methods must be overridden: +# - self.klass must return the class that the subclass is mocking +# - self.klass_name must return a string that matches the class being mocked +# +# Some examples using the EquipmentModelMock subclass: +# A mock that can be "found" with EquipmentModel#find: +# EquipmentModelMock.new(traits: [:findable]) +# A mock with a set of attributes: +# EquipmentModelMock.new(name: 'Camera', late_fee: 3) +# A mock with attributes and method stubs: +# EquipmentModelMock.new(name: 'Camera', model_restriced: false) +# A findable mock with attributes: +# EquipmentModelMock.new(traits: [:findable], name: 'Camera') +# +# A trait can be any method that exists on the mocker superclass or child class. +# To create an EquipmentModel that belongs to an existing category, camera: +# EquipmentModelMock.new(traits: [[:with_category, cat: camera]]) +# +# Use caution before adding methods -- any method defined here should be usable +# by all subclasses, with the exception of the association stub methods. + +class Mocker < RSpec::Mocks::InstanceVerifyingDouble + include RSpec::Mocks + + FIND_METHODS = [:find, :find_by_id].freeze + + def initialize(traits: [], **attrs) + # from RSpec::Mocks::ExampleMethods + # combination of #declare_verifying_double and #declare_double + ref = ObjectReference.for(self.class.klass_name) + RSpec::Mocks.configuration.verifying_double_callbacks.each do |block| + block.call(ref) + end + attrs ||= {} + super(ref, attrs) + as_null_object + process_traits(traits) + end + + def process_traits(traits) + traits.each { |t| send(*t) } + end + + private + + def klass + Object + end + + def klass_name + 'Object' + end + + def spy + self + end + + # lets us use rspec-mock syntax in mockers + def receive(method_name, &block) + Matchers::Receive.new(method_name, block) + end + + def allow(target) + AllowanceTarget.new(target) + end + + # Traits + def findable + id = FactoryGirl.generate(:unique_id) + allow(spy).to receive(:id).and_return(id) + FIND_METHODS.each do |method| + allow(self.class.klass).to receive(method) + allow(self.class.klass).to receive(method).with(id).and_return(spy) + allow(self.class.klass).to receive(method).with(id.to_s).and_return(spy) + end + end + + # Generalized association stubs + def child_of_has_many(mocked_parent:, parent_sym:, child_sym:) + allow(spy).to receive(parent_sym).and_return(mocked_parent) + children = if mocked_parent.send(child_sym).is_a? Array + mocked_parent.send(child_sym) << spy + else + [spy] + end + allow(mocked_parent).to receive(child_sym).and_return(children) + end + + def parent_has_many(mocked_children:, parent_sym:, child_sym:) + if mocked_children.is_a? Array + mocked_children.each do |child| + allow(child).to receive(parent_sym).and_return(spy) + end + end + allow(spy).to receive(child_sym).and_return(mocked_children) + end +end diff --git a/spec/support/mockers/reservation.rb b/spec/support/mockers/reservation.rb new file mode 100644 index 000000000..4c1f09914 --- /dev/null +++ b/spec/support/mockers/reservation.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +require Rails.root.join('spec/support/mockers/mocker.rb') + +class ReservationMock < Mocker + def self.klass + Reservation + end + + def self.klass_name + 'Reservation' + end + + private + + def for_user(user:) + child_of_has_many(mocked_parent: user, parent_sym: :reserver, + child_sym: :reservations) + end +end diff --git a/spec/support/mockers/user.rb b/spec/support/mockers/user.rb new file mode 100644 index 000000000..1270ac456 --- /dev/null +++ b/spec/support/mockers/user.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +require Rails.root.join('spec/support/mockers/mocker.rb') + +class UserMock < Mocker + def initialize(role = :user, traits: [], **attrs) + attrs = FactoryGirl.attributes_for(role).merge attrs + traits = [:findable] if traits.empty? + super(traits: traits, **attrs) + end + + def self.klass + User + end + + def self.klass_name + 'User' + end +end diff --git a/spec/support/shared_examples/controller_examples.rb b/spec/support/shared_examples/controller_examples.rb new file mode 100644 index 000000000..3d645a7b7 --- /dev/null +++ b/spec/support/shared_examples/controller_examples.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true +require 'spec_helper' + +shared_examples_for 'successful request' do |template| + it { is_expected.to respond_with(:success) } + it { is_expected.to render_template(template) } + it { is_expected.not_to set_flash } +end + +shared_examples_for 'redirected request' do + it { expect(response).to be_redirect } + it { is_expected.to set_flash } +end