2020-01-17 00:09:00 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require 'spec_helper'
|
|
|
|
|
|
|
|
describe Snippets::CreateService do
|
|
|
|
describe '#execute' do
|
|
|
|
let_it_be(:user) { create(:user) }
|
|
|
|
let_it_be(:admin) { create(:user, :admin) }
|
|
|
|
let(:opts) { base_opts.merge(extra_opts) }
|
|
|
|
let(:base_opts) do
|
|
|
|
{
|
|
|
|
title: 'Test snippet',
|
|
|
|
file_name: 'snippet.rb',
|
|
|
|
content: 'puts "hello world"',
|
|
|
|
visibility_level: Gitlab::VisibilityLevel::PRIVATE
|
|
|
|
}
|
|
|
|
end
|
|
|
|
let(:extra_opts) { {} }
|
|
|
|
let(:creator) { admin }
|
|
|
|
|
2020-02-28 00:09:08 +00:00
|
|
|
subject { described_class.new(project, creator, opts).execute }
|
2020-01-17 00:09:00 +00:00
|
|
|
|
|
|
|
let(:snippet) { subject.payload[:snippet] }
|
|
|
|
|
|
|
|
shared_examples 'a service that creates a snippet' do
|
|
|
|
it 'creates a snippet with the provided attributes' do
|
|
|
|
expect(snippet.title).to eq(opts[:title])
|
|
|
|
expect(snippet.file_name).to eq(opts[:file_name])
|
|
|
|
expect(snippet.content).to eq(opts[:content])
|
|
|
|
expect(snippet.visibility_level).to eq(opts[:visibility_level])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_examples 'public visibility level restrictions apply' do
|
|
|
|
let(:extra_opts) { { visibility_level: Gitlab::VisibilityLevel::PUBLIC } }
|
|
|
|
|
|
|
|
before do
|
|
|
|
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when user is not an admin' do
|
|
|
|
let(:creator) { user }
|
|
|
|
|
|
|
|
it 'responds with an error' do
|
|
|
|
expect(subject).to be_error
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not create a public snippet' do
|
|
|
|
expect(subject.message).to match('has been restricted')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when user is an admin' do
|
|
|
|
it 'responds with success' do
|
|
|
|
expect(subject).to be_success
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'creates a public snippet' do
|
|
|
|
expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'when visibility level is passed as a string' do
|
|
|
|
let(:extra_opts) { { visibility: 'internal' } }
|
|
|
|
|
|
|
|
before do
|
|
|
|
base_opts.delete(:visibility_level)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'assigns the correct visibility level' do
|
|
|
|
expect(subject).to be_success
|
|
|
|
expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_examples 'spam check is performed' do
|
|
|
|
shared_examples 'marked as spam' do
|
2020-04-07 03:09:15 +00:00
|
|
|
it 'marks a snippet as spam' do
|
2020-01-17 00:09:00 +00:00
|
|
|
expect(snippet).to be_spam
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'invalidates the snippet' do
|
|
|
|
expect(snippet).to be_invalid
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'creates a new spam_log' do
|
|
|
|
expect { snippet }
|
2020-01-23 15:08:46 +00:00
|
|
|
.to have_spam_log(title: snippet.title, noteable_type: snippet.class.name)
|
2020-01-17 00:09:00 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'assigns a spam_log to an issue' do
|
|
|
|
expect(snippet.spam_log).to eq(SpamLog.last)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
let(:extra_opts) do
|
|
|
|
{ visibility_level: Gitlab::VisibilityLevel::PUBLIC, request: double(:request, env: {}) }
|
|
|
|
end
|
|
|
|
|
|
|
|
before do
|
2020-01-30 15:09:15 +00:00
|
|
|
expect_next_instance_of(Spam::AkismetService) do |akismet_service|
|
2020-01-17 00:09:00 +00:00
|
|
|
expect(akismet_service).to receive_messages(spam?: true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
[true, false, nil].each do |allow_possible_spam|
|
|
|
|
context "when recaptcha_disabled flag is #{allow_possible_spam.inspect}" do
|
|
|
|
before do
|
|
|
|
stub_feature_flags(allow_possible_spam: allow_possible_spam) unless allow_possible_spam.nil?
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'marked as spam'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_examples 'snippet create data is tracked' do
|
|
|
|
let(:counter) { Gitlab::UsageDataCounters::SnippetCounter }
|
|
|
|
|
|
|
|
it 'increments count when create succeeds' do
|
|
|
|
expect { subject }.to change { counter.read(:create) }.by 1
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when create fails' do
|
|
|
|
let(:opts) { {} }
|
|
|
|
|
|
|
|
it 'does not increment count' do
|
|
|
|
expect { subject }.not_to change { counter.read(:create) }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_examples 'an error service response when save fails' do
|
|
|
|
let(:extra_opts) { { content: nil } }
|
|
|
|
|
|
|
|
it 'responds with an error' do
|
|
|
|
expect(subject).to be_error
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not create the snippet' do
|
|
|
|
expect { subject }.not_to change { Snippet.count }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-02-28 18:09:07 +00:00
|
|
|
shared_examples 'creates repository and files' do
|
|
|
|
it 'creates repository' do
|
2020-02-17 12:09:20 +00:00
|
|
|
subject
|
|
|
|
|
2020-02-28 18:09:07 +00:00
|
|
|
expect(snippet.repository.exists?).to be_truthy
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'commit the files to the repository' do
|
|
|
|
subject
|
|
|
|
|
|
|
|
blob = snippet.repository.blob_at('master', base_opts[:file_name])
|
|
|
|
|
|
|
|
expect(blob.data).to eq base_opts[:content]
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when repository creation action fails' do
|
|
|
|
before do
|
|
|
|
allow_next_instance_of(Snippet) do |instance|
|
|
|
|
allow(instance).to receive(:create_repository).and_return(nil)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not create the snippet' do
|
|
|
|
expect { subject }.not_to change { Snippet.count }
|
|
|
|
end
|
|
|
|
|
2020-05-13 21:08:55 +00:00
|
|
|
it 'returns a generic creation error' do
|
|
|
|
expect(snippet.errors[:repository]).to eq ['Error creating the snippet - Repository could not be created']
|
2020-02-28 18:09:07 +00:00
|
|
|
end
|
2020-03-27 00:08:09 +00:00
|
|
|
|
|
|
|
it 'does not return a snippet with an id' do
|
|
|
|
expect(snippet.id).to be_nil
|
|
|
|
end
|
2020-02-28 18:09:07 +00:00
|
|
|
end
|
|
|
|
|
2020-05-13 21:08:55 +00:00
|
|
|
context 'when repository creation fails with invalid file name' do
|
|
|
|
let(:extra_opts) { { file_name: 'invalid://file/name/here' } }
|
|
|
|
|
|
|
|
it 'returns an appropriate error' do
|
|
|
|
expect(snippet.errors[:repository]).to eq ['Error creating the snippet - Invalid file name']
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-02-28 18:09:07 +00:00
|
|
|
context 'when the commit action fails' do
|
|
|
|
before do
|
|
|
|
allow_next_instance_of(SnippetRepository) do |instance|
|
|
|
|
allow(instance).to receive(:multi_files_action).and_raise(SnippetRepository::CommitError.new('foobar'))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not create the snippet' do
|
|
|
|
expect { subject }.not_to change { Snippet.count }
|
|
|
|
end
|
|
|
|
|
2020-03-10 12:08:16 +00:00
|
|
|
it 'destroys the created repository' do
|
|
|
|
expect_next_instance_of(Repository) do |instance|
|
|
|
|
expect(instance).to receive(:remove).and_call_original
|
|
|
|
end
|
2020-02-28 18:09:07 +00:00
|
|
|
|
|
|
|
subject
|
|
|
|
end
|
2020-03-23 12:09:47 +00:00
|
|
|
|
|
|
|
it 'destroys the snippet_repository' do
|
|
|
|
subject
|
|
|
|
|
|
|
|
expect(SnippetRepository.count).to be_zero
|
|
|
|
end
|
2020-03-23 21:09:46 +00:00
|
|
|
|
|
|
|
it 'logs the error' do
|
|
|
|
expect(Gitlab::AppLogger).to receive(:error).with('foobar')
|
|
|
|
|
|
|
|
subject
|
|
|
|
end
|
2020-02-28 18:09:07 +00:00
|
|
|
|
2020-05-13 21:08:55 +00:00
|
|
|
it 'returns a generic error' do
|
2020-02-28 18:09:07 +00:00
|
|
|
response = subject
|
|
|
|
|
|
|
|
expect(response).to be_error
|
2020-05-13 21:08:55 +00:00
|
|
|
expect(response.payload[:snippet].errors[:repository]).to eq ['Error creating the snippet']
|
2020-02-28 18:09:07 +00:00
|
|
|
end
|
2020-02-17 12:09:20 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
context 'when snippet creation fails' do
|
|
|
|
let(:extra_opts) { { content: nil } }
|
|
|
|
|
|
|
|
it 'does not create repository' do
|
2020-02-28 18:09:07 +00:00
|
|
|
expect do
|
|
|
|
subject
|
|
|
|
end.not_to change(Snippet, :count)
|
2020-02-17 12:09:20 +00:00
|
|
|
|
|
|
|
expect(snippet.repository_exists?).to be_falsey
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-04-22 12:09:29 +00:00
|
|
|
shared_examples 'after_save callback to store_mentions' do |mentionable_class|
|
2020-04-21 15:21:10 +00:00
|
|
|
context 'when mentionable attributes change' do
|
|
|
|
let(:extra_opts) { { description: "Description with #{user.to_reference}" } }
|
|
|
|
|
|
|
|
it 'saves mentions' do
|
2020-04-22 12:09:29 +00:00
|
|
|
expect_next_instance_of(mentionable_class) do |instance|
|
2020-04-21 15:21:10 +00:00
|
|
|
expect(instance).to receive(:store_mentions!).and_call_original
|
|
|
|
end
|
|
|
|
expect(snippet.user_mentions.count).to eq 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when mentionable attributes do not change' do
|
|
|
|
it 'does not call store_mentions' do
|
2020-04-22 12:09:29 +00:00
|
|
|
expect_next_instance_of(mentionable_class) do |instance|
|
2020-04-21 15:21:10 +00:00
|
|
|
expect(instance).not_to receive(:store_mentions!)
|
|
|
|
end
|
|
|
|
expect(snippet.user_mentions.count).to eq 0
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when save fails' do
|
|
|
|
it 'does not call store_mentions' do
|
|
|
|
base_opts.delete(:title)
|
|
|
|
|
2020-04-22 12:09:29 +00:00
|
|
|
expect_next_instance_of(mentionable_class) do |instance|
|
2020-04-21 15:21:10 +00:00
|
|
|
expect(instance).not_to receive(:store_mentions!)
|
|
|
|
end
|
|
|
|
expect(snippet.valid?).to be false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-02-28 18:09:07 +00:00
|
|
|
context 'when ProjectSnippet' do
|
2020-01-17 00:09:00 +00:00
|
|
|
let_it_be(:project) { create(:project) }
|
|
|
|
|
|
|
|
before do
|
|
|
|
project.add_developer(user)
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'a service that creates a snippet'
|
|
|
|
it_behaves_like 'public visibility level restrictions apply'
|
|
|
|
it_behaves_like 'spam check is performed'
|
|
|
|
it_behaves_like 'snippet create data is tracked'
|
|
|
|
it_behaves_like 'an error service response when save fails'
|
2020-02-28 18:09:07 +00:00
|
|
|
it_behaves_like 'creates repository and files'
|
2020-04-22 12:09:29 +00:00
|
|
|
it_behaves_like 'after_save callback to store_mentions', ProjectSnippet
|
2020-05-18 06:08:14 +00:00
|
|
|
|
|
|
|
context 'when uploaded files are passed to the service' do
|
|
|
|
let(:extra_opts) { { files: ['foo'] } }
|
|
|
|
|
|
|
|
it 'does not move uploaded files to the snippet' do
|
|
|
|
expect_next_instance_of(described_class) do |instance|
|
|
|
|
expect(instance).to receive(:move_temporary_files).and_call_original
|
|
|
|
end
|
|
|
|
|
|
|
|
expect_any_instance_of(FileMover).not_to receive(:execute)
|
|
|
|
|
|
|
|
subject
|
|
|
|
end
|
|
|
|
end
|
2020-01-17 00:09:00 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
context 'when PersonalSnippet' do
|
|
|
|
let(:project) { nil }
|
|
|
|
|
|
|
|
it_behaves_like 'a service that creates a snippet'
|
|
|
|
it_behaves_like 'public visibility level restrictions apply'
|
|
|
|
it_behaves_like 'spam check is performed'
|
|
|
|
it_behaves_like 'snippet create data is tracked'
|
|
|
|
it_behaves_like 'an error service response when save fails'
|
2020-02-28 18:09:07 +00:00
|
|
|
it_behaves_like 'creates repository and files'
|
2020-04-22 12:09:29 +00:00
|
|
|
it_behaves_like 'after_save callback to store_mentions', PersonalSnippet
|
2020-05-18 06:08:14 +00:00
|
|
|
|
|
|
|
context 'when the snippet description contains files' do
|
|
|
|
include FileMoverHelpers
|
|
|
|
|
|
|
|
let(:title) { 'Title' }
|
|
|
|
let(:picture_secret) { SecureRandom.hex }
|
|
|
|
let(:text_secret) { SecureRandom.hex }
|
|
|
|
let(:picture_file) { "/-/system/user/#{creator.id}/#{picture_secret}/picture.jpg" }
|
|
|
|
let(:text_file) { "/-/system/user/#{creator.id}/#{text_secret}/text.txt" }
|
|
|
|
let(:files) { [picture_file, text_file] }
|
|
|
|
let(:description) do
|
|
|
|
"Description with picture: ![picture](/uploads#{picture_file}) and "\
|
|
|
|
"text: [text.txt](/uploads#{text_file})"
|
|
|
|
end
|
|
|
|
|
|
|
|
before do
|
|
|
|
allow(FileUtils).to receive(:mkdir_p)
|
|
|
|
allow(FileUtils).to receive(:move)
|
|
|
|
end
|
|
|
|
|
|
|
|
let(:extra_opts) { { description: description, title: title, files: files } }
|
|
|
|
|
|
|
|
it 'stores the snippet description correctly' do
|
|
|
|
stub_file_mover(text_file)
|
|
|
|
stub_file_mover(picture_file)
|
|
|
|
|
|
|
|
snippet = subject.payload[:snippet]
|
|
|
|
|
|
|
|
expected_description = "Description with picture: "\
|
|
|
|
"![picture](/uploads/-/system/personal_snippet/#{snippet.id}/#{picture_secret}/picture.jpg) and "\
|
|
|
|
"text: [text.txt](/uploads/-/system/personal_snippet/#{snippet.id}/#{text_secret}/text.txt)"
|
|
|
|
|
|
|
|
expect(snippet.description).to eq(expected_description)
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when there is a validation error' do
|
|
|
|
let(:title) { nil }
|
|
|
|
|
|
|
|
it 'does not move uploaded files to the snippet' do
|
|
|
|
expect_any_instance_of(described_class).not_to receive(:move_temporary_files)
|
|
|
|
|
|
|
|
subject
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2020-01-17 00:09:00 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|