diff --git a/.github/workflows/retrospring.yml b/.github/workflows/retrospring.yml index bf6914fb..b1ee1108 100644 --- a/.github/workflows/retrospring.yml +++ b/.github/workflows/retrospring.yml @@ -67,3 +67,7 @@ jobs: env: POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} REDIS_URL: "redis://localhost:${{ job.services.redis.ports[6379] }}" + - uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage/coverage.xml diff --git a/Gemfile b/Gemfile index 5605025c..2a61b709 100644 --- a/Gemfile +++ b/Gemfile @@ -90,14 +90,14 @@ group :development, :test do gem 'puma' gem 'rspec-rails', '~> 3.9' gem 'rspec-its', '~> 1.3' - gem "rspec-sidekiq", "~> 3.0" + gem "rspec-sidekiq", "~> 3.0", require: false gem 'factory_bot_rails', require: false gem 'faker' gem 'capybara' gem 'poltergeist' gem 'simplecov', require: false gem 'simplecov-json', require: false - gem 'simplecov-rcov', require: false + gem 'simplecov-cobertura', require: false gem 'database_cleaner' gem 'better_errors' gem 'letter_opener' # Use this just in local test environments diff --git a/Gemfile.lock b/Gemfile.lock index 77859523..781aa040 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -444,12 +444,12 @@ GEM simplecov (0.18.5) docile (~> 1.1) simplecov-html (~> 0.11) + simplecov-cobertura (1.3.1) + simplecov (~> 0.8) simplecov-html (0.12.2) simplecov-json (0.2.1) json simplecov - simplecov-rcov (0.2.3) - simplecov (>= 0.4.1) spring (2.1.0) sprockets (3.7.2) concurrent-ruby (~> 1.0) @@ -572,8 +572,8 @@ DEPENDENCIES sass-rails (~> 5.0) sidekiq (< 6) simplecov + simplecov-cobertura simplecov-json - simplecov-rcov spring (~> 2.0) sweetalert-rails timecop diff --git a/app/controllers/ajax/group_controller.rb b/app/controllers/ajax/group_controller.rb index cb29f6f2..352147ef 100644 --- a/app/controllers/ajax/group_controller.rb +++ b/app/controllers/ajax/group_controller.rb @@ -19,7 +19,7 @@ class Ajax::GroupController < AjaxController params.require :user begin - target_user = User.find_by_screen_name(params[:user]) + target_user = User.find_by_screen_name!(params[:user]) group = Group.create! user: current_user, display_name: params[:name] rescue ActiveRecord::RecordInvalid => e NewRelic::Agent.notice_error(e) @@ -85,7 +85,7 @@ class Ajax::GroupController < AjaxController add = params[:add] == 'true' begin - group = current_user.groups.find_by_name(params[:group]) + group = current_user.groups.find_by_name!(params[:group]) rescue ActiveRecord::RecordNotFound => e NewRelic::Agent.notice_error(e) @response[:status] = :notfound @@ -93,7 +93,7 @@ class Ajax::GroupController < AjaxController return end - target_user = User.find_by_screen_name(params[:user]) + target_user = User.find_by_screen_name!(params[:user]) if add group.add_member target_user if group.members.find_by_user_id(target_user.id).nil? diff --git a/app/controllers/ajax/inbox_controller.rb b/app/controllers/ajax/inbox_controller.rb index 0aa99cdc..f0aa994d 100644 --- a/app/controllers/ajax/inbox_controller.rb +++ b/app/controllers/ajax/inbox_controller.rb @@ -46,6 +46,8 @@ class Ajax::InboxController < AjaxController end def remove_all + raise unless user_signed_in? + begin Inbox.where(user: current_user).each { |i| i.remove } rescue => e diff --git a/app/controllers/ajax/moderation_controller.rb b/app/controllers/ajax/moderation_controller.rb index 850c3564..b2f25ad8 100644 --- a/app/controllers/ajax/moderation_controller.rb +++ b/app/controllers/ajax/moderation_controller.rb @@ -111,7 +111,7 @@ class Ajax::ModerationController < AjaxController params.require :permaban reason = params[:reason] - target = User.find_by_screen_name(params[:user]) + target = User.find_by_screen_name!(params[:user]) unban = params[:ban] == "0" perma = params[:permaban] == "1" @@ -149,7 +149,7 @@ class Ajax::ModerationController < AjaxController status = params[:status] == 'true' - target_user = User.find_by_screen_name(params[:user]) + target_user = User.find_by_screen_name!(params[:user]) @response[:message] = I18n.t('messages.moderation.privilege.nope') return unless %w(moderator admin).include? params[:type].downcase diff --git a/app/controllers/ajax/question_controller.rb b/app/controllers/ajax/question_controller.rb index acdc3109..81ee5ef7 100644 --- a/app/controllers/ajax/question_controller.rb +++ b/app/controllers/ajax/question_controller.rb @@ -27,9 +27,11 @@ class Ajax::QuestionController < AjaxController params.require :anonymousQuestion params.require :rcpt + is_never_anonymous = user_signed_in? && (params[:rcpt].start_with?('grp:') || params[:rcpt] == 'followers') + begin question = Question.create!(content: params[:question], - author_is_anonymous: params[:anonymousQuestion], + author_is_anonymous: is_never_anonymous ? false : params[:anonymousQuestion], user: current_user) rescue ActiveRecord::RecordInvalid => e NewRelic::Agent.notice_error(e) @@ -38,6 +40,11 @@ class Ajax::QuestionController < AjaxController return end + if !user_signed_in? && !question.author_is_anonymous + question.delete + return + end + unless current_user.nil? current_user.increment! :asked_count unless params[:anonymousQuestion] == 'true' end @@ -53,19 +60,27 @@ class Ajax::QuestionController < AjaxController QuestionWorker.perform_async params[:rcpt], current_user.id, question.id rescue ActiveRecord::RecordNotFound => e NewRelic::Agent.notice_error(e) + question.delete @response[:status] = :not_found @response[:message] = I18n.t('messages.question.create.not_found') return end end else - if User.find(params[:rcpt]).nil? + u = User.find_by_id(params[:rcpt]) + if u.nil? @response[:status] = :not_found @response[:message] = I18n.t('messages.question.create.not_found') + question.delete return end - Inbox.create!(user_id: params[:rcpt], question_id: question.id, new: true) + if !u.privacy_allow_anonymous_questions && question.author_is_anonymous + question.delete + return + end + + Inbox.create!(user_id: u.id, question_id: question.id, new: true) end @response[:status] = :okay diff --git a/app/controllers/ajax/report_controller.rb b/app/controllers/ajax/report_controller.rb index c49ec9b4..6e6ef762 100644 --- a/app/controllers/ajax/report_controller.rb +++ b/app/controllers/ajax/report_controller.rb @@ -19,7 +19,7 @@ class Ajax::ReportController < AjaxController object = case obj when 'User' - User.find_by_screen_name params[:id] + User.find_by_screen_name! params[:id] when 'Question' Question.find params[:id] when 'Answer' diff --git a/app/controllers/ajax_controller.rb b/app/controllers/ajax_controller.rb index 8331130d..03947c1b 100644 --- a/app/controllers/ajax_controller.rb +++ b/app/controllers/ajax_controller.rb @@ -6,6 +6,18 @@ class AjaxController < ApplicationController respond_to :json + rescue_from(StandardError) do |e| + NewRelic::Agent.notice_error(e) + + @response = { + success: false, + message: "Something went wrong", + status: :err + } + + return_response + end + rescue_from(ActiveRecord::RecordNotFound) do |e| NewRelic::Agent.notice_error(e) diff --git a/app/models/question.rb b/app/models/question.rb index 65daf030..78ea192d 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -1,7 +1,7 @@ class Question < ApplicationRecord include Question::AnswerMethods - belongs_to :user + belongs_to :user, optional: true has_many :answers, dependent: :destroy has_many :inboxes, dependent: :destroy diff --git a/spec/controllers/ajax/answer_controller_spec.rb b/spec/controllers/ajax/answer_controller_spec.rb index 55aa7ad6..b234a41b 100644 --- a/spec/controllers/ajax/answer_controller_spec.rb +++ b/spec/controllers/ajax/answer_controller_spec.rb @@ -3,14 +3,7 @@ require "rails_helper" -describe Ajax::AnswerController, type: :controller do - shared_examples "returns the expected response" do - it "returns the expected response" do - expect(JSON.parse(subject.body)).to match(expected_response) - end - end - - let(:user) { FactoryBot.create(:user) } +describe Ajax::AnswerController, :ajax_controller, type: :controller do let(:question) { FactoryBot.create(:question, user: FactoryBot.build(:user, privacy_allow_stranger_answers: asker_allows_strangers)) } let(:asker_allows_strangers) { true } @@ -186,7 +179,7 @@ describe Ajax::AnswerController, type: :controller do describe "#destroy" do let(:answer_user) { user } - let(:question) { FactoryBot.create(:question, user: FactoryBot.create(:user)) } + let(:question) { FactoryBot.create(:question) } let(:answer) { FactoryBot.create(:answer, user: answer_user, question: question) } let(:answer_id) { answer.id } diff --git a/spec/controllers/ajax/comment_controller_spec.rb b/spec/controllers/ajax/comment_controller_spec.rb new file mode 100644 index 00000000..d1e1b0be --- /dev/null +++ b/spec/controllers/ajax/comment_controller_spec.rb @@ -0,0 +1,226 @@ +# coding: utf-8 +# frozen_string_literal: true + +require "rails_helper" + +describe Ajax::CommentController, :ajax_controller, type: :controller do + let(:answer) { FactoryBot.create(:answer, user: FactoryBot.create(:user)) } + + describe "#create" do + let(:params) do + { + answer: answer_id, + comment: comment + }.compact + end + + subject { post(:create, params: params) } + + context "when user is signed in" do + shared_examples "creates the comment" do + it "creates a comment to the answer" do + expect { subject }.to(change { Comment.count }.by(1)) + expect(answer.reload.comments.ids).to include(Comment.last.id) + end + + include_examples "returns the expected response" + end + + shared_examples "does not create the comment" do + it "does not create a comment" do + expect { subject }.not_to(change { Comment.count }) + end + + include_examples "returns the expected response" + end + + before(:each) { sign_in(user) } + + context "when all parameters are given" do + let(:comment) { "// Here be dragons." } + + context "when answer exists" do + let(:answer_id) { answer.id } + + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything, + "render" => anything, + "count" => 1 + } + end + + include_examples "creates the comment" + + context "when comment is too long" do + let(:comment) { "E" * 621 } + let(:expected_response) do + { + "success" => false, + "status" => "rec_inv", + "message" => anything + } + end + + include_examples "does not create the comment" + end + end + + context "when answer does not exist" do + let(:answer_id) { "nein!" } + + let(:expected_response) do + { + "success" => false, + "status" => anything, + "message" => anything + } + end + + include_examples "does not create the comment" + end + end + + context "when some parameters are missing" do + let(:answer_id) { nil } + let(:comment) { "" } + + let(:expected_response) do + { + "success" => false, + "status" => "parameter_error", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:answer_id) { answer.id } + let(:comment) { "HACKED" } + + let(:expected_response) do + { + "success" => false, + "status" => "err", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + describe "#destroy" do + let(:answer_user) { FactoryBot.create(:user) } + let(:answer) { FactoryBot.create(:answer, user: answer_user) } + let(:comment_user) { user } + let(:comment) { FactoryBot.create(:comment, user: comment_user, answer: answer) } + let(:comment_id) { comment.id } + + let(:params) do + { + comment: comment_id + } + end + + subject { delete(:destroy, params: params) } + + context "when user is signed in" do + shared_examples "deletes the comment" do + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything, + "count" => 0 + } + end + + it "deletes the comment" do + comment # ensure we already have it in the db + expect { subject }.to(change { Comment.count }.by(-1)) + end + + include_examples "returns the expected response" + end + + shared_examples "does not delete the comment" do + let(:expected_response) do + { + "success" => false, + "status" => "nopriv", + "message" => anything + } + end + + it "does not delete the comment" do + comment # ensure we already have it in the db + expect { subject }.not_to(change { Comment.count }) + end + + include_examples "returns the expected response" + end + + before(:each) { sign_in(user) } + + context "when the comment exists and was made by the current user" do + include_examples "deletes the comment" + end + + context "when the comment exists and was not made by the current user" do + let(:comment_user) { FactoryBot.create(:user) } + + include_examples "does not delete the comment" + + context "when the current user created the answer" do + let(:answer_user) { user } + + include_examples "deletes the comment" + end + + %i[moderator administrator].each do |privileged_role| + context "when the current user is a #{privileged_role}" do + around do |example| + user.add_role privileged_role + example.run + user.remove_role privileged_role + end + + include_examples "deletes the comment" + end + end + end + + context "when the comment does not exist" do + let(:comment_id) { "sonic_the_hedgehog" } + + let(:expected_response) do + { + "success" => false, + "status" => anything, + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:expected_response) do + { + "success" => false, + "status" => "nopriv", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end +end diff --git a/spec/controllers/ajax/friend_controller_spec.rb b/spec/controllers/ajax/friend_controller_spec.rb new file mode 100644 index 00000000..89b846cf --- /dev/null +++ b/spec/controllers/ajax/friend_controller_spec.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Ajax::FriendController, :ajax_controller, type: :controller do + describe "#create" do + let(:params) do + { + screen_name: screen_name + } + end + + subject { post(:create, params: params) } + + context "when user is signed in" do + before(:each) { sign_in(user) } + + context "when target user exists" do + let(:target_user) { FactoryBot.create(:user) } + let(:screen_name) { target_user.screen_name } + + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + + it "creates a follow relationship" do + expect(user.friends.ids).not_to include(target_user.id) + expect { subject }.to(change { user.friends.count }.by(1)) + expect(user.friends.ids).to include(target_user.id) + end + + include_examples "returns the expected response" + end + + context "when target user does not exist" do + let(:screen_name) { "tripmeister_eder" } + + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + + it "does not create a follow relationship" do + expect { subject }.not_to(change { user.friends.count }) + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:screen_name) { "tutenchamun" } + let(:expected_response) do + { + "success" => false, + "status" => "fail", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + describe "#destroy" do + let(:params) do + { + screen_name: screen_name + } + end + + subject { delete(:destroy, params: params) } + + context "when user is signed in" do + before(:each) { sign_in(user) } + + context "when target user exists" do + let(:target_user) { FactoryBot.create(:user) } + let(:screen_name) { target_user.screen_name } + + before(:each) { target_user } + + context "when user follows target" do + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + + before(:each) { user.follow target_user } + + it "destroys a follow relationship" do + expect(user.friends.ids).to include(target_user.id) + expect { subject }.to(change { user.friends.count }.by(-1)) + expect(user.friends.ids).not_to include(target_user.id) + end + + include_examples "returns the expected response" + end + + context "when user does not already follow target" do + let(:expected_response) do + { + "success" => false, + "status" => "fail", + "message" => anything + } + end + + it "does not destroy a follow relationship" do + expect { subject }.not_to(change { user.friends.count }) + end + + include_examples "returns the expected response" + end + end + + context "when target user does not exist" do + let(:screen_name) { "tripmeister_eder" } + + let(:expected_response) do + { + "success" => false, + "status" => "fail", + "message" => anything + } + end + + it "does not destroy a follow relationship" do + expect { subject }.not_to(change { user.friends.count }) + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:screen_name) { "tutenchamun" } + let(:expected_response) do + { + "success" => false, + "status" => "fail", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end +end diff --git a/spec/controllers/ajax/group_controller_spec.rb b/spec/controllers/ajax/group_controller_spec.rb new file mode 100644 index 00000000..dd9e171f --- /dev/null +++ b/spec/controllers/ajax/group_controller_spec.rb @@ -0,0 +1,341 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Ajax::GroupController, :ajax_controller, type: :controller do + let(:target_user) { FactoryBot.create(:user) } + + describe "#create" do + let(:name) { "I signori della gallassia" } + let(:target_user_param) { target_user.screen_name } + let(:params) do + { + "name" => name, + "user" => target_user_param + } + end + + subject { post(:create, params: params) } + + context "when user is signed in" do + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything, + "render" => anything + } + end + + before(:each) { sign_in(user) } + + it "creates the group" do + expect { subject }.to(change { user.groups.count }.by(1)) + end + + include_examples "returns the expected response" + + context "when name param is missing" do + let(:name) { "" } + let(:expected_response) do + { + "success" => false, + "status" => "toolong", + "message" => anything + } + end + + it "does not create the group" do + expect { subject }.not_to(change { user.groups.count }) + end + + include_examples "returns the expected response" + end + + context "when target user does not exist" do + let(:target_user_param) { "giuseppe-drogo" } + let(:expected_response) do + { + "success" => false, + "status" => "notfound", + "message" => anything + } + end + + it "does not create the group" do + expect { subject }.not_to(change { user.groups.count }) + end + + include_examples "returns the expected response" + end + + context "when group name is invalid for reasons" do + let(:name) { "\u{1f43e}" } + let(:expected_response) do + { + "success" => false, + "status" => "toolong", + "message" => anything + } + end + + it "does not create the group" do + expect { subject }.not_to(change { user.groups.count }) + end + + include_examples "returns the expected response" + end + + context "when group already exists" do + before(:each) { post(:create, params: params) } + let(:expected_response) do + { + "success" => false, + "status" => "exists", + "message" => anything + } + end + + it "does not create the group" do + expect { subject }.not_to(change { user.groups.count }) + end + + include_examples "returns the expected response" + end + + context "when someone else created a group with the same name" do + before(:each) do + FactoryBot.create(:group, user: target_user, display_name: name) + end + + it "creates the group" do + expect { subject }.to(change { user.groups.count }.by(1)) + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:expected_response) do + { + "success" => false, + "status" => "noauth", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + describe "#destroy" do + let(:name) { "I signori della gallassia" } + let(:group) { FactoryBot.create(:group, user: user, display_name: name) } + let(:group_param) { group.name } + let(:params) do + { + "group" => group_param + } + end + + subject { delete(:destroy, params: params) } + + context "when user is signed in" do + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + + before(:each) { sign_in(user) } + + it "deletes the group" do + group + expect { subject }.to(change { user.groups.count }.by(-1)) + end + + include_examples "returns the expected response" + + context "when group param is missing" do + let(:group_param) { "" } + let(:expected_response) do + { + "success" => false, + "status" => "parameter_error", + "message" => anything + } + end + + it "does not delete the group" do + expect { subject }.not_to(change { user.groups.count }) + end + + include_examples "returns the expected response" + end + + context "when group does not exist" do + let(:group_param) { "the-foobars-and-the-dingdongs" } + let(:expected_response) do + { + "success" => false, + "status" => "err", + "message" => anything + } + end + + it "does not delete the group" do + expect { subject }.not_to(change { user.groups.count }) + end + + include_examples "returns the expected response" + end + + context "when someone else created a group with the same name" do + before(:each) do + group + FactoryBot.create(:group, user: target_user, display_name: name) + end + + it "deletes the group" do + expect { subject }.to(change { user.groups.count }.by(-1)) + end + + it "does not delete the other users' group" do + expect { subject }.not_to(change { target_user.groups.count }) + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:expected_response) do + { + "success" => false, + "status" => "noauth", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + describe "#membership" do + let(:name) { "The Agency" } + let(:members) { [] } + let(:group) { FactoryBot.create(:group, user: user, display_name: name, members: members) } + let(:group_param) { group.name } + let(:target_user_param) { target_user.screen_name } + let(:params) do + { + "group" => group_param, + "user" => target_user_param, + "add" => add_param + } + end + + subject { post(:membership, params: params) } + + context "when user is signed in" do + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything, + "checked" => expected_checked + } + end + + before(:each) { sign_in(user) } + + context "when add is false" do + let(:add_param) { "false" } + let(:expected_checked) { false } + + it "does not do anything" do + expect { subject }.not_to(change { group.members }) + expect(group.members.map { |gm| gm.user.id }.sort ).to eq([]) + end + + include_examples "returns the expected response" + + context "when the user was already added to the group" do + let(:members) { [target_user] } + + it "removes the user from the group" do + expect { subject }.to(change { group.reload.members.map { |gm| gm.user.id }.sort }.from([target_user.id]).to([])) + end + + include_examples "returns the expected response" + end + end + + context "when add is true" do + let(:add_param) { "true" } + let(:expected_checked) { true } + + it "adds the user to the group" do + expect { subject }.to(change { group.reload.members.map { |gm| gm.user.id }.sort }.from([]).to([target_user.id])) + end + + include_examples "returns the expected response" + + context "when the user was already added to the group" do + let(:members) { [target_user] } + + it "does not add the user to the group again" do + expect { subject }.not_to(change { group.members }) + expect(group.members.map { |gm| gm.user.id }.sort ).to eq([target_user.id]) + end + + include_examples "returns the expected response" + end + end + + context "when group does not exist" do + let(:group_param) { "the-good-agency" } + let(:add_param) { "add" } + let(:expected_response) do + { + "success" => false, + "status" => "notfound", + "message" => anything + } + end + + include_examples "returns the expected response" + end + + context "when target user does not exist" do + let(:target_user_param) { "erwin-proell" } + let(:add_param) { "add" } + let(:expected_response) do + { + "success" => false, + "status" => "not_found", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:add_param) { "whatever" } + let(:expected_response) do + { + "success" => false, + "status" => "noauth", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end +end diff --git a/spec/controllers/ajax/inbox_controller_spec.rb b/spec/controllers/ajax/inbox_controller_spec.rb new file mode 100644 index 00000000..3d99ffe7 --- /dev/null +++ b/spec/controllers/ajax/inbox_controller_spec.rb @@ -0,0 +1,244 @@ +# coding: utf-8 +# frozen_string_literal: true + +require "rails_helper" + +describe Ajax::InboxController, :ajax_controller, type: :controller do + describe "#create" do + subject { post(:create) } + + context "when user is signed in" do + before(:each) { sign_in(user) } + + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything, + "render" => anything + } + end + + it "creates a generated question to the user's inbox" do + allow(QuestionGenerator).to receive(:generate).and_return("Is Mayonnaise an instrument?") + expect { subject }.to(change { user.inboxes.count }.by(1)) + expect(user.inboxes.last.question.author_is_anonymous).to eq(true) + expect(user.inboxes.last.question.author_name).to eq("justask") + expect(user.inboxes.last.question.user).to eq(user) + expect(user.inboxes.last.question.content).to eq("Is Mayonnaise an instrument?") + end + + include_examples "returns the expected response" + end + + context "when user is not signed in" do + let(:expected_response) do + { + "success" => false, + "status" => "noauth", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + describe "#remove" do + let(:params) do + { + id: inbox_entry_id + } + end + + subject { delete(:remove, params: params) } + + context "when user is signed in" do + before(:each) { sign_in(user) } + + context "when inbox entry exists" do + let(:inbox_entry) { FactoryBot.create(:inbox, user: inbox_user) } + let(:inbox_entry_id) { inbox_entry.id } + + # ensure the inbox entry exists + before(:each) { inbox_entry } + + context "when inbox entry belongs to the current user" do + let(:inbox_user) { user } + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + + it "removes the inbox entry" do + expect { subject }.to(change { user.inboxes.count }.by(-1)) + expect { Inbox.find(inbox_entry.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + include_examples "returns the expected response" + end + + context "when inbox entry does not belong to the current user" do + let(:inbox_user) { FactoryBot.create(:user) } + let(:expected_response) do + { + "success" => false, + "status" => "fail", + "message" => anything + } + end + + it "does not remove the inbox entry" do + expect { subject }.not_to(change { Inbox.count }) + expect { Inbox.find(inbox_entry.id) }.not_to raise_error + end + + include_examples "returns the expected response" + end + end + + context "when inbox entry does not exist" do + let(:inbox_entry_id) { "Nein!" } + let(:expected_response) do + { + "success" => false, + "status" => "not_found", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:inbox_entry_id) { "HACKED" } + let(:expected_response) do + { + "success" => false, + "status" => "not_found", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + describe "#remove_all" do + subject { delete(:remove_all) } + + context "when user is signed in" do + before(:each) { sign_in(user) } + + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + + include_examples "returns the expected response" + + context "when user has some inbox entries" do + let(:some_other_user) { FactoryBot.create(:user) } + before do + 10.times { FactoryBot.create(:inbox, user: user) } + 10.times { FactoryBot.create(:inbox, user: some_other_user) } + end + + it "deletes all the entries from the user's inbox" do + expect { subject }.to(change { [Inbox.count, user.inboxes.count] }.from([20, 10]).to([10, 0])) + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:expected_response) do + { + "success" => false, + "status" => "err", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + describe "#remove_all_author" do + let(:params) do + { + author: author + } + end + + subject { delete(:remove_all_author, params: params) } + + context "when user is signed in" do + before(:each) { sign_in(user) } + let(:author) { user.screen_name } + + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + + include_examples "returns the expected response" + + context "when user has some inbox entries" do + let(:some_other_user) { FactoryBot.create(:user) } + let(:author) { some_other_user.screen_name } + before do + normal_question = FactoryBot.create(:question, user: some_other_user, author_is_anonymous: false) + anon_question = FactoryBot.create(:question, user: some_other_user, author_is_anonymous: true) + + 10.times { FactoryBot.create(:inbox, user: user) } + 3.times { FactoryBot.create(:inbox, user: user, question: normal_question) } + 2.times { FactoryBot.create(:inbox, user: user, question: anon_question) } + end + + it "deletes all the entries asked by some other user which are not anonymous from the user's inbox" do + expect { subject }.to(change { user.inboxes.count }.from(15).to(12)) + end + + include_examples "returns the expected response" + end + + context "when author is unknown" do + let(:author) { "schmarrn" } + let(:expected_response) do + { + "success" => false, + "status" => "err", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:author) { "hackerman1337" } + let(:expected_response) do + { + "success" => false, + "status" => "err", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end +end diff --git a/spec/controllers/ajax/moderation_controller_spec.rb b/spec/controllers/ajax/moderation_controller_spec.rb new file mode 100644 index 00000000..013bdd25 --- /dev/null +++ b/spec/controllers/ajax/moderation_controller_spec.rb @@ -0,0 +1,650 @@ +# coding: utf-8 +# frozen_string_literal: true + +require "rails_helper" + +describe Ajax::ModerationController, :ajax_controller, type: :controller do + shared_examples "fails when report does not exist" do + let(:report_id) { "Burgenland" } + let(:expected_response) do + { + "success" => false, + "status" => "not_found", + "message" => anything + } + end + + include_examples "returns the expected response" + end + + let(:target_user) { FactoryBot.create(:user) } + let(:report) do + Reports::User.create!( + user: user, + target_id: target_user.id + ) + end + let(:user_role) { :moderator } + + before do + user.add_role user_role if user_role + sign_in(user) + end + + describe "#vote" do + let(:params) do + { + id: report_id, + upvote: upvote + } + end + + subject { post(:vote, params: params) } + + context "when report exists" do + let(:report_id) { report.id } + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything, + "count" => expected_count + } + end + + context "when upvote is true" do + let(:upvote) { "true" } + let(:expected_count) { 1 } + + it "creates a moderation vote" do + expect { subject }.to(change { ModerationVote.count }.by(1)) + expect(report.moderation_votes.last.user).to eq(user) + expect(report.moderation_votes.last.upvote).to eq(true) + end + + include_examples "returns the expected response" + + context "when moderation vote already exists" do + let(:expected_response) do + { + "success" => false, + "status" => "fail", + "message" => anything + } + end + + before { post(:vote, params: params) } + + it "does not create a new moderation vote" do + expect { subject }.to_not(change { ModerationVote.count }) + end + + include_examples "returns the expected response" + end + end + + context "when upvote is false" do + let(:upvote) { "false" } + let(:expected_count) { 0 } + + it "creates a moderation vote" do + expect { subject }.to(change { ModerationVote.count }.by(1)) + expect(report.moderation_votes.last.user).to eq(user) + expect(report.moderation_votes.last.upvote).to eq(false) + end + + context "when moderation vote already exists" do + let(:expected_response) do + { + "success" => false, + "status" => "fail", + "message" => anything + } + end + + before { post(:vote, params: params) } + + it "does not create a new moderation vote" do + expect { subject }.to_not(change { ModerationVote.count }) + end + + include_examples "returns the expected response" + end + end + end + + it_behaves_like "fails when report does not exist" do + let(:upvote) { "true" } + + it "does not create a moderation vote" do + expect { subject }.to_not(change { ModerationVote.count }) + end + end + end + + describe "#destroy_vote" do + let(:params) do + { + id: report_id + } + end + + subject { post(:destroy_vote, params: params) } + + context "when report exists" do + let(:report_id) { report.id } + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything, + "count" => expected_count + } + end + + context "when the user already voted" do + let(:expected_count) { 0 } + + before { post(:vote, params: params.merge("upvote" => true)) } + + it "removes a moderation vote" do + expect { subject }.to(change { ModerationVote.count }.by(-1)) + end + + include_examples "returns the expected response" + end + + context "when the user has not voted yet" do + let(:expected_count) { 0 } + let(:expected_response) do + { + "success" => false, + "status" => "fail", + "message" => anything + } + end + + it "does not create a new moderation vote" do + expect { subject }.to_not(change { ModerationVote.count }) + end + + include_examples "returns the expected response" + end + end + + it_behaves_like "fails when report does not exist" do + it "does not create a moderation vote" do + expect { subject }.to_not(change { ModerationVote.count }) + end + end + end + + describe "#destroy_report" do + let(:params) do + { + id: report_id + } + end + + subject { post(:destroy_report, params: params) } + + context "when report exists" do + let(:report_id) { report.id } + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + + before { report } + + it "does not actually destroy the report" do + expect { subject }.to_not(change { Report.count }) + end + + it "only marks the report as deleted" do + expect { subject }.to(change { report.reload.deleted }.from(false).to(true)) + end + + include_examples "returns the expected response" + end + + it_behaves_like "fails when report does not exist" + end + + describe "#create_comment" do + let(:params) do + { + id: report_id, + comment: comment + } + end + let(:comment) { "ZEFIX NUAMOI!" } + + subject { post(:create_comment, params: params) } + + context "when report exists" do + let(:report_id) { report.id } + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything, + "render" => anything, + "count" => 1 + } + end + + it "creates a moderation comment" do + expect { subject }.to(change { ModerationComment.count }.by(1)) + expect(report.moderation_comments.last.user).to eq(user) + expect(report.moderation_comments.last.content).to eq(comment) + end + + include_examples "returns the expected response" + + context "when comment is blank" do + let(:comment) { "" } + let(:expected_response) do + { + "success" => false, + "status" => "parameter_error", + "message" => anything + } + end + + it "does not create a moderation comment" do + expect { subject }.to_not(change { ModerationComment.count }) + end + + include_examples "returns the expected response" + end + + context "when comment is the letter E 621 times" do + let(:comment) { "E" * 621 } + let(:expected_response) do + { + "success" => false, + "status" => "rec_inv", + "message" => anything + } + end + + it "does not create a moderation comment" do + expect { subject }.to_not(change { ModerationComment.count }) + end + + include_examples "returns the expected response" + end + end + + it_behaves_like "fails when report does not exist" do + it "does not create a moderation comment" do + expect { subject }.to_not(change { ModerationComment.count }) + end + end + end + + describe "#destroy_comment" do + let(:comment) { ModerationComment.create!(user: comment_user, report: report, content: "sigh") } + let(:params) do + { + comment: comment_id + } + end + + subject { post(:destroy_comment, params: params) } + + context "when comment exists" do + let(:comment_id) { comment.id } + + before { comment } + + context "when comment was made by the current user" do + let(:comment_user) { user } + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + + it "destroys the comment" do + expect { subject }.to(change { ModerationComment.count }.by(-1)) + expect { comment.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + include_examples "returns the expected response" + end + + context "when comment was made by someone else" do + let(:comment_user) { FactoryBot.create(:user) } + let(:expected_response) do + { + "success" => false, + "status" => "nopriv", + "message" => anything + } + end + + it "does not destroy the comment" do + expect { subject }.not_to(change { ModerationComment.count }) + expect { comment.reload }.not_to raise_error + end + + include_examples "returns the expected response" + + context "when current user is an administrator" do + let(:user_role) { :administrator } + + it "does not destroy the comment" do + expect { subject }.not_to(change { ModerationComment.count }) + expect { comment.reload }.not_to raise_error + end + + include_examples "returns the expected response" + end + end + end + + context "when comment does not exist" do + let(:comment_id) { "Rügenwalder" } + let(:expected_response) do + { + "success" => false, + "status" => "not_found", + "message" => anything + } + end + + it "does not destroy any comment" do + expect { subject }.not_to(change { ModerationComment.count }) + end + + include_examples "returns the expected response" + end + end + + describe "#ban" do + let(:params) do + { + user: user_param, + ban: ban, + permaban: permaban, + reason: "just a prank, bro", + until: wrongly_formatted_date_ugh + } + end + + subject { post(:ban, params: params) } + + context "when user exists" do + shared_examples "does not ban administrators" do + let(:expected_response) do + { + "success" => false, + "status" => "nopriv", + "message" => anything + } + end + + before { target_user.add_role :administrator } + + it "does not ban the target user" do + subject + expect(target_user).not_to be_banned + end + + include_examples "returns the expected response" + end + + let(:user_param) { target_user.screen_name } + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + + before { target_user } + + context "when ban = 0" do + let(:ban) { "0" } + let(:wrongly_formatted_date_ugh) { nil } + + "01".each_char do |pb| + context "when permaban = #{pb}" do + let(:permaban) { pb } + + context "when user is already banned" do + before { target_user.ban } + + it "unbans the user" do + expect { subject }.to(change { target_user.reload.banned? }.from(true).to(false)) + end + + include_examples "returns the expected response" + end + + context "when user is not yet banned" do + it "does not change the status of the ban" do + expect { subject }.not_to(change { target_user.reload.banned? }) + end + + include_examples "returns the expected response" + end + end + end + end + + context "when ban = 1" do + let(:ban) { "1" } + let(:wrongly_formatted_date_ugh) { "4/20/2420 12:00 AM" } + + context "when permaban = 0" do + let(:permaban) { "0" } + + it "bans the user until 2420-04-20" do + expect { subject }.to(change { target_user.reload.banned? }.from(false).to(true)) + expect(target_user).not_to be_permanently_banned + expect(target_user.ban_reason).to eq("just a prank, bro") + expect(target_user.banned_until).to eq(DateTime.strptime(wrongly_formatted_date_ugh, "%m/%d/%Y %I:%M %p")) + end + + include_examples "returns the expected response" + + it_behaves_like "does not ban administrators" + end + + context "when permaban = 1" do + let(:permaban) { "1" } + + it "bans the user for all eternity" do + expect { subject }.to(change { target_user.reload.banned? }.from(false).to(true)) + expect(target_user).to be_permanently_banned + expect(target_user.ban_reason).to eq("just a prank, bro") + expect(target_user.banned_until).to be_nil + end + + include_examples "returns the expected response" + + it_behaves_like "does not ban administrators" + end + end + end + + context "when user does not exist" do + let(:user_param) { "fritz-fantom" } + let(:ban) { "1" } + let(:permaban) { "1" } + let(:wrongly_formatted_date_ugh) { "4/20/2420 12:00 AM" } + let(:expected_response) do + { + "success" => false, + "status" => "not_found", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + describe "#privilege" do + valid_role_pairs = { + moderator: :moderator, + admin: :administrator + }.freeze + + let(:params) do + { + user: user_param, + type: type, + status: status + } + end + + subject { post(:privilege, params: params) } + + context "when user exists" do + let(:user_param) { target_user.screen_name } + before { target_user } + + { + nil => "has no extra roles", + :moderator => "is a moderator" + }.each do |u_role, context_desc| + context "when the current user #{context_desc}" do + let(:user_role) { u_role } + let(:expected_response) do + { + "success" => false, + "status" => "nopriv", + "message" => anything + } + end + + valid_role_pairs.each do |type, role_name| + context "when type is #{type}" do + let(:type) { type } + + context "when status is true" do + let(:status) { "true" } + + it "does not modify the roles on the target user" do + expect { subject }.not_to(change { target_user.reload.roles.to_a }) + end + + include_examples "returns the expected response" + end + + context "when status is false" do + let(:status) { "true" } + + before { target_user.add_role role_name } + + it "does not modify the roles on the target user" do + expect { subject }.not_to(change { target_user.reload.roles.to_a }) + end + + include_examples "returns the expected response" + end + end + end + end + end + + context "when the current user is an administrator" do + let(:user_role) { :administrator } + + valid_role_pairs.each do |type, role_name| + context "when type is #{type}" do + let(:type) { type } + + context "when status is true" do + let(:status) { "true" } + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything, + "checked" => true + } + end + + it "adds the #{role_name} role to the target user" do + expect { subject }.to(change { target_user.roles.reload.to_a }) + expect(target_user).to have_role(role_name) + end + + include_examples "returns the expected response" + end + + context "when status is false" do + let(:status) { "false" } + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything, + "checked" => false + } + end + + before { target_user.add_role role_name } + + it "removes the #{role_name} role from the target user" do + expect { subject }.to(change { target_user.reload.roles.to_a }) + expect(target_user).not_to have_role(role_name) + end + + include_examples "returns the expected response" + end + end + end + + context "when type is some bogus value" do + let(:type) { "some bogus value" } + let(:expected_response) do + { + "success" => false, + "status" => "err", + "message" => anything + } + end + + %w[true false].each do |s| + context "when status is #{s}" do + let(:status) { s } + + it "does not modify the roles on the target user" do + expect { subject }.not_to(change { target_user.reload.roles.to_a }) + end + + include_examples "returns the expected response" + end + end + end + end + end + + context "when user does not exist" do + let(:user_param) { "fritz-fantom" } + let(:type) { "admin" } + let(:status) { "true" } + let(:expected_response) do + { + "success" => false, + "status" => "not_found", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end +end diff --git a/spec/controllers/ajax/question_controller_spec.rb b/spec/controllers/ajax/question_controller_spec.rb new file mode 100644 index 00000000..add68805 --- /dev/null +++ b/spec/controllers/ajax/question_controller_spec.rb @@ -0,0 +1,399 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Ajax::QuestionController, :ajax_controller, type: :controller do + describe "#create" do + shared_examples "creates the question" do |check_for_inbox = true| + it "creates the question" do + expect { subject }.to(change { Question.count }.by(1)) + expect(Question.last.content).to eq(question_content) + expect(Question.last.author_is_anonymous).to be(expected_question_anonymous) + expect(Question.last.user).to eq(expected_question_user) + end + + if check_for_inbox + it "adds the question to the target users' inbox" do + expect { subject }.to(change { target_user.inboxes.count }.by(1)) + expect(target_user.inboxes.last.question.content).to eq(question_content) + end + end + + include_examples "returns the expected response" + end + + shared_examples "does not create the question" do |check_for_inbox = true| + it "does not create the question" do + expect { subject }.not_to(change { Question.count }) + end + + if check_for_inbox + it "does not add the question to the target users' inbox" do + expect { subject }.not_to(change { target_user.inboxes.count }) + end + end + + include_examples "returns the expected response" + end + + shared_examples "enqueues a QuestionWorker job" do |expected_rcpt| + it "enqueues a QuestionWorker job" do + allow(QuestionWorker).to receive(:perform_async) + subject + expect(QuestionWorker).to have_received(:perform_async).with(expected_rcpt, user.id, Question.last.id) + end + + include_examples "returns the expected response" + end + + shared_examples "does not enqueue a QuestionWorker job" do + it "does not enqueue a QuestionWorker job" do + allow(QuestionWorker).to receive(:perform_async) + subject + expect(QuestionWorker).not_to have_received(:perform_async) + end + + include_examples "returns the expected response" + end + + let(:target_user) { FactoryBot.create(:user, privacy_allow_anonymous_questions: user_allows_anonymous_questions) } + let(:params) do + { + question: question_content, + anonymousQuestion: anonymous_question, + rcpt: rcpt + } + end + + subject { post(:create, params: params) } + + context "when user is signed in" do + let(:question_content) { "Was letzte Preis?" } + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + let(:expected_question_user) { user } + + before(:each) { sign_in(user) } + + context "when rcpt is a valid user" do + let(:rcpt) { target_user.id } + + context "when user allows anonymous questions" do + let(:user_allows_anonymous_questions) { true } + + context "when anonymousQuestion is true" do + let(:anonymous_question) { "true" } + let(:expected_question_anonymous) { true } + + include_examples "creates the question" + end + + context "when anonymousQuestion is false" do + let(:anonymous_question) { "false" } + let(:expected_question_anonymous) { false } + + include_examples "creates the question" + end + end + + context "when user does not allow anonymous questions" do + let(:user_allows_anonymous_questions) { false } + + context "when anonymousQuestion is true" do + let(:anonymous_question) { "true" } + let(:expected_response) do + { + "success" => false, + "status" => "unknown", + "message" => anything + } + end + + include_examples "does not create the question" + end + + context "when anonymousQuestion is false" do + let(:anonymous_question) { "false" } + let(:expected_question_anonymous) { false } + + include_examples "creates the question" + end + end + end + + context "when rcpt is followers" do + let(:rcpt) { "followers" } + + context "when anonymousQuestion is true" do + let(:anonymous_question) { "true" } + let(:expected_question_anonymous) { false } + + include_examples "creates the question", false + include_examples "enqueues a QuestionWorker job", "followers" + end + + context "when anonymousQuestion is false" do + let(:anonymous_question) { "false" } + let(:expected_question_anonymous) { false } + + include_examples "creates the question", false + include_examples "enqueues a QuestionWorker job", "followers" + end + end + + context "when rcpt is a group" do + let(:rcpt) { "grp:foobar" } + + context "when group exists" do + let(:group) { FactoryBot.create(:group, display_name: "FooBar", user: user) } + before { group } + + context "when anonymousQuestion is true" do + let(:anonymous_question) { "true" } + let(:expected_question_anonymous) { false } + + include_examples "creates the question", false + include_examples "enqueues a QuestionWorker job", "grp:foobar" + end + + context "when anonymousQuestion is false" do + let(:anonymous_question) { "false" } + let(:expected_question_anonymous) { false } + + include_examples "creates the question", false + include_examples "enqueues a QuestionWorker job", "grp:foobar" + end + end + + context "when group does not exist" do + let(:expected_response) do + { + "success" => false, + "status" => "not_found", + "message" => anything + } + end + + context "when anonymousQuestion is true" do + let(:anonymous_question) { "true" } + + include_examples "does not create the question", false + include_examples "does not enqueue a QuestionWorker job" + end + + context "when anonymousQuestion is false" do + let(:anonymous_question) { "false" } + + include_examples "does not create the question", false + include_examples "does not enqueue a QuestionWorker job" + end + end + end + + context "when rcpt is a non-existent user" do + let(:rcpt) { "tripmeister_eder" } + let(:anonymous_question) { "false" } + let(:expected_response) do + { + "success" => false, + "status" => "not_found", + "message" => anything + } + end + + include_examples "does not create the question", false + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:target_user) { FactoryBot.create(:user, privacy_allow_anonymous_questions: user_allows_anonymous_questions) } + let(:question_content) { "Was letzte Preis?" } + let(:anonymous_question) { "true" } + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + let(:expected_question_anonymous) { true } + let(:expected_question_user) { nil } + + context "when rcpt is a valid user" do + let(:rcpt) { target_user.id } + + context "when user allows anonymous questions" do + let(:user_allows_anonymous_questions) { true } + + include_examples "creates the question" + + context "when anonymousQuestion is false" do + let(:anonymous_question) { "false" } + let(:expected_response) do + { + "success" => false, + "status" => "unknown", + "message" => anything + } + end + + include_examples "does not create the question" + end + end + + context "when user does not allow anonymous questions" do + let(:user_allows_anonymous_questions) { false } + let(:expected_response) do + { + "success" => false, + "status" => "unknown", + "message" => anything + } + end + + include_examples "does not create the question" + + context "when anonymousQuestion is false" do + let(:anonymous_question) { "false" } + + include_examples "does not create the question" + end + end + end + + context "when rcpt is followers" do + let(:rcpt) { "followers" } + + include_examples "does not enqueue a QuestionWorker job" + end + + context "when rcpt is a group" do + let(:rcpt) { "grp:foobar" } + + include_examples "does not enqueue a QuestionWorker job" + end + + context "when rcpt is a non-existent user" do + let(:rcpt) { "tripmeister_eder" } + let(:expected_response) do + { + "success" => false, + "status" => "not_found", + "message" => anything + } + end + + include_examples "does not create the question", false + end + end + end + + describe "#destroy" do + shared_examples "does not delete the question" do |expected_status| + let(:expected_response) do + { + "success" => false, + "status" => expected_status, + "message" => anything + } + end + + it "does not delete the question" do + question # ensure we already have it in the db + expect { subject }.not_to(change { Question.count }) + end + + include_examples "returns the expected response" + end + + let(:question_user) { user } + let(:question) { FactoryBot.create(:question, user: question_user) } + let(:question_id) { question.id } + + let(:params) do + { + question: question_id + } + end + + subject { delete(:destroy, params: params) } + + context "when user is signed in" do + shared_examples "deletes the question" do + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + + it "deletes the question" do + question # ensure we already have it in the db + expect { subject }.to(change { Question.count }.by(-1)) + end + + include_examples "returns the expected response" + end + + before(:each) { sign_in(user) } + + context "when the question exists and was made by the current user" do + include_examples "deletes the question" + end + + context "when the question exists and was not made by the current user" do + let(:question_user) { FactoryBot.create(:user) } + + include_examples "does not delete the question", "not_authorized" + + %i[moderator administrator].each do |privileged_role| + context "when the current user is a #{privileged_role}" do + around do |example| + user.add_role privileged_role + example.run + user.remove_role privileged_role + end + + include_examples "deletes the question" + end + end + end + + context "when the question exists and was not made by any registered user" do + let(:question_user) { nil } + + include_examples "does not delete the question", "not_authorized" + + %i[moderator administrator].each do |privileged_role| + context "when the current user is a #{privileged_role}" do + around do |example| + user.add_role privileged_role + example.run + user.remove_role privileged_role + end + + include_examples "deletes the question" + end + end + end + + context "when the question does not exist" do + let(:question_id) { "sonic_the_hedgehog" } + + include_examples "does not delete the question", "not_found" + end + end + + context "when user is not signed in" do + include_examples "does not delete the question", "err" + end + end +end diff --git a/spec/controllers/ajax/report_controller_spec.rb b/spec/controllers/ajax/report_controller_spec.rb new file mode 100644 index 00000000..dd8c4c10 --- /dev/null +++ b/spec/controllers/ajax/report_controller_spec.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Ajax::ReportController, :ajax_controller, type: :controller do + describe "#create" do + let(:params) do + { + id: id, + type: type, + reason: reason + } + end + subject { post(:create, params: params) } + + context "when user is signed in" do + shared_examples "reporting an item" do |type| + let(:type) { type } + let(:id) { object.id } + + context "when #{type} exists" do + before { object } + + context "when reason is empty" do + let(:reason) { "" } + + it "creates a report of type Reports::#{type.capitalize}" do + report_klass = "Reports::#{type.capitalize}".constantize + expect { subject }.to(change { report_klass.count }.by(1)) + expect(report_klass.last.target).to eq(object) + expect(report_klass.last.reason).to be_blank + end + + include_examples "returns the expected response" + end + + context "when reason is not empty" do + let(:reason) { "I don't like this" } + + it "creates a report of type Reports::#{type.capitalize}" do + report_klass = "Reports::#{type.capitalize}".constantize + expect { subject }.to(change { report_klass.count }.by(1)) + expect(report_klass.last.target).to eq(object) + expect(report_klass.last.reason).to eq(reason) + end + + include_examples "returns the expected response" + end + end + + context "when #{type} does not exist" do + let(:id) { "nonexistent" } + let(:expected_response) do + { + "success" => false, + "status" => "not_found", + "message" => anything, + } + end + + context "when reason is empty" do + let(:reason) { "" } + + it "does not create a report" do + expect { subject }.not_to(change { Report.count }) + end + + include_examples "returns the expected response" + end + + context "when reason is not empty" do + let(:reason) { "I don't like this" } + + it "does not create a report" do + expect { subject }.not_to(change { Report.count }) + end + + include_examples "returns the expected response" + end + end + end + + let(:target_user) { FactoryBot.create(:user) } + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything, + } + end + + before(:each) { sign_in(user) } + + it_behaves_like "reporting an item", "user" do + let(:object) { target_user } + let(:id) { object.screen_name } + end + + it_behaves_like "reporting an item", "question" do + let(:object) { FactoryBot.create(:question, user: target_user) } + end + + it_behaves_like "reporting an item", "answer" do + let(:object) { FactoryBot.create(:answer, user: target_user) } + end + + it_behaves_like "reporting an item", "comment" do + let(:answer) { FactoryBot.create(:answer, user: target_user) } + let(:object) { FactoryBot.create(:comment, user: target_user, answer: answer) } + end + + context "when type is anything else" do + let(:id) { "whatever" } + let(:type) { "whatever" } + let(:reason) { "whatever" } + let(:expected_response) do + { + "success" => false, + "status" => "err", + "message" => anything, + } + end + + it "does not create a report" do + expect { subject }.not_to(change { Report.count }) + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:id) { "peter_zwegat" } + let(:type) { "user" } + let(:reason) { "I'm broke now thanks to this bloke" } + let(:expected_response) do + { + "success" => false, + "status" => "err", + "message" => anything + } + end + + it "does not create a report" do + expect { subject }.not_to(change { Report.count }) + end + + include_examples "returns the expected response" + end + end +end diff --git a/spec/controllers/ajax/smile_controller_spec.rb b/spec/controllers/ajax/smile_controller_spec.rb new file mode 100644 index 00000000..07a20de4 --- /dev/null +++ b/spec/controllers/ajax/smile_controller_spec.rb @@ -0,0 +1,292 @@ +# coding: utf-8 +# frozen_string_literal: true + +require "rails_helper" + +describe Ajax::SmileController, :ajax_controller, type: :controller do + describe "#create" do + let(:params) do + { + id: answer_id + }.compact + end + let(:answer) { FactoryBot.create(:answer, user: user) } + + subject { post(:create, params: params) } + + context "when user is signed in" do + before(:each) { sign_in(user) } + + context "when answer exists" do + let(:answer_id) { answer.id } + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + + it "creates a smile to the answer" do + expect { subject }.to(change { Smile.count }.by(1)) + expect(answer.reload.smiles.ids).to include(Smile.last.id) + end + + include_examples "returns the expected response" + end + + context "when answer does not exist" do + let(:answer_id) { "nein!" } + + let(:expected_response) do + { + "success" => false, + "status" => anything, + "message" => anything + } + end + + it "does not create a smile" do + expect { subject }.not_to(change { Smile.count }) + end + + include_examples "returns the expected response" + end + + context "when some parameters are missing" do + let(:answer_id) { nil } + + let(:expected_response) do + { + "success" => false, + "status" => "parameter_error", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:answer_id) { answer.id } + + let(:expected_response) do + { + "success" => false, + "status" => "fail", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + describe "#destroy" do + let(:answer) { FactoryBot.create(:answer, user: user) } + let(:smile) { FactoryBot.create(:smile, user: user, answer: answer) } + let(:answer_id) { answer.id } + + let(:params) do + { + id: answer_id + } + end + + subject { delete(:destroy, params: params) } + + context "when user is signed in" do + before(:each) { sign_in(user) } + + context "when the smile exists" do + # ensure we already have it in the db + before(:each) { smile } + + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + + it "deletes the smile" do + expect { subject }.to(change { Smile.count }.by(-1)) + end + + include_examples "returns the expected response" + end + + context "when the smile does not exist" do + let(:answer_id) { "sonic_the_hedgehog" } + + let(:expected_response) do + { + "success" => false, + "status" => anything, + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:expected_response) do + { + "success" => false, + "status" => "fail", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + describe "#create_comment" do + let(:params) do + { + id: comment_id + }.compact + end + let(:answer) { FactoryBot.create(:answer, user: user) } + let(:comment) { FactoryBot.create(:comment, user: user, answer: answer) } + + subject { post(:create_comment, params: params) } + + context "when user is signed in" do + before(:each) { sign_in(user) } + + context "when comment exists" do + let(:comment_id) { comment.id } + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + + it "creates a smile to the comment" do + expect { subject }.to(change { CommentSmile.count }.by(1)) + expect(comment.reload.smiles.ids).to include(CommentSmile.last.id) + end + + include_examples "returns the expected response" + end + + context "when comment does not exist" do + let(:comment_id) { "nein!" } + + let(:expected_response) do + { + "success" => false, + "status" => anything, + "message" => anything + } + end + + it "does not create a smile" do + expect { subject }.not_to(change { CommentSmile.count }) + end + + include_examples "returns the expected response" + end + + context "when some parameters are missing" do + let(:comment_id) { nil } + + let(:expected_response) do + { + "success" => false, + "status" => "parameter_error", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:comment_id) { comment.id } + + let(:expected_response) do + { + "success" => false, + "status" => "fail", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + describe "#destroy_comment" do + let(:answer) { FactoryBot.create(:answer, user: user) } + let(:comment) { FactoryBot.create(:comment, user: user, answer: answer) } + let(:comment_smile) { FactoryBot.create(:comment_smile, user: user, comment: comment) } + let(:comment_id) { comment.id } + + let(:params) do + { + id: comment_id + } + end + + subject { delete(:destroy_comment, params: params) } + + context "when user is signed in" do + before(:each) { sign_in(user) } + + context "when the smile exists" do + # ensure we already have it in the db + before(:each) { comment_smile } + + let(:expected_response) do + { + "success" => true, + "status" => "okay", + "message" => anything + } + end + + it "deletes the smile" do + expect { subject }.to(change { CommentSmile.count }.by(-1)) + end + + include_examples "returns the expected response" + end + + context "when the smile does not exist" do + let(:answer_id) { "sonic_the_hedgehog" } + + let(:expected_response) do + { + "success" => false, + "status" => anything, + "message" => anything + } + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:expected_response) do + { + "success" => false, + "status" => "fail", + "message" => anything + } + end + + include_examples "returns the expected response" + end + end +end diff --git a/spec/controllers/ajax/subscription_controller_spec.rb b/spec/controllers/ajax/subscription_controller_spec.rb new file mode 100644 index 00000000..6c82f400 --- /dev/null +++ b/spec/controllers/ajax/subscription_controller_spec.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Ajax::SubscriptionController, :ajax_controller, type: :controller do + # need to use a different user here, as after a create the user owning the + # answer is automatically subscribed to it + let(:answer_user) { FactoryBot.create(:user) } + let(:answer) { FactoryBot.create(:answer, user: answer_user) } + + describe "#subscribe" do + let(:params) do + { + answer: answer_id + } + end + + subject { post(:subscribe, params: params) } + + context "when user is signed in" do + before(:each) { sign_in(user) } + + context "when answer exists" do + let(:answer_id) { answer.id } + let(:expected_response) do + { + "success" => true, + "status" => 418, + "message" => anything + } + end + + context "when subscription does not exist" do + it "creates a subscription on the answer" do + expect { subject }.to(change { answer.subscriptions.count }.by(1)) + expect(answer.subscriptions.where(is_active: true).map { |s| s.user.id }.sort).to eq([answer_user.id, user.id].sort) + end + + include_examples "returns the expected response" + end + + context "when subscription already exists" do + before(:each) { Subscription.subscribe(user, answer) } + + it "does not modify the answer's subscriptions" do + expect { subject }.to(change { answer.subscriptions.count }.by(0)) + expect(answer.subscriptions.where(is_active: true).map { |s| s.user.id }.sort).to eq([answer_user.id, user.id].sort) + end + + include_examples "returns the expected response" + end + end + + context "when answer does not exist" do + let(:answer_id) { "Bielefeld" } + let(:expected_response) do + { + "success" => false, + "status" => "not_found", + "message" => anything + } + end + + it "does not create a new subscription" do + expect { subject }.not_to(change { Subscription.count }) + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:answer_id) { answer.id } + + it "redirects to somewhere else, apparently" do + subject + expect(response).to be_a_redirect + end + end + end + + describe "#unsubscribe" do + let(:params) do + { + answer: answer_id + } + end + + subject { post(:unsubscribe, params: params) } + + context "when user is signed in" do + before(:each) { sign_in(user) } + + context "when answer exists" do + let(:answer_id) { answer.id } + let(:expected_response) do + { + "success" => true, + "status" => 418, + "message" => anything + } + end + + context "when subscription exists" do + before(:each) { Subscription.subscribe(user, answer) } + + it "removes an active subscription from the answer" do + expect { subject }.to(change { answer.subscriptions.where(is_active: true).count }.by(-1)) + expect(answer.subscriptions.where(is_active: true).map { |s| s.user.id }.sort).to eq([answer_user.id].sort) + end + + include_examples "returns the expected response" + end + + context "when subscription does not exist" do + let(:expected_response) do + { + "success" => false, + "status" => 418, + "message" => anything + } + end + + it "does not modify the answer's subscriptions" do + expect { subject }.to(change { answer.subscriptions.count }.by(0)) + expect(answer.subscriptions.where(is_active: true).map { |s| s.user.id }.sort).to eq([answer_user.id].sort) + end + + include_examples "returns the expected response" + end + end + + context "when answer does not exist" do + let(:answer_id) { "Bielefeld" } + let(:expected_response) do + { + "success" => false, + "status" => "not_found", + "message" => anything + } + end + + it "does not create a new subscription" do + expect { subject }.not_to(change { Subscription.count }) + end + + include_examples "returns the expected response" + end + end + + context "when user is not signed in" do + let(:answer_id) { answer.id } + + it "redirects to somewhere else, apparently" do + subject + expect(response).to be_a_redirect + end + end + end +end diff --git a/spec/factories/comment.rb b/spec/factories/comment.rb new file mode 100644 index 00000000..7ce5a312 --- /dev/null +++ b/spec/factories/comment.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :comment do + transient do + answer_content { Faker::Lorem.sentence } + end + + content { Faker::Lorem.sentence } + answer { FactoryBot.build(:answer, content: answer_content) } + end +end diff --git a/spec/factories/comment_smile.rb b/spec/factories/comment_smile.rb new file mode 100644 index 00000000..3a2b585e --- /dev/null +++ b/spec/factories/comment_smile.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :comment_smile do + user { FactoryBot.build(:user) } + comment { FactoryBot.build(:comment) } + end +end diff --git a/spec/factories/group.rb b/spec/factories/group.rb new file mode 100644 index 00000000..6c0ce165 --- /dev/null +++ b/spec/factories/group.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :group do + sequence(:display_name) { |i| "#{Faker::Internet.username(specifier: 0..12, separators: %w[_])}#{i}" } + user { FactoryBot.build(:user) } + + transient do + members { [] } + end + + after(:create) do |group, evaluator| + evaluator.members.each do |member| + GroupMember.create(group_id: group.id, user_id: member.id) + end + end + end +end diff --git a/spec/factories/smile.rb b/spec/factories/smile.rb new file mode 100644 index 00000000..1ce23750 --- /dev/null +++ b/spec/factories/smile.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :smile do + user { FactoryBot.build(:user) } + answer { FactoryBot.build(:answer) } + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 5fc46546..742cffa8 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -12,6 +12,7 @@ require "rspec/its" require "devise" require "capybara/rails" require "capybara/rspec" +require "rspec-sidekiq" # Requires supporting ruby files with custom matchers and macros, etc, in # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are diff --git a/spec/shared_examples/ajax_controllers.rb b/spec/shared_examples/ajax_controllers.rb new file mode 100644 index 00000000..c5907548 --- /dev/null +++ b/spec/shared_examples/ajax_controllers.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +RSpec.shared_context "AjaxController context" do + let(:user) { FactoryBot.create(:user) } + + shared_examples "returns the expected response" do + it "returns the expected response" do + expect(JSON.parse(subject.body)).to match(expected_response) + end + end +end + +RSpec.configure do |c| + c.include_context "AjaxController context", ajax_controller: true +end diff --git a/spec/support/simplecov.rb b/spec/support/simplecov.rb index 2cc94286..c07246da 100644 --- a/spec/support/simplecov.rb +++ b/spec/support/simplecov.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true require "simplecov" -# require "simplecov-rcov" -# SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter SimpleCov.start "rails" + +if ENV.key?("GITHUB_ACTIONS") + require "simplecov-cobertura" + SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter +end