diff --git a/app/models/reservation.rb b/app/models/reservation.rb index 2c83a52f9..d8ba1b7c8 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -138,7 +138,7 @@ def find_renewal_date due_date end - def eligible_for_renew? + def eligible_for_renew? # rubocop:disable CyclomaticComplexity # determines if a reservation is eligible for renewal, based on how many # days before the due date it is and the max number of times one is # allowed to renew @@ -156,7 +156,8 @@ def eligible_for_renew? max_renewal_days = equipment_model.maximum_renewal_days_before_due ((due_date - Time.zone.today).to_i < max_renewal_days) && (self.times_renewed < max_renewal_times) && - equipment_model.maximum_renewal_length > 0 + equipment_model.maximum_renewal_length > 0 && + equipment_model.available_count(due_date + 1.day) > 0 end def to_cart diff --git a/spec/controllers/equipment_items_controller_spec.rb b/spec/controllers/equipment_items_controller_spec.rb index 841854fcd..202e38d84 100644 --- a/spec/controllers/equipment_items_controller_spec.rb +++ b/spec/controllers/equipment_items_controller_spec.rb @@ -13,7 +13,7 @@ end it { is_expected.to respond_with(:success) } it { is_expected.to render_template(:index) } - it { is_expected.not_to set_the_flash } + it { is_expected.not_to set_flash } context 'without show deleted' do let!(:item_other_cat_active) { FactoryGirl.create(:equipment_item) } let!(:item_other_cat_inactive) do @@ -111,7 +111,7 @@ end it { is_expected.to respond_with(:success) } it { is_expected.to render_template(:show) } - it { is_expected.not_to set_the_flash } + it { is_expected.not_to set_flash } it 'should set to correct equipment item' do expect(assigns(:equipment_item)).to eq(item) end @@ -136,7 +136,7 @@ end it { is_expected.to respond_with(:success) } it { is_expected.to render_template(:new) } - it { is_expected.not_to set_the_flash } + it { is_expected.not_to set_flash } it 'assigns a new equipment item to @equipment_item' do expect(assigns(:equipment_item)).to be_new_record expect(assigns(:equipment_item)).to be_kind_of(EquipmentItem) @@ -181,7 +181,7 @@ expect(EquipmentItem.last.notes).not_to be_nil expect(EquipmentItem.last.notes).not_to be('') end - it { is_expected.to set_the_flash } + it { is_expected.to set_flash } it { is_expected.to redirect_to(EquipmentItem.last.equipment_model) } end context 'without valid attributes' do @@ -189,7 +189,7 @@ post :create, equipment_item: FactoryGirl.attributes_for( :equipment_item, name: nil) end - it { is_expected.not_to set_the_flash } + it { is_expected.not_to set_flash } it { is_expected.to render_template(:new) } it 'should not save' do expect do @@ -218,7 +218,7 @@ end it { is_expected.to respond_with(:success) } it { is_expected.to render_template(:edit) } - it { is_expected.not_to set_the_flash } + it { is_expected.not_to set_flash } it 'sets @equipment_item to selected item' do expect(assigns(:equipment_item)).to eq(item) end @@ -242,7 +242,7 @@ equipment_item: FactoryGirl.attributes_for(:equipment_item, name: 'Obj') end - it { is_expected.to set_the_flash } + it { is_expected.to set_flash } it 'sets @equipment_item to selected item' do expect(assigns(:equipment_item)).to eq(item) end @@ -262,7 +262,7 @@ equipment_item: FactoryGirl.attributes_for(:equipment_item, name: nil) end - it { is_expected.not_to set_the_flash } + it { is_expected.not_to set_flash } it 'should not update attributes' do item.reload expect(item.name).not_to be_nil diff --git a/spec/factories/equipment_models.rb b/spec/factories/equipment_models.rb index 7b0dd7cab..cb93ef93a 100644 --- a/spec/factories/equipment_models.rb +++ b/spec/factories/equipment_models.rb @@ -18,7 +18,7 @@ category { FactoryGirl.create(:category, max_per_user: 1) } end - factory :equipment_model_with_object do + factory :equipment_model_with_item do after(:create) do |model| FactoryGirl.create(:equipment_item, equipment_model: model) end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 3a91500f2..0ae080d69 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -25,6 +25,11 @@ password_confirmation 'passw0rd' end + factory :superuser do + role 'superuser' + view_mode 'superuser' + end + factory :admin do role 'admin' view_mode 'admin' diff --git a/spec/features/auth_spec.rb b/spec/features/auth_spec.rb index 1852defaa..844fb2412 100644 --- a/spec/features/auth_spec.rb +++ b/spec/features/auth_spec.rb @@ -58,6 +58,22 @@ env_wrapper('CAS_AUTH' => nil) { example.run } end + context 'testing login and logout helpers' do + before { sign_in_as_user(@checkout_person) } + it 'signs in the right user' do + visit root_path + expect(page).to have_content(@checkout_person.name) + expect(page).not_to have_link 'Sign In', href: new_user_session_path + end + + it 'can also sign out' do + sign_out + visit root_path + expect(page).to have_link 'Sign In', href: new_user_session_path + expect(page).not_to have_content(@checkout_person.name) + end + end + context 'with new user' do context 'can register' do before do diff --git a/spec/features/equipment_model_views_spec.rb b/spec/features/equipment_model_views_spec.rb new file mode 100644 index 000000000..50f9275fc --- /dev/null +++ b/spec/features/equipment_model_views_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe 'Equipment model views' do + before(:each) { app_setup } + subject { page } + + context 'index view' do + before { visit equipment_models_path } + it { is_expected.to have_content('Equipment Models') } + it { is_expected.to have_content(@eq_model.name) } + end +end diff --git a/spec/features/guest_spec.rb b/spec/features/guest_spec.rb index a6c492d08..d08f9e9a4 100644 --- a/spec/features/guest_spec.rb +++ b/spec/features/guest_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'guest users' do +describe 'guest users', type: :feature do # Shared Examples shared_examples 'unauthorized' do context 'visiting protected route' do @@ -44,7 +44,7 @@ it 'goes to the correct page with sign in link' do visit url_path expect(current_path).to eq(url_path) - expect(page).to have_link('Sign In') + expect(page).to have_link('Sign In', href: new_user_session_path) end end @@ -87,17 +87,14 @@ describe 'can use the catalog' do before :each do - visit '/' - within(:css, "#add_to_cart_#{EquipmentModel.first.id}") do - click_link 'Add to Cart' - end + add_item_to_cart(@eq_model) visit '/' end it 'can add items to cart' do expect(page.find(:css, '#list_items_in_cart')).to have_link( - EquipmentModel.first.name, - href: equipment_model_path(EquipmentModel.first)) + @eq_model.name, + href: equipment_model_path(@eq_model)) end it 'can remove items from cart' do @@ -105,16 +102,13 @@ href: "/remove_from_cart/#{EquipmentModel.first.id}" visit '/' expect(page.find(:css, '#list_items_in_cart')).not_to have_link( - EquipmentModel.first.name, - href: equipment_model_path(EquipmentModel.first)) + @eq_model.name, + href: equipment_model_path(@eq_model)) end it 'can change the dates' do @new_date = Time.zone.today + 5.days - # fill in both visible / datepicker and hidden field - fill_in 'cart_due_date_cart', with: @new_date.to_s - find(:xpath, "//input[@id='date_end_alt']").set @new_date.to_s - find('#cart_form').submit_form! + update_cart_due_date(@new_date.to_s) visit '/' expect(page.find('#cart_due_date_cart').value).to \ eq(@new_date.strftime('%m/%d/%Y')) diff --git a/spec/features/rails_admin_spec.rb b/spec/features/rails_admin_spec.rb new file mode 100644 index 000000000..877d5deef --- /dev/null +++ b/spec/features/rails_admin_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +describe 'Active Admin', type: :feature do + before(:all) { app_setup } + + context 'as superuser' do + before { sign_in_as_user(@superuser) } + after { sign_out } + + shared_examples 'can access route' do |model| + let(:path) do + admin_routes.index_url(model_name: model, host: Capybara.default_host) + end + + it do + visit path + expect(page.current_url).to eq(path) + end + end + + it 'can access the dashboard' do + visit admin_routes.dashboard_path + expect(page).to have_content 'Site Administration' + expect(page.current_url).to \ + eq(admin_routes.dashboard_url(host: Capybara.default_host)) + end + + it_behaves_like 'can access route', :announcement + it_behaves_like 'can access route', :app_config + it_behaves_like 'can access route', :blackout + it_behaves_like 'can access route', :category + it_behaves_like 'can access route', :checkin_procedure + it_behaves_like 'can access route', :checkout_procedure + it_behaves_like 'can access route', :equipment_item + it_behaves_like 'can access route', :equipment_model + it_behaves_like 'can access route', :requirement + it_behaves_like 'can access route', :reservation + it_behaves_like 'can access route', :user + end + + context 'as other roles' do + shared_examples 'cannot access route' do |model| + if model + let(:path) { admin_routes.index_path(model_name: model) } + else + let(:path) { admin_routes.dashboard_path } + end + + it do + visit path + expect(page.current_url).to eq(root_url(host: Capybara.default_host)) + end + end + + context 'as patron' do + before { sign_in_as_user(@user) } + after { sign_out } + + it_behaves_like 'cannot access route' + it_behaves_like 'cannot access route', :announcement + it_behaves_like 'cannot access route', :app_config + it_behaves_like 'cannot access route', :blackout + it_behaves_like 'cannot access route', :category + it_behaves_like 'cannot access route', :checkin_procedure + it_behaves_like 'cannot access route', :checkout_procedure + it_behaves_like 'cannot access route', :equipment_item + it_behaves_like 'cannot access route', :equipment_model + it_behaves_like 'cannot access route', :requirement + it_behaves_like 'cannot access route', :reservation + it_behaves_like 'cannot access route', :user + end + + context 'as checkout person' do + before { sign_in_as_user(@checkout_person) } + after { sign_out } + + it_behaves_like 'cannot access route' + it_behaves_like 'cannot access route', :announcement + it_behaves_like 'cannot access route', :app_config + it_behaves_like 'cannot access route', :blackout + it_behaves_like 'cannot access route', :category + it_behaves_like 'cannot access route', :checkin_procedure + it_behaves_like 'cannot access route', :checkout_procedure + it_behaves_like 'cannot access route', :equipment_item + it_behaves_like 'cannot access route', :equipment_model + it_behaves_like 'cannot access route', :requirement + it_behaves_like 'cannot access route', :reservation + it_behaves_like 'cannot access route', :user + end + + context 'as admin' do + before { sign_in_as_user(@admin) } + after { sign_out } + + it_behaves_like 'cannot access route' + it_behaves_like 'cannot access route', :announcement + it_behaves_like 'cannot access route', :app_config + it_behaves_like 'cannot access route', :blackout + it_behaves_like 'cannot access route', :category + it_behaves_like 'cannot access route', :checkin_procedure + it_behaves_like 'cannot access route', :checkout_procedure + it_behaves_like 'cannot access route', :equipment_item + it_behaves_like 'cannot access route', :equipment_model + it_behaves_like 'cannot access route', :requirement + it_behaves_like 'cannot access route', :reservation + it_behaves_like 'cannot access route', :user + end + end +end diff --git a/spec/features/reservations_spec.rb b/spec/features/reservations_spec.rb new file mode 100644 index 000000000..038db750d --- /dev/null +++ b/spec/features/reservations_spec.rb @@ -0,0 +1,332 @@ +require 'spec_helper' + +describe 'Reservations', type: :feature do + before(:all) { app_setup } + + context 'can be created' do + before(:each) { empty_cart } + + shared_examples 'can create valid reservation' do |reserver| + let(:reserver) { reserver } + str = reserver ? 'for other user successfully' : 'successfully' + + it str do + visit root_path + change_reserver(reserver) if reserver + add_item_to_cart(@eq_model) + update_cart_start_date(Time.zone.today) + due_date = Time.zone.today + 1.day + update_cart_due_date(due_date) + # request catalog since our update cart methods only return the cart + # partial from the JS update (so the 'Reserve' link wouldn't be there) + visit root_path + click_link 'Reserve', href: new_reservation_path + + # set reserver to current user if unset, check confirmation page + reserver = reserver ? reserver : @current_user + expect(page).to have_content reserver.name + expect(page).to have_content Time.zone.today.to_s(:long) + expect(page).to have_content due_date.to_s(:long) + expect(page).to have_content @eq_model.name + expect(page).to have_content 'Create Reservation' + click_button 'Finalize Reservation' + + # check that reservation was created with correct dates + query = Reservation.for_eq_model(@eq_model).for_reserver(reserver.id) + expect(query.count).to eq(1) + expect(query.first.start_date).to eq(Time.zone.today) + expect(query.first.due_date).to eq(due_date) + expect(query.first.approval_status).to eq('auto') + end + end + + shared_examples 'can create reservation request' do |reserver| + let(:reserver) { reserver } + str = reserver ? 'for other user successfully' : 'successfully' + + it str do + visit root_path + change_reserver(reserver) if reserver + add_item_to_cart(@eq_model) + update_cart_start_date(Time.zone.today) + # violate length validation + bad_due_date = + Time.zone.today + (@eq_model.maximum_checkout_length + 1).days + update_cart_due_date(bad_due_date) + # request catalog since our update cart methods only return the cart + # partial from the JS update (so the 'Reserve' link wouldn't be there) + visit root_path + click_link 'Reserve', href: new_reservation_path + + # set reserver to current user if unset, check confirmation page + reserver = reserver ? reserver : @current_user + expect(page).to have_content reserver.name + expect(page).to have_content Time.zone.today.to_s(:long) + expect(page).to have_content bad_due_date.to_s(:long) + expect(page).to have_content @eq_model.name + expect(page).to have_content 'File Reservation Request' + find(:xpath, "//textarea[@id='reservation_notes']").set 'Because' + click_button 'Submit Request' + + # check that reservation was created with correct dates + # can't use for_eq_model since it calls finalized + query = Reservation.where(equipment_model_id: @eq_model.id) + .for_reserver(reserver.id) + expect(query.count).to eq(1) + expect(query.first.start_date).to eq(Time.zone.today) + expect(query.first.due_date).to eq(bad_due_date) + expect(query.first.approval_status).to eq('requested') + end + end + + shared_examples 'can create failing reservation' do |reserver| + let(:reserver) { reserver } + str = reserver ? 'for other user successfully' : 'successfully' + + it str do + visit root_path + change_reserver(reserver) if reserver + add_item_to_cart(@eq_model) + update_cart_start_date(Time.zone.today) + # violate length validation + bad_due_date = + Time.zone.today + (@eq_model.maximum_checkout_length + 1).days + update_cart_due_date(bad_due_date) + # request catalog since our update cart methods only return the cart + # partial from the JS update (so the 'Reserve' link wouldn't be there) + visit root_path + click_link 'Reserve', href: new_reservation_path + + # set reserver to current user if unset, check confirmation page + reserver = reserver ? reserver : @current_user + expect(page).to have_content reserver.name + expect(page).to have_content Time.zone.today.to_s(:long) + expect(page).to have_content bad_due_date.to_s(:long) + expect(page).to have_content @eq_model.name + expect(page).to have_content 'Create Reservation' + expect(page).to have_content 'Please be aware of the following errors:' + find(:xpath, "//textarea[@id='reservation_notes']").set 'Because' + click_button 'Finalize Reservation' + + # check that reservation was created with correct dates + # can't use for_eq_model since it calls finalized + query = Reservation.where(equipment_model_id: @eq_model.id) + .for_reserver(reserver.id) + expect(query.count).to eq(1) + expect(query.first.start_date).to eq(Time.zone.today) + expect(query.first.due_date).to eq(bad_due_date) + expect(query.first.approval_status).to eq('auto') + end + end + + context 'by patrons' do + before { sign_in_as_user(@user) } + after { sign_out } + + it_behaves_like 'can create valid reservation' + it_behaves_like 'can create reservation request' + end + + context 'by checkout persons' do + before { sign_in_as_user(@checkout_person) } + after { sign_out } + + context 'without override permissions' do + before { AppConfig.first.update_attributes(override_on_create: false) } + + it_behaves_like 'can create valid reservation', @user + it_behaves_like 'can create reservation request', @user + end + + context 'with override permissions' do + before { AppConfig.first.update_attributes(override_on_create: true) } + + it_behaves_like 'can create failing reservation', @user + end + end + + context 'by admins' do + before { sign_in_as_user(@admin) } + after { sign_out } + + context 'with override disabled' do + before { AppConfig.first.update_attributes(override_on_create: false) } + + it_behaves_like 'can create valid reservation', @user + it_behaves_like 'can create failing reservation', @user + end + + context 'with override enabled' do + before { AppConfig.first.update_attributes(override_on_create: true) } + + it_behaves_like 'can create failing reservation', @user + end + end + + context 'by superusers' do + before { sign_in_as_user(@superuser) } + after { sign_out } + + context 'with override disabled' do + before { AppConfig.first.update_attributes(override_on_create: false) } + + it_behaves_like 'can create valid reservation', @user + it_behaves_like 'can create failing reservation', @user + end + + context 'with override enabled' do + before { AppConfig.first.update_attributes(override_on_create: true) } + + it_behaves_like 'can create failing reservation', @user + end + end + end + + context 'equipment processing' do + before(:each) do + @res = FactoryGirl.create :valid_reservation, reserver: @user, + equipment_model: @eq_model + end + + shared_examples 'can handle reservation transactions' do + it 'checks out and checks in successfully' do + # check out + visit manage_reservations_for_user_path(@user) + select "#{@eq_model.equipment_items.first.name}", from: 'Equipment Item' + click_button 'Check-Out Equipment' + + expect(page).to have_content 'Check-Out Receipt' + expect(page).to have_content current_user.name + @res.reload + expect(@res.equipment_item_id).to eq(@eq_model.equipment_items.first.id) + expect(@res.checkout_handler).to eq(current_user) + expect(@res.checked_out).not_to be_nil + + # check in + visit manage_reservations_for_user_path(@user) + check "#{@res.equipment_item.name}" + click_button 'Check-In Equipment' + + expect(page).to have_content 'Check-In Receipt' + expect(page).to have_content current_user.name + @res.reload + expect(@res.checkin_handler).to eq(current_user) + expect(@res.checked_in).not_to be_nil + end + end + + context 'as patron' do + before { sign_in_as_user(@user) } + after { sign_out } + + it 'should redirect to the catalog page' do + visit manage_reservations_for_user_path(@user) + expect(page).to have_content 'Catalog' + expect(page.current_url).to eq(root_url) + end + end + + context 'as checkout person' do + before { sign_in_as_user(@checkout_person) } + after { sign_out } + + it_behaves_like 'can handle reservation transactions' + end + + context 'as admin' do + before { sign_in_as_user(@admin) } + after { sign_out } + + it_behaves_like 'can handle reservation transactions' + end + + context 'as superuser' do + before { sign_in_as_user(@superuser) } + after { sign_out } + + it_behaves_like 'can handle reservation transactions' + end + end + + context 'renewing reservations' do + before(:each) do + @res = + FactoryGirl.create :checked_out_reservation, reserver: @user, + equipment_model: @eq_model + end + + shared_examples 'can renew reservation when enabled and available' do + it do + AppConfig.first.update_attributes(enable_renewals: true) + visit reservation_path(@res) + expect(page).to have_content 'You are currently eligible to renew' + click_link 'Renew Now', href: renew_reservation_path(@res) + expect(page).to have_content 'Your reservation has been renewed' + expect { @res.reload }.to change { @res.due_date } + end + end + + shared_examples 'cannot see renew button when disabled' do + it do + AppConfig.first.update_attributes(enable_renewals: false) + visit reservation_path(@res) + expect(page).not_to have_link 'Renew Now', + href: renew_reservation_path(@res) + end + end + + shared_examples 'cannot renew reservation when unavailable' do + it do + AppConfig.first.update_attributes(enable_renewals: true) + FactoryGirl.create :reservation, equipment_model: @eq_model, + start_date: @res.due_date + 1.day, + due_date: @res.due_date + 2.days + visit reservation_path(@res) + expect(page).to have_content 'This item is not currently eligible '\ + 'for renewal.' + end + end + + context 'as patron' do + before { sign_in_as_user(@user) } + after { sign_out } + + it_behaves_like 'can renew reservation when enabled and available' + it_behaves_like 'cannot see renew button when disabled' + it_behaves_like 'cannot renew reservation when unavailable' + end + + context 'as checkout person' do + before { sign_in_as_user(@checkout_person) } + after { sign_out } + + it_behaves_like 'can renew reservation when enabled and available' + it_behaves_like 'cannot see renew button when disabled' + it_behaves_like 'cannot renew reservation when unavailable' + end + + context 'as admin' do + before { sign_in_as_user(@admin) } + after { sign_out } + + it_behaves_like 'can renew reservation when enabled and available' + it_behaves_like 'cannot see renew button when disabled' + it_behaves_like 'cannot renew reservation when unavailable' + end + + context 'as superuser' do + before { sign_in_as_user(@superuser) } + after { sign_out } + + it_behaves_like 'can renew reservation when enabled and available' + it_behaves_like 'cannot renew reservation when unavailable' + + it 'can see renew button when disabled' do + AppConfig.first.update_attributes(enable_renewals: false) + visit reservation_path(@res) + expect(page).to have_link 'Renew Now', + href: renew_reservation_path(@res) + end + end + end +end diff --git a/spec/support/controller_helpers.rb b/spec/support/controller_helpers.rb index c8a3090ea..87e36a0f3 100644 --- a/spec/support/controller_helpers.rb +++ b/spec/support/controller_helpers.rb @@ -1,4 +1,4 @@ -# some basic helpers to simulate controller methods in specs +# some basic helpers to simulate devise controller methods in specs module ControllerHelpers def current_user user_session_info = diff --git a/spec/support/feature_helpers.rb b/spec/support/feature_helpers.rb index 953a8b299..0d7b8f39a 100644 --- a/spec/support/feature_helpers.rb +++ b/spec/support/feature_helpers.rb @@ -3,12 +3,20 @@ module FeatureHelpers def app_setup @app_config = FactoryGirl.create(:app_config) @category = FactoryGirl.create(:category) - @equipment_model_with_object = - FactoryGirl.create(:equipment_model_with_object, category: @category) + @eq_model = + FactoryGirl.create(:equipment_model, category: @category) + @eq_item = FactoryGirl.create(:equipment_item, equipment_model: @eq_model) @admin = FactoryGirl.create(:admin) + @superuser = FactoryGirl.create(:superuser) + @checkout_person = FactoryGirl.create(:checkout_person) @user = FactoryGirl.create(:user) end + def empty_cart + visit '/' + click_link 'Empty Cart' + end + def fill_in_registration fill_in 'Email', with: (0...8).map { (65 + rand(26)).chr }.join + '@example.com' @@ -19,8 +27,60 @@ def fill_in_registration fill_in 'Affiliation', with: 'Yale' end - def fill_in_login - fill_in 'Email', with: @user.email + def fill_in_login(user = @user) + fill_in 'Email', with: user.email fill_in 'Password', with: 'passw0rd' end + + def update_cart_start_date(new_date_str) + # fill in both visible / datepicker and hidden field + fill_in 'cart_start_date_cart', with: new_date_str + find(:xpath, "//input[@id='date_start_alt']").set new_date_str + find('#cart_form').submit_form! + end + + def update_cart_due_date(new_date_str) + # fill in both visible / datepicker and hidden field + fill_in 'cart_due_date_cart', with: new_date_str + find(:xpath, "//input[@id='date_end_alt']").set new_date_str + find('#cart_form').submit_form! + end + + def add_item_to_cart(eq_model) + # visit catalog to make sure our css selector works + visit root_path + within(:css, "#add_to_cart_#{eq_model.id}") do + click_link 'Add to Cart' + end + end + + def change_reserver(reserver) + fill_in 'Reserving For', with: reserver.id + find('#cart_form').submit_form! + end + + def sign_in_as_user(user) + visit root_path + click_link 'Sign In', match: :first + fill_in_login(user) + click_button 'Sign in' + @current_user = user + end + + def sign_out + visit root_path + click_link 'Log Out' + @current_user = nil + end + + def current_user + visit root_path + click_link 'My Profile' + email = find('.page-header h1 small').text + User.find_by_email(email) + end + + def admin_routes + RailsAdmin::Engine.routes.url_helpers + end end