diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 00000000..b892af61 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,42 @@ +kind: pipeline +type: docker +name: default + +steps: + - name: build and test + image: circleci/ruby:2.6.9-bullseye-node-browsers-legacy + commands: + - sudo apt install --no-install-recommends -y libmagic-dev + - sudo -E bundle install --path /bundle --without production,development + - sudo -E bundle exec rake foodsoft:setup:stock_config || true + - sudo -E bundle exec rake db:schema:load + - sudo -E bundle exec rake rspec-rerun:spec + + volumes: + - name: gem-cache + path: /bundle + - name: tmp + path: /drone/src/tmp + environment: + RAILS_LOG_TO_STDOUT: true + RAILS_ENV: test + COVERAGE: lcov + DATABASE_URL: mysql2://user:password@mariadb/test?encoding=utf8mb4 + DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL: true + PARALLEL_TEST_PROCESSORS: 15 + +services: + - name: mariadb + image: mariadb + environment: + MYSQL_USER: user + MYSQL_PASSWORD: password + MYSQL_DATABASE: test + MYSQL_ROOT_PASSWORD: password + +volumes: + - name: gem-cache + host: + path: /tmp/cache + - name: tmp + temp: {} diff --git a/Gemfile b/Gemfile index 83b90aba..a63f7b30 100644 --- a/Gemfile +++ b/Gemfile @@ -111,6 +111,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 6e0b1937..d884da34 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -360,6 +360,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) @@ -614,6 +618,7 @@ DEPENDENCIES rack-cors rails (~> 6.1) 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/finance/balancing_controller.rb b/app/controllers/finance/balancing_controller.rb index 09c109f8..2dca6f71 100644 --- a/app/controllers/finance/balancing_controller.rb +++ b/app/controllers/finance/balancing_controller.rb @@ -22,7 +22,7 @@ class Finance::BalancingController < Finance::BaseController when 'order_number_reverse' then @articles.order('articles.order_number DESC') else - @articles + @articles # TODO: We will never get here end render layout: false if request.xhr? @@ -105,6 +105,6 @@ class Finance::BalancingController < Finance::BaseController end redirect_to finance_order_index_url, notice: t('finance.balancing.close_all_direct_with_invoice.notice', count: count) rescue => error - redirect_to finance_order_index_url, alert: t('errors.general_msg', msg: error.message) + redirect_to finance_order_index_url, alert: t('errors.general_msg', msg: error.message) #TODO: this can't be reached end end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 6f677b6b..01567a6e 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -63,8 +63,9 @@ class HomeController < ApplicationController # cancel personal memberships direct from the myProfile-page def cancel_membership + # TODO: membership_id not used anymore? 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/app_config.yml b/spec/app_config.yml index 2e146be9..861bded3 100644 --- a/spec/app_config.yml +++ b/spec/app_config.yml @@ -7,6 +7,7 @@ default: &defaults multi_coop_install: false default_scope: 'f' + tax_default: 0 name: FC Minimal diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb new file mode 100644 index 00000000..eb11200e --- /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 + ApplicationController.new.send(:store_controller) + expect(ApplicationController.current).to be_instance_of ApplicationController + 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..56529200 --- /dev/null +++ b/spec/controllers/articles_controller_spec.rb @@ -0,0 +1,325 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ArticlesController, type: :controller do + let(:user) { create :user, :role_article_meta } + let(:article_categoryA) { create :article_category, name: "AAAA" } + let(:article_categoryB) { create :article_category, name: "BBBB" } + let(:articleA) { create :article, name: 'AAAA', note: "AAAA", unit: '250 g', article_category: article_categoryA, availability: false } + let(:articleB) { create :article, name: 'BBBB', note: "BBBB", unit: '500 g', article_category: article_categoryB, availability: true } + let(:articleC) { create :article, name: 'CCCC', note: "CCCC", unit: '500 g', article_category: article_categoryB, availability: true } + + let(:supplier) { create :supplier, articles: [articleA, articleB] } + let(:order) { create :order } + + + before { login user } + + describe 'GET index' do + it 'assigns sorting on articles' do + sortings = [ + ['name', [articleA, articleB]], + ['name_reverse', [articleB, articleA]], + ['unit', [articleA, articleB]], + ['unit_reverse', [articleB, articleA]], + ['article_category', [articleA, articleB]], + ['article_category_reverse', [articleB, articleA]], + ['note', [articleA, articleB]], + ['note_reverse', [articleB, articleA]], + ['availability', [articleA, articleB]], + ['availability_reverse', [articleB, articleA]] + ] + sortings.each do |sorting| + get :index, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, 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 :index, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id }, format: :csv + expect(response.header["Content-Type"]).to include("text/csv") + expect(response.body).to include(articleA.unit, articleB.unit) + end + end + + describe "new" do + it 'renders form for a new article' do + get :new, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id }, xhr: true + expect(response).to have_http_status(:success) + end + end + + describe "copy" do + it 'renders form with copy of an article' do + get :copy, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, article_id: articleA.id }, xhr: true + expect(assigns(:article).attributes).to eq(articleA.dup.attributes) + expect(response).to have_http_status(:success) + end + end + # TODO: + + describe "#create" do + it 'creates a new article' do + valid_attributes = articleA.attributes.except("id") + valid_attributes["name"] = "ABAB" + get :create, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, 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 :create, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, 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 :edit, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, id: articleA.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 :edit_all, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id }, 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 :update, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, id: articleA.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 attributes' do + get :update, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, id: articleA.id, article: { name: nil } }, xhr: true + expect(response).to render_template('articles/new') + end + end + + describe "#update_all" do + xit 'updates all articles' do + # never used and controller method bugged + get :update_all, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, articles: [articleA, articleB] } + puts assigns(:articles).count + expect(response).to have_http_status(:success) + end + end + + describe "#update_selected" do + let(:order_article) { create :order_article, order: order, article: articleC } + before do + order_article + end + + it 'updates selected articles' do + get :update_selected, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, selected_articles: [articleA.id, articleB.id] } + expect(response).to have_http_status(:redirect) + end + + it 'destroys selected articles' do + get :update_selected, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, selected_articles: [articleA.id, articleB.id], selected_action: "destroy" } + articleA.reload + articleB.reload + expect(articleA.deleted? && articleB.deleted?).to be_truthy + expect(response).to have_http_status(:redirect) + end + + it 'sets availability false on selected articles' do + get :update_selected, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, selected_articles: [articleA.id, articleB.id], selected_action: "setNotAvailable" } + articleA.reload + articleB.reload + expect(articleA.availability || articleB.availability).to be_falsey + expect(response).to have_http_status(:redirect) + end + + it 'sets availability true on selected articles' do + get :update_selected, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, selected_articles: [articleA.id, articleB.id], selected_action: "setAvailable" } + articleA.reload + articleB.reload + expect(articleA.availability && articleB.availability).to be_truthy + expect(response).to have_http_status(:redirect) + end + + it 'fails deletion if one article is in open order' do + get :update_selected, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, selected_articles: [articleA.id, articleC.id], selected_action: "destroy" } + articleA.reload + articleC.reload + expect(articleA.deleted? || articleC.deleted?).to be_falsey + expect(response).to have_http_status(:redirect) + end + end + + describe "#parse_upload" do + # let(:file) { fixture_file_upload(Rails.root.join('spec/fixtures/files/upload_test.csv')) } + + # before do + # file + # end + # TODO: Cannot use Rack attributes in controller?? + # #":String + + xit 'updates particles from spreadsheet' do + get :parse_upload, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, articles: { file: file, outlist_absent: "1", convert_units: "1" } } + # {articleA.id => articleA, articleB.id => articleB}} + expect(response).to have_http_status(:redirect) + end + end + + describe "#sync" do + # TODO: double render error in controller + xit 'updates particles from spreadsheet' do + get :sync, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id } + expect(response).to have_http_status(:redirect) + end + end + + describe "#destroy" do + let(:order_article) { create :order_article, order: order, article: articleC } + before do + order_article + end + + it 'does not delete article if order open' do + get :destroy, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, id: articleC.id }, xhr: true + expect(assigns(:article).deleted?).to be_falsey + expect(response).to have_http_status(:success) + expect(response).to render_template('articles/destroy') + end + + it 'deletes article if order closed' do + get :destroy, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, id: articleB.id }, xhr: true + expect(assigns(:article).deleted?).to be_truthy + 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: articleC } + before do + order_article + articleA + articleB + articleC + end + + it 'deletes articles' do + # TODO: double render error in controller + get :update_synchronized, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, outlisted_articles: { articleA.id => articleA, articleB.id => articleB } } + articleA.reload + articleB.reload + expect(articleA.deleted? && articleB.deleted?).to be_truthy + expect(response).to have_http_status(:redirect) + end + + it 'updates articles' do + get :update_synchronized, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, articles: { articleA.id => { name: "NewNameA" }, articleB.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 :update_synchronized, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, articles: { articleA.id => { unit: "2000 g" }, articleB.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 :update_synchronized, params: { + foodcoop: FoodsoftConfig[:default_scope], + supplier_id: supplier.id, + outlisted_articles: { articleA.id => articleA }, + articles: { + articleA.id => { name: "NewName" }, + articleB.id => { name: "AAAA" } + } + } + error_array = [assigns(:updated_articles).first.errors.first, assigns(:updated_articles).last.errors.first] + expect(error_array.any?).to be_falsey + expect(response).to have_http_status(:redirect) + end + + it 'does not delete articles in open order' do + get :update_synchronized, params: { + foodcoop: FoodsoftConfig[:default_scope], + supplier_id: supplier.id, + outlisted_articles: { articleC.id => articleC } + } + articleC.reload + expect(articleC.deleted?).to be_falsey + expect(response).to have_http_status(:success) + end + + it 'assigns updated article_pairs on error' do + get :update_synchronized, params: { + foodcoop: FoodsoftConfig[:default_scope], + supplier_id: supplier.id, + articles: { articleA.id => { name: "DDDD" } }, + outlisted_articles: { articleC.id => articleC } + } + expect(assigns(:updated_article_pairs).first).to eq([articleA, { name: "DDDD" }]) + articleC.reload + expect(articleC.deleted?).to be_falsey + expect(response).to have_http_status(:success) + end + + it 'updates articles in open order' do + get :update_synchronized, params: { + foodcoop: FoodsoftConfig[:default_scope], + supplier_id: supplier.id, + articles: { articleC.id => { name: "DDDD" } } + } + articleC.reload + expect(articleC.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(:articleS) { create :article, name: 'SSSS', note: "AAAA", unit: '250 g', article_category: article_categoryA, availability: false } + + let(:supplier_with_shared) { create :supplier, articles: [articleS], shared_supplier: shared_supplier } + + it 'renders view with articles' do + get :shared, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier_with_shared.id, name_cont_all_joined: "shared" }, xhr: true + expect(assigns(:supplier).shared_supplier.shared_articles.any?).to be_truthy + expect(assigns(:articles).any?).to be_truthy + 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_categoryA + end + + it 'fills form with article details' do + get :import, params: { foodcoop: FoodsoftConfig[:default_scope], article_category_id: article_categoryB.id, direct: "true", supplier_id: supplier.id, shared_article_id: shared_article.id }, xhr: true + expect(assigns(:article).nil?).to be_falsey + 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 :import, params: { foodcoop: FoodsoftConfig[:default_scope], article_category_id: article_categoryB.id, supplier_id: supplier.id, shared_article_id: shared_article.id }, xhr: true + expect(assigns(:article).nil?).to be_falsey + 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..65d7f13e --- /dev/null +++ b/spec/controllers/concerns/auth_concern_spec.rb @@ -0,0 +1,184 @@ +# 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 + end + + # unit testing protected/private methods + describe 'protected/private methods' do + let(:user) { create :user } + + describe '#current_user' do + before { login user } + + describe 'with valid session' do + it "returns current_user" do + subject.session[:user_id] = user.id + subject.params[:foodcoop] = FoodsoftConfig[:default_scope] + expect(subject.send(:current_user)).to eq user + expect(assigns(:current_user)).to eq user + end + end + + describe 'with invalid session' do + it "not returns current_user" do + subject.session[:user_id] = '' + subject.params[:foodcoop] = FoodsoftConfig[:default_scope] + expect(subject.send(:current_user)).to be_nil + expect(assigns(:current_user)).to be_nil + end + end + end + + describe '#deny_access' do + xit "redirects to root_url" do + expect(subject.send(:deny_access)).to redirect_to(root_url) + end + end + + describe '#login' do + it "sets user in session" do + subject.send(:login, user) + expect(subject.session[:user_id]).to eq user.id + expect(subject.session[:scope]).to eq FoodsoftConfig.scope + expect(subject.session[:locale]).to eq user.locale + end + end + + describe '#login_and_redirect_to_return_to' do + xit "redirects to already set target" do + subject.session[:return_to] = my_profile_url + subject.send(:login_and_redirect_to_return_to, user) + expect(subject.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 :authenticate_blank, params: { foodcoop: FoodsoftConfig[:default_scope] } + 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 :authenticate_unknown_group, params: { foodcoop: FoodsoftConfig[:default_scope] } + 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 :authenticate_pickups, params: { foodcoop: FoodsoftConfig[:default_scope] } + 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 :authenticate_finance_or_orders, params: { foodcoop: FoodsoftConfig[:default_scope] } + 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 :authenticate_finance_or_orders, params: { foodcoop: FoodsoftConfig[:default_scope] } + 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 :try_authenticate_membership_or_admin, params: { foodcoop: FoodsoftConfig[:default_scope], 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 } + + it 'authenticates token' do + login user + routes.draw { get "try_authenticate_or_token" => "dummy_auth#try_authenticate_or_token" } + get :try_authenticate_or_token, params: { foodcoop: FoodsoftConfig[:default_scope], token: token_msg } + expect(response).to_not have_http_status(:redirect) + end + + it 'redirects on faulty token' do + login user + routes.draw { get "try_authenticate_or_token" => "dummy_auth#try_authenticate_or_token" } + get :try_authenticate_or_token, params: { foodcoop: FoodsoftConfig[:default_scope], 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 + login user + routes.draw { get "try_authenticate_or_token" => "dummy_auth#try_authenticate_or_token" } + get :try_authenticate_or_token, params: { foodcoop: FoodsoftConfig[:default_scope] } + expect(response).to have_http_status(:success) + end + end + end +end \ No newline at end of file diff --git a/spec/controllers/finance/balancing_controller_spec.rb b/spec/controllers/finance/balancing_controller_spec.rb new file mode 100644 index 00000000..dbeb262d --- /dev/null +++ b/spec/controllers/finance/balancing_controller_spec.rb @@ -0,0 +1,212 @@ +# 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 :index, params: { foodcoop: FoodsoftConfig[:default_scope] } + 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 :new, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :new, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :update_summary, params: { foodcoop: FoodsoftConfig[:default_scope], 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 } + + # TODO: how to check for js.erb calls? + it 'calls article update' do + get :new_on_order_article_update, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :new_on_order_article_create, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :edit_note, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :update_note, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :update_note, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :edit_transport, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :update_transport, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :update_transport, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :confirm, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :close, params: { foodcoop: FoodsoftConfig[:default_scope], id: order.id, type: fft.id } + expect(assigns(:order).closed?).to be_falsey + 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 :close, params: { foodcoop: FoodsoftConfig[:default_scope], id: order1.id, type: fft.id } + expect(assigns(:order).closed?).to be_truthy + 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 :close_direct, params: { foodcoop: FoodsoftConfig[:default_scope], id: order.id } + expect(assigns(:order).closed?).to be_truthy + end + + it 'closes order directly' do + get :close_direct, params: { foodcoop: FoodsoftConfig[:default_scope], id: order.id } + expect(assigns(:order).closed?).to be_truthy + 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 :close_all_direct_with_invoice, params: { foodcoop: FoodsoftConfig[:default_scope] } + order.reload + expect(order.closed?).to be_truthy + 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 :close_all_direct_with_invoice, params: { foodcoop: FoodsoftConfig[:default_scope] } + order1.reload + expect(order1.closed?).to be_falsey + 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..40c80c57 --- /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 :index, params: { foodcoop: FoodsoftConfig[:default_scope] } + 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 \ No newline at end of file diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb new file mode 100644 index 00000000..5fd086fb --- /dev/null +++ b/spec/controllers/home_controller_spec.rb @@ -0,0 +1,189 @@ +# 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 :profile, params: { foodcoop: FoodsoftConfig[:default_scope] } + 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 :index, params: { foodcoop: FoodsoftConfig[:default_scope] } + + 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 :profile, params: { foodcoop: FoodsoftConfig[:default_scope] } + 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 :reference_calculator, params: { foodcoop: FoodsoftConfig[:default_scope] } + 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 :reference_calculator, params: { foodcoop: FoodsoftConfig[:default_scope] } + 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 :update_profile, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :update_profile, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :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(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 :ordergroup, params: { foodcoop: FoodsoftConfig[:default_scope] } + 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 :ordergroup, params: { foodcoop: FoodsoftConfig[:default_scope] } + 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 :ordergroup, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :cancel_membership, params: { foodcoop: FoodsoftConfig[:default_scope] } + 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 :cancel_membership, + params: { foodcoop: FoodsoftConfig[:default_scope], + 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 + 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..0e795893 --- /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 :accept_invitation, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :accept_invitation, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :accept_invitation, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :accept_invitation, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :accept_invitation, params: { foodcoop: FoodsoftConfig[:default_scope], 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 :accept_invitation, params: { foodcoop: FoodsoftConfig[:default_scope], 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 \ No newline at end of file diff --git a/spec/factories/delivery.rb b/spec/factories/delivery.rb new file mode 100644 index 00000000..0252064d --- /dev/null +++ b/spec/factories/delivery.rb @@ -0,0 +1,9 @@ +require 'factory_bot' + +FactoryBot.define do + factory :delivery do + supplier { create :supplier } + invoice { create :invoice } + date { Faker::Date.backward(days: 14) } + end +end \ No newline at end of file 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/factories/supplier.rb b/spec/factories/supplier.rb index 67ba3528..5780dc46 100644 --- a/spec/factories/supplier.rb +++ b/spec/factories/supplier.rb @@ -5,6 +5,7 @@ FactoryBot.define do name { Faker::Company.name.truncate(30) } phone { Faker::PhoneNumber.phone_number } address { Faker::Address.street_address } + email { Faker::Internet.email } transient do article_count { 0 } 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/integration/admin_spec.rb b/spec/integration/admin_spec.rb new file mode 100644 index 00000000..c929a44a --- /dev/null +++ b/spec/integration/admin_spec.rb @@ -0,0 +1,29 @@ +require_relative '../spec_helper' + +feature Admin::BaseController do + let(:admin) { create :admin } + let(:users) { create_list :user, 2 } + let(:workgroups) { create_list :workgroup, 3 } + let(:groups) { create_list :group, 4 } + + before { login admin } + + describe 'base#index' do + before do + users + end + + it 'is accessible with workgroups existing' do + workgroups + visit admin_root_path + expect(page).to have_content(I18n.t('admin.base.index.newest_users')) + expect(page).to have_content(users.first.name) + end + + # TODO: + it 'raising error when groups existing' do + groups + expect{ visit admin_root_path }.to raise_error(ActionView::Template::Error) + end + end +end diff --git a/spec/integration/home_spec.rb b/spec/integration/home_spec.rb new file mode 100644 index 00000000..ecfa5f7c --- /dev/null +++ b/spec/integration/home_spec.rb @@ -0,0 +1,23 @@ +require_relative '../spec_helper' + +feature HomeController do + let(:user) { create :user } + + before { login user } + + describe 'my profile' do + before { visit my_profile_path } + + it 'is accessible' do + expect(page).to have_selector 'input[id=user_first_name]' + expect(find_field('user_first_name').value).to eq(user.first_name) + end + + it 'updates first name' do + fill_in 'user_first_name', with: 'foo' + click_button('Save') + expect(User.find(user.id).first_name).to eq 'foo' + expect(page).to have_selector '.alert-success' + end + end +end diff --git a/spec/integration/supplier_spec.rb b/spec/integration/supplier_spec.rb index ea015d06..e4a81a0a 100644 --- a/spec/integration/supplier_spec.rb +++ b/spec/integration/supplier_spec.rb @@ -2,12 +2,11 @@ require_relative '../spec_helper' feature 'supplier' do let(:supplier) { create :supplier } + let(:user) { create :user, groups: [create(:workgroup, role_suppliers: true)] } + + before { login user } describe 'create new' do - let(:user) { create :user, groups: [create(:workgroup, role_suppliers: true)] } - - before { login user } - it 'can be created' do create :supplier_category visit suppliers_path @@ -28,4 +27,36 @@ feature 'supplier' do expect(page).to have_content(supplier.name) end end + + describe 'existing', js: true do + it 'can be shown' do + supplier + visit suppliers_path + click_link supplier.name + expect(page).to have_content(supplier.address) + expect(page).to have_content(supplier.phone) + expect(page).to have_content(supplier.email) + end + + it 'can be updated' do + new_name = Faker::Company.name.truncate(30) + supplier + visit suppliers_path + click_link I18n.t('ui.edit') + fill_in I18n.t('activerecord.attributes.supplier.name'), with: new_name + click_button 'Update Supplier' + expect(supplier.reload.name).to eq new_name + end + + it 'can be destroyed' do + supplier + visit suppliers_path + expect(page).to have_content(supplier.name) + accept_confirm do + click_link I18n.t('ui.delete') + end + expect(page).not_to have_content(supplier.name) + expect(supplier.reload.deleted?).to be true + end + end end diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb index 526b5417..d75c49b6 100644 --- a/spec/models/article_spec.rb +++ b/spec/models/article_spec.rb @@ -9,6 +9,38 @@ describe Article do expect(article2).to be_invalid end + it 'can be deleted' do + article.mark_as_deleted() + expect(article.deleted?).to be true + end + + describe 'convert units' do + it 'returns nil when equal' do expect(article.convert_units(article)).to be nil end + it 'returns false when invalid unit' do + article1 = build :article, supplier: supplier, unit: "invalid" + expect(article.convert_units(article1)).to be false + end + it 'converts from ST to KI' do + article1 = build :article, supplier: supplier, unit: "ST" + article2 = build :article, supplier: supplier, name: "banana 10-12 St", price: 12.34, unit: "KI" + new_price, new_unit_quantity = article1.convert_units(article2) + expect(new_unit_quantity).to eq 10 + expect(new_price).to eq 1.23 + end + it 'converts from g to kg' do + article1 = build :article, supplier: supplier, unit: "kg" + article2 = build :article, supplier: supplier, unit: "g", price: 0.12, unit_quantity: 1500 + new_price, new_unit_quantity = article1.convert_units(article2) + expect(new_unit_quantity).to eq 1.5 + expect(new_price).to eq 120 + end + end + + it 'computes changed article attributes' do # not done yet! + article2 = build :article, supplier: supplier, name: "banana" + expect(article.unequal_attributes(article2)[:name]).to eq "banana" + end + it 'computes the gross price correctly' do article.deposit = 0 article.tax = 12 diff --git a/spec/models/delivery_spec.rb b/spec/models/delivery_spec.rb new file mode 100644 index 00000000..b48449ab --- /dev/null +++ b/spec/models/delivery_spec.rb @@ -0,0 +1,23 @@ +require_relative '../spec_helper' + +describe Delivery do + let(:delivery) { create :delivery } + let(:stock_article) { create :stock_article, price: 3 } + + it 'creates new stock_changes' do + delivery.new_stock_changes = ([ + { + quantity: 1, + stock_article: stock_article + }, + { + quantity: 2, + stock_article: stock_article + } + ]) + + expect(delivery.stock_changes.last[:stock_article_id]).to be stock_article.id + expect(delivery.includes_article?(stock_article)).to be true + expect(delivery.sum(:net)).to eq 9 + end +end diff --git a/spec/models/group_order_article_spec.rb b/spec/models/group_order_article_spec.rb index ddb9158a..1e7a2d3a 100644 --- a/spec/models/group_order_article_spec.rb +++ b/spec/models/group_order_article_spec.rb @@ -40,13 +40,22 @@ describe GroupOrderArticle do goa.update_quantities(0, 0) expect(GroupOrderArticle.exists?(goa.id)).to be false end + + it 'updates quantity and tolerance' do + goa.update_quantities(2,2) + goa.update_quantities(1,1) + expect(goa.quantity).to eq(1) + expect(goa.tolerance).to eq(1) + goa.update_quantities(1,2) + expect(goa.tolerance).to eq(2) + end end describe 'distribution strategy' do let(:article) { create :article, supplier: order.supplier, unit_quantity: 1 } let(:oa) { order.order_articles.create(:article => article) } let(:goa) { create :group_order_article, group_order: go, order_article: oa } - let!(:goaq) { create :group_order_article_quantity, group_order_article: goa, quantity: 4 } + let!(:goaq) { create :group_order_article_quantity, group_order_article: goa, quantity: 4, tolerance: 6} it 'can calculate the result for the distribution strategy "first order first serve"' do res = goa.calculate_result(2) @@ -55,9 +64,13 @@ describe GroupOrderArticle do it 'can calculate the result for the distribution strategy "no automatic distribution"' do FoodsoftConfig[:distribution_strategy] = FoodsoftConfig::DistributionStrategy::NO_AUTOMATIC_DISTRIBUTION - res = goa.calculate_result(2) expect(res).to eq(quantity: 4, tolerance: 0, total: 4) end + + it 'determines tolerance correctly' do + res = goa.calculate_result(6) + expect(res).to eq(quantity: 4, tolerance: 2, total: 6) + end end end diff --git a/spec/models/supplier_spec.rb b/spec/models/supplier_spec.rb index 72e870ed..4f73dd37 100644 --- a/spec/models/supplier_spec.rb +++ b/spec/models/supplier_spec.rb @@ -3,6 +3,35 @@ require_relative '../spec_helper' describe Supplier do let(:supplier) { create :supplier } + context 'syncs from file' do + it 'imports and updates articles' do + article1 = create(:article, supplier: supplier, order_number: 177813, unit: '250 g', price: 0.1) + article2 = create(:article, supplier: supplier, order_number: 12345) + supplier.articles = [article1, article2] + options = { filename: "foodsoft_file_01.csv" } + options[:outlist_absent] = true + options[:convert_units] = true + updated_article_pairs, outlisted_articles, new_articles = supplier.sync_from_file( Rails.root.join("spec/fixtures/foodsoft_file_01.csv"), options) + expect(new_articles.length).to be > 0 + expect(updated_article_pairs.first[1][:name]).to eq "Tomaten" + expect(outlisted_articles.first).to eq article2 + end + end + + it 'return correct tolerance' do + supplier = create :supplier, articles: create_list(:article, 1, unit_quantity: 1) + expect(supplier.has_tolerance?).to be false + supplier2 = create :supplier, articles: create_list(:article, 1, unit_quantity: 2) + expect(supplier2.has_tolerance?).to be true + end + + it 'deletes the supplier and its articles' do + supplier = create :supplier, article_count: 3 + supplier.articles.each{ |a| expect(a).to receive(:mark_as_deleted) } + supplier.mark_as_deleted + expect(supplier.deleted?).to be(true) + end + it 'has a unique name' do supplier2 = build :supplier, name: supplier.name expect(supplier2).to be_invalid diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 88dea423..666ab06f 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..d27685b4 --- /dev/null +++ b/spec/support/spec_test_helper.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# spec/support/spec_test_helper.rb +module SpecTestHelper + def login_admin + login(:admin) + end + + 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 +end + +# spec/spec_helper.rb +RSpec.configure do |config| + config.include SpecTestHelper, :type => :controller +end