diff --git a/Gemfile b/Gemfile index c4a704735661525b639a31c866c180872b0958bf..b6abf01139092a36802ac3311b2786b6f4ac839e 100644 --- a/Gemfile +++ b/Gemfile @@ -482,7 +482,7 @@ gem 'net-ntp' gem 'ssh_data', '~> 1.3' # Spamcheck GRPC protocol definitions -gem 'spamcheck', '~> 0.1.0' +gem 'spamcheck', '~> 1.0.0' # Gitaly GRPC protocol definitions gem 'gitaly', '~> 15.3.0-rc4' diff --git a/Gemfile.lock b/Gemfile.lock index febed30c06126d8d5d6872d91f2b6d22bc8fe596..79de891dee357413ce41497558443d868e4bb1a7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1314,7 +1314,7 @@ GEM sorted_set (1.0.3) rbtree set (~> 1.0) - spamcheck (0.1.0) + spamcheck (1.0.0) grpc (~> 1.0) spring (2.1.1) spring-commands-rspec (1.0.4) @@ -1746,7 +1746,7 @@ DEPENDENCIES slack-messenger (~> 2.3.4) snowplow-tracker (~> 0.6.1) solargraph (~> 0.45.0) - spamcheck (~> 0.1.0) + spamcheck (~> 1.0.0) spring (~> 2.1.0) spring-commands-rspec (~> 1.0.4) sprite-factory (~> 1.7) diff --git a/app/models/snippet.rb b/app/models/snippet.rb index fd882633a440d58307b2c7474567813a20c0be0f..55dcc39e72cd5693847c7555a9d67a389274e1c3 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -91,7 +91,7 @@ def content_html_invalidated? participant :notes_with_associations attr_spammable :title, spam_title: true - attr_spammable :content, spam_description: true + attr_spammable :description, spam_description: true attr_encrypted :secret_token, key: Settings.attr_encrypted_db_key_base_truncated, @@ -276,13 +276,7 @@ def notes_with_associations def check_for_spam?(user:) visibility_level_changed?(to: Snippet::PUBLIC) || - (public? && (title_changed? || content_changed?)) - end - - # snippets are the biggest sources of spam - override :allow_possible_spam? - def allow_possible_spam? - false + (public? && (title_changed? || description_changed?)) end def spammable_entity_type diff --git a/app/services/snippets/base_service.rb b/app/services/snippets/base_service.rb index 1a04c4fcedd97cb99c71426e21ac3d1c69224fd4..42e62d65ee4ff7c175615acd4334223e6c19b88c 100644 --- a/app/services/snippets/base_service.rb +++ b/app/services/snippets/base_service.rb @@ -73,6 +73,15 @@ def repository_error_message(error) message end + def file_paths_to_commit + paths = [] + snippet_actions.to_commit_actions.each do |action| + paths << { path: action[:file_path] } + end + + paths + end + def files_to_commit(snippet) snippet_actions.to_commit_actions.presence || build_actions_from_params(snippet) end diff --git a/app/services/snippets/create_service.rb b/app/services/snippets/create_service.rb index 6d3b63de9fd75dadef5c6d65eb19b2d1e51c29fe..e0bab4cd6adc4101bf6356916a4d2e497ebf3c1f 100644 --- a/app/services/snippets/create_service.rb +++ b/app/services/snippets/create_service.rb @@ -24,7 +24,8 @@ def execute spammable: @snippet, spam_params: spam_params, user: current_user, - action: :create + action: :create, + extra_features: { files: file_paths_to_commit } ).execute if save_and_commit diff --git a/app/services/snippets/update_service.rb b/app/services/snippets/update_service.rb index 76d5063c3370ff6ce03c4b691b1e1d4750e309c9..067680f2abc6b7965b0568b0c44163b38101c72c 100644 --- a/app/services/snippets/update_service.rb +++ b/app/services/snippets/update_service.rb @@ -23,11 +23,14 @@ def execute(snippet) update_snippet_attributes(snippet) + files = snippet.all_files.map { |f| { path: f } } + file_paths_to_commit + Spam::SpamActionService.new( spammable: snippet, spam_params: spam_params, user: current_user, - action: :update + action: :update, + extra_features: { files: files } ).execute if save_and_commit(snippet) diff --git a/app/services/spam/spam_action_service.rb b/app/services/spam/spam_action_service.rb index 4fa9c0e4993ee06ce1d5694e92d05d05f169fe2b..9c52e9f0cd3f4e50d3a0a211fd20a300f3a5fd77 100644 --- a/app/services/spam/spam_action_service.rb +++ b/app/services/spam/spam_action_service.rb @@ -4,11 +4,12 @@ module Spam class SpamActionService include SpamConstants - def initialize(spammable:, spam_params:, user:, action:) + def initialize(spammable:, spam_params:, user:, action:, extra_features: {}) @target = spammable @spam_params = spam_params @user = user @action = action + @extra_features = extra_features end # rubocop:disable Metrics/AbcSize @@ -40,7 +41,7 @@ def execute private - attr_reader :user, :action, :target, :spam_params, :spam_log + attr_reader :user, :action, :target, :spam_params, :spam_log, :extra_features ## # In order to be proceed to the spam check process, the target must be @@ -124,7 +125,9 @@ def spam_verdict_service SpamVerdictService.new(target: target, user: user, options: options, - context: context) + context: context, + extra_features: extra_features + ) end def noteable_type diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb index e73b2666c02c8d8a3a60a7be6032adb4f4c3a386..382545556ab55d00f9fc93855d68614dc3122deb 100644 --- a/app/services/spam/spam_verdict_service.rb +++ b/app/services/spam/spam_verdict_service.rb @@ -5,11 +5,12 @@ class SpamVerdictService include AkismetMethods include SpamConstants - def initialize(user:, target:, options:, context: {}) + def initialize(user:, target:, options:, context: {}, extra_features: {}) @target = target @user = user @options = options @context = context + @extra_features = extra_features end def execute @@ -61,7 +62,7 @@ def execute private - attr_reader :user, :target, :options, :context + attr_reader :user, :target, :options, :context, :extra_features def akismet_verdict if akismet.spam? @@ -75,7 +76,8 @@ def spamcheck_verdict return unless Gitlab::CurrentSettings.spam_check_endpoint_enabled begin - result, attribs, _error = spamcheck_client.issue_spam?(spam_issue: target, user: user, context: context) + result, attribs, _error = spamcheck_client.spam?(spammable: target, user: user, context: context, + extra_features: extra_features) # @TODO log if error is not nil https://gitlab.com/gitlab-org/gitlab/-/issues/329545 return [nil, attribs] unless result diff --git a/lib/gitlab/spamcheck/client.rb b/lib/gitlab/spamcheck/client.rb index 40b01552244ad4809697c470c4ebe5087dc8ff30..b7ac6224e5ca2c2dd24bad1e0c52f955b868a9c6 100644 --- a/lib/gitlab/spamcheck/client.rb +++ b/lib/gitlab/spamcheck/client.rb @@ -33,33 +33,50 @@ def initialize @endpoint_url = @endpoint_url.sub(URL_SCHEME_REGEX, '') end - def issue_spam?(spam_issue:, user:, context: {}) - issue = build_issue_protobuf(issue: spam_issue, user: user, context: context) + def spam?(spammable:, user:, context: {}, extra_features: {}) + metadata = { 'authorization' => Gitlab::CurrentSettings.spam_check_api_key } + protobuf_args = { spammable: spammable, user: user, context: context, extra_features: extra_features } + + pb, grpc_method = build_protobuf(**protobuf_args) + response = grpc_method.call(pb, metadata: metadata) - response = grpc_client.check_for_spam_issue(issue, - metadata: { 'authorization' => - Gitlab::CurrentSettings.spam_check_api_key }) verdict = convert_verdict_to_gitlab_constant(response.verdict) [verdict, response.extra_attributes.to_h, response.error] end private + def get_spammable_mappings(spammable) + case spammable + when Issue + [::Spamcheck::Issue, grpc_client.method(:check_for_spam_issue)] + when Snippet + [::Spamcheck::Snippet, grpc_client.method(:check_for_spam_snippet)] + else + raise ArgumentError, "Not a spammable type: #{spammable.class.name}" + end + end + 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.project = build_project_protobuf(issue) - issue_pb.action = ACTION_MAPPING.fetch(context.fetch(:action)) if context.has_key?(:action) - issue_pb.user = build_user_protobuf(user) - issue_pb + def build_protobuf(spammable:, user:, context:, extra_features:) + protobuf_class, grpc_method = get_spammable_mappings(spammable) + pb = protobuf_class.new(**extra_features) + pb.title = spammable.spam_title || '' + pb.description = spammable.spam_description || '' + pb.created_at = convert_to_pb_timestamp(spammable.created_at) if spammable.created_at + pb.updated_at = convert_to_pb_timestamp(spammable.updated_at) if spammable.updated_at + pb.action = ACTION_MAPPING.fetch(context.fetch(:action)) if context.has_key?(:action) + pb.user = build_user_protobuf(user) + + unless spammable.project.nil? + pb.user_in_project = user.authorized_project?(spammable.project) + pb.project = build_project_protobuf(spammable) + end + + [pb, grpc_method] end def build_user_protobuf(user) diff --git a/spec/lib/gitlab/spamcheck/client_spec.rb b/spec/lib/gitlab/spamcheck/client_spec.rb index 956ed2a976fb2ddcc098a951c44fa3d7984da005..2fe978125c4da6db48b7ff4bee727b88eb887730 100644 --- a/spec/lib/gitlab/spamcheck/client_spec.rb +++ b/spec/lib/gitlab/spamcheck/client_spec.rb @@ -17,6 +17,7 @@ end let_it_be(:issue) { create(:issue, description: 'Test issue description') } + let_it_be(:snippet) { create(:personal_snippet, :public, description: 'Test issue description') } let(:response) do verdict = ::Spamcheck::SpamVerdict.new @@ -26,7 +27,7 @@ verdict end - subject { described_class.new.issue_spam?(spam_issue: issue, user: user) } + subject { described_class.new.spam?(spammable: issue, user: user) } before do stub_application_setting(spam_check_endpoint_url: endpoint) @@ -56,10 +57,11 @@ end end - describe '#issue_spam?' do + shared_examples 'check for spam' do before do allow_next_instance_of(::Spamcheck::SpamcheckService::Stub) do |instance| allow(instance).to receive(:check_for_spam_issue).and_return(response) + allow(instance).to receive(:check_for_spam_snippet).and_return(response) end end @@ -89,12 +91,26 @@ end end - describe "#build_issue_protobuf", :aggregate_failures do - it 'builds the expected protobuf object' do + describe "#spam?", :aggregate_failures do + describe 'issue' do + subject { described_class.new.spam?(spammable: issue, user: user) } + + it_behaves_like "check for spam" + end + + describe 'snippet' do + subject { described_class.new.spam?(spammable: snippet, user: user, extra_features: { files: [{ path: "file.rb" }] }) } + + it_behaves_like "check for spam" + end + end + + describe "#build_protobuf", :aggregate_failures do + it 'builds the expected issue protobuf object' do cxt = { action: :create } - issue_pb = described_class.new.send(:build_issue_protobuf, - issue: issue, user: user, - context: cxt) + issue_pb, _ = described_class.new.send(:build_protobuf, + spammable: issue, user: user, + context: cxt, extra_features: {}) 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 @@ -104,6 +120,22 @@ expect(issue_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE) expect(issue_pb.user.username).to eq user.username end + + it 'builds the expected snippet protobuf object' do + cxt = { action: :create } + snippet_pb, _ = described_class.new.send(:build_protobuf, + spammable: snippet, user: user, + context: cxt, extra_features: { files: [{ path: 'first.rb' }, { path: 'second.rb' }] }) + expect(snippet_pb.title).to eq snippet.title + expect(snippet_pb.description).to eq snippet.description + expect(snippet_pb.created_at).to eq timestamp_to_protobuf_timestamp(snippet.created_at) + expect(snippet_pb.updated_at).to eq timestamp_to_protobuf_timestamp(snippet.updated_at) + expect(snippet_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE) + expect(snippet_pb.user.username).to eq user.username + expect(snippet_pb.user.username).to eq user.username + expect(snippet_pb.files.first.path).to eq 'first.rb' + expect(snippet_pb.files.last.path).to eq 'second.rb' + end end describe '#build_user_protobuf', :aggregate_failures do @@ -143,6 +175,19 @@ end end + describe "#get_spammable_mappings", :aggregate_failures do + it 'is an expected spammable' do + protobuf_class, _ = described_class.new.send(:get_spammable_mappings, issue) + expect(protobuf_class).to eq ::Spamcheck::Issue + end + + it 'is an unexpected spammable' do + expect { described_class.new.send(:get_spammable_mappings, 'spam') }.to raise_error( + ArgumentError, 'Not a spammable type: String' + ) + end + end + private def timestamp_to_protobuf_timestamp(timestamp) diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 72519ed168337ba003259b9ef4b4e30cf0b9cd7b..6e2dd6e76a965eedd77e044174f4a4713bd82103 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -256,6 +256,7 @@ allow_next_instance_of(Spam::AkismetService) do |instance| allow(instance).to receive(:spam?).and_return(true) end + stub_feature_flags(allow_possible_spam: false) project.add_developer(user) end @@ -311,6 +312,8 @@ allow_next_instance_of(Spam::AkismetService) do |instance| allow(instance).to receive(:spam?).and_return(true) end + + stub_feature_flags(allow_possible_spam: false) end context 'when the snippet is private' do diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 0dd6e484e8d30107373c0dfd37d0358d5bdab682..031bcb612f408ebe724971c4dfa27b7f6cf37955 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -340,6 +340,7 @@ allow_next_instance_of(Spam::AkismetService) do |instance| allow(instance).to receive(:spam?).and_return(true) end + stub_feature_flags(allow_possible_spam: false) end context 'when the snippet is private' do @@ -405,6 +406,7 @@ allow_next_instance_of(Spam::AkismetService) do |instance| allow(instance).to receive(:spam?).and_return(true) end + stub_feature_flags(allow_possible_spam: false) end context 'when the snippet is private' do diff --git a/spec/services/spam/spam_action_service_spec.rb b/spec/services/spam/spam_action_service_spec.rb index bd8418d70920cca7098ab57c5b56a16aa988e632..4dfec9735ba7296506c809bf290295688220218c 100644 --- a/spec/services/spam/spam_action_service_spec.rb +++ b/spec/services/spam/spam_action_service_spec.rb @@ -6,6 +6,8 @@ include_context 'includes Spam constants' let(:issue) { create(:issue, project: project, author: author) } + let(:personal_snippet) { create(:personal_snippet, :public, author: author) } + let(:project_snippet) { create(:project_snippet, :public, author: author) } let(:fake_ip) { '1.2.3.4' } let(:fake_user_agent) { 'fake-user-agent' } let(:fake_referer) { 'fake-http-referer' } @@ -27,6 +29,7 @@ before do issue.spam = false + personal_snippet.spam = false end describe 'constructor argument validation' do @@ -50,24 +53,24 @@ end end - shared_examples 'creates a spam log' do + shared_examples 'creates a spam log' do |target_type| it do expect { subject } - .to log_spam(title: issue.title, description: issue.description, noteable_type: 'Issue') + .to log_spam(title: target.title, description: target.description, noteable_type: target_type) # TODO: These checks should be incorporated into the `log_spam` RSpec matcher above new_spam_log = SpamLog.last expect(new_spam_log.user_id).to eq(user.id) - expect(new_spam_log.title).to eq(issue.title) - expect(new_spam_log.description).to eq(issue.description) + expect(new_spam_log.title).to eq(target.title) + expect(new_spam_log.description).to eq(target.spam_description) expect(new_spam_log.source_ip).to eq(fake_ip) expect(new_spam_log.user_agent).to eq(fake_user_agent) - expect(new_spam_log.noteable_type).to eq('Issue') + expect(new_spam_log.noteable_type).to eq(target_type) expect(new_spam_log.via_api).to eq(true) end end - describe '#execute' do + shared_examples 'execute spam action service' do |target_type| let(:fake_captcha_verification_service) { double(:captcha_verification_service) } let(:fake_verdict_service) { double(:spam_verdict_service) } let(:allowlisted) { false } @@ -82,20 +85,22 @@ let(:verdict_service_args) do { - target: issue, + target: target, user: user, options: verdict_service_opts, context: { action: :create, - target_type: 'Issue' - } + target_type: target_type + }, + extra_features: extra_features } end let_it_be(:existing_spam_log) { create(:spam_log, user: user, recaptcha_verified: false) } subject do - described_service = described_class.new(spammable: issue, spam_params: spam_params, user: user, action: :create) + described_service = described_class.new(spammable: target, spam_params: spam_params, extra_features: + extra_features, user: user, action: :create) allow(described_service).to receive(:allowlisted?).and_return(allowlisted) described_service.execute end @@ -136,7 +141,7 @@ context 'when spammable attributes have not changed' do before do - issue.closed_at = Time.zone.now + allow(target).to receive(:has_changes_to_save?).and_return(true) end it 'does not create a spam log' do @@ -146,11 +151,11 @@ context 'when spammable attributes have changed' do let(:expected_service_check_response_message) do - /Check Issue spammable model for any errors or CAPTCHA requirement/ + /Check #{target_type} spammable model for any errors or CAPTCHA requirement/ end before do - issue.description = 'Lovely Spam! Wonderful Spam!' + target.description = 'Lovely Spam! Wonderful Spam!' end context 'when allowlisted' do @@ -170,13 +175,13 @@ allow(fake_verdict_service).to receive(:execute).and_return(DISALLOW) end - it_behaves_like 'creates a spam log' + it_behaves_like 'creates a spam log', target_type it 'marks as spam' do response = subject expect(response.message).to match(expected_service_check_response_message) - expect(issue).to be_spam + expect(target).to be_spam end end @@ -185,13 +190,13 @@ allow(fake_verdict_service).to receive(:execute).and_return(BLOCK_USER) end - it_behaves_like 'creates a spam log' + it_behaves_like 'creates a spam log', target_type it 'marks as spam' do response = subject expect(response.message).to match(expected_service_check_response_message) - expect(issue).to be_spam + expect(target).to be_spam end end @@ -200,20 +205,20 @@ allow(fake_verdict_service).to receive(:execute).and_return(CONDITIONAL_ALLOW) end - it_behaves_like 'creates a spam log' + it_behaves_like 'creates a spam log', target_type 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 + expect(target).not_to be_spam end it 'marks as needing reCAPTCHA' do response = subject expect(response.message).to match(expected_service_check_response_message) - expect(issue).to be_needs_recaptcha + expect(target).to be_needs_recaptcha end end @@ -222,20 +227,20 @@ allow(fake_verdict_service).to receive(:execute).and_return(OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM) end - it_behaves_like 'creates a spam log' + it_behaves_like 'creates a spam log', target_type 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 + expect(target).not_to be_spam end it 'does not mark as needing CAPTCHA' do response = subject expect(response.message).to match(expected_service_check_response_message) - expect(issue).not_to be_needs_recaptcha + expect(target).not_to be_needs_recaptcha end end @@ -249,7 +254,7 @@ end it 'clears spam flags' do - expect(issue).to receive(:clear_spam_flags!) + expect(target).to receive(:clear_spam_flags!) subject end @@ -265,7 +270,7 @@ end it 'clears spam flags' do - expect(issue).to receive(:clear_spam_flags!) + expect(target).to receive(:clear_spam_flags!) subject end @@ -285,4 +290,27 @@ end end end + + describe '#execute' do + describe 'issue' do + let(:target) { issue } + let(:extra_features) { {} } + + it_behaves_like 'execute spam action service', 'Issue' + end + + describe 'project snippet' do + let(:target) { project_snippet } + let(:extra_features) { { files: [{ path: 'project.rb' }] } } + + it_behaves_like 'execute spam action service', 'ProjectSnippet' + end + + describe 'personal snippet' do + let(:target) { personal_snippet } + let(:extra_features) { { files: [{ path: 'personal.rb' }] } } + + it_behaves_like 'execute spam action service', 'PersonalSnippet' + end + end end diff --git a/spec/services/spam/spam_verdict_service_spec.rb b/spec/services/spam/spam_verdict_service_spec.rb index 082b8f909f95f21c000de290e47aa0cd33cd6702..02dbc1004bfb5b53b3ab524e899b8109f993d412 100644 --- a/spec/services/spam/spam_verdict_service_spec.rb +++ b/spec/services/spam/spam_verdict_service_spec.rb @@ -17,9 +17,10 @@ let(:check_for_spam) { true } let_it_be(:user) { create(:user) } let_it_be(:issue) { create(:issue, author: user) } + let_it_be(:snippet) { create(:personal_snippet, :public, author: user) } let(:service) do - described_class.new(user: user, target: issue, options: {}) + described_class.new(user: user, target: target, options: {}) end let(:attribs) do @@ -31,7 +32,7 @@ stub_feature_flags(allow_possible_spam: false) end - describe '#execute' do + shared_examples 'execute spam verdict service' do subject { service.execute } before do @@ -172,7 +173,8 @@ end end - describe '#akismet_verdict' do + shared_examples 'akismet verdict' do + let(:target) { issue } subject { service.send(:akismet_verdict) } context 'if Akismet is enabled' do @@ -227,7 +229,7 @@ end end - describe '#spamcheck_verdict' do + shared_examples 'spamcheck verdict' do subject { service.send(:spamcheck_verdict) } context 'if a Spam Check endpoint enabled and set to a URL' do @@ -254,7 +256,7 @@ before do allow(service).to receive(:spamcheck_client).and_return(spam_client) - allow(spam_client).to receive(:issue_spam?).and_return([verdict, attribs, error]) + allow(spam_client).to receive(:spam?).and_return([verdict, attribs, error]) end context 'if the result is a NOOP verdict' do @@ -365,7 +367,7 @@ let(:attribs) { nil } before do - allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::Aborted) + allow(spam_client).to receive(:spam?).and_raise(GRPC::Aborted) end it 'returns nil' do @@ -387,7 +389,7 @@ let(:attribs) { nil } before do - allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::DeadlineExceeded) + allow(spam_client).to receive(:spam?).and_raise(GRPC::DeadlineExceeded) end it 'returns nil' do @@ -416,4 +418,46 @@ end end end + + describe '#execute' do + describe 'issue' do + let(:target) { issue } + + it_behaves_like 'execute spam verdict service' + end + + describe 'snippet' do + let(:target) { snippet } + + it_behaves_like 'execute spam verdict service' + end + end + + describe '#akismet_verdict' do + describe 'issue' do + let(:target) { issue } + + it_behaves_like 'akismet verdict' + end + + describe 'snippet' do + let(:target) { snippet } + + it_behaves_like 'akismet verdict' + end + end + + describe '#spamcheck_verdict' do + describe 'issue' do + let(:target) { issue } + + it_behaves_like 'spamcheck verdict' + end + + describe 'snippet' do + let(:target) { snippet } + + it_behaves_like 'spamcheck verdict' + end + end end diff --git a/spec/support/shared_examples/services/snippets_shared_examples.rb b/spec/support/shared_examples/services/snippets_shared_examples.rb index 7629cfa976dc710a329dd6c6210ccb8582995175..65893d84798528bbeb49d63c99e8642b5b5d457f 100644 --- a/spec/support/shared_examples/services/snippets_shared_examples.rb +++ b/spec/support/shared_examples/services/snippets_shared_examples.rb @@ -14,7 +14,8 @@ spammable: kind_of(Snippet), spam_params: spam_params, user: an_instance_of(User), - action: action + action: action, + extra_features: { files: an_instance_of(Array) } } ) do |instance| expect(instance).to receive(:execute)