Commit ee43fc44 authored by charlieablett's avatar charlieablett

Refactor and move SpamService

- Rename to SpamCheckService so it's obvious what it does
- Use `execute` method for consistency with other services
- Update unit tests
- move ActiveRecord logic into the model
- Add unit tests for new SpamLog method
parent 67abf2b7
...@@ -12,4 +12,8 @@ class SpamLog < ApplicationRecord ...@@ -12,4 +12,8 @@ class SpamLog < ApplicationRecord
def text def text
[title, description].join("\n") [title, description].join("\n")
end end
def self.verify_recaptcha!(id:, user_id:)
find_by(id: id, user_id: user_id)&.update!(recaptcha_verified: true)
end
end end
...@@ -22,14 +22,14 @@ module SpamCheckMethods ...@@ -22,14 +22,14 @@ module SpamCheckMethods
# a dirty instance, which means it should be already assigned with the new # a dirty instance, which means it should be already assigned with the new
# attribute values. # attribute values.
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
# rubocop: disable CodeReuse/ActiveRecord
def spam_check(spammable, user) def spam_check(spammable, user)
spam_service = SpamService.new(spammable: spammable, request: @request) SpamCheckService.new(
spammable: spammable,
spam_service.when_recaptcha_verified(@recaptcha_verified, @api) do request: @request
user.spam_logs.find_by(id: @spam_log_id)&.update!(recaptcha_verified: true) ).execute(api: @api,
end recaptcha_verified: @recaptcha_verified,
spam_log_id: @spam_log_id,
user_id: user.id)
end end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop:enable Gitlab/ModuleWithInstanceVariables # rubocop:enable Gitlab/ModuleWithInstanceVariables
end end
# frozen_string_literal: true # frozen_string_literal: true
class SpamService class SpamCheckService
include AkismetMethods include AkismetMethods
attr_accessor :spammable, :request, :options attr_accessor :spammable, :request, :options
...@@ -21,14 +21,14 @@ class SpamService ...@@ -21,14 +21,14 @@ class SpamService
end end
end end
def when_recaptcha_verified(recaptcha_verified, api = false) def execute(api: false, recaptcha_verified:, spam_log_id:, user_id:)
# In case it's a request which is already verified through recaptcha, yield
# block.
if recaptcha_verified if recaptcha_verified
yield # If it's a request which is already verified through recaptcha,
# update the spam log accordingly.
SpamLog.verify_recaptcha!(user_id: user_id, id: spam_log_id)
else else
# Otherwise, it goes to Akismet and check if it's a spam. If that's the # Otherwise, it goes to Akismet for spam check.
# case, it assigns spammable record as "spam" and create a SpamLog record. # If so, it assigns spammable object as "spam" and creates a SpamLog record.
possible_spam = check(api) possible_spam = check(api)
spammable.spam = possible_spam unless spammable.allow_possible_spam? spammable.spam = possible_spam unless spammable.allow_possible_spam?
spammable.spam_log = spam_log spammable.spam_log = spam_log
...@@ -38,9 +38,9 @@ class SpamService ...@@ -38,9 +38,9 @@ class SpamService
private private
def check(api) def check(api)
return false unless request && check_for_spam? return unless request
return unless check_for_spam?
return false unless akismet.spam? return unless akismet.spam?
create_spam_log(api) create_spam_log(api)
true true
......
...@@ -31,4 +31,29 @@ describe SpamLog do ...@@ -31,4 +31,29 @@ describe SpamLog do
expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
describe '.verify_recaptcha!' do
let(:spam_log) { create(:spam_log, id: 123, user: admin, recaptcha_verified: false) }
context 'the record cannot be found' do
it 'updates nothing' do
expect(instance_of(described_class)).not_to receive(:update!)
described_class.verify_recaptcha!(id: 9999, user_id: admin.id)
expect(spam_log.recaptcha_verified).to be_falsey
end
it 'does not error despite not finding a record' do
expect { described_class.verify_recaptcha!(id: 9999, user_id: admin.id) }.not_to raise_error
end
end
context 'the record exists' do
it 'updates recaptcha_verified' do
expect { described_class.verify_recaptcha!(id: spam_log.id, user_id: admin.id) }
.to change { spam_log.reload.recaptcha_verified }.from(false).to(true)
end
end
end
end end
...@@ -389,7 +389,7 @@ describe API::Issues do ...@@ -389,7 +389,7 @@ describe API::Issues do
end end
before do before do
expect_next_instance_of(SpamService) do |spam_service| expect_next_instance_of(SpamCheckService) do |spam_service|
expect(spam_service).to receive_messages(check_for_spam?: true) expect(spam_service).to receive_messages(check_for_spam?: true)
end end
expect_next_instance_of(AkismetService) do |akismet_service| expect_next_instance_of(AkismetService) do |akismet_service|
......
...@@ -194,7 +194,7 @@ describe API::Issues do ...@@ -194,7 +194,7 @@ describe API::Issues do
end end
before do before do
expect_next_instance_of(SpamService) do |spam_service| expect_next_instance_of(SpamCheckService) do |spam_service|
expect(spam_service).to receive_messages(check_for_spam?: true) expect(spam_service).to receive_messages(check_for_spam?: true)
end end
expect_next_instance_of(AkismetService) do |akismet_service| expect_next_instance_of(AkismetService) do |akismet_service|
......
...@@ -385,7 +385,7 @@ describe Issues::CreateService do ...@@ -385,7 +385,7 @@ describe Issues::CreateService do
context 'when recaptcha was not verified' do context 'when recaptcha was not verified' do
before do before do
expect_next_instance_of(SpamService) do |spam_service| expect_next_instance_of(SpamCheckService) do |spam_service|
expect(spam_service).to receive_messages(check_for_spam?: true) expect(spam_service).to receive_messages(check_for_spam?: true)
end end
end end
......
...@@ -2,24 +2,87 @@ ...@@ -2,24 +2,87 @@
require 'spec_helper' require 'spec_helper'
describe SpamService do describe SpamCheckService do
describe '#when_recaptcha_verified' do let(:fake_ip) { '1.2.3.4' }
def check_spam(issue, request, recaptcha_verified) let(:fake_user_agent) { 'fake-user-agent' }
described_class.new(spammable: issue, request: request).when_recaptcha_verified(recaptcha_verified) do let(:fake_referrer) { 'fake-http-referrer' }
'yielded' let(:env) do
{ 'action_dispatch.remote_ip' => fake_ip,
'HTTP_USER_AGENT' => fake_user_agent,
'HTTP_REFERRER' => fake_referrer }
end
let(:request) { double(:request, env: env) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue, project: project, author: user) }
before do
issue.spam = false
end
describe '#initialize' do
subject { described_class.new(spammable: issue, request: request) }
context 'when the request is nil' do
let(:request) { nil }
it 'assembles the options with information from the spammable' do
aggregate_failures do
expect(subject.options[:ip_address]).to eq(issue.ip_address)
expect(subject.options[:user_agent]).to eq(issue.user_agent)
expect(subject.options.key?(:referrer)).to be_falsey
end
end end
end end
it 'yields block when recaptcha was already verified' do context 'when the request is present' do
issue = build_stubbed(:issue) let(:request) { double(:request, env: env) }
expect(check_spam(issue, nil, true)).to eql('yielded') it 'assembles the options with information from the spammable' do
aggregate_failures do
expect(subject.options[:ip_address]).to eq(fake_ip)
expect(subject.options[:user_agent]).to eq(fake_user_agent)
expect(subject.options[:referrer]).to eq(fake_referrer)
end
end
end
end
describe '#execute' do
let(:request) { double(:request, env: env) }
let(:params) do
{ user_id: user.id, api: nil, spam_log_id: 123 }
end
subject do
described_service = described_class.new(spammable: issue, request: request)
described_service.execute(params.merge(recaptcha_verified: recaptcha_verified, spam_log_id: 123))
end
context 'when recaptcha was already verified' do
let(:recaptcha_verified) { true }
let_it_be(:existing_spam_log) { create(:spam_log, user: user, recaptcha_verified: false, id: 123) }
it "updates spam log and doesn't check Akismet" do
aggregate_failures do
expect(SpamLog).not_to receive(:create!)
expect(an_instance_of(described_class)).not_to receive(:check)
end
subject
end
it 'updates spam log' do
subject
expect(existing_spam_log.reload.recaptcha_verified).to be_truthy
end
end end
context 'when recaptcha was not verified' do context 'when recaptcha was not verified' do
let(:project) { create(:project, :public) } let(:recaptcha_verified) { false }
let(:issue) { create(:issue, project: project) }
let(:request) { double(:request, env: {}) }
context 'when spammable attributes have not changed' do context 'when spammable attributes have not changed' do
before do before do
...@@ -29,11 +92,11 @@ describe SpamService do ...@@ -29,11 +92,11 @@ describe SpamService do
end end
it 'returns false' do it 'returns false' do
expect(check_spam(issue, request, false)).to be_falsey expect(subject).to be_falsey
end end
it 'does not create a spam log' do it 'does not create a spam log' do
expect { check_spam(issue, request, false) } expect { subject }
.not_to change { SpamLog.count } .not_to change { SpamLog.count }
end end
end end
...@@ -44,24 +107,6 @@ describe SpamService do ...@@ -44,24 +107,6 @@ describe SpamService do
end end
context 'when indicated as spam by akismet' do context 'when indicated as spam by akismet' do
shared_examples 'akismet spam' do
it "doesn't check as spam when request is missing" do
check_spam(issue, nil, false)
expect(issue).not_to be_spam
end
it 'creates a spam log' do
expect { check_spam(issue, request, false) }
.to log_spam(title: issue.title, description: issue.description, noteable_type: 'Issue')
end
it 'does not yield to the block' do
expect(check_spam(issue, request, false))
.to eql(SpamLog.last)
end
end
before do before do
allow(AkismetService).to receive(:new).and_return(double(spam?: true)) allow(AkismetService).to receive(:new).and_return(double(spam?: true))
end end
...@@ -74,9 +119,9 @@ describe SpamService do ...@@ -74,9 +119,9 @@ describe SpamService do
it_behaves_like 'akismet spam' it_behaves_like 'akismet spam'
it 'checks as spam' do it 'checks as spam' do
check_spam(issue, request, false) subject
expect(issue.spam).to be_truthy expect(issue.reload.spam).to be_truthy
end end
end end
...@@ -84,9 +129,9 @@ describe SpamService do ...@@ -84,9 +129,9 @@ describe SpamService do
it_behaves_like 'akismet spam' it_behaves_like 'akismet spam'
it 'does not check as spam' do it 'does not check as spam' do
check_spam(issue, request, false) subject
expect(issue.spam).to be_nil expect(issue.spam).to be_falsey
end end
end end
end end
...@@ -97,11 +142,11 @@ describe SpamService do ...@@ -97,11 +142,11 @@ describe SpamService do
end end
it 'returns false' do it 'returns false' do
expect(check_spam(issue, request, false)).to be_falsey expect(subject).to be_falsey
end end
it 'does not create a spam log' do it 'does not create a spam log' do
expect { check_spam(issue, request, false) } expect { subject }
.not_to change { SpamLog.count } .not_to change { SpamLog.count }
end end
end end
......
# frozen_string_literal: true
shared_examples 'akismet spam' do
context 'when request is missing' do
subject { described_class.new(spammable: issue, request: nil) }
it "doesn't check as spam when request is missing" do
subject
expect(issue).not_to be_spam
end
end
context 'when request exists' do
it 'creates a spam log' do
expect { subject }
.to log_spam(title: issue.title, description: issue.description, noteable_type: 'Issue')
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment