diff --git a/.travis.yml b/.travis.yml index ba7b0b9b5..730632329 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,19 @@ language: ruby cache: bundler -bundler_args: '--without production development' -env: DATABASE_URL=postgres://postgres@localhost/timeoverflow_test +bundler_args: '--without development' +env: + global: + - DATABASE_URL=postgres://postgres@localhost/timeoverflow_test + - CC_TEST_REPORTER_ID=025bc15a0fa9afa52d86ee24fea845cf1d363f48a466bcf2cef8ab80c29acb28 before_script: - bundle exec rake db:setup + - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + - chmod +x ./cc-test-reporter + - ./cc-test-reporter before-build # allow elasticsearch to be ready - https://docs.travis-ci.com/user/database-setup/#ElasticSearch - sleep 10 +after_script: + - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT services: - elasticsearch addons: diff --git a/Gemfile b/Gemfile index 361440368..e034724ba 100644 --- a/Gemfile +++ b/Gemfile @@ -63,10 +63,6 @@ group :test do gem 'faker', '~> 1.9' gem 'capybara', '~> 3.13' gem 'selenium-webdriver', '~> 3.141' - gem 'chromedriver-helper', '~> 2.1' + gem 'webdrivers', '~> 3.0' gem 'simplecov', '~> 0.16.1', require: false end - -group :production do - gem 'rails_12factor', '0.0.3' -end diff --git a/Gemfile.lock b/Gemfile.lock index 20335b45e..1a521d77c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -53,8 +53,6 @@ GEM sshkit (>= 1.6.1, != 1.7.0) arbre (1.1.1) activesupport (>= 3.0.0) - archive-zip (0.11.0) - io-like (~> 0.3.0) arel (6.0.4) ast (2.4.0) autoprefixer-rails (6.3.1) @@ -96,9 +94,6 @@ GEM xpath (~> 3.2) childprocess (0.9.0) ffi (~> 1.0, >= 1.0.11) - chromedriver-helper (2.1.0) - archive-zip (~> 0.10) - nokogiri (~> 1.8) chronic (0.10.2) coderay (1.1.2) coffee-rails (4.1.0) @@ -175,7 +170,6 @@ GEM has_scope (~> 0.6) railties (>= 4.2, <= 5.2) responders - io-like (0.3.0) jquery-rails (4.3.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) @@ -219,6 +213,7 @@ GEM net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (2.9.2) + net_http_ssl_fix (0.0.10) netrc (0.11.0) nokogiri (1.8.5) mini_portile2 (~> 2.3.0) @@ -266,11 +261,6 @@ GEM rails-i18n (4.0.3) i18n (~> 0.6) railties (~> 4.0) - rails_12factor (0.0.3) - rails_serve_static_assets - rails_stdout_logging - rails_serve_static_assets (0.0.3) - rails_stdout_logging (0.0.3) railties (4.2.11) actionpack (= 4.2.11) activesupport (= 4.2.11) @@ -393,6 +383,11 @@ GEM binding_of_caller (>= 0.7.2) railties (>= 4.0) sprockets-rails (>= 2.0, < 4.0) + webdrivers (3.7.2) + net_http_ssl_fix + nokogiri (~> 1.6) + rubyzip (~> 1.0) + selenium-webdriver (~> 3.0) whenever (0.9.4) chronic (>= 0.6.3) xpath (3.2.0) @@ -412,7 +407,6 @@ DEPENDENCIES capistrano-rails (~> 1.1) capistrano-rbenv (~> 2.1) capybara (~> 3.13) - chromedriver-helper (~> 2.1) coffee-rails dalli database_cleaner (= 1.6.2) @@ -435,7 +429,6 @@ DEPENDENCIES pundit (~> 2.0.0) rails (~> 4.2) rails-i18n - rails_12factor (= 0.0.3) rdiscount rollbar (= 2.8.3) rspec-rails (~> 3.8.2) @@ -453,6 +446,7 @@ DEPENDENCIES uglifier (= 2.7.2) unicorn web-console (= 2.1.3) + webdrivers (~> 3.0) whenever RUBY VERSION diff --git a/README.md b/README.md index 564c5ef6d..42e7a523c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -# TimeOverflow [![View performance data on Skylight](https://badges.skylight.io/problem/grDTNuzZRnyu.svg)](https://oss.skylight.io/app/applications/grDTNuzZRnyu) [![Build Status](https://travis-ci.org/coopdevs/timeoverflow.svg)](https://travis-ci.org/coopdevs/timeoverflow) [![Code Climate](https://codeclimate.com/github/timeoverflow/timeoverflow/badges/gpa.svg)](https://codeclimate.com/github/timeoverflow/timeoverflow) +# TimeOverflow +[![View performance data on Skylight](https://badges.skylight.io/problem/grDTNuzZRnyu.svg)](https://oss.skylight.io/app/applications/grDTNuzZRnyu) +[![Build Status](https://travis-ci.org/coopdevs/timeoverflow.svg)](https://travis-ci.org/coopdevs/timeoverflow) +[![Maintainability](https://api.codeclimate.com/v1/badges/f82c6d98a2441c84f2ef/maintainability)](https://codeclimate.com/github/coopdevs/timeoverflow/maintainability) +[![Test Coverage](https://api.codeclimate.com/v1/badges/f82c6d98a2441c84f2ef/test_coverage)](https://codeclimate.com/github/coopdevs/timeoverflow/test_coverage) + #### www.timeoverflow.org :globe_with_meridians: Read this [in English](docs/README.en.md) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 90a0a1a37..42b73e2d0 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -1,6 +1,7 @@ #= require give_time #= require tags #= require mobile_app_libs +#= require multi_select2 $(document).on 'click', 'a[data-popup]', (event) -> window.open($(this).attr('href'), 'popup', 'width=600,height=600') diff --git a/app/assets/javascripts/give_time.js b/app/assets/javascripts/give_time.js new file mode 100644 index 000000000..125af643f --- /dev/null +++ b/app/assets/javascripts/give_time.js @@ -0,0 +1,36 @@ +$( document ).ready(function () { + jQuery.validator.addMethod( + "either-hours-minutes-informed", + function (value, element) { + return Boolean($("#transfer_hours").val() || $("#transfer_minutes").val()); + }, + "Hours or minutes should be informed" + ); + + function submitHandler(form) { + // just submit when fields are not visible, in order to not break the multi + // transfer wizard + if (!$("#transfer_hours").is(":visible")) { + form.submit(); + return; + } + + var amount = $("#transfer_hours").val() * 3600 + $("#transfer_minutes").val() * 60; + $("#transfer_amount").val(amount); + + if (amount == 0) { + $(form).find('.js-error-amount').removeClass('invisible').show(); + return; + } + + if ($(form).valid()) { + form.submit(); + } + } + + var config = { + submitHandler: submitHandler + }; + + $("#multi_transfer, #new_transfer").validate(config); +}) diff --git a/app/assets/javascripts/give_time.js.coffee b/app/assets/javascripts/give_time.js.coffee deleted file mode 100644 index 0465c0dc2..000000000 --- a/app/assets/javascripts/give_time.js.coffee +++ /dev/null @@ -1,18 +0,0 @@ -jQuery.validator.addMethod "either-hours-minutes-informed", ((value, element) -> - $("#transfer_hours").val() or $("#transfer_minutes").val() -), "Hours or minutes should be informed" - -giveTimeReadyFn = () -> - config = - submitHandler: (form) -> - amount = $("#transfer_hours").val() * 3600 + $("#transfer_minutes").val() * 60 - $("#transfer_amount").val(amount) - - if amount > 0 - form.submit() - else - $(form).find('.form-actions .error').removeClass('invisible').show() - - $( "#new_transfer" ).validate(config) - -$( document ).ready giveTimeReadyFn diff --git a/app/assets/javascripts/multi_select2.js b/app/assets/javascripts/multi_select2.js new file mode 100644 index 000000000..9bca71db3 --- /dev/null +++ b/app/assets/javascripts/multi_select2.js @@ -0,0 +1,32 @@ +/** + * A setup function to make select2 jquery plugin play as an ordinary form when + * using the "multiple" flag. + * + * Also allows options to be disabled. + * + * + * @param {String} selector - CSS selector pointing to a ") - expect(response.body).to include("") + expect(response.body).to include("") end end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 552c5f4c5..698af6d57 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -40,12 +40,14 @@ describe "GET #index" do before { login(user) } - it 'sorts the users by their member_uid asc by default' do - member.increment!(:member_uid, Member.maximum(:member_uid) + 1) + it 'sorts the users by their last_sign_in_at desc by default' do + member.user.update_column(:last_sign_in_at, DateTime.now) + another_member.user.update_column(:last_sign_in_at, nil) get :index - expect(assigns(:members).last).to eq(member) + expect(assigns(:members).first).to eq(member) + expect(assigns(:members).last).to eq(another_member) end it 'allows to sort by member_uid' do @@ -81,8 +83,6 @@ context "with an normal logged user" do it "populates and array of users" do - login(user) - get "index" expect(assigns(:members).map(&:user)) @@ -101,9 +101,31 @@ end end + context 'when searching' do + it 'allows to search by member_uid' do + user = Fabricate(:user, username: 'foo', email: 'foo@email.com') + member = Fabricate(:member, user: user, organization: test_organization, member_uid: 1000) + + get :index, q: { user_username_or_user_email_or_member_uid_search_contains: 1000 } + + expect(assigns(:members)).to include(member) + end + end + end + + describe "GET #manage" do + before { login(user) } + + it 'sorts the users by their member_uid asc by default' do + member.increment!(:member_uid, Member.maximum(:member_uid) + 1) + + get :manage + + expect(assigns(:members).last).to eq(member) + end + context 'when sorting by balance' do before do - login(user) member_admin.account.update_attribute(:balance, 3600) end @@ -111,7 +133,7 @@ let(:direction) { 'desc' } it 'orders the rows by their balance' do - get :index, q: { s: "account_balance #{direction}" } + get :manage, q: { s: "account_balance #{direction}" } expect(assigns(:members).pluck(:user_id).first).to eq(admin_user.id) end @@ -121,23 +143,12 @@ let(:direction) { 'asc' } it 'orders the rows by their balance' do - get :index, q: { s: "account_balance #{direction}" } + get :manage, q: { s: "account_balance #{direction}" } expect(assigns(:members).pluck(:user_id).last).to eq(admin_user.id) end end end - - context 'when searching' do - it 'allows to search by member_uid' do - user = Fabricate(:user, username: 'foo', email: 'foo@email.com') - member = Fabricate(:member, user: user, organization: test_organization, member_uid: 1000) - - get :index, q: { user_username_or_user_email_or_member_uid_search_contains: 1000 } - - expect(assigns(:members)).to include(member) - end - end end describe "GET #show" do @@ -151,7 +162,7 @@ end it 'links to new_transfer_path for his individual offers' do - offer = Fabricate(:offer, user: user, publisher: user, organization: test_organization) + offer = Fabricate(:offer, user: user, organization: test_organization) get "show", id: user.id expect(response.body).to include( @@ -176,7 +187,7 @@ end it 'links to new_transfer_path for his individual offers' do - offer = Fabricate(:offer, user: user, publisher: user, organization: test_organization) + offer = Fabricate(:offer, user: user, organization: test_organization) get "show", id: user.id expect(response.body).to include( diff --git a/spec/fabricators/post_fabricator.rb b/spec/fabricators/post_fabricator.rb index 367e6bc6f..919bd56b4 100644 --- a/spec/fabricators/post_fabricator.rb +++ b/spec/fabricators/post_fabricator.rb @@ -4,9 +4,6 @@ user { Fabricate(:user) } description { Faker::Lorem.paragraph } category { Fabricate(:category) } - permanent { false } - joinable { false } - global { false } active { true } end @@ -19,9 +16,6 @@ user { Fabricate(:user) } description { Faker::Lorem.paragraph } category { Fabricate(:category) } - permanent { false } - joinable { false } - global { false } active { true } end @@ -34,9 +28,6 @@ user { Fabricate(:user) } description { Faker::Lorem.paragraph } category { Fabricate(:category) } - permanent { false } - joinable { false } - global { false } active { true } end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 817b3d1c1..5196f52c5 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -36,7 +36,7 @@ end describe '#display_id' do - subject { member.display_id(nil) } + subject { member.display_id } it { is_expected.to eq(member.member_uid) } end diff --git a/spec/models/organization_spec.rb b/spec/models/organization_spec.rb index a7cc30296..8b0a9710e 100644 --- a/spec/models/organization_spec.rb +++ b/spec/models/organization_spec.rb @@ -4,17 +4,9 @@ let(:organization) { Fabricate(:organization) } describe '#display_id' do - subject { organization.display_id(destination_accountable) } + subject { organization.display_id } - context 'when the destination_accountable is an organization' do - let(:destination_accountable) { Fabricate(:organization) } - it { is_expected.to eq(organization.account.accountable_id) } - end - - context 'when the destination_accountable is not an organization' do - let(:destination_accountable) { Fabricate(:member) } - it { is_expected.to eq('') } - end + it { is_expected.to eq(organization.account.accountable_id) } end describe 'ensure_url validation' do diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 3dee6f67b..54d87727f 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -4,7 +4,6 @@ describe 'Relations' do it { is_expected.to belong_to(:category) } it { is_expected.to belong_to(:user) } - it { is_expected.to belong_to(:publisher) } it { is_expected.to have_many(:transfers) } it { is_expected.to have_many(:movements) } it { is_expected.to have_many(:events) } diff --git a/spec/models/transfer_sources_options_spec.rb b/spec/models/transfer_sources_options_spec.rb index 90ee6f9bf..2827f8777 100644 --- a/spec/models/transfer_sources_options_spec.rb +++ b/spec/models/transfer_sources_options_spec.rb @@ -2,7 +2,7 @@ RSpec.describe TransferSourcesOptions do let(:transfer_sources_options) do - described_class.new(sources, destination_accountable) + described_class.new(sources) end describe '#to_a' do @@ -14,8 +14,6 @@ Fabricate(:member, organization: organization, member_uid: 1) end - let(:destination_accountable) { Fabricate(:organization) } - let(:sources) do [organization.account, member.account, newer_member.account] end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index c37a39111..1ecab1cfc 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -17,6 +17,8 @@ it { is_expected.to validate_presence_of :username } + it { is_expected.to validate_inclusion_of(:gender).in_array(User::GENDERS) } + describe "#setup_and_save_user" do it "sets a fake email before attempting to save user" do user = Fabricate.build(:user, email: "") diff --git a/spec/services/operations/many_to_one_spec.rb b/spec/services/operations/many_to_one_spec.rb new file mode 100644 index 000000000..e17828d6e --- /dev/null +++ b/spec/services/operations/many_to_one_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +RSpec.describe Operations::Transfers::ManyToOne do + let(:source_accounts) { 5.times.map { Fabricate(:account) } } + let(:destination_account) { Fabricate(:account) } + + let(:operation) do + Operations::Transfers::ManyToOne.new( + from: source_accounts.map(&:id), + to: [destination_account.id], + transfer_params: { amount: 3600, reason: 'why not' } + ) + end + + describe '#perform' do + it 'creates multiple transfers' do + expect { operation.perform }.to change { Transfer.count }.by(5) + end + + it 'creates many movements towards destination account' do + expect { operation.perform }.to change { Movement.where(account_id: destination_account.id).count }.by(5) + end + + it 'creates one movement from each source account' do + expect { operation.perform }.to change { Movement.where(account_id: source_accounts.map(&:id)).map(&:account_id).uniq.count }.by(5) + end + end +end diff --git a/spec/services/operations/one_to_many_spec.rb b/spec/services/operations/one_to_many_spec.rb new file mode 100644 index 000000000..40c26a54b --- /dev/null +++ b/spec/services/operations/one_to_many_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +RSpec.describe Operations::Transfers::OneToMany do + let(:source_account) { Fabricate(:account) } + let(:destination_accounts) { 5.times.map { Fabricate(:account) } } + + let(:operation) do + Operations::Transfers::OneToMany.new( + from: [source_account.id], + to: destination_accounts.map(&:id), + transfer_params: { amount: 3600, reason: 'why not' } + ) + end + + describe '#perform' do + it 'creates multiple transfers' do + expect { operation.perform }.to change { Transfer.count }.by(5) + end + + it 'creates many movements from source account' do + expect { operation.perform }.to change { Movement.where(account_id: source_account.id).count }.by(5) + end + + it 'creates one movement towards each target account' do + expect { operation.perform }.to change { Movement.where(account_id: destination_accounts.map(&:id)).map(&:account_id).uniq.count }.by(5) + end + end +end diff --git a/spec/services/operations/one_to_one_spec.rb b/spec/services/operations/one_to_one_spec.rb new file mode 100644 index 000000000..b088a7ade --- /dev/null +++ b/spec/services/operations/one_to_one_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +RSpec.describe Operations::Transfers::OneToMany do + let(:source_account) { Fabricate(:account) } + let(:destination_account) { Fabricate(:account) } + + let(:operation) do + Operations::Transfers::OneToOne.new( + from: [source_account.id], + to: [destination_account.id], + transfer_params: { amount: 3600, reason: 'why not' } + ) + end + + describe '#perform' do + it 'creates multiple transfers' do + expect { operation.perform }.to change { Transfer.count }.by(1) + end + + it 'creates one movement towards destination account' do + expect { operation.perform }.to change { Movement.where(account_id: source_account.id).count }.by(1) + end + + it 'creates one movement from each source account' do + expect { operation.perform }.to change { Movement.where(account_id: destination_account.id).count }.by(1) + end + end +end + diff --git a/spec/services/operations/transfers_spec.rb b/spec/services/operations/transfers_spec.rb new file mode 100644 index 000000000..c77c3740d --- /dev/null +++ b/spec/services/operations/transfers_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +RSpec.describe Operations::Transfers do + describe 'create' do + let(:operation) { + Operations::Transfers.create( + from: from, + to: to, + transfer_params: {} + ) + } + + context 'when there is one source and many targets' do + let(:from) { [1] } + let(:to) { [2, 3] } + + it 'instantiates a OneToMany operation' do + expect(operation).to be_a(Operations::Transfers::OneToMany) + end + end + + + context 'when there many sources and one target' do + let(:from) { [1, 2] } + let(:to) { [3] } + + it 'instantiates a ManyToOne operation' do + expect(operation).to be_a(Operations::Transfers::ManyToOne) + end + end + + context 'when weird shit is passed' do + let(:from) { [] } + let(:to) { [] } + + it do + expect { operation }.to raise_error(ArgumentError) + end + end + end +end + diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7a75c877f..44cd48558 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -10,7 +10,7 @@ require 'capybara/rspec' require 'database_cleaner' require 'fabrication' -require 'chromedriver-helper' +require 'webdrivers' require 'selenium/webdriver' require 'faker' require 'shoulda/matchers'