diff --git a/Gemfile b/Gemfile index a6e27fae..56f320cf 100644 --- a/Gemfile +++ b/Gemfile @@ -112,6 +112,7 @@ group :test do gem 'rspec-core' gem 'rspec-rerun' gem 'i18n-spec' + gem 'rails-controller-testing' # code coverage gem 'simplecov', require: false gem 'simplecov-lcov', require: false diff --git a/Gemfile.lock b/Gemfile.lock index c53687fb..97f90f3e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -350,6 +350,10 @@ GEM sprockets-rails (>= 2.0.0) rails-assets-listjs (0.2.0.beta.4) railties (>= 3.1) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) @@ -606,6 +610,7 @@ DEPENDENCIES rack-cors rails (~> 5.2) rails-assets-listjs (= 0.2.0.beta.4) + rails-controller-testing rails-i18n rails-settings-cached (= 0.4.3) rails_tokeninput diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 86f9e2eb..d01a78ca 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -18,7 +18,7 @@ class HomeController < ApplicationController @bank_accounts = @types.includes(:bank_account).map(&:bank_account).uniq.compact @bank_accounts = [BankAccount.last] if @bank_accounts.empty? else - redirect_to root_url, alert: I18n.t('group_orders.errors.no_member') + redirect_to root_path, alert: I18n.t('group_orders.errors.no_member') end end @@ -26,7 +26,7 @@ class HomeController < ApplicationController if @current_user.update(user_params) @current_user.ordergroup.update(ordergroup_params) if ordergroup_params session[:locale] = @current_user.locale - redirect_to my_profile_url, notice: I18n.t('home.changes_saved') + redirect_to my_profile_path, notice: I18n.t('home.changes_saved') else render :profile end @@ -64,7 +64,7 @@ class HomeController < ApplicationController # cancel personal memberships direct from the myProfile-page def cancel_membership if params[:membership_id] - membership = @current_user.memberships.find!(params[:membership_id]) + membership = @current_user.memberships.find(params[:membership_id]) else membership = @current_user.memberships.find_by_group_id!(params[:group_id]) end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb new file mode 100644 index 00000000..35db7574 --- /dev/null +++ b/spec/controllers/application_controller_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ApplicationController, type: :controller do + describe 'current' do + it 'returns current ApplicationController' do + described_class.new.send(:store_controller) + expect(described_class.current).to be_instance_of described_class + end + end +end diff --git a/spec/controllers/articles_controller_spec.rb b/spec/controllers/articles_controller_spec.rb new file mode 100644 index 00000000..ab14e30e --- /dev/null +++ b/spec/controllers/articles_controller_spec.rb @@ -0,0 +1,340 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ArticlesController, type: :controller do + let(:user) { create :user, :role_article_meta } + let(:article_category_a) { create :article_category, name: 'AAAA' } + let(:article_category_b) { create :article_category, name: 'BBBB' } + let(:article_category_c) { create :article_category, name: 'CCCC' } + let(:article_a) { create :article, name: 'AAAA', note: 'CCC', unit: '750 g', article_category: article_category_b, availability: false } + let(:article_b) { create :article, name: 'BBBB', note: 'AAA', unit: '500 g', article_category: article_category_a, availability: true } + let(:article_c) { create :article, name: 'CCCC', note: 'BBB', unit: '250 g', article_category: article_category_c, availability: true } + + let(:supplier) { create :supplier, articles: [article_a, article_b, article_c] } + let(:order) { create :order } + let(:order2) { create :order } + + def get_with_supplier(action, params: {}, xhr: false, format: nil) + params['supplier_id'] = supplier.id + get_with_defaults(action, params: params, xhr: xhr, format: format) + end + + def post_with_supplier(action, params: {}, xhr: false, format: nil) + params['supplier_id'] = supplier.id + post_with_defaults(action, params: params, xhr: xhr, format: format) + end + + before { login user } + + describe 'GET index' do + it 'assigns sorting on articles' do + sortings = [ + ['name', [article_a, article_b, article_c]], + ['name_reverse', [article_c, article_b, article_a]], + ['note', [article_b, article_c, article_a]], + ['note_reverse', [article_a, article_c, article_b]], + ['unit', [article_c, article_b, article_a]], + ['unit_reverse', [article_a, article_b, article_c]], + ['article_category', [article_b, article_a, article_c]], + ['article_category_reverse', [article_c, article_a, article_b]], + ['availability', [article_a, article_b, article_c]], + ['availability_reverse', [article_b, article_c, article_a]] + ] + sortings.each do |sorting| + get_with_supplier :index, params: { sort: sorting[0] } + expect(response).to have_http_status(:success) + expect(assigns(:articles).to_a).to eq(sorting[1]) + end + end + + it 'triggers an article csv' do + get_with_supplier :index, format: :csv + expect(response.header['Content-Type']).to include('text/csv') + expect(response.body).to include(article_a.unit, article_b.unit) + end + end + + describe 'new' do + it 'renders form for a new article' do + get_with_supplier :new, xhr: true + expect(response).to have_http_status(:success) + end + end + + describe 'copy' do + it 'renders form with copy of an article' do + get_with_supplier :copy, params: { article_id: article_a.id }, xhr: true + expect(assigns(:article).attributes).to eq(article_a.dup.attributes) + expect(response).to have_http_status(:success) + end + end + + describe '#create' do + it 'creates a new article' do + valid_attributes = article_a.attributes.except('id') + valid_attributes['name'] = 'ABAB' + get_with_supplier :create, params: { article: valid_attributes }, xhr: true + expect(response).to have_http_status(:success) + end + + it 'fails to create a new article and renders #new' do + get_with_supplier :create, params: { article: { id: nil } }, xhr: true + expect(response).to have_http_status(:success) + expect(response).to render_template('articles/new') + end + end + + describe 'edit' do + it 'opens form to edit article attributes' do + get_with_supplier :edit, params: { id: article_a.id }, xhr: true + expect(response).to have_http_status(:success) + expect(response).to render_template('articles/new') + end + end + + describe '#edit all' do + it 'renders edit_all' do + get_with_supplier :edit_all, xhr: true + expect(response).to have_http_status(:success) + expect(response).to render_template('articles/edit_all') + end + end + + describe '#update' do + it 'updates article attributes' do + get_with_supplier :update, params: { id: article_a.id, article: { unit: '300 g' } }, xhr: true + expect(assigns(:article).unit).to eq('300 g') + expect(response).to have_http_status(:success) + end + + it 'updates article with empty name attribute' do + get_with_supplier :update, params: { id: article_a.id, article: { name: nil } }, xhr: true + expect(response).to render_template('articles/new') + end + end + + describe '#update_all' do + it 'updates all articles' do + get_with_supplier :update_all, params: { articles: { "#{article_a.id}": attributes_for(:article), "#{article_b.id}": attributes_for(:article) } } + expect(response).to have_http_status(:redirect) + end + + it 'fails on updating all articles' do + get_with_supplier :update_all, params: { articles: { "#{article_a.id}": attributes_for(:article, name: 'ab') } } + expect(response).to have_http_status(:success) + expect(response).to render_template('articles/edit_all') + end + end + + describe '#update_selected' do + let(:order_article) { create :order_article, order: order, article: article_c } + + before do + order_article + end + + it 'updates selected articles' do + get_with_supplier :update_selected, params: { selected_articles: [article_a.id, article_b.id] } + expect(response).to have_http_status(:redirect) + end + + it 'destroys selected articles' do + get_with_supplier :update_selected, params: { selected_articles: [article_a.id, article_b.id], selected_action: 'destroy' } + article_a.reload + article_b.reload + expect(article_a).to be_deleted + expect(article_b).to be_deleted + expect(response).to have_http_status(:redirect) + end + + it 'sets availability false on selected articles' do + get_with_supplier :update_selected, params: { selected_articles: [article_a.id, article_b.id], selected_action: 'setNotAvailable' } + article_a.reload + article_b.reload + expect(article_a).not_to be_availability + expect(article_b).not_to be_availability + expect(response).to have_http_status(:redirect) + end + + it 'sets availability true on selected articles' do + get_with_supplier :update_selected, params: { selected_articles: [article_a.id, article_b.id], selected_action: 'setAvailable' } + article_a.reload + article_b.reload + expect(article_a).to be_availability + expect(article_b).to be_availability + expect(response).to have_http_status(:redirect) + end + + it 'fails deletion if one article is in open order' do + get_with_supplier :update_selected, params: { selected_articles: [article_a.id, article_c.id], selected_action: 'destroy' } + article_a.reload + article_c.reload + expect(article_a).not_to be_deleted + expect(article_c).not_to be_deleted + expect(response).to have_http_status(:redirect) + end + end + + describe '#parse_upload' do + let(:file) { Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/files/upload_test.csv'), original_filename: 'upload_test.csv') } + + it 'updates particles from spreadsheet' do + post_with_supplier :parse_upload, params: { articles: { file: file, outlist_absent: '1', convert_units: '1' } } + expect(response).to have_http_status(:success) + end + + it 'missing file not updates particles from spreadsheet' do + post_with_supplier :parse_upload, params: { articles: { file: nil, outlist_absent: '1', convert_units: '1' } } + expect(response).to have_http_status(:redirect) + expect(flash[:alert]).to match(I18n.t('errors.general_msg', msg: "undefined method `original_filename' for \"\":String").to_s) + end + end + + describe '#sync' do + # TODO: double render error in controller + it 'throws double render error' do + expect do + post :sync, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id } + end.to raise_error(AbstractController::DoubleRenderError) + end + + xit 'updates particles from spreadsheet' do + post :sync, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, articles: { '#{article_a.id}': attributes_for(:article), '#{article_b.id}': attributes_for(:article) } } + expect(response).to have_http_status(:redirect) + end + end + + describe '#destroy' do + let(:order_article) { create :order_article, order: order, article: article_c } + + before do + order_article + end + + it 'does not delete article if order open' do + get_with_supplier :destroy, params: { id: article_c.id }, xhr: true + expect(assigns(:article)).not_to be_deleted + expect(response).to have_http_status(:success) + expect(response).to render_template('articles/destroy') + end + + it 'deletes article if order closed' do + get_with_supplier :destroy, params: { id: article_b.id }, xhr: true + expect(assigns(:article)).to be_deleted + expect(response).to have_http_status(:success) + expect(response).to render_template('articles/destroy') + end + end + + describe '#update_synchronized' do + let(:order_article) { create :order_article, order: order, article: article_c } + + before do + order_article + article_a + article_b + article_c + end + + it 'deletes articles' do + # TODO: double render error in controller + get_with_supplier :update_synchronized, params: { outlisted_articles: { article_a.id => article_a, article_b.id => article_b } } + article_a.reload + article_b.reload + expect(article_a).to be_deleted + expect(article_b).to be_deleted + expect(response).to have_http_status(:redirect) + end + + it 'updates articles' do + get_with_supplier :update_synchronized, params: { articles: { article_a.id => { name: 'NewNameA' }, article_b.id => { name: 'NewNameB' } } } + expect(assigns(:updated_articles).first.name).to eq 'NewNameA' + expect(response).to have_http_status(:redirect) + end + + it 'does not update articles if article with same name exists' do + get_with_supplier :update_synchronized, params: { articles: { article_a.id => { unit: '2000 g' }, article_b.id => { name: 'AAAA' } } } + error_array = [assigns(:updated_articles).first.errors.first, assigns(:updated_articles).last.errors.first] + expect(error_array).to include([:name, 'name is already taken']) + expect(response).to have_http_status(:success) + end + + it 'does update articles if article with same name was deleted before' do + get_with_supplier :update_synchronized, params: { + outlisted_articles: { article_a.id => article_a }, + articles: { + article_a.id => { name: 'NewName' }, + article_b.id => { name: 'AAAA' } + } + } + error_array = [assigns(:updated_articles).first.errors.first, assigns(:updated_articles).last.errors.first] + expect(error_array).not_to be_any + expect(response).to have_http_status(:redirect) + end + + it 'does not delete articles in open order' do + get_with_supplier :update_synchronized, params: { outlisted_articles: { article_c.id => article_c } } + article_c.reload + expect(article_c).not_to be_deleted + expect(response).to have_http_status(:success) + end + + it 'assigns updated article_pairs on error' do + get_with_supplier :update_synchronized, params: { + articles: { article_a.id => { name: 'DDDD' } }, + outlisted_articles: { article_c.id => article_c } + } + expect(assigns(:updated_article_pairs).first).to eq([article_a, { name: 'DDDD' }]) + article_c.reload + expect(article_c).not_to be_deleted + expect(response).to have_http_status(:success) + end + + it 'updates articles in open order' do + get_with_supplier :update_synchronized, params: { articles: { article_c.id => { name: 'DDDD' } } } + article_c.reload + expect(article_c.name).to eq 'DDDD' + expect(response).to have_http_status(:redirect) + end + end + + describe '#shared' do + let(:shared_supplier) { create :shared_supplier, shared_articles: [shared_article] } + let(:shared_article) { create :shared_article, name: 'shared' } + let(:article_s) { create :article, name: 'SSSS', note: 'AAAA', unit: '250 g', article_category: article_category_a, availability: false } + + let(:supplier_with_shared) { create :supplier, articles: [article_s], shared_supplier: shared_supplier } + + it 'renders view with articles' do + get_with_defaults :shared, params: { supplier_id: supplier_with_shared.id, name_cont_all_joined: 'shared' }, xhr: true + expect(assigns(:supplier).shared_supplier.shared_articles).to be_any + expect(assigns(:articles)).to be_any + expect(response).to have_http_status(:success) + end + end + + describe '#import' do + let(:shared_supplier) { create :shared_supplier, shared_articles: [shared_article] } + let(:shared_article) { create :shared_article, name: 'shared' } + + before do + shared_article + article_category_a + end + + it 'fills form with article details' do + get_with_supplier :import, params: { article_category_id: article_category_b.id, direct: 'true', shared_article_id: shared_article.id }, xhr: true + expect(assigns(:article)).not_to be_nil + expect(response).to have_http_status(:success) + expect(response).to render_template(:create) + end + + it 'does redirect to :new if param :direct not set' do + get_with_supplier :import, params: { article_category_id: article_category_b.id, shared_article_id: shared_article.id }, xhr: true + expect(assigns(:article)).not_to be_nil + expect(response).to have_http_status(:success) + expect(response).to render_template(:new) + end + end +end diff --git a/spec/controllers/concerns/auth_concern_spec.rb b/spec/controllers/concerns/auth_concern_spec.rb new file mode 100644 index 00000000..10bf8ec7 --- /dev/null +++ b/spec/controllers/concerns/auth_concern_spec.rb @@ -0,0 +1,212 @@ +# frozen_string_literal: true + +require 'spec_helper' + +class DummyAuthController < ApplicationController; end + +describe 'Auth concern', type: :controller do + controller DummyAuthController do + # Defining a dummy action for an anynomous controller which inherits from the described class. + def authenticate_blank + authenticate + end + + def authenticate_unknown_group + authenticate('nooby') + end + + def authenticate_pickups + authenticate('pickups') + head :ok unless performed? + end + + def authenticate_finance_or_orders + authenticate('finance_or_orders') + head :ok unless performed? + end + + def try_authenticate_membership_or_admin + authenticate_membership_or_admin + end + + def try_authenticate_or_token + authenticate_or_token('xyz') + head :ok unless performed? + end + + def call_deny_access + deny_access + end + + def call_current_user + current_user + end + + def call_login_and_redirect_to_return_to + user = User.find(params[:user_id]) + login_and_redirect_to_return_to(user) + end + + def call_login + user = User.find(params[:user_id]) + login(user) + end + end + + # unit testing protected/private methods + describe 'protected/private methods' do + let(:user) { create :user } + let(:wrong_user) { create :user } + + describe '#current_user' do + before do + login user + routes.draw { get 'call_current_user' => 'dummy_auth#call_current_user' } + end + + describe 'with valid session' do + it 'returns current_user' do + get_with_defaults :call_current_user, params: { user_id: user.id }, format: JSON + expect(assigns(:current_user)).to eq user + end + end + + describe 'with invalid session' do + it 'not returns current_user' do + session[:user_id] = nil + get_with_defaults :call_current_user, params: { user_id: nil }, format: JSON + expect(assigns(:current_user)).to be_nil + end + end + end + + describe '#deny_access' do + it 'redirects to root_url' do + login user + routes.draw { get 'deny_access' => 'dummy_auth#call_deny_access' } + get_with_defaults :call_deny_access + expect(response).to redirect_to(root_url) + end + end + + describe '#login' do + before do + routes.draw { get 'call_login' => 'dummy_auth#call_login' } + end + + it 'sets user in session' do + login wrong_user + get_with_defaults :call_login, params: { user_id: user.id }, format: JSON + expect(session[:user_id]).to eq user.id + expect(session[:scope]).to eq FoodsoftConfig.scope + expect(session[:locale]).to eq user.locale + end + end + + describe '#login_and_redirect_to_return_to' do + it 'redirects to already set target' do + login user + session[:return_to] = my_profile_url + routes.draw { get 'call_login_and_redirect_to_return_to' => 'dummy_auth#call_login_and_redirect_to_return_to' } + get_with_defaults :call_login_and_redirect_to_return_to, params: { user_id: user.id } + expect(session[:return_to]).to be_nil + end + end + end + + describe 'authenticate' do + describe 'not logged in' do + it 'does not authenticate' do + routes.draw { get 'authenticate_blank' => 'dummy_auth#authenticate_blank' } + get_with_defaults :authenticate_blank + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(login_path) + expect(flash[:alert]).to match(I18n.t('application.controller.error_authn')) + end + end + + describe 'logged in' do + let(:user) { create :user } + let(:pickups_user) { create :user, :role_pickups } + let(:finance_user) { create :user, :role_finance } + let(:orders_user) { create :user, :role_orders } + + it 'does not authenticate with unknown group' do + login user + routes.draw { get 'authenticate_unknown_group' => 'dummy_auth#authenticate_unknown_group' } + get_with_defaults :authenticate_unknown_group + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to match(I18n.t('application.controller.error_denied', sign_in: ActionController::Base.helpers.link_to(I18n.t('application.controller.error_denied_sign_in'), login_path))) + end + + it 'does not authenticate with pickups group' do + login pickups_user + routes.draw { get 'authenticate_pickups' => 'dummy_auth#authenticate_pickups' } + get_with_defaults :authenticate_pickups + expect(response).to have_http_status(:success) + end + + it 'does not authenticate with finance group' do + login finance_user + routes.draw { get 'authenticate_finance_or_orders' => 'dummy_auth#authenticate_finance_or_orders' } + get_with_defaults :authenticate_finance_or_orders + expect(response).to have_http_status(:success) + end + + it 'does not authenticate with orders group' do + login orders_user + routes.draw { get 'authenticate_finance_or_orders' => 'dummy_auth#authenticate_finance_or_orders' } + get_with_defaults :authenticate_finance_or_orders + expect(response).to have_http_status(:success) + end + end + end + + describe 'authenticate_membership_or_admin' do + describe 'logged in' do + let(:pickups_user) { create :user, :role_pickups } + let(:workgroup) { create :workgroup } + + it 'redirects with not permitted group' do + group_id = workgroup.id + login pickups_user + routes.draw { get 'try_authenticate_membership_or_admin' => 'dummy_auth#try_authenticate_membership_or_admin' } + get_with_defaults :try_authenticate_membership_or_admin, params: { id: group_id } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to match(I18n.t('application.controller.error_members_only')) + end + end + end + + describe 'authenticate_or_token' do + describe 'logged in' do + let(:token_verifier) { TokenVerifier.new('xyz') } + let(:token_msg) { token_verifier.generate } + let(:user) { create :user } + + before { login user } + + it 'authenticates token' do + routes.draw { get 'try_authenticate_or_token' => 'dummy_auth#try_authenticate_or_token' } + get_with_defaults :try_authenticate_or_token, params: { token: token_msg } + expect(response).not_to have_http_status(:redirect) + end + + it 'redirects on faulty token' do + routes.draw { get 'try_authenticate_or_token' => 'dummy_auth#try_authenticate_or_token' } + get_with_defaults :try_authenticate_or_token, params: { token: 'abc' } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to match(I18n.t('application.controller.error_token')) + end + + it 'authenticates current user on empty token' do + routes.draw { get 'try_authenticate_or_token' => 'dummy_auth#try_authenticate_or_token' } + get_with_defaults :try_authenticate_or_token + expect(response).to have_http_status(:success) + end + end + end +end diff --git a/spec/controllers/finance/balancing_controller_spec.rb b/spec/controllers/finance/balancing_controller_spec.rb new file mode 100644 index 00000000..d62b9974 --- /dev/null +++ b/spec/controllers/finance/balancing_controller_spec.rb @@ -0,0 +1,211 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Finance::BalancingController, type: :controller do + let(:user) { create :user, :role_finance, :role_orders, groups: [create(:ordergroup)] } + + before { login user } + + describe 'GET index' do + let(:order) { create :order } + + it 'renders index page' do + get_with_defaults :index + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/balancing/index') + end + end + + describe 'new balancing' do + let(:supplier) { create :supplier } + let(:article1) { create :article, name: 'AAAA', supplier: supplier, unit_quantity: 1 } + let(:article2) { create :article, name: 'AAAB', supplier: supplier, unit_quantity: 1 } + + let(:order) { create :order, supplier: supplier, article_ids: [article1.id, article2.id] } + + let(:go1) { create :group_order, order: order } + let(:go2) { create :group_order, order: order } + let(:oa1) { order.order_articles.find_by_article_id(article1.id) } + let(:oa2) { order.order_articles.find_by_article_id(article2.id) } + let(:oa3) { order2.order_articles.find_by_article_id(article2.id) } + let(:goa1) { create :group_order_article, group_order: go1, order_article: oa1 } + let(:goa2) { create :group_order_article, group_order: go1, order_article: oa2 } + + before do + goa1.update_quantities(3, 0) + goa2.update_quantities(1, 0) + oa1.update_results! + oa2.update_results! + end + + it 'renders new order page' do + get_with_defaults :new, params: { order_id: order.id } + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/balancing/new') + end + + it 'assigns sorting on articles' do + sortings = [ + ['name', [oa1, oa2]], + ['name_reverse', [oa2, oa1]], + ['order_number', [oa1, oa2]], + ['order_number_reverse', [oa1, oa2]] # just one order + ] + sortings.each do |sorting| + get_with_defaults :new, params: { order_id: order.id, sort: sorting[0] } + expect(response).to have_http_status(:success) + expect(assigns(:articles).to_a).to eq(sorting[1]) + end + end + end + + describe 'update summary' do + let(:order) { create(:order) } + + it 'shows the summary view' do + get_with_defaults :update_summary, params: { id: order.id }, xhr: true + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/balancing/update_summary') + end + end + + describe 'new_on_order' do + let(:order) { create(:order) } + let(:order_article) { order.order_articles.first } + + it 'calls article update' do + get_with_defaults :new_on_order_article_update, params: { id: order.id, order_article_id: order_article.id }, xhr: true + expect(response).not_to render_template(layout: 'application') + expect(response).to render_template('finance/balancing/new_on_order_article_update') + end + + it 'calls article create' do + get_with_defaults :new_on_order_article_create, params: { id: order.id, order_article_id: order_article.id }, xhr: true + expect(response).not_to render_template(layout: 'application') + expect(response).to render_template('finance/balancing/new_on_order_article_create') + end + end + + describe 'edit_note' do + let(:order) { create(:order) } + + it 'updates order note' do + get_with_defaults :edit_note, params: { id: order.id, order: { note: 'Hello' } }, xhr: true + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/balancing/edit_note') + end + end + + describe 'update_note' do + let(:order) { create(:order) } + + it 'updates order note' do + get_with_defaults :update_note, params: { id: order.id, order: { note: 'Hello' } }, xhr: true + expect(response).to have_http_status(:success) + end + + it 'redirects to edit note on failed update' do + get_with_defaults :update_note, params: { id: order.id, order: { article_ids: nil } }, xhr: true + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/balancing/edit_note') + end + end + + describe 'transport' do + let(:order) { create(:order) } + + it 'calls the edit transport view' do + get_with_defaults :edit_transport, params: { id: order.id }, xhr: true + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/balancing/edit_transport') + end + + it 'does redirect if order valid' do + get_with_defaults :update_transport, params: { id: order.id, order: { ends: Time.now } }, xhr: true + expect(response).to have_http_status(:redirect) + expect(assigns(:order).errors.count).to eq(0) + expect(response).to redirect_to(new_finance_order_path(order_id: order.id)) + end + + it 'does redirect if order invalid' do + get_with_defaults :update_transport, params: { id: order.id, order: { starts: Time.now + 2, ends: Time.now } }, xhr: true + expect(assigns(:order).errors.count).to eq(1) + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(new_finance_order_path(order_id: order.id)) + end + end + + describe 'confirm' do + let(:order) { create(:order) } + + it 'renders the confirm template' do + get_with_defaults :confirm, params: { id: order.id }, xhr: true + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/balancing/confirm') + end + end + + describe 'close and update account balances' do + let(:order) { create(:order) } + let(:order1) { create(:order, ends: Time.now) } + let(:fft) { create(:financial_transaction_type) } + + it 'does not close order if ends not set' do + get_with_defaults :close, params: { id: order.id, type: fft.id } + expect(assigns(:order)).not_to be_closed + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(new_finance_order_url(order_id: order.id)) + end + + it 'closes order' do + get_with_defaults :close, params: { id: order1.id, type: fft.id } + expect(assigns(:order)).to be_closed + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(finance_order_index_url) + end + end + + describe 'close direct' do + let(:order) { create(:order) } + + it 'does not close order if already closed' do + order.close_direct!(user) + get_with_defaults :close_direct, params: { id: order.id } + expect(assigns(:order)).to be_closed + end + + it 'closes order directly' do + get_with_defaults :close_direct, params: { id: order.id } + expect(assigns(:order)).to be_closed + end + end + + describe 'close all direct' do + let(:invoice) { create(:invoice) } + let(:invoice1) { create(:invoice) } + let(:order) { create(:order, state: 'finished', ends: Time.now + 2.hours, invoice: invoice) } + let(:order1) { create(:order, state: 'finished', ends: Time.now + 2.hours) } + + before do + order + order1 + end + + it 'does close orders' do + get_with_defaults :close_all_direct_with_invoice + order.reload + expect(order).to be_closed + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(finance_order_index_url) + end + + it 'does not close orders when invoice not set' do + get_with_defaults :close_all_direct_with_invoice + order1.reload + expect(order1).not_to be_closed + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(finance_order_index_url) + end + end +end diff --git a/spec/controllers/finance/base_controller_spec.rb b/spec/controllers/finance/base_controller_spec.rb new file mode 100644 index 00000000..388f3a17 --- /dev/null +++ b/spec/controllers/finance/base_controller_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Finance::BaseController, type: :controller do + let(:user) { create :user, :role_finance, :role_orders, :ordergroup } + + before { login user } + + describe 'GET index' do + let(:fin_trans) { create_list :financial_transaction, 3, user: user, ordergroup: user.ordergroup } + let(:orders) { create_list :order, 2, state: 'finished' } + let(:invoices) { create_list :invoice, 4 } + + before do + fin_trans + orders + invoices + end + + it 'renders index page' do + get_with_defaults :index + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/index') + expect(assigns(:financial_transactions).size).to eq(fin_trans.size) + expect(assigns(:orders).size).to eq(orders.size) + expect(assigns(:unpaid_invoices).size).to eq(invoices.size) + end + end +end diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb new file mode 100644 index 00000000..be106282 --- /dev/null +++ b/spec/controllers/home_controller_spec.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe HomeController, type: :controller do + let(:user) { create :user } + + describe 'GET index' do + describe 'NOT logged in' do + it 'redirects' do + get_with_defaults :profile + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(login_path) + end + end + + describe 'logegd in' do + before { login user } + + it 'assigns tasks' do + get_with_defaults :index + + expect(assigns(:unaccepted_tasks)).not_to be_nil + expect(assigns(:next_tasks)).not_to be_nil + expect(assigns(:unassigned_tasks)).not_to be_nil + expect(response).to render_template('home/index') + end + end + end + + describe 'GET profile' do + before { login user } + + it 'renders dashboard' do + get_with_defaults :profile + expect(response).to have_http_status(:success) + expect(response).to render_template('home/profile') + end + end + + describe 'GET reference_calculator' do + describe 'with simple user' do + before { login user } + + it 'redirects to home' do + get_with_defaults :reference_calculator + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(root_path) + end + end + + describe 'with ordergroup user' do + let(:og_user) { create :user, :ordergroup } + + before { login og_user } + + it 'renders reference calculator' do + get_with_defaults :reference_calculator + expect(response).to have_http_status(:success) + expect(response).to render_template('home/reference_calculator') + end + end + end + + describe 'GET update_profile' do + describe 'with simple user' do + let(:unchanged_attributes) { user.attributes.slice('first_name', 'last_name', 'email') } + let(:changed_attributes) { attributes_for :user } + let(:invalid_attributes) { { email: 'e.mail.com' } } + + before { login user } + + it 'renders profile after update with invalid attributes' do + get_with_defaults :update_profile, params: { user: invalid_attributes } + expect(response).to have_http_status(:success) + expect(response).to render_template('home/profile') + expect(assigns(:current_user).errors.present?).to be true + end + + it 'redirects to profile after update with unchanged attributes' do + get_with_defaults :update_profile, params: { user: unchanged_attributes } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(my_profile_path) + end + + it 'redirects to profile after update' do + patch :update_profile, params: { foodcoop: FoodsoftConfig[:default_scope], user: changed_attributes } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(my_profile_path) + expect(flash[:notice]).to match(/#{I18n.t('home.changes_saved')}/) + expect(user.reload.attributes.slice(:first_name, :last_name, :email)).to eq(changed_attributes.slice('first_name', 'last_name', 'email')) + end + end + + describe 'with ordergroup user' do + let(:og_user) { create :user, :ordergroup } + let(:unchanged_attributes) { og_user.attributes.slice('first_name', 'last_name', 'email') } + let(:changed_attributes) { unchanged_attributes.merge({ ordergroup: { contact_address: 'new Adress 7' } }) } + + before { login og_user } + + it 'redirects to home after update' do + get_with_defaults :update_profile, params: { user: changed_attributes } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(my_profile_path) + expect(og_user.reload.ordergroup.contact_address).to eq('new Adress 7') + end + end + end + + describe 'GET ordergroup' do + describe 'with simple user' do + before { login user } + + it 'redirects to home' do + get_with_defaults :ordergroup + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(root_path) + end + end + + describe 'with ordergroup user' do + let(:og_user) { create :user, :ordergroup } + + before { login og_user } + + it 'renders ordergroup' do + get_with_defaults :ordergroup + expect(response).to have_http_status(:success) + expect(response).to render_template('home/ordergroup') + end + + describe 'assigns sortings' do + let(:fin_trans1) { create :financial_transaction, user: og_user, ordergroup: og_user.ordergroup, note: 'A', amount: 100 } + let(:fin_trans2) { create :financial_transaction, user: og_user, ordergroup: og_user.ordergroup, note: 'B', amount: 200, created_on: Time.now + 1.minute } + + before do + fin_trans1 + fin_trans2 + end + + it 'by criteria' do + sortings = [ + ['date', [fin_trans1, fin_trans2]], + ['note', [fin_trans1, fin_trans2]], + ['amount', [fin_trans1, fin_trans2]], + ['date_reverse', [fin_trans2, fin_trans1]], + ['note_reverse', [fin_trans2, fin_trans1]], + ['amount_reverse', [fin_trans2, fin_trans1]] + ] + sortings.each do |sorting| + get_with_defaults :ordergroup, params: { sort: sorting[0] } + expect(response).to have_http_status(:success) + expect(assigns(:financial_transactions).to_a).to eq(sorting[1]) + end + end + end + end + end + + describe 'GET cancel_membership' do + describe 'with simple user without group' do + before { login user } + + it 'fails' do + expect do + get_with_defaults :cancel_membership + end.to raise_error(ActiveRecord::RecordNotFound) + expect do + get_with_defaults :cancel_membership, params: { membership_id: 424242 } + end.to raise_error(ActiveRecord::RecordNotFound) + end + end + + describe 'with ordergroup user' do + let(:fin_user) { create :user, :role_finance } + + before { login fin_user } + + it 'removes user from group' do + membership = fin_user.memberships.first + get_with_defaults :cancel_membership, params: { group_id: fin_user.groups.first.id } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(my_profile_path) + expect(flash[:notice]).to match(/#{I18n.t('home.ordergroup_cancelled', :group => membership.group.name)}/) + end + + it 'removes user membership' do + membership = fin_user.memberships.first + get_with_defaults :cancel_membership, params: { membership_id: membership.id } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(my_profile_path) + expect(flash[:notice]).to match(/#{I18n.t('home.ordergroup_cancelled', :group => membership.group.name)}/) + end + end + end +end diff --git a/spec/controllers/login_controller_spec.rb b/spec/controllers/login_controller_spec.rb new file mode 100644 index 00000000..c824e429 --- /dev/null +++ b/spec/controllers/login_controller_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe LoginController, type: :controller do + let(:invite) { create :invite } + + describe 'GET accept_invitation' do + let(:expired_invite) { create :expired_invite } + + describe 'with valid token' do + it 'accepts invitation' do + get_with_defaults :accept_invitation, params: { token: invite.token } + expect(response).to have_http_status(:success) + expect(response).to render_template('login/accept_invitation') + end + end + + describe 'with invalid token' do + it 'redirects to login' do + get_with_defaults :accept_invitation, params: { token: invite.token + 'XX' } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(login_url) + expect(flash[:alert]).to match(I18n.t('login.controller.error_invite_invalid')) + end + end + + describe 'with timed out token' do + it 'redirects to login' do + get_with_defaults :accept_invitation, params: { token: expired_invite.token } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(login_url) + expect(flash[:alert]).to match(I18n.t('login.controller.error_invite_invalid')) + end + end + + describe 'without group' do + it 'redirects to login' do + invite.group.destroy + get_with_defaults :accept_invitation, params: { token: invite.token } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(login_url) + expect(flash[:alert]).to match(I18n.t('login.controller.error_group_invalid')) + end + end + end + + describe 'POST accept_invitation' do + describe 'with invalid parameters' do + it 'renders accept_invitation view' do + post_with_defaults :accept_invitation, params: { token: invite.token, user: invite.user.slice('first_name') } + expect(response).to have_http_status(:success) + expect(response).to render_template('login/accept_invitation') + expect(assigns(:user).errors.present?).to be true + end + end + + describe 'with valid parameters' do + it 'redirects to login' do + post_with_defaults :accept_invitation, params: { token: invite.token, user: invite.user.slice('first_name', 'password') } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(login_url) + expect(flash[:notice]).to match(I18n.t('login.controller.accept_invitation.notice')) + end + end + end +end diff --git a/spec/factories/invite.rb b/spec/factories/invite.rb new file mode 100644 index 00000000..51d48840 --- /dev/null +++ b/spec/factories/invite.rb @@ -0,0 +1,15 @@ +require 'factory_bot' + +FactoryBot.define do + factory :invite do + user { create :user } + group { create :group } + email { Faker::Internet.email } + + factory :expired_invite do + after :create do |invite| + invite.update_column(:expires_at, Time.now.yesterday) + end + end + end +end diff --git a/spec/factories/order_article.rb b/spec/factories/order_article.rb new file mode 100644 index 00000000..99ca8701 --- /dev/null +++ b/spec/factories/order_article.rb @@ -0,0 +1,8 @@ +require 'factory_bot' + +FactoryBot.define do + factory :order_article do + order { create :order } + article { create :article } + end +end diff --git a/spec/fixtures/files/upload_test.csv b/spec/fixtures/files/upload_test.csv new file mode 100644 index 00000000..ac2f59b0 --- /dev/null +++ b/spec/fixtures/files/upload_test.csv @@ -0,0 +1,3 @@ +avail.;Order number;Name;Note;Manufacturer;Origin;Unit;Price (net);VAT;Deposit;Unit quantity;"";"";Category +"";;AAAA;AAAA;;;500 g;25.55;6.0;0.0;1;"";"";AAAA +"";;BBBB;BBBB;;;250 g;12.11;6.0;0.0;2;"";"";BBBB diff --git a/spec/fixtures/upload_test.csv b/spec/fixtures/upload_test.csv new file mode 100644 index 00000000..ac2f59b0 --- /dev/null +++ b/spec/fixtures/upload_test.csv @@ -0,0 +1,3 @@ +avail.;Order number;Name;Note;Manufacturer;Origin;Unit;Price (net);VAT;Deposit;Unit quantity;"";"";Category +"";;AAAA;AAAA;;;500 g;25.55;6.0;0.0;1;"";"";AAAA +"";;BBBB;BBBB;;;250 g;12.11;6.0;0.0;2;"";"";BBBB diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 88dea423..41894406 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -21,6 +21,10 @@ Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } RSpec.configure do |config| # We use capybara with webkit, and need database_cleaner + config.before(:suite) do + DatabaseCleaner.clean_with(:truncation) + end + config.before(:each) do DatabaseCleaner.strategy = (RSpec.current_example.metadata[:js] ? :truncation : :transaction) DatabaseCleaner.start @@ -51,8 +55,8 @@ RSpec.configure do |config| # --seed 1234 config.order = "random" + config.include SpecTestHelper, type: :controller config.include SessionHelper, type: :feature - # Automatically determine spec from directory structure, see: # https://www.relishapp.com/rspec/rspec-rails/v/3-0/docs/directory-structure config.infer_spec_type_from_file_location! diff --git a/spec/support/spec_test_helper.rb b/spec/support/spec_test_helper.rb new file mode 100644 index 00000000..58a1c0ef --- /dev/null +++ b/spec/support/spec_test_helper.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module SpecTestHelper + def login(user) + user = User.where(:nick => user.nick).first if user.is_a?(Symbol) + session[:user_id] = user.id + session[:scope] = FoodsoftConfig[:default_scope] # Save scope in session to not allow switching between foodcoops with one account + session[:locale] = user.locale + end + + def current_user + User.find(session[:user_id]) + end + + def get_with_defaults(action, params: {}, xhr: false, format: nil) + params['foodcoop'] = FoodsoftConfig[:default_scope] + get action, params: params, xhr: xhr, format: format + end + + def post_with_defaults(action, params: {}, xhr: false, format: nil) + params['foodcoop'] = FoodsoftConfig[:default_scope] + post action, params: params, xhr: xhr, format: format + end +end + +RSpec.configure do |config| + config.include SpecTestHelper, :type => :controller +end