From a0c9641994ef74969e86d451f888456e7b95b448 Mon Sep 17 00:00:00 2001 From: Georg Gadinger Date: Sun, 19 Apr 2020 21:47:19 +0200 Subject: [PATCH 1/2] Remove useless specs --- .gitignore | 2 + .rubocop.yml | 8 +- spec/controllers/user_controller_spec.rb | 13 -- spec/factories/10_users.rb | 9 -- spec/factories/answers.rb | 6 - spec/factories/notifications.rb | 8 - spec/factories/questions.rb | 6 - spec/factories/users.rb | 21 +++ spec/features/users/banned_spec.rb | 45 ------ spec/features/users/follow_user_spec.rb | 28 ---- spec/features/users/inbox_spec.rb | 144 ----------------- spec/features/users/sign_in_spec.rb | 45 ------ spec/features/users/sign_out_spec.rb | 19 --- spec/features/users/user_show_spec.rb | 66 -------- spec/helpers/subscribe_helper_spec.rb | 15 -- spec/models/answer_spec.rb | 19 --- spec/models/notification_spec.rb | 5 - spec/rails_helper.rb | 51 +++++- spec/spec_helper.rb | 147 +++++++++--------- spec/support/devise.rb | 3 - .../{factory_girl.rb => factory_bot.rb} | 4 + spec/support/helpers.rb | 5 - spec/support/helpers/session_helpers.rb | 19 --- spec/support/simplecov.rb | 6 + spec/support/wait_for_ajax.rb | 15 -- 25 files changed, 155 insertions(+), 554 deletions(-) delete mode 100644 spec/controllers/user_controller_spec.rb delete mode 100644 spec/factories/10_users.rb delete mode 100644 spec/factories/answers.rb delete mode 100644 spec/factories/notifications.rb delete mode 100644 spec/factories/questions.rb create mode 100644 spec/factories/users.rb delete mode 100644 spec/features/users/banned_spec.rb delete mode 100644 spec/features/users/follow_user_spec.rb delete mode 100644 spec/features/users/inbox_spec.rb delete mode 100644 spec/features/users/sign_in_spec.rb delete mode 100644 spec/features/users/sign_out_spec.rb delete mode 100644 spec/features/users/user_show_spec.rb delete mode 100644 spec/helpers/subscribe_helper_spec.rb delete mode 100644 spec/models/answer_spec.rb delete mode 100644 spec/models/notification_spec.rb delete mode 100644 spec/support/devise.rb rename spec/support/{factory_girl.rb => factory_bot.rb} (56%) delete mode 100644 spec/support/helpers.rb delete mode 100644 spec/support/helpers/session_helpers.rb create mode 100644 spec/support/simplecov.rb delete mode 100644 spec/support/wait_for_ajax.rb diff --git a/.gitignore b/.gitignore index 2f31a86f..ccd5a704 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ desktop.ini # ignore exports /public/export + +/spec/examples.txt diff --git a/.rubocop.yml b/.rubocop.yml index e5740e5d..47316f61 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,6 @@ AllCops: - RunRailsCops: true + Rails: + Enabled: true Exclude: - 'vendor/**/*' - 'db/schema.rb' @@ -7,7 +8,7 @@ AllCops: Metrics/ClassLength: Enabled: false -Metrics/LineLength: +Layout/LineLength: Enabled: false Metrics/MethodLength: @@ -16,9 +17,6 @@ Metrics/MethodLength: Metrics/ModuleLength: Enabled: false -Style/BracesAroundHashParameters: - Enabled: false - Style/ClassAndModuleChildren: Enabled: false diff --git a/spec/controllers/user_controller_spec.rb b/spec/controllers/user_controller_spec.rb deleted file mode 100644 index cfd08f08..00000000 --- a/spec/controllers/user_controller_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'rails_helper' - -RSpec.describe UserController, :type => :controller do - before do - @user = create(:user) - end - - it 'responds successfully with a HTTP 200 status code' do - get :show, username: @user.screen_name, page: 1 - expect(response).to be_success - expect(response).to have_http_status(200) - end -end diff --git a/spec/factories/10_users.rb b/spec/factories/10_users.rb deleted file mode 100644 index dc6400f4..00000000 --- a/spec/factories/10_users.rb +++ /dev/null @@ -1,9 +0,0 @@ -FactoryBot.define do - factory :user do |u| - u.sequence(:screen_name) { |n| "#{Faker::Internet.user_name 0..12, %w(_)}#{n}" } - u.sequence(:email) { |n| "#{n}#{Faker::Internet.email}" } - u.password { "P4s5w0rD" } - u.sequence(:confirmed_at) { Time.zone.now } - u.display_name { Faker::Name.name } - end -end diff --git a/spec/factories/answers.rb b/spec/factories/answers.rb deleted file mode 100644 index 64d7bd0c..00000000 --- a/spec/factories/answers.rb +++ /dev/null @@ -1,6 +0,0 @@ -FactoryBot.define do - factory :answer do |u| - u.sequence(:content) { |n| "This is an answer. I'm number #{n}!" } - u.user { FactoryBot.create(:user) } - end -end diff --git a/spec/factories/notifications.rb b/spec/factories/notifications.rb deleted file mode 100644 index 05dcc2fb..00000000 --- a/spec/factories/notifications.rb +++ /dev/null @@ -1,8 +0,0 @@ -FactoryBot.define do - factory :notification do - target_type { "MyString" } - target_id { 1 } - recipient_id { 1 } - new { false } - end -end diff --git a/spec/factories/questions.rb b/spec/factories/questions.rb deleted file mode 100644 index d35c40ca..00000000 --- a/spec/factories/questions.rb +++ /dev/null @@ -1,6 +0,0 @@ -FactoryBot.define do - factory :question do |u| - u.sequence(:content) { |n| "#{QuestionGenerator.generate}#{n}" } - u.author_is_anonymous { true } - end -end diff --git a/spec/factories/users.rb b/spec/factories/users.rb new file mode 100644 index 00000000..488ff726 --- /dev/null +++ b/spec/factories/users.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :user do + sequence(:screen_name) { |i| "#{Faker::Internet.username(specifier: 0..12, separators: %w(_))}#{i}" } + sequence(:email) { |i| "#{i}#{Faker::Internet.email}" } + password { "P4s5w0rD" } + confirmed_at { Time.now.utc } + display_name { Faker::Name.name } + + transient do + roles { [] } + end + + after(:create) do |user, evaluator| + evaluator.roles.each do |role| + user.add_role role + end + end + end +end diff --git a/spec/features/users/banned_spec.rb b/spec/features/users/banned_spec.rb deleted file mode 100644 index 4d041bb0..00000000 --- a/spec/features/users/banned_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -include Warden::Test::Helpers -Warden.test_mode! - -# Feature: Ban users -# As a user -# I want to get banned -# So I can't sign in anymore -feature "Ban users", :devise do - - after :each do - Warden.test_reset! - end - - # Scenario: User gets banned - # Given I am signed in - # When I visit another page - # And I am banned - # Then I see the sign in page - scenario "user gets banned", js: true do - me = FactoryBot.create :user - - login_as me, scope: :user - visit root_path - expect(page).to have_text("Timeline") - page.driver.render Rails.root.join("tmp/ban_#{Time.now.to_i}_1.png"), full: true - - me.permanently_banned = true - me.save - - visit "/inbox" - - expect(current_path).to eq(new_user_session_path) - page.driver.render Rails.root.join("tmp/ban_#{Time.now.to_i}_2.png"), full: true - end - - scenario 'user visits banned user profiles', js: true do - evil_user = FactoryBot.create :user - evil_user.permanently_banned = true - evil_user.save - - visit show_user_profile_path(evil_user.screen_name) - expect(page).to have_text('BANNED') - page.driver.render Rails.root.join("tmp/ban_#{Time.now.to_i}_3.png"), full: true - end -end diff --git a/spec/features/users/follow_user_spec.rb b/spec/features/users/follow_user_spec.rb deleted file mode 100644 index d028f751..00000000 --- a/spec/features/users/follow_user_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -include Warden::Test::Helpers -Warden.test_mode! - -feature "User profile page", :devise do - - after :each do - Warden.test_reset! - end - - scenario "user gets followed", js: true do - me = FactoryBot.create(:user) - other = FactoryBot.create(:user) - - login_as me, scope: :user - visit show_user_profile_path(other.screen_name) - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_1.png"), full: true - - click_button "Follow" - wait_for_ajax - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_2.png"), full: true - - expect(page).to have_text("FOLLOWING") - - click_link 'Follower' - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_3.png"), full: true - expect(page).to have_text(me.screen_name) - end -end diff --git a/spec/features/users/inbox_spec.rb b/spec/features/users/inbox_spec.rb deleted file mode 100644 index 12f38e38..00000000 --- a/spec/features/users/inbox_spec.rb +++ /dev/null @@ -1,144 +0,0 @@ -include Warden::Test::Helpers -Warden.test_mode! - -# Feature: Answer questions -# As a user -# I want to go to the inbox -# So I can answer and get new questions -feature "Inbox", :devise do - - after :each do - Warden.test_reset! - end - - # Scenario: User answers a question - # Given I am signed in - # When I visit the inbox - # And I have a question in my inbox - # Then I can answer my question - # And see the answer on my user profile - scenario "user answers a question", js: true do - me = FactoryBot.create :user - question = FactoryBot.create :question - Inbox.create question: question, user: me, new: true - - login_as me, scope: :user - visit root_path - - click_link "Inbox" - expect(page).to have_text(question.content) - fill_in "ib-answer", with: Faker::Lorem.sentence - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_2.png"), full: true - - click_button "Answer" - wait_for_ajax - expect(page).not_to have_text(question.content) - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_3.png"), full: true - - visit show_user_profile_path(me.screen_name) - expect(page).to have_text(question.content) - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_4.png"), full: true - end - - # Scenario: User generates new question - # Given I am signed in - # When I visit the inbox - # And I click "Get new question" - # Then I get a new question - scenario 'user generates new question', js: true do - me = FactoryBot.create :user - - login_as me, scope: :user - visit inbox_path - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_1.png"), full: true - - click_button "Get new question" - wait_for_ajax - expect(page).to have_text('Answer') - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_2.png"), full: true - end - - # Scenario: User with privacy options generates new question - # Given I am signed in - # When I visit the inbox - # And I click "Get new question" - # And I don't want to receive questions by anonymous users - # Then I get a new question - scenario 'user with privacy options generates new question', js: true do - me = FactoryBot.create :user - me.privacy_allow_anonymous_questions = false - me.save - - login_as me, scope: :user - visit inbox_path - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_1.png"), full: true - - click_button "Get new question" - wait_for_ajax - expect(page).to have_text('Answer') - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_2.png"), full: true - end - -=begin - # Scenario: User deletes a question - # Given I am signed in - # When I visit the inbox - # And I have a question in my inbox - # And I delete the question - # Then don't see it anymore in my inbox - scenario "user deletes a question", js: true do - me = FactoryBot.create :user - question = FactoryBot.create :question - Inbox.create question: question, user: me - - login_as me, scope: :user - visit inbox_path - expect(page).to have_text(question.content) - - click_button "Delete" - expect(page).to have_text('Really delete?') - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_1.png"), full: true - - # this apparently doesn't get triggered :( - page.find('.sweet-alert').click_button 'Delete' - wait_for_ajax - - login_as me, scope: :user - visit inbox_path - expect(page).not_to have_text(question.content) - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_2.png"), full: true - end - - # Scenario: User deletes all questions - # Given I am signed in - # When I visit the inbox - # And I have a few questions in my inbox - # And I click on "Delete all questions" - # Then don't see them anymore in my inbox - scenario "user deletes all questions", js: true do - me = FactoryBot.create :user - 5.times do - question = FactoryBot.create :question - Inbox.create question: question, user: me - end - - login_as me, scope: :user - visit inbox_path - expect(page).to have_text('Answer'.upcase) - - click_button "Delete all questions" - expect(page).to have_text('Really delete 5 questions?') - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_1.png"), full: true - - page.find('.sweet-alert').click_button 'Delete' - wait_for_ajax - - puts me.inbox.all - - login_as me, scope: :user - visit inbox_path - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_2.png"), full: true - expect(page).not_to have_text('Answer'.upcase) - end -=end -end diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb deleted file mode 100644 index d9945eb8..00000000 --- a/spec/features/users/sign_in_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# Feature: Sign in -# As a user -# I want to sign in -# So I can visit protected areas of the site -feature 'Sign in', :devise do - - scenario 'user cannot sign in if not registered', js: true do - user = FactoryBot.build(:user) - signin(user.screen_name, user.password) - expect(page).to have_content I18n.t 'devise.failure.not_found_in_database', authentication_keys: 'login' - end - - # Scenario: User can sign in with valid credentials - # Given I exist as a user - # And I am not signed in - # When I sign in with valid credentials - # Then I see a success message - scenario 'user can sign in with valid credentials', js: true do - user = FactoryBot.create(:user) - signin(user.email, user.password) - expect(page).to have_content I18n.t 'devise.sessions.signed_in' - end - - # Scenario: User cannot sign in with wrong email - # Given I exist as a user - # And I am not signed in - # When I sign in with a wrong email - # Then I see an invalid email message - scenario 'user cannot sign in with wrong email', js: true do - user = FactoryBot.create(:user) - signin('invalid@email.com', user.password) - expect(page).to have_content I18n.t 'devise.failure.not_found_in_database', authentication_keys: 'login' - end - - # Scenario: User cannot sign in with wrong password - # Given I exist as a user - # And I am not signed in - # When I sign in with a wrong password - # Then I see an invalid password message - scenario 'user cannot sign in with wrong password', js: true do - user = FactoryBot.create(:user) - signin(user.email, 'what the fuck is my p4s5w0rD again?') - expect(page).to have_content I18n.t 'devise.failure.invalid', authentication_keys: 'login' - end -end \ No newline at end of file diff --git a/spec/features/users/sign_out_spec.rb b/spec/features/users/sign_out_spec.rb deleted file mode 100644 index 2e027e9b..00000000 --- a/spec/features/users/sign_out_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# Feature: Sign out -# As a user -# I want to sign out -# So I can protect my account from unauthorized access -feature 'Sign out', :devise do - - # Scenario: User signs out successfully - # Given I am signed in - # When I sign out - # Then I see a signed out message - scenario 'user signs out successfully', js: true do - user = FactoryBot.create(:user) - signin(user.email, user.password) - expect(page).to have_content I18n.t 'devise.sessions.signed_in' - click_link user.screen_name - click_link 'Logout' - expect(page).to have_content I18n.t 'devise.sessions.signed_out' - end -end \ No newline at end of file diff --git a/spec/features/users/user_show_spec.rb b/spec/features/users/user_show_spec.rb deleted file mode 100644 index ef7a5834..00000000 --- a/spec/features/users/user_show_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -include Warden::Test::Helpers -Warden.test_mode! - -# Feature: User profile page -# As a user -# I want to visit my user profile page -# So I can see my personal account data -feature "User profile page", :devise do - - after :each do - Warden.test_reset! - end - - # Scenario: User sees own profile - # Given I am signed in - # When I visit the user profile page - # Then I see my own screen name and follower count - scenario 'user sees own profile', js: true do - user = FactoryBot.create(:user) - - login_as(user, :scope => :user) - - visit show_user_profile_path(user.screen_name) - - expect(page).to have_content user.screen_name - expect(page).to have_content user.follower_count - end - - # Scenario: User sees another user's profile - # Given I am signed in - # When I visit another user's profile - # Then I see that user's screen name and follower count - scenario "user sees another user's profile", js: true do - me = FactoryBot.create(:user) - other = FactoryBot.create(:user) - - login_as me, scope: :user - - visit show_user_profile_path(other.screen_name) - - expect(page).to have_content other.screen_name - expect(page).to have_content other.follower_count - end - - # Scenario: User gets asked a question - # Given I am signed in - # When I visit another user's profile - # And I fill something in the question box - # And I click on "Ask" - # Then I see "Question asked successfully." - scenario "user gets asked a question", js: true do - me = FactoryBot.create(:user) - other = FactoryBot.create(:user) - - login_as me, scope: :user - visit show_user_profile_path(other.screen_name) - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_1.png"), full: true - - fill_in "qb-question", with: Faker::Lorem.sentence - click_button "Ask" - wait_for_ajax - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_2.png"), full: true - - expect(page).to have_text("Question asked successfully.") - end -end diff --git a/spec/helpers/subscribe_helper_spec.rb b/spec/helpers/subscribe_helper_spec.rb deleted file mode 100644 index cb8dc23c..00000000 --- a/spec/helpers/subscribe_helper_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'rails_helper' - -# Specs in this file have access to a helper object that includes -# the SubscribeHelper. For example: -# -# describe SubscribeHelper do -# describe "string concat" do -# it "concats two strings with spaces" do -# expect(helper.concat_strings("this","that")).to eq("this that") -# end -# end -# end -RSpec.describe SubscribeHelper, :type => :helper do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/answer_spec.rb b/spec/models/answer_spec.rb deleted file mode 100644 index 44a3f12b..00000000 --- a/spec/models/answer_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'rails_helper' - -RSpec.describe Answer, :type => :model do - before :each do - @answer = Answer.new( - content: 'This is an answer.', - user: FactoryBot.create(:user), - question: FactoryBot.create(:question) - ) - end - - subject { @answer } - - it { should respond_to(:content) } - - it '#content returns a string' do - expect(@answer.content).to match 'This is an answer.' - end -end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb deleted file mode 100644 index f79213f9..00000000 --- a/spec/models/notification_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe Notification, :type => :model do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 5bbadc73..7517cd89 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,8 +1,16 @@ +# frozen_string_literal: true + # This file is copied to spec/ when you run 'rails generate rspec:install' -ENV["RAILS_ENV"] ||= 'test' -require 'spec_helper' +ENV["RAILS_ENV"] ||= "test" require File.expand_path("../../config/environment", __FILE__) -require 'rspec/rails' +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require "spec_helper" +require "rspec/rails" +# Add additional requires below this line. Rails is not loaded until this point! +require "devise" +require "capybara/rails" +require "capybara/rspec" # Requires supporting ruby files with custom matchers and macros, etc, in # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are @@ -11,20 +19,40 @@ require 'rspec/rails' # run twice. It is recommended that you do not name files matching this glob to # end with _spec.rb. You can configure this pattern with the --pattern # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. -Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } -# Checks for pending migrations before tests are run. +# Checks for pending migration and applies them before tests are run. # If you are not using ActiveRecord, you can remove this line. ActiveRecord::Migration.maintain_test_schema! RSpec.configure do |config| + config.before :suite do + # Run webpack compilation before suite, so assets exists in public/packs + # It would be better to run the webpack compilation only if at least one :js spec + # should be executed, but `when_first_matching_example_defined` + # does not work with `config.infer_spec_type_from_file_location!` + # see https://github.com/rspec/rspec-core/issues/2366 + + unless ENV.key?("DISABLE_WEBPACK_IN_TESTS") + start = Time.now # rubocop:disable all + `bin/webpack` + puts "processing time of webpack: \033[32;1m#{(Time.now - start).round(3).to_s.ljust(5, '0')}s\033[0m" # rubocop:disable all + end + end + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures - # config.fixture_path = "#{::Rails.root}/spec/fixtures" + config.fixture_path = "#{::Rails.root}/spec/fixtures" # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false # instead of true. - config.use_transactional_fixtures = false + config.use_transactional_fixtures = true # RSpec Rails can automatically mix in different behaviours to your tests # based on their file location, for example enabling you to call `get` and @@ -40,4 +68,13 @@ RSpec.configure do |config| # The different available types are documented in the features, such as in # https://relishapp.com/rspec/rspec-rails/docs config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") + + config.include Devise::Test::ControllerHelpers, type: :controller end + +Dir[Rails.root.join "spec", "shared_examples", "*.rb"].sort.each { |f| require f } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 10b74ba3..77ae7ed2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,99 +1,102 @@ -require 'simplecov' -require 'simplecov-json' -require 'simplecov-rcov' +# frozen_string_literal: true -SimpleCov.formatters = [ - SimpleCov::Formatter::HTMLFormatter, - SimpleCov::Formatter::JSONFormatter, - SimpleCov::Formatter::RcovFormatter -] -SimpleCov.start - -require 'capybara/poltergeist' -Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new app, - js_errors: false, - timeout: 180 -end -Capybara.javascript_driver = :poltergeist - -require 'factory_bot_rails' +require "support/simplecov" +require "support/factory_bot" # This file was generated by the `rails generate rspec:install` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. -# The generated `.rspec` file contains `--require spec_helper` which will cause this -# file to always be loaded, without a need to explicitly require it in any files. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. # # Given that it is always loaded, you are encouraged to keep this file as # light-weight as possible. Requiring heavyweight dependencies from this file # will add to the boot time of your test suite on EVERY test run, even for an -# individual file that may not need all of that loaded. Instead, make a -# separate helper file that requires this one and then use it only in the specs -# that actually need it. +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. # # The `.rspec` file also contains a few flags that are not defaults but that # users commonly want. # # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| -# The settings below are suggested to provide a good initial experience -# with RSpec, but feel free to customize to your heart's content. -=begin - # These two settings work together to allow you to limit a spec run - # to individual examples or groups you care about by tagging them with - # `:focus` metadata. When nothing is tagged with `:focus`, all examples - # get run. - config.filter_run :focus - config.run_all_when_everything_filtered = true - - # Many RSpec users commonly either run the entire suite or an individual - # file, and it's useful to allow more verbose output when running an - # individual spec file. - if config.files_to_run.one? - # Use the documentation formatter for detailed output, - # unless a formatter has already been configured - # (e.g. via a command-line flag). - config.default_formatter = 'doc' - end - - # Print the 10 slowest examples and example groups at the - # end of the spec run, to help surface which specs are running - # particularly slow. - config.profile_examples = 10 - - # Run specs in random order to surface order dependencies. If you find an - # order dependency and want to debug it, you can fix the order by providing - # the seed, which is printed after each run. - # --seed 1234 - config.order = :random - - # Seed global randomization in this process using the `--seed` CLI option. - # Setting this allows you to use `--seed` to deterministically reproduce - # test failures related to randomization by passing the same `--seed` value - # as the one that triggered the failure. - Kernel.srand config.seed - # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest # assertions if you prefer. config.expect_with :rspec do |expectations| - # Enable only the newer, non-monkey-patching expect syntax. - # For more details, see: - # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax - expectations.syntax = :expect + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true end # rspec-mocks config goes here. You can use an alternate test double # library (such as bogus or mocha) by changing the `mock_with` option here. config.mock_with :rspec do |mocks| - # Enable only the newer, non-monkey-patching expect syntax. - # For more details, see: - # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ - mocks.syntax = :expect - # Prevents you from mocking or stubbing a method that does not exist on - # a real object. This is generally recommended. + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. mocks.verify_partial_doubles = true end -=end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # # This allows you to limit a spec run to individual examples or groups + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `:focus` + # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + # + # # Allows RSpec to persist some state between runs in order to support + # # the `--only-failures` and `--next-failure` CLI options. We recommend + # # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + # + # # Limits the available syntax to the non-monkey patched syntax that is + # # recommended. For more details, see: + # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + # config.disable_monkey_patching! + # + # # Many RSpec users commonly either run the entire suite or an individual + # # file, and it's useful to allow more verbose output when running an + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = 'doc' + # end + # + # # Print the 10 slowest examples and example groups at the + # # end of the spec run, to help surface which specs are running + # # particularly slow. + # config.profile_examples = 10 + # + # # Run specs in random order to surface order dependencies. If you find an + # # order dependency and want to debug it, you can fix the order by providing + # # the seed, which is printed after each run. + # # --seed 1234 + # config.order = :random + # + # # Seed global randomization in this process using the `--seed` CLI option. + # # Setting this allows you to use `--seed` to deterministically reproduce + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed end diff --git a/spec/support/devise.rb b/spec/support/devise.rb deleted file mode 100644 index 21dd1e8b..00000000 --- a/spec/support/devise.rb +++ /dev/null @@ -1,3 +0,0 @@ -RSpec.configure do |config| - config.include Devise::TestHelpers, type: :controller -end \ No newline at end of file diff --git a/spec/support/factory_girl.rb b/spec/support/factory_bot.rb similarity index 56% rename from spec/support/factory_girl.rb rename to spec/support/factory_bot.rb index c7890e49..31663a9d 100644 --- a/spec/support/factory_girl.rb +++ b/spec/support/factory_bot.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + +require "factory_bot_rails" + RSpec.configure do |config| config.include FactoryBot::Syntax::Methods end diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb deleted file mode 100644 index 5730fdb5..00000000 --- a/spec/support/helpers.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'support/helpers/session_helpers' - -RSpec.configure do |config| - config.include Features::SessionHelpers, type: :feature -end \ No newline at end of file diff --git a/spec/support/helpers/session_helpers.rb b/spec/support/helpers/session_helpers.rb deleted file mode 100644 index 6ee1933e..00000000 --- a/spec/support/helpers/session_helpers.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Features - module SessionHelpers - def sign_up_with(screen_name, email, password, confirmation) - visit new_user_registration_path - fill_in 'User name', with: screen_name - fill_in 'Email address', with: email - fill_in 'Password', with: password - fill_in 'Password confirmation', :with => confirmation - click_button 'Sign up' - end - - def signin(email, password) - visit new_user_session_path - fill_in 'User name', with: email - fill_in 'Password', with: password - click_button 'Sign in' - end - end -end \ No newline at end of file diff --git a/spec/support/simplecov.rb b/spec/support/simplecov.rb new file mode 100644 index 00000000..2cc94286 --- /dev/null +++ b/spec/support/simplecov.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require "simplecov" +# require "simplecov-rcov" +# SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter +SimpleCov.start "rails" diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb deleted file mode 100644 index 9d772404..00000000 --- a/spec/support/wait_for_ajax.rb +++ /dev/null @@ -1,15 +0,0 @@ -module WaitForAjax - def wait_for_ajax - Timeout.timeout(Capybara.default_wait_time) do - loop until finished_all_ajax_requests? - end - end - - def finished_all_ajax_requests? - page.evaluate_script('jQuery.active').zero? - end -end - -RSpec.configure do |config| - config.include WaitForAjax, type: :feature -end From 946bb3ae9d8714550a009869355dd02cf7c2f89d Mon Sep 17 00:00:00 2001 From: Georg Gadinger Date: Sun, 19 Apr 2020 22:35:58 +0200 Subject: [PATCH 2/2] Use Rolify for admin and moderator roles --- Gemfile | 2 + Gemfile.lock | 2 + Rakefile | 52 ++++++++++--------- app/controllers/ajax/moderation_controller.rb | 20 ++++--- app/models/role.rb | 15 ++++++ app/models/user.rb | 10 +++- app/views/layouts/_profile.html.haml | 2 +- app/views/user/_actions.html.haml | 2 +- app/views/user/_modal_privileges.html.haml | 2 +- .../user/_modal_privileges_item.html.haml | 5 +- app/views/user/_profile_info.html.haml | 6 +-- app/views/user/data.html.haml | 2 +- config/initializers/rails_admin.rb | 5 +- config/initializers/rolify.rb | 12 +++++ config/routes.rb | 14 ++--- .../20200419184442_rolify_create_roles.rb | 20 +++++++ .../20200419185535_create_initial_roles.rb | 34 ++++++++++++ db/schema.rb | 20 ++++++- db/seeds.rb | 4 ++ lib/exporter.rb | 21 ++++++-- .../role_constrained_routes_spec.rb | 51 ++++++++++++++++++ 21 files changed, 246 insertions(+), 55 deletions(-) create mode 100644 app/models/role.rb create mode 100644 config/initializers/rolify.rb create mode 100644 db/migrate/20200419184442_rolify_create_roles.rb create mode 100644 db/migrate/20200419185535_create_initial_roles.rb create mode 100644 spec/integration/role_constrained_routes_spec.rb diff --git a/Gemfile b/Gemfile index 42a75408..e20ff191 100644 --- a/Gemfile +++ b/Gemfile @@ -42,6 +42,8 @@ gem 'tiny-color-rails' gem 'jquery-minicolors-rails' gem 'colorize' +gem "rolify", "~> 5.2" + source "https://rails-assets.org" do gem 'rails-assets-growl' gem 'rails-assets-jquery', '~> 2.2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 44319795..c70c9ee6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -382,6 +382,7 @@ GEM responders (3.0.0) actionpack (>= 5.0) railties (>= 5.0) + rolify (5.2.0) rspec-core (3.9.1) rspec-support (~> 3.9.1) rspec-expectations (3.9.1) @@ -554,6 +555,7 @@ DEPENDENCIES rake redcarpet redis + rolify (~> 5.2) rspec-rails (~> 3.9) ruby-progressbar sanitize diff --git a/Rakefile b/Rakefile index 6ce0e0dc..ab71e59f 100644 --- a/Rakefile +++ b/Rakefile @@ -105,44 +105,48 @@ namespace :justask do end end - desc "Gives admin status to an user." + desc "Gives admin status to a user." task :admin, [:screen_name] => :environment do |t, args| - fail "screen name required" if args[:screen_name].nil? + abort "screen name required" if args[:screen_name].nil? + user = User.find_by_screen_name(args[:screen_name]) - fail "user #{args[:screen_name]} not found" if user.nil? - user.admin = true - user.save! - puts "#{user.screen_name} is now an admin." + abort "user #{args[:screen_name]} not found" if user.nil? + + user.add_role :administrator + puts "#{user.screen_name} is now an administrator." end - desc "Removes admin status from an user." + desc "Removes admin status from a user." task :deadmin, [:screen_name] => :environment do |t, args| - fail "screen name required" if args[:screen_name].nil? + abort "screen name required" if args[:screen_name].nil? + user = User.find_by_screen_name(args[:screen_name]) - fail "user #{args[:screen_name]} not found" if user.nil? - user.admin = false - user.save! - puts "#{user.screen_name} is no longer an admin." + abort "user #{args[:screen_name]} not found" if user.nil? + + user.remove_role :administrator + puts "#{user.screen_name} is no longer an administrator." end - desc "Gives moderator status to an user." + desc "Gives moderator status to a user." task :mod, [:screen_name] => :environment do |t, args| - fail "screen name required" if args[:screen_name].nil? + abort "screen name required" if args[:screen_name].nil? + user = User.find_by_screen_name(args[:screen_name]) - fail "user #{args[:screen_name]} not found" if user.nil? - user.moderator = true - user.save! - puts "#{user.screen_name} is now an moderator." + abort "user #{args[:screen_name]} not found" if user.nil? + + user.add_role :moderator + puts "#{user.screen_name} is now a moderator." end - desc "Removes moderator status from an user." + desc "Removes moderator status from a user." task :demod, [:screen_name] => :environment do |t, args| - fail "screen name required" if args[:screen_name].nil? + abort "screen name required" if args[:screen_name].nil? + user = User.find_by_screen_name(args[:screen_name]) - fail "user #{args[:screen_name]} not found" if user.nil? - user.moderator = false - user.save! - puts "#{user.screen_name} is no longer an moderator." + abort "user #{args[:screen_name]} not found" if user.nil? + + user.remove_role :moderator + puts "#{user.screen_name} is no longer a moderator." end desc "Hits an user with the banhammer." diff --git a/app/controllers/ajax/moderation_controller.rb b/app/controllers/ajax/moderation_controller.rb index 26889361..9ec82abb 100644 --- a/app/controllers/ajax/moderation_controller.rb +++ b/app/controllers/ajax/moderation_controller.rb @@ -125,9 +125,9 @@ class Ajax::ModerationController < ApplicationController unban = params[:ban] == "0" perma = params[:permaban] == "1" - buntil = DateTime.strptime params[:until], "%m/%d/%Y %I:%M %p" unless unban or perma + buntil = DateTime.strptime params[:until], "%m/%d/%Y %I:%M %p" unless unban || perma - if not unban and target.admin? + if !unban && target.has_role?(:administrator) @status = :nopriv @message = I18n.t('messages.moderation.ban.nopriv') @success = false @@ -166,7 +166,7 @@ class Ajax::ModerationController < ApplicationController @message = I18n.t('messages.moderation.privilege.nope') return unless %w(blogger supporter moderator admin contributor translator).include? params[:type].downcase - if %w(supporter moderator admin).include?(params[:type].downcase) and !current_user.admin? + if %w(supporter moderator admin).include?(params[:type].downcase) && !current_user.has_role?(:administrator) @status = :nopriv @message = I18n.t('messages.moderation.privilege.nopriv') @success = false @@ -174,7 +174,9 @@ class Ajax::ModerationController < ApplicationController end @checked = status - case params[:type].downcase + type = params[:type].downcase + target_role = {"admin" => "administrator"}.fetch(type, type).to_sym + case type when 'blogger' target_user.blogger = status when 'contributor' @@ -183,10 +185,12 @@ class Ajax::ModerationController < ApplicationController target_user.translator = status when 'supporter' target_user.supporter = status - when 'moderator' - target_user.moderator = status - when 'admin' - target_user.admin = status + when 'moderator', 'admin' + if status + target_user.add_role target_role + else + target_user.remove_role target_role + end end target_user.save! diff --git a/app/models/role.rb b/app/models/role.rb new file mode 100644 index 00000000..5db08032 --- /dev/null +++ b/app/models/role.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Role < ApplicationRecord + has_and_belongs_to_many :users, join_table: :users_roles + + belongs_to :resource, + polymorphic: true, + optional: true + + validates :resource_type, + inclusion: { in: Rolify.resource_types }, + allow_nil: true + + scopify +end diff --git a/app/models/user.rb b/app/models/user.rb index 43e88e4f..119f8d81 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,6 +5,8 @@ class User < ApplicationRecord :recoverable, :rememberable, :trackable, :validatable, :confirmable, :authentication_keys => [:login] + rolify + # attr_accessor :login has_many :questions, dependent: :destroy @@ -183,7 +185,7 @@ class User < ApplicationRecord # @return [Boolean] is the user a moderator? def mod? - self.moderator? || self.admin? + has_role?(:moderator) || has_role?(:administrator) end # region stuff used for reporting/moderation @@ -258,4 +260,10 @@ class User < ApplicationRecord end !self.export_processing end + + # %w[admin moderator].each do |m| + # define_method(m) { raise "not allowed: #{m}" } + # define_method(m+??) { raise "not allowed: #{m}?"} + # define_method(m+?=) { |*a| raise "not allowed: #{m}="} + # end end diff --git a/app/views/layouts/_profile.html.haml b/app/views/layouts/_profile.html.haml index 9983862f..d01c7293 100644 --- a/app/views/layouts/_profile.html.haml +++ b/app/views/layouts/_profile.html.haml @@ -15,7 +15,7 @@ %i.fa.fa-fw.fa-cog = t('views.navigation.settings') %li.divider - - if current_user.admin? + - if current_user.has_role?(:administrator) %li %a{href: rails_admin_path} %i.fa.fa-fw.fa-cogs diff --git a/app/views/user/_actions.html.haml b/app/views/user/_actions.html.haml index ec19de32..7f3ae676 100644 --- a/app/views/user/_actions.html.haml +++ b/app/views/user/_actions.html.haml @@ -33,7 +33,7 @@ %a{href: '#', data: { target: "#modal-privileges", toggle: :modal }} %i.fa.fa-wrench = raw t('views.actions.privilege', user: user.screen_name) - - unless user.admin? + - unless user.has_role?(:administrator) %li %a{href: '#', data: { target: "#modal-ban", toggle: :modal }} %i.fa.fa-ban diff --git a/app/views/user/_modal_privileges.html.haml b/app/views/user/_modal_privileges.html.haml index 351e030c..f54ec705 100644 --- a/app/views/user/_modal_privileges.html.haml +++ b/app/views/user/_modal_privileges.html.haml @@ -11,7 +11,7 @@ = render 'user/modal_privileges_item', privilege: 'blogger', description: t('views.modal.privilege.blogger'), user: @user = render 'user/modal_privileges_item', privilege: 'contributor', description: t('views.modal.privilege.contributor'), user: @user = render 'user/modal_privileges_item', privilege: 'translator', description: t('views.modal.privilege.translator'), user: @user - - if current_user.admin? + - if current_user.has_role?(:administrator) = render 'user/modal_privileges_item', privilege: 'supporter', description: t('views.modal.privilege.supporter'), user: @user = render 'user/modal_privileges_item', privilege: 'moderator', description: t('views.modal.privilege.moderator'),user: @user = render 'user/modal_privileges_item', privilege: 'admin', description: t('views.modal.privilege.admin'), user: @user diff --git a/app/views/user/_modal_privileges_item.html.haml b/app/views/user/_modal_privileges_item.html.haml index 92d498dd..5164a049 100644 --- a/app/views/user/_modal_privileges_item.html.haml +++ b/app/views/user/_modal_privileges_item.html.haml @@ -1,8 +1,11 @@ - description ||= '' +- role_mapping = {"admin" => "administrator"} +- requires_role = %w[admin moderator].include?(privilege) +- checked = requires_role ? user.has_role?(role_mapping.fetch(privilege, privilege).to_sym) : user.public_send("#{privilege}?") %li.list-group-item{id: "privilege-#{privilege}"} .media .pull-left.j2-table - %input.input--center{type: :checkbox, name: 'check-your-privileges', data: { type: privilege, user: user.screen_name }, checked: user.send("#{privilege}?"), autocomplete: 'off'} + %input.input--center{type: :checkbox, name: 'check-your-privileges', data: { type: privilege, user: user.screen_name }, checked: checked, autocomplete: 'off'} .media-body .list-group-item-heading= privilege.capitalize - unless description.blank? diff --git a/app/views/user/_profile_info.html.haml b/app/views/user/_profile_info.html.haml index 57404894..0403a7e0 100644 --- a/app/views/user/_profile_info.html.haml +++ b/app/views/user/_profile_info.html.haml @@ -1,11 +1,11 @@ .panel.panel-default#profile %img.profile--avatar{src: @user.profile_picture.url(:large)} - - if user_signed_in? && current_user.admin? - - if @user.admin? + - if user_signed_in? && current_user.has_role?(:administrator) + - if @user.has_role?(:administrator) .profile--panel-badge.panel-badge-danger %i.fa.fa-flask = t 'views.user.title.admin' - - if @user.moderator? + - if @user.has_role?(:moderator) .profile--panel-badge.panel-badge-success %i.fa.fa-users = t 'views.user.title.moderator' diff --git a/app/views/user/data.html.haml b/app/views/user/data.html.haml index 9cff91a2..8dae340a 100644 --- a/app/views/user/data.html.haml +++ b/app/views/user/data.html.haml @@ -95,7 +95,7 @@ %p.data-heading Admin %p - - if current_user.admin? + - if current_user.has_role?(:administrator) %span.label.label-success %i.fa.fa-fw.fa-check - else diff --git a/config/initializers/rails_admin.rb b/config/initializers/rails_admin.rb index 2c804526..e89e849a 100644 --- a/config/initializers/rails_admin.rb +++ b/config/initializers/rails_admin.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # workaround to get pagination right if defined? WillPaginate Kaminari.configure do |config| @@ -6,12 +8,11 @@ if defined? WillPaginate end RailsAdmin.config do |config| - config.main_app_name = ['justask', 'Kontrollzentrum'] ## == Authentication == config.authenticate_with do - redirect_to main_app.root_path unless current_user.try :admin? + redirect_to main_app.root_path unless current_user&.has_role?(:administrator) end config.current_user_method(&:current_user) diff --git a/config/initializers/rolify.rb b/config/initializers/rolify.rb new file mode 100644 index 00000000..d6d8dffd --- /dev/null +++ b/config/initializers/rolify.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +Rolify.configure do |config| + # By default ORM adapter is ActiveRecord. uncomment to use mongoid + # config.use_mongoid + + # Dynamic shortcuts for User class (user.is_admin? like methods). Default is: false + # config.use_dynamic_shortcuts + + # Configuration to remove roles from database once the last resource is removed. Default is: true + config.remove_role_if_empty = false +end diff --git a/config/routes.rb b/config/routes.rb index 8176f058..e5ac3191 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,19 +2,19 @@ require 'sidekiq/web' Rails.application.routes.draw do start = Time.now - # Admin panel - mount RailsAdmin::Engine => '/justask_admin', as: 'rails_admin' - # Sidekiq constraints ->(req) { req.env["warden"].authenticate?(scope: :user) && - req.env['warden'].user.admin? } do + req.env["warden"].user.has_role?(:administrator) } do + # Admin panel + mount RailsAdmin::Engine => "/justask_admin", as: "rails_admin" + mount Sidekiq::Web, at: "/sidekiq" - mount PgHero::Engine, at: "/pghero", as: 'pghero' + mount PgHero::Engine, at: "/pghero", as: "pghero" end # Moderation panel - constraints ->(req) { req.env['warden'].authenticate?(scope: :user) && - (req.env['warden'].user.mod?) } do + constraints ->(req) { req.env["warden"].authenticate?(scope: :user) && + req.env["warden"].user.mod? } do match '/moderation/priority(/:user_id)', to: 'moderation#priority', via: :get, as: :moderation_priority match '/moderation/ip/:user_id', to: 'moderation#ip', via: :get, as: :moderation_ip match '/moderation(/:type)', to: 'moderation#index', via: :get, as: :moderation, defaults: {type: 'all'} diff --git a/db/migrate/20200419184442_rolify_create_roles.rb b/db/migrate/20200419184442_rolify_create_roles.rb new file mode 100644 index 00000000..23ab21f9 --- /dev/null +++ b/db/migrate/20200419184442_rolify_create_roles.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class RolifyCreateRoles < ActiveRecord::Migration[5.2] + def change + create_table(:roles) do |t| + t.string :name + t.references :resource, polymorphic: true + + t.timestamps + end + + create_table(:users_roles, id: false) do |t| + t.references :user + t.references :role + end + + add_index(:roles, %i[name resource_type resource_id]) + add_index(:users_roles, %i[user_id role_id]) + end +end diff --git a/db/migrate/20200419185535_create_initial_roles.rb b/db/migrate/20200419185535_create_initial_roles.rb new file mode 100644 index 00000000..f3f0788c --- /dev/null +++ b/db/migrate/20200419185535_create_initial_roles.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class CreateInitialRoles < ActiveRecord::Migration[5.2] + def up + %w[Administrator Moderator].each do |role| + Role.where(name: role.parameterize).first_or_create + end + + { + admin: :administrator, + moderator: :moderator + }.each do |legacy_role, new_role| + User.where(legacy_role => true).each do |u| + puts "-- migrating #{u.screen_name} (#{u.id}) from field:#{legacy_role} to role:#{new_role}" + u.add_role new_role + u.public_send("#{legacy_role}=", false) + u.save! + end + end + end + + def down + { + administrator: :admin, + moderator: :moderator + }.each do |new_role, legacy_role| + User.with_role(new_role).each do |u| + puts "-- migrating #{u.screen_name} (#{u.id}) from role:#{new_role} to field:#{legacy_role}" + u.public_send("#{legacy_role}=", true) + u.save! + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 439625e6..02d34dfe 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2016_01_05_165913) do +ActiveRecord::Schema.define(version: 2020_04_19_185535) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -137,6 +137,16 @@ ActiveRecord::Schema.define(version: 2016_01_05_165913) do t.string "reason" end + create_table "roles", force: :cascade do |t| + t.string "name" + t.string "resource_type" + t.bigint "resource_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" + t.index ["resource_type", "resource_id"], name: "index_roles_on_resource_type_and_resource_id" + end + create_table "services", id: :serial, force: :cascade do |t| t.string "type", null: false t.integer "user_id", null: false @@ -270,4 +280,12 @@ ActiveRecord::Schema.define(version: 2016_01_05_165913) do t.index ["screen_name"], name: "index_users_on_screen_name", unique: true end + create_table "users_roles", id: false, force: :cascade do |t| + t.bigint "user_id" + t.bigint "role_id" + t.index ["role_id"], name: "index_users_roles_on_role_id" + t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id" + t.index ["user_id"], name: "index_users_roles_on_user_id" + end + end diff --git a/db/seeds.rb b/db/seeds.rb index 4edb1e85..9874789c 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -5,3 +5,7 @@ # # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) # Mayor.create(name: 'Emanuel', city: cities.first) + +%w[Administrator Moderator].each do |role| + Role.where(name: role.parameterize).first_or_create +end diff --git a/lib/exporter.rb b/lib/exporter.rb index 08f3e3c9..1086708f 100644 --- a/lib/exporter.rb +++ b/lib/exporter.rb @@ -1,9 +1,13 @@ +# frozen_string_literal: true + require 'json' require 'yaml' require 'httparty' require 'securerandom' class Exporter + EXPORT_ROLES = [:administrator, :moderator].freeze + def initialize(user) @user = user @obj = {} @@ -30,10 +34,10 @@ class Exporter private def collect_user_info - %i(admin answered_count asked_count ban_reason banned_until bio blogger comment_smiled_count commented_count + %i(answered_count asked_count ban_reason banned_until bio blogger comment_smiled_count commented_count confirmation_sent_at confirmed_at contributor created_at crop_h crop_h_h crop_h_w crop_h_x crop_h_y crop_w crop_x crop_y current_sign_in_at current_sign_in_ip display_name email follower_count friend_count - id last_sign_in_at last_sign_in_ip locale location moderator motivation_header permanently_banned + id last_sign_in_at last_sign_in_ip locale location motivation_header permanently_banned privacy_allow_anonymous_questions privacy_allow_public_timeline privacy_allow_stranger_answers privacy_show_in_search profile_header_content_type profile_header_file_name profile_header_file_size profile_header_updated_at profile_picture_content_type profile_picture_file_name profile_picture_file_size @@ -41,6 +45,10 @@ class Exporter updated_at website).each do |f| @obj[f] = @user.send f end + + EXPORT_ROLES.each do |role| + @obj[role] = @user.has_role?(role) + end end def collect_questions @@ -221,11 +229,16 @@ class Exporter def user_stub(user) uobj = {} - %i(admin answered_count asked_count bio blogger comment_smiled_count commented_count contributor created_at - display_name follower_count friend_count id location moderator motivation_header permanently_banned screen_name + %i(answered_count asked_count bio blogger comment_smiled_count commented_count contributor created_at + display_name follower_count friend_count id location motivation_header permanently_banned screen_name smiled_count supporter translator website).each do |f| uobj[f] = user.send f end + + EXPORT_ROLES.each do |role| + uobj[role] = user.has_role?(role) + end + uobj end end diff --git a/spec/integration/role_constrained_routes_spec.rb b/spec/integration/role_constrained_routes_spec.rb new file mode 100644 index 00000000..4b093876 --- /dev/null +++ b/spec/integration/role_constrained_routes_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'role-constrained routes', type: :request do + shared_examples_for 'fails to access route' do + it 'fails to access route' do + # 404 = no user found -- we have a fallback route if something could not be matched + expect(subject).to eq 404 + end + end + + shared_examples_for 'routes for' do |roles, subject_block, skip_reason: nil| + before { skip(skip_reason) } if skip_reason + + subject(&subject_block) + + context 'not signed in' do + include_examples 'fails to access route' + end + + roles.each do |role| + context "signed in user without #{role} role" do + let(:user) { FactoryBot.create(:user, password: 'test1234') } + + before(:each) do + post '/sign_in', params: { user: { login: user.email, password: user.password } } + end + + include_examples 'fails to access route' + end + + context "signed in user with #{role} role" do + let(:user) { FactoryBot.create(:user, password: 'test1234', roles: [role]) } + + before(:each) do + post '/sign_in', params: { user: { login: user.email, password: user.password } } + end + + it 'can access route' do + expect(subject).to be_in 200..299 + end + end + end + end + + it_behaves_like('routes for', [:administrator], -> { get('/justask_admin') }) + it_behaves_like('routes for', [:administrator], -> { get('/sidekiq') }) + it_behaves_like('routes for', [:administrator], -> { get('/pghero') }, skip_reason: 'PG::InFailedSqlTransaction due to 5.1 upgrade, works fine outside specs though') + it_behaves_like('routes for', %i[administrator moderator], -> { get('/moderation') }) +end