# frozen_string_literal: true require 'spec_helper' describe Suggestions::ApplyService do include ProjectForksHelper def build_position(args = {}) default_args = { old_path: "files/ruby/popen.rb", new_path: "files/ruby/popen.rb", old_line: nil, new_line: 9, diff_refs: merge_request.diff_refs } Gitlab::Diff::Position.new(default_args.merge(args)) end shared_examples 'successfully creates commit and updates suggestion' do def apply(suggestion) result = subject.execute(suggestion) expect(result[:status]).to eq(:success) end it 'updates the file with the new contents' do apply(suggestion) blob = project.repository.blob_at_branch(merge_request.source_branch, position.new_path) expect(blob.data).to eq(expected_content) end it 'updates suggestion applied and commit_id columns' do expect { apply(suggestion) } .to change(suggestion, :applied) .from(false).to(true) .and change(suggestion, :commit_id) .from(nil) end it 'created commit has users email and name' do apply(suggestion) commit = project.repository.commit expect(user.commit_email).not_to eq(user.email) expect(commit.author_email).to eq(user.commit_email) expect(commit.committer_email).to eq(user.commit_email) expect(commit.author_name).to eq(user.name) end context 'when a custom suggestion commit message' do before do project.update!(suggestion_commit_message: message) apply(suggestion) end context 'is not specified' do let(:expected_value) { "Apply suggestion to files/ruby/popen.rb" } context 'is nil' do let(:message) { nil } it 'sets default commit message' do expect(project.repository.commit.message).to eq(expected_value) end end context 'is an empty string' do let(:message) { '' } it 'sets default commit message' do expect(project.repository.commit.message).to eq(expected_value) end end end context 'is specified' do let(:message) { 'refactor: %{project_path} %{project_name} %{file_path} %{branch_name} %{username} %{user_full_name}' } it 'sets custom commit message' do expect(project.repository.commit.message).to eq("refactor: project-1 Project_1 files/ruby/popen.rb master test.user Test User") end end end end let(:project) { create(:project, :repository, path: 'project-1', name: 'Project_1') } let(:user) { create(:user, :commit_email, name: 'Test User', username: 'test.user') } let(:position) { build_position } let(:diff_note) do create(:diff_note_on_merge_request, noteable: merge_request, position: position, project: project) end let(:suggestion) do create(:suggestion, :content_from_repo, note: diff_note, to_content: " raise RuntimeError, 'Explosion'\n # explosion?\n") end subject { described_class.new(user) } context 'patch is appliable' do let(:expected_content) do <<-CONTENT.strip_heredoc require 'fileutils' require 'open3' module Popen extend self def popen(cmd, path=nil) unless cmd.is_a?(Array) raise RuntimeError, 'Explosion' # explosion? end path ||= Dir.pwd vars = { "PWD" => path } options = { chdir: path } unless File.directory?(path) FileUtils.mkdir_p(path) end @cmd_output = "" @cmd_status = 0 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| @cmd_output << stdout.read @cmd_output << stderr.read @cmd_status = wait_thr.value.exitstatus end return @cmd_output, @cmd_status end end CONTENT end context 'non-fork project' do let(:merge_request) do create(:merge_request, source_project: project, target_project: project, source_branch: 'master') end before do project.add_maintainer(user) end it_behaves_like 'successfully creates commit and updates suggestion' context 'when it fails to apply because the file was changed' do it 'returns error message' do service = instance_double(Files::UpdateService) expect(Files::UpdateService).to receive(:new) .and_return(service) allow(service).to receive(:execute) .and_raise(Files::UpdateService::FileChangedError) result = subject.execute(suggestion) expect(result).to eq(message: 'The file has been changed', status: :error) end end context 'when HEAD from position is different from source branch HEAD on repo' do it 'returns error message' do allow(suggestion).to receive(:appliable?) { true } allow(suggestion.position).to receive(:head_sha) { 'old-sha' } allow(suggestion.noteable).to receive(:source_branch_sha) { 'new-sha' } result = subject.execute(suggestion) expect(result).to eq(message: 'The file has been changed', status: :error) end end context 'multiple suggestions applied' do let(:expected_content) do <<-CONTENT.strip_heredoc require 'fileutils' require 'open3' module Popen extend self def popen(cmd, path=nil) unless cmd.is_a?(Array) # v1 change end path ||= Dir.pwd # v1 change vars = { "PWD" => path } options = { chdir: path } # v2 change unless File.directory?(path) FileUtils.mkdir_p(path) end @cmd_output = "" # v2 change Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| @cmd_output << stdout.read @cmd_output << stderr.read @cmd_status = wait_thr.value.exitstatus end return @cmd_output, @cmd_status end end CONTENT end def create_suggestion(diff, old_line: nil, new_line: nil, from_content:, to_content:, path:) position = Gitlab::Diff::Position.new(old_path: path, new_path: path, old_line: old_line, new_line: new_line, diff_refs: diff.diff_refs) suggestion_note = create(:diff_note_on_merge_request, noteable: merge_request, original_position: position, position: position, project: project) create(:suggestion, note: suggestion_note, from_content: from_content, to_content: to_content) end def apply_suggestion(suggestion) suggestion.reload merge_request.reload merge_request.clear_memoized_shas result = subject.execute(suggestion) expect(result[:status]).to eq(:success) refresh = MergeRequests::RefreshService.new(project, user) refresh.execute(merge_request.diff_head_sha, suggestion.commit_id, merge_request.source_branch_ref) result end def fetch_raw_diff(suggestion) project.reload.commit(suggestion.commit_id).diffs.diff_files.first.diff.diff end it 'applies multiple suggestions in subsequent versions correctly' do diff = merge_request.merge_request_diff path = 'files/ruby/popen.rb' suggestion_1_changes = { old_line: nil, new_line: 13, from_content: "\n", to_content: "# v1 change\n", path: path } suggestion_2_changes = { old_line: 24, new_line: 31, from_content: " @cmd_output << stderr.read\n", to_content: "# v2 change\n", path: path } suggestion_1 = create_suggestion(diff, suggestion_1_changes) suggestion_2 = create_suggestion(diff, suggestion_2_changes) apply_suggestion(suggestion_1) suggestion_1_diff = fetch_raw_diff(suggestion_1) # rubocop: disable Layout/TrailingWhitespace expected_suggestion_1_diff = <<-CONTENT.strip_heredoc @@ -10,7 +10,7 @@ module Popen end path ||= Dir.pwd - +# v1 change vars = { "PWD" => path } CONTENT # rubocop: enable Layout/TrailingWhitespace apply_suggestion(suggestion_2) suggestion_2_diff = fetch_raw_diff(suggestion_2) # rubocop: disable Layout/TrailingWhitespace expected_suggestion_2_diff = <<-CONTENT.strip_heredoc @@ -28,7 +28,7 @@ module Popen Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| @cmd_output << stdout.read - @cmd_output << stderr.read +# v2 change @cmd_status = wait_thr.value.exitstatus end CONTENT # rubocop: enable Layout/TrailingWhitespace expect(suggestion_1_diff.strip).to eq(expected_suggestion_1_diff.strip) expect(suggestion_2_diff.strip).to eq(expected_suggestion_2_diff.strip) end end context 'multi-line suggestion' do let(:expected_content) do <<~CONTENT require 'fileutils' require 'open3' module Popen extend self # multi # line vars = { "PWD" => path } options = { chdir: path } unless File.directory?(path) FileUtils.mkdir_p(path) end @cmd_output = "" @cmd_status = 0 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| @cmd_output << stdout.read @cmd_output << stderr.read @cmd_status = wait_thr.value.exitstatus end return @cmd_output, @cmd_status end end CONTENT end let(:suggestion) do create(:suggestion, :content_from_repo, note: diff_note, lines_above: 2, lines_below: 3, to_content: "# multi\n# line\n") end it_behaves_like 'successfully creates commit and updates suggestion' end context 'remove an empty line suggestion' do let(:expected_content) do <<~CONTENT require 'fileutils' require 'open3' module Popen extend self def popen(cmd, path=nil) unless cmd.is_a?(Array) raise RuntimeError, "System commands must be given as an array of strings" end path ||= Dir.pwd vars = { "PWD" => path } options = { chdir: path } unless File.directory?(path) FileUtils.mkdir_p(path) end @cmd_output = "" @cmd_status = 0 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| @cmd_output << stdout.read @cmd_output << stderr.read @cmd_status = wait_thr.value.exitstatus end return @cmd_output, @cmd_status end end CONTENT end let(:position) { build_position(new_line: 13) } let(:suggestion) do create(:suggestion, :content_from_repo, note: diff_note, to_content: "") end it_behaves_like 'successfully creates commit and updates suggestion' end end context 'fork-project' do let(:project) { create(:project, :public, :repository) } let(:forked_project) do fork_project_with_submodules(project, user, repository: project.repository) end let(:merge_request) do create(:merge_request, source_branch: 'conflict-resolvable-fork', source_project: forked_project, target_branch: 'conflict-start', target_project: project) end let!(:diff_note) do create(:diff_note_on_merge_request, noteable: merge_request, position: position, project: project) end before do project.add_maintainer(user) end it 'updates file in the source project' do expect(Files::UpdateService).to receive(:new) .with(merge_request.source_project, user, anything) .and_call_original subject.execute(suggestion) end end end context 'no permission' do let(:merge_request) do create(:merge_request, source_project: project, target_project: project) end let(:diff_note) do create(:diff_note_on_merge_request, noteable: merge_request, position: position, project: project) end context 'user cannot write in project repo' do before do project.add_reporter(user) end it 'returns error' do result = subject.execute(suggestion) expect(result).to eq(message: "You are not allowed to push into this branch", status: :error) end end end context 'patch is not appliable' do let(:merge_request) do create(:merge_request, source_project: project, target_project: project) end let(:diff_note) do create(:diff_note_on_merge_request, noteable: merge_request, position: position, project: project) end before do project.add_maintainer(user) end context 'diff file was not found' do it 'returns error message' do expect(suggestion.note).to receive(:latest_diff_file) { nil } result = subject.execute(suggestion) expect(result).to eq(message: 'Suggestion is not appliable', status: :error) end end context 'suggestion is eligible to be outdated' do it 'returns error message' do expect(suggestion).to receive(:outdated?) { true } result = subject.execute(suggestion) expect(result).to eq(message: 'Suggestion is not appliable', status: :error) end end context 'suggestion was already applied' do it 'returns success status' do result = subject.execute(suggestion) expect(result[:status]).to eq(:success) end end context 'note is outdated' do before do allow(diff_note).to receive(:active?) { false } end it 'returns error message' do result = subject.execute(suggestion) expect(result).to eq(message: 'Suggestion is not appliable', status: :error) end end context 'suggestion was already applied' do before do suggestion.update!(applied: true, commit_id: 'sha') end it 'returns error message' do result = subject.execute(suggestion) expect(result).to eq(message: 'Suggestion is not appliable', status: :error) end end end end