Commit 61318cf1 authored by Stan Hu's avatar Stan Hu

Merge branch '299558-integrate-with-anti-spam-service' into 'master'

Integrate Spamcheck gRPC gem [RUN ALL RSPEC][RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!52385
parents 5d6d4d4c d063fc83
...@@ -477,6 +477,9 @@ group :ed25519 do ...@@ -477,6 +477,9 @@ group :ed25519 do
gem 'bcrypt_pbkdf', '~> 1.0' gem 'bcrypt_pbkdf', '~> 1.0'
end end
# Spamcheck GRPC protocol definitions
gem 'spamcheck', '~> 0.0.5'
# Gitaly GRPC protocol definitions # Gitaly GRPC protocol definitions
gem 'gitaly', '~> 13.11.0.pre.rc1' gem 'gitaly', '~> 13.11.0.pre.rc1'
......
...@@ -1211,6 +1211,8 @@ GEM ...@@ -1211,6 +1211,8 @@ GEM
thor (~> 1.0) thor (~> 1.0)
tilt (~> 2.0) tilt (~> 2.0)
yard (~> 0.9, >= 0.9.24) yard (~> 0.9, >= 0.9.24)
spamcheck (0.0.5)
grpc (~> 1.0)
spring (2.1.1) spring (2.1.1)
spring-commands-rspec (1.0.4) spring-commands-rspec (1.0.4)
spring (>= 0.9.1) spring (>= 0.9.1)
...@@ -1606,6 +1608,7 @@ DEPENDENCIES ...@@ -1606,6 +1608,7 @@ DEPENDENCIES
slack-messenger (~> 2.3.4) slack-messenger (~> 2.3.4)
snowplow-tracker (~> 0.6.1) snowplow-tracker (~> 0.6.1)
solargraph (~> 0.40.4) solargraph (~> 0.40.4)
spamcheck (~> 0.0.5)
spring (~> 2.1.0) spring (~> 2.1.0)
spring-commands-rspec (~> 1.0.4) spring-commands-rspec (~> 1.0.4)
sprockets (~> 3.7.0) sprockets (~> 3.7.0)
......
...@@ -375,7 +375,7 @@ class ApplicationSetting < ApplicationRecord ...@@ -375,7 +375,7 @@ class ApplicationSetting < ApplicationRecord
if: :external_authorization_service_enabled if: :external_authorization_service_enabled
validates :spam_check_endpoint_url, validates :spam_check_endpoint_url,
addressable_url: true, allow_blank: true addressable_url: { schemes: %w(grpc) }, allow_blank: true
validates :spam_check_endpoint_url, validates :spam_check_endpoint_url,
presence: true, presence: true,
......
...@@ -20,7 +20,7 @@ module Spam ...@@ -20,7 +20,7 @@ module Spam
created_at: DateTime.current, created_at: DateTime.current,
author: owner_name, author: owner_name,
author_email: owner_email, author_email: owner_email,
referrer: options[:referrer] referer: options[:referer]
} }
begin begin
......
...@@ -53,7 +53,7 @@ module Spam ...@@ -53,7 +53,7 @@ module Spam
if request if request
options[:ip_address] = request.env['action_dispatch.remote_ip'].to_s options[:ip_address] = request.env['action_dispatch.remote_ip'].to_s
options[:user_agent] = request.env['HTTP_USER_AGENT'] options[:user_agent] = request.env['HTTP_USER_AGENT']
options[:referrer] = request.env['HTTP_REFERRER'] options[:referer] = request.env['HTTP_REFERER']
else else
# TODO: This code is never used, because we do not perform a verification if there is not a # TODO: This code is never used, because we do not perform a verification if there is not a
# request. Why? Should it be deleted? Or should we check even if there is no request? # request. Why? Should it be deleted? Or should we check even if there is no request?
...@@ -123,6 +123,11 @@ module Spam ...@@ -123,6 +123,11 @@ module Spam
# https://gitlab.com/gitlab-org/gitlab/-/issues/214739 # https://gitlab.com/gitlab-org/gitlab/-/issues/214739
target.spam! unless target.allow_possible_spam? target.spam! unless target.allow_possible_spam?
create_spam_log(api) create_spam_log(api)
when BLOCK_USER
# TODO: improve BLOCK_USER handling, non-existent until now
# https://gitlab.com/gitlab-org/gitlab/-/issues/329666
target.spam! unless target.allow_possible_spam?
create_spam_log(api)
when ALLOW when ALLOW
target.clear_spam_flags! target.clear_spam_flags!
end end
......
...@@ -10,25 +10,44 @@ module Spam ...@@ -10,25 +10,44 @@ module Spam
@request = request @request = request
@user = user @user = user
@options = options @options = options
@verdict_params = assemble_verdict_params(context) @context = context
end end
def execute def execute
external_spam_check_result = external_verdict spamcheck_result = nil
external_spam_check_round_trip_time = Benchmark.realtime do
spamcheck_result = spamcheck_verdict
end
akismet_result = akismet_verdict akismet_result = akismet_verdict
# filter out anything we don't recognise, including nils. # filter out anything we don't recognise, including nils.
valid_results = [external_spam_check_result, akismet_result].compact.select { |r| SUPPORTED_VERDICTS.key?(r) } valid_results = [spamcheck_result, akismet_result].compact.select { |r| SUPPORTED_VERDICTS.key?(r) }
# Treat nils - such as service unavailable - as ALLOW # Treat nils - such as service unavailable - as ALLOW
return ALLOW unless valid_results.any? return ALLOW unless valid_results.any?
# Favour the most restrictive result. # Favour the most restrictive result.
valid_results.min_by { |v| SUPPORTED_VERDICTS[v][:priority] } final_verdict = valid_results.min_by { |v| SUPPORTED_VERDICTS[v][:priority] }
logger.info(class: self.class.name,
akismet_verdict: akismet_verdict,
spam_check_verdict: spamcheck_result,
spam_check_rtt: external_spam_check_round_trip_time.real,
final_verdict: final_verdict,
username: user.username,
user_id: user.id,
target_type: target.class.to_s,
project_id: target.project_id
)
final_verdict
end end
private private
attr_reader :user, :target, :request, :options, :verdict_params attr_reader :user, :target, :request, :options, :context
def akismet_verdict def akismet_verdict
if akismet.spam? if akismet.spam?
...@@ -38,54 +57,34 @@ module Spam ...@@ -38,54 +57,34 @@ module Spam
end end
end end
def external_verdict def spamcheck_verdict
return unless Gitlab::CurrentSettings.spam_check_endpoint_enabled return unless Gitlab::CurrentSettings.spam_check_endpoint_enabled
return if endpoint_url.blank?
begin begin
result = Gitlab::HTTP.post(endpoint_url, body: verdict_params.to_json, headers: { 'Content-Type' => 'application/json' }) result, _error = spamcheck_client.issue_spam?(spam_issue: target, user: user, context: context)
return unless result return unless result
json_result = Gitlab::Json.parse(result).with_indifferent_access # @TODO log if error is not nil https://gitlab.com/gitlab-org/gitlab/-/issues/329545
# @TODO metrics/logging
# Expecting:
# error: (string or nil)
# verdict: (string or nil)
# @TODO log if json_result[:error]
json_result[:verdict] # Duplicate logic with Akismet logic in #akismet_verdict
rescue *Gitlab::HTTP::HTTP_ERRORS => e if Gitlab::Recaptcha.enabled? && result != ALLOW
# @TODO: log error via try_post https://gitlab.com/gitlab-org/gitlab/-/issues/219223 CONDITIONAL_ALLOW
else
result
end
rescue StandardError => e
Gitlab::ErrorTracking.log_exception(e) Gitlab::ErrorTracking.log_exception(e)
nil # Default to ALLOW if any errors occur
rescue StandardError
# @TODO log
ALLOW ALLOW
end end
end end
def assemble_verdict_params(context) def spamcheck_client
return {} unless endpoint_url.present? @spamcheck_client ||= Gitlab::Spamcheck::Client.new
project = target.try(:project)
context.merge({
target: {
title: target.spam_title,
description: target.spam_description,
type: target.class.to_s
},
user: {
created_at: user.created_at,
email: user.email,
username: user.username
},
user_in_project: user.authorized_project?(project)
})
end end
def endpoint_url def logger
@endpoint_url ||= Gitlab::CurrentSettings.current_application_settings.spam_check_endpoint_url @logger ||= Gitlab::AppJsonLogger.build
end end
end end
end end
---
title: Integrate with the Spamcheck anti-spam engine
merge_request: 52385
author:
type: added
# frozen_string_literal: true
require 'spamcheck'
module Gitlab
module Spamcheck
class Client
include ::Spam::SpamConstants
DEFAULT_TIMEOUT_SECS = 2
VERDICT_MAPPING = {
::Spamcheck::SpamVerdict::Verdict::ALLOW => ALLOW,
::Spamcheck::SpamVerdict::Verdict::CONDITIONAL_ALLOW => CONDITIONAL_ALLOW,
::Spamcheck::SpamVerdict::Verdict::DISALLOW => DISALLOW,
::Spamcheck::SpamVerdict::Verdict::BLOCK => BLOCK_USER
}.freeze
ACTION_MAPPING = {
create: ::Spamcheck::Action::CREATE,
update: ::Spamcheck::Action::UPDATE
}.freeze
def initialize
@endpoint_url = Gitlab::CurrentSettings.current_application_settings.spam_check_endpoint_url
# remove the `grpc://` as it's only useful to ensure we're expecting to
# connect with Spamcheck
@endpoint_url = @endpoint_url.gsub(%r(^grpc:\/\/), '')
creds =
if Rails.env.development? || Rails.env.test?
:this_channel_is_insecure
else
GRPC::Core::ChannelCredentials.new
end
@stub = ::Spamcheck::SpamcheckService::Stub.new(@endpoint_url, creds,
timeout: DEFAULT_TIMEOUT_SECS)
end
def issue_spam?(spam_issue:, user:, context: {})
issue = build_issue_protobuf(issue: spam_issue, user: user, context: context)
response = @stub.check_for_spam_issue(issue,
metadata: { 'authorization' =>
Gitlab::CurrentSettings.spam_check_api_key })
verdict = convert_verdict_to_gitlab_constant(response.verdict)
[verdict, response.error]
end
private
def convert_verdict_to_gitlab_constant(verdict)
VERDICT_MAPPING.fetch(::Spamcheck::SpamVerdict::Verdict.resolve(verdict), verdict)
end
def build_issue_protobuf(issue:, user:, context:)
issue_pb = ::Spamcheck::Issue.new
issue_pb.title = issue.spam_title || ''
issue_pb.description = issue.spam_description || ''
issue_pb.created_at = convert_to_pb_timestamp(issue.created_at) if issue.created_at
issue_pb.updated_at = convert_to_pb_timestamp(issue.updated_at) if issue.updated_at
issue_pb.user_in_project = user.authorized_project?(issue.project)
issue_pb.action = ACTION_MAPPING.fetch(context.fetch(:action)) if context.has_key?(:action)
issue_pb.user = build_user_protobuf(user)
issue_pb
end
def build_user_protobuf(user)
user_pb = ::Spamcheck::User.new
user_pb.username = user.username
user_pb.org = user.organization || ''
user_pb.created_at = convert_to_pb_timestamp(user.created_at)
user_pb.emails << build_email(user.email, user.confirmed?)
user.emails.each do |email|
user_pb.emails << build_email(email.email, email.confirmed?)
end
user_pb
end
def build_email(email, verified)
email_pb = ::Spamcheck::User::Email.new
email_pb.email = email
email_pb.verified = verified
email_pb
end
def convert_to_pb_timestamp(ar_timestamp)
Google::Protobuf::Timestamp.new(seconds: ar_timestamp.to_time.to_i,
nanos: ar_timestamp.to_time.nsec)
end
end
end
end
...@@ -425,7 +425,7 @@ RSpec.describe 'Admin updates settings' do ...@@ -425,7 +425,7 @@ RSpec.describe 'Admin updates settings' do
check 'Enable reCAPTCHA for login' check 'Enable reCAPTCHA for login'
fill_in 'IPs per user', with: 15 fill_in 'IPs per user', with: 15
check 'Enable Spam Check via external API endpoint' check 'Enable Spam Check via external API endpoint'
fill_in 'URL of the external Spam Check endpoint', with: 'https://www.example.com/spamcheck' fill_in 'URL of the external Spam Check endpoint', with: 'grpc://www.example.com/spamcheck'
fill_in 'Spam Check API Key', with: 'SPAM_CHECK_API_KEY' fill_in 'Spam Check API Key', with: 'SPAM_CHECK_API_KEY'
click_button 'Save changes' click_button 'Save changes'
end end
...@@ -435,7 +435,7 @@ RSpec.describe 'Admin updates settings' do ...@@ -435,7 +435,7 @@ RSpec.describe 'Admin updates settings' do
expect(current_settings.login_recaptcha_protection_enabled).to be true expect(current_settings.login_recaptcha_protection_enabled).to be true
expect(current_settings.unique_ips_limit_per_user).to eq(15) expect(current_settings.unique_ips_limit_per_user).to eq(15)
expect(current_settings.spam_check_endpoint_enabled).to be true expect(current_settings.spam_check_endpoint_enabled).to be true
expect(current_settings.spam_check_endpoint_url).to eq 'https://www.example.com/spamcheck' expect(current_settings.spam_check_endpoint_url).to eq 'grpc://www.example.com/spamcheck'
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Spamcheck::Client do
include_context 'includes Spam constants'
let(:endpoint) { 'grpc://grpc.test.url' }
let_it_be(:user) { create(:user, organization: 'GitLab') }
let(:verdict_value) { nil }
let(:error_value) { "" }
let_it_be(:issue) { create(:issue, description: 'Test issue description') }
let(:response) do
verdict = ::Spamcheck::SpamVerdict.new
verdict.verdict = verdict_value
verdict.error = error_value
verdict
end
subject { described_class.new.issue_spam?(spam_issue: issue, user: user) }
before do
stub_application_setting(spam_check_endpoint_url: endpoint)
end
describe '#issue_spam?' do
before do
allow_next_instance_of(::Spamcheck::SpamcheckService::Stub) do |instance|
allow(instance).to receive(:check_for_spam_issue).and_return(response)
end
end
using RSpec::Parameterized::TableSyntax
where(:verdict, :expected) do
::Spamcheck::SpamVerdict::Verdict::ALLOW | Spam::SpamConstants::ALLOW
::Spamcheck::SpamVerdict::Verdict::CONDITIONAL_ALLOW | Spam::SpamConstants::CONDITIONAL_ALLOW
::Spamcheck::SpamVerdict::Verdict::DISALLOW | Spam::SpamConstants::DISALLOW
::Spamcheck::SpamVerdict::Verdict::BLOCK | Spam::SpamConstants::BLOCK_USER
end
with_them do
let(:verdict_value) { verdict }
it "returns expected spam constant" do
expect(subject).to eq([expected, ""])
end
end
end
describe "#build_issue_protobuf", :aggregate_failures do
it 'builds the expected protobuf object' do
cxt = { action: :create }
issue_pb = described_class.new.send(:build_issue_protobuf,
issue: issue, user: user,
context: cxt)
expect(issue_pb.title).to eq issue.title
expect(issue_pb.description).to eq issue.description
expect(issue_pb.user_in_project). to be false
expect(issue_pb.created_at).to eq timestamp_to_protobuf_timestamp(issue.created_at)
expect(issue_pb.updated_at).to eq timestamp_to_protobuf_timestamp(issue.updated_at)
expect(issue_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE)
expect(issue_pb.user.username).to eq user.username
end
end
describe '#build_user_proto_buf', :aggregate_failures do
it 'builds the expected protobuf object' do
user_pb = described_class.new.send(:build_user_protobuf, user)
expect(user_pb.username).to eq user.username
expect(user_pb.org).to eq user.organization
expect(user_pb.created_at).to eq timestamp_to_protobuf_timestamp(user.created_at)
expect(user_pb.emails.count).to be 1
expect(user_pb.emails.first.email).to eq user.email
expect(user_pb.emails.first.verified).to eq user.confirmed?
end
context 'when user has multiple email addresses' do
let(:secondary_email) {create(:email, :confirmed, user: user)}
before do
user.emails << secondary_email
end
it 'adds emsils to the user pb object' do
user_pb = described_class.new.send(:build_user_protobuf, user)
expect(user_pb.emails.count).to eq 2
expect(user_pb.emails.first.email).to eq user.email
expect(user_pb.emails.first.verified).to eq user.confirmed?
expect(user_pb.emails.last.email).to eq secondary_email.email
expect(user_pb.emails.last.verified).to eq secondary_email.confirmed?
end
end
end
private
def timestamp_to_protobuf_timestamp(timestamp)
Google::Protobuf::Timestamp.new(seconds: timestamp.to_time.to_i,
nanos: timestamp.to_time.nsec)
end
end
...@@ -216,7 +216,8 @@ RSpec.describe ApplicationSetting do ...@@ -216,7 +216,8 @@ RSpec.describe ApplicationSetting do
setting.spam_check_endpoint_enabled = true setting.spam_check_endpoint_enabled = true
end end
it { is_expected.to allow_value('https://example.org/spam_check').for(:spam_check_endpoint_url) } it { is_expected.to allow_value('grpc://example.org/spam_check').for(:spam_check_endpoint_url) }
it { is_expected.not_to allow_value('https://example.org/spam_check').for(:spam_check_endpoint_url) }
it { is_expected.not_to allow_value('nonsense').for(:spam_check_endpoint_url) } it { is_expected.not_to allow_value('nonsense').for(:spam_check_endpoint_url) }
it { is_expected.not_to allow_value(nil).for(:spam_check_endpoint_url) } it { is_expected.not_to allow_value(nil).for(:spam_check_endpoint_url) }
it { is_expected.not_to allow_value('').for(:spam_check_endpoint_url) } it { is_expected.not_to allow_value('').for(:spam_check_endpoint_url) }
...@@ -227,7 +228,8 @@ RSpec.describe ApplicationSetting do ...@@ -227,7 +228,8 @@ RSpec.describe ApplicationSetting do
setting.spam_check_endpoint_enabled = false setting.spam_check_endpoint_enabled = false
end end
it { is_expected.to allow_value('https://example.org/spam_check').for(:spam_check_endpoint_url) } it { is_expected.to allow_value('grpc://example.org/spam_check').for(:spam_check_endpoint_url) }
it { is_expected.not_to allow_value('https://example.org/spam_check').for(:spam_check_endpoint_url) }
it { is_expected.not_to allow_value('nonsense').for(:spam_check_endpoint_url) } it { is_expected.not_to allow_value('nonsense').for(:spam_check_endpoint_url) }
it { is_expected.to allow_value(nil).for(:spam_check_endpoint_url) } it { is_expected.to allow_value(nil).for(:spam_check_endpoint_url) }
it { is_expected.to allow_value('').for(:spam_check_endpoint_url) } it { is_expected.to allow_value('').for(:spam_check_endpoint_url) }
......
...@@ -123,7 +123,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do ...@@ -123,7 +123,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
issues_create_limit: 300, issues_create_limit: 300,
raw_blob_request_limit: 300, raw_blob_request_limit: 300,
spam_check_endpoint_enabled: true, spam_check_endpoint_enabled: true,
spam_check_endpoint_url: 'https://example.com/spam_check', spam_check_endpoint_url: 'grpc://example.com/spam_check',
spam_check_api_key: 'SPAM_CHECK_API_KEY', spam_check_api_key: 'SPAM_CHECK_API_KEY',
disabled_oauth_sign_in_sources: 'unknown', disabled_oauth_sign_in_sources: 'unknown',
import_sources: 'github,bitbucket', import_sources: 'github,bitbucket',
...@@ -169,7 +169,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do ...@@ -169,7 +169,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['issues_create_limit']).to eq(300) expect(json_response['issues_create_limit']).to eq(300)
expect(json_response['raw_blob_request_limit']).to eq(300) expect(json_response['raw_blob_request_limit']).to eq(300)
expect(json_response['spam_check_endpoint_enabled']).to be_truthy expect(json_response['spam_check_endpoint_enabled']).to be_truthy
expect(json_response['spam_check_endpoint_url']).to eq('https://example.com/spam_check') expect(json_response['spam_check_endpoint_url']).to eq('grpc://example.com/spam_check')
expect(json_response['spam_check_api_key']).to eq('SPAM_CHECK_API_KEY') expect(json_response['spam_check_api_key']).to eq('SPAM_CHECK_API_KEY')
expect(json_response['disabled_oauth_sign_in_sources']).to eq([]) expect(json_response['disabled_oauth_sign_in_sources']).to eq([])
expect(json_response['import_sources']).to match_array(%w(github bitbucket)) expect(json_response['import_sources']).to match_array(%w(github bitbucket))
......
...@@ -9,11 +9,11 @@ RSpec.describe Spam::SpamActionService do ...@@ -9,11 +9,11 @@ RSpec.describe Spam::SpamActionService do
let(:issue) { create(:issue, project: project, author: user) } let(:issue) { create(:issue, project: project, author: user) }
let(:fake_ip) { '1.2.3.4' } let(:fake_ip) { '1.2.3.4' }
let(:fake_user_agent) { 'fake-user-agent' } let(:fake_user_agent) { 'fake-user-agent' }
let(:fake_referrer) { 'fake-http-referrer' } let(:fake_referer) { 'fake-http-referer' }
let(:env) do let(:env) do
{ 'action_dispatch.remote_ip' => fake_ip, { 'action_dispatch.remote_ip' => fake_ip,
'HTTP_USER_AGENT' => fake_user_agent, 'HTTP_USER_AGENT' => fake_user_agent,
'HTTP_REFERRER' => fake_referrer } 'HTTP_REFERER' => fake_referer }
end end
let_it_be(:project) { create(:project, :public) } let_it_be(:project) { create(:project, :public) }
...@@ -80,7 +80,7 @@ RSpec.describe Spam::SpamActionService do ...@@ -80,7 +80,7 @@ RSpec.describe Spam::SpamActionService do
{ {
ip_address: fake_ip, ip_address: fake_ip,
user_agent: fake_user_agent, user_agent: fake_user_agent,
referrer: fake_referrer referer: fake_referer
} }
end end
...@@ -222,6 +222,38 @@ RSpec.describe Spam::SpamActionService do ...@@ -222,6 +222,38 @@ RSpec.describe Spam::SpamActionService do
end end
end end
context 'spam verdict service advises to block the user' do
before do
allow(fake_verdict_service).to receive(:execute).and_return(BLOCK_USER)
end
context 'when allow_possible_spam feature flag is false' do
before do
stub_feature_flags(allow_possible_spam: false)
end
it_behaves_like 'only checks for spam if a request is provided'
it 'marks as spam' do
response = subject
expect(response.message).to match(expected_service_check_response_message)
expect(issue).to be_spam
end
end
context 'when allow_possible_spam feature flag is true' do
it_behaves_like 'only checks for spam if a request is provided'
it 'does not mark as spam' do
response = subject
expect(response.message).to match(expected_service_check_response_message)
expect(issue).not_to be_spam
end
end
end
context 'when spam verdict service conditionally allows' do context 'when spam verdict service conditionally allows' do
before do before do
allow(fake_verdict_service).to receive(:execute).and_return(CONDITIONAL_ALLOW) allow(fake_verdict_service).to receive(:execute).and_return(CONDITIONAL_ALLOW)
......
...@@ -7,18 +7,18 @@ RSpec.describe Spam::SpamVerdictService do ...@@ -7,18 +7,18 @@ RSpec.describe Spam::SpamVerdictService do
let(:fake_ip) { '1.2.3.4' } let(:fake_ip) { '1.2.3.4' }
let(:fake_user_agent) { 'fake-user-agent' } let(:fake_user_agent) { 'fake-user-agent' }
let(:fake_referrer) { 'fake-http-referrer' } let(:fake_referer) { 'fake-http-referer' }
let(:env) do let(:env) do
{ 'action_dispatch.remote_ip' => fake_ip, { 'action_dispatch.remote_ip' => fake_ip,
'HTTP_USER_AGENT' => fake_user_agent, 'HTTP_USER_AGENT' => fake_user_agent,
'HTTP_REFERRER' => fake_referrer } 'HTTP_REFERER' => fake_referer }
end end
let(:request) { double(:request, env: env) } let(:request) { double(:request, env: env) }
let(:check_for_spam) { true } let(:check_for_spam) { true }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:issue) { build(:issue, author: user) } let_it_be(:issue) { create(:issue, author: user) }
let(:service) do let(:service) do
described_class.new(user: user, target: issue, request: request, options: {}) described_class.new(user: user, target: issue, request: request, options: {})
end end
...@@ -28,7 +28,7 @@ RSpec.describe Spam::SpamVerdictService do ...@@ -28,7 +28,7 @@ RSpec.describe Spam::SpamVerdictService do
before do before do
allow(service).to receive(:akismet_verdict).and_return(nil) allow(service).to receive(:akismet_verdict).and_return(nil)
allow(service).to receive(:external_verdict).and_return(nil) allow(service).to receive(:spamcheck_verdict).and_return(nil)
end end
context 'if all services return nil' do context 'if all services return nil' do
...@@ -63,7 +63,7 @@ RSpec.describe Spam::SpamVerdictService do ...@@ -63,7 +63,7 @@ RSpec.describe Spam::SpamVerdictService do
context 'and they are supported' do context 'and they are supported' do
before do before do
allow(service).to receive(:akismet_verdict).and_return(DISALLOW) allow(service).to receive(:akismet_verdict).and_return(DISALLOW)
allow(service).to receive(:external_verdict).and_return(BLOCK_USER) allow(service).to receive(:spamcheck_verdict).and_return(BLOCK_USER)
end end
it 'renders the more restrictive verdict' do it 'renders the more restrictive verdict' do
...@@ -74,7 +74,7 @@ RSpec.describe Spam::SpamVerdictService do ...@@ -74,7 +74,7 @@ RSpec.describe Spam::SpamVerdictService do
context 'and one is supported' do context 'and one is supported' do
before do before do
allow(service).to receive(:akismet_verdict).and_return('nonsense') allow(service).to receive(:akismet_verdict).and_return('nonsense')
allow(service).to receive(:external_verdict).and_return(BLOCK_USER) allow(service).to receive(:spamcheck_verdict).and_return(BLOCK_USER)
end end
it 'renders the more restrictive verdict' do it 'renders the more restrictive verdict' do
...@@ -85,7 +85,7 @@ RSpec.describe Spam::SpamVerdictService do ...@@ -85,7 +85,7 @@ RSpec.describe Spam::SpamVerdictService do
context 'and none are supported' do context 'and none are supported' do
before do before do
allow(service).to receive(:akismet_verdict).and_return('nonsense') allow(service).to receive(:akismet_verdict).and_return('nonsense')
allow(service).to receive(:external_verdict).and_return('rubbish') allow(service).to receive(:spamcheck_verdict).and_return('rubbish')
end end
it 'renders the more restrictive verdict' do it 'renders the more restrictive verdict' do
...@@ -150,48 +150,87 @@ RSpec.describe Spam::SpamVerdictService do ...@@ -150,48 +150,87 @@ RSpec.describe Spam::SpamVerdictService do
end end
end end
describe '#external_verdict' do describe '#spamcheck_verdict' do
subject { service.send(:external_verdict) } subject { service.send(:spamcheck_verdict) }
context 'if a Spam Check endpoint enabled and set to a URL' do context 'if a Spam Check endpoint enabled and set to a URL' do
let(:spam_check_body) { {} } let(:spam_check_body) { {} }
let(:spam_check_http_status) { nil } let(:endpoint_url) { "grpc://www.spamcheckurl.com/spam_check" }
let(:spam_client) do
Gitlab::Spamcheck::Client.new
end
before do before do
stub_application_setting(spam_check_endpoint_enabled: true) stub_application_setting(spam_check_endpoint_enabled: true)
stub_application_setting(spam_check_endpoint_url: "http://www.spamcheckurl.com/spam_check") stub_application_setting(spam_check_endpoint_url: endpoint_url)
stub_request(:post, /.*spamcheckurl.com.*/).to_return( body: spam_check_body.to_json, status: spam_check_http_status )
end end
context 'if the endpoint is accessible' do context 'if the endpoint is accessible' do
let(:spam_check_http_status) { 200 } let(:error) { '' }
let(:error) { nil }
let(:verdict) { nil } let(:verdict) { nil }
let(:spam_check_body) do
{ verdict: verdict, error: error } before do
allow(service).to receive(:spamcheck_client).and_return(spam_client)
allow(spam_client).to receive(:issue_spam?).and_return([verdict, error])
end end
context 'the result is a valid verdict' do context 'the result is a valid verdict' do
let(:verdict) { 'allow' } let(:verdict) { ALLOW }
it 'returns the verdict' do it 'returns the verdict' do
expect(subject).to eq ALLOW expect(subject).to eq ALLOW
end end
end end
context 'the verdict is an unexpected string' do context 'when recaptcha is enabled' do
let(:verdict) { 'this is fine' } before do
allow(Gitlab::Recaptcha).to receive(:enabled?).and_return(true)
end
it 'returns the string' do using RSpec::Parameterized::TableSyntax
expect(subject).to eq verdict
# rubocop: disable Lint/BinaryOperatorWithIdenticalOperands
where(:verdict_value, :expected) do
::Spam::SpamConstants::ALLOW | ::Spam::SpamConstants::ALLOW
::Spam::SpamConstants::CONDITIONAL_ALLOW | ::Spam::SpamConstants::CONDITIONAL_ALLOW
::Spam::SpamConstants::DISALLOW | ::Spam::SpamConstants::CONDITIONAL_ALLOW
::Spam::SpamConstants::BLOCK_USER | ::Spam::SpamConstants::CONDITIONAL_ALLOW
end
# rubocop: enable Lint/BinaryOperatorWithIdenticalOperands
with_them do
let(:verdict) { verdict_value }
it "returns expected spam constant" do
expect(subject).to eq(expected)
end
end end
end end
context 'the JSON is malformed' do context 'when recaptcha is disabled' do
let(:spam_check_body) { 'this is fine' } before do
allow(Gitlab::Recaptcha).to receive(:enabled?).and_return(false)
end
it 'returns allow' do [::Spam::SpamConstants::ALLOW,
expect(subject).to eq ALLOW ::Spam::SpamConstants::CONDITIONAL_ALLOW,
::Spam::SpamConstants::DISALLOW,
::Spam::SpamConstants::BLOCK_USER].each do |verdict_value|
let(:verdict) { verdict_value }
let(:expected) { verdict_value }
it "returns expected spam constant" do
expect(subject).to eq(expected)
end
end
end
context 'the verdict is an unexpected value' do
let(:verdict) { :this_is_fine }
it 'returns the string' do
expect(subject).to eq verdict
end end
end end
...@@ -219,11 +258,13 @@ RSpec.describe Spam::SpamVerdictService do ...@@ -219,11 +258,13 @@ RSpec.describe Spam::SpamVerdictService do
end end
end end
context 'the HTTP status is not 200' do context 'the requested is aborted' do
let(:spam_check_http_status) { 500 } before do
allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::Aborted)
end
it 'returns nil' do it 'returns nil' do
expect(subject).to be_nil expect(subject).to be(ALLOW)
end end
end end
...@@ -239,11 +280,11 @@ RSpec.describe Spam::SpamVerdictService do ...@@ -239,11 +280,11 @@ RSpec.describe Spam::SpamVerdictService do
context 'if the endpoint times out' do context 'if the endpoint times out' do
before do before do
stub_request(:post, /.*spamcheckurl.com.*/).to_timeout allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::DeadlineExceeded)
end end
it 'returns nil' do it 'returns nil' do
expect(subject).to be_nil expect(subject).to be(ALLOW)
end end
end 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