# frozen_string_literal: true require 'spec_helper' RSpec.describe Projects::CompareController, feature_category: :source_code_management do include ProjectForksHelper using RSpec::Parameterized::TableSyntax let_it_be(:project) { create(:project, :repository, :public) } let_it_be(:user) { create(:user) } let(:private_fork) { fork_project(project, nil, repository: true).tap { |fork| fork.update!(visibility: 'private') } } let(:public_fork) do fork_project(project, nil, repository: true).tap do |fork| fork.update!(visibility: 'public') # Create a reference that only exists in this project fork.repository.create_ref('refs/heads/improve/awesome', 'refs/heads/improve/more-awesome') end end before do sign_in(user) project.add_maintainer(user) end describe 'GET index' do let(:params) { { namespace_id: project.namespace, project_id: project } } render_views before do get :index, params: params end it 'returns successfully' do expect(response).to be_successful end context 'with incorrect parameters' do let(:params) { super().merge(from: { invalid: :param }, to: { also: :invalid }) } it 'returns successfully' do expect(response).to be_successful end end context 'with missing parameters' do let(:params) { super().merge(from: '', to: '') } it 'returns successfully' do expect(response).to be_successful end end end describe 'GET show' do render_views subject(:show_request) { get :show, params: request_params } let(:request_params) do { namespace_id: project.namespace, project_id: project, from_project_id: from_project_id, from: from_ref, to: to_ref, w: whitespace, page: page, straight: straight } end let(:whitespace) { nil } let(:straight) { nil } let(:page) { nil } context 'when the refs exist in the same project' do context 'when we set the white space param' do let(:from_project_id) { nil } let(:from_ref) { '08f22f25' } let(:to_ref) { '66eceea0' } let(:whitespace) { 1 } it 'shows some diffs with ignore whitespace change option' do show_request expect(response).to be_successful diff_file = assigns(:diffs).diff_files.first expect(diff_file).not_to be_nil expect(assigns(:commits).length).to be >= 1 # without whitespace option, there are more than 2 diff_splits diff_splits = diff_file.diff.diff.split("\n") expect(diff_splits.length).to be <= 2 end end context 'when we do not set the white space param' do let(:from_project_id) { nil } let(:from_ref) { 'improve%2Fawesome' } let(:to_ref) { 'feature' } let(:whitespace) { nil } it 'sets the diffs and commits ivars' do show_request expect(response).to be_successful expect(assigns(:diffs).diff_files.first).not_to be_nil expect(assigns(:commits).length).to be >= 1 end end end context 'when refs have CI::Pipeline' do let(:from_project_id) { nil } let(:from_ref) { '08f22f25' } let(:to_ref) { '59e29889' } before do create(:ci_pipeline, project: project) end it 'avoids N+1 queries' do control = ActiveRecord::QueryRecorder.new { show_request } # Only 1 query to ci/pipeline.rb is allowed expect(control.find_query(/pipeline\.rb/, 1)).to be_empty end end context 'when the refs exist in different projects that the user can see' do let(:from_project_id) { public_fork.id } let(:from_ref) { 'improve%2Fmore-awesome' } let(:to_ref) { 'feature' } let(:whitespace) { nil } it 'shows the diff' do show_request expect(response).to be_successful expect(assigns(:diffs).diff_files.first).not_to be_nil expect(assigns(:commits).length).to be >= 1 end end context 'when comparing missing commits between source and target' do let(:from_project_id) { nil } let(:from_ref) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } let(:to_ref) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' } let(:page) { 1 } context 'when comparing them in the other direction' do let(:straight) { "false" } let(:from_ref) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' } let(:to_ref) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } it 'the commits are there' do show_request expect(response).to be_successful expect(assigns(:commits).length).to be >= 2 expect(assigns(:diffs).raw_diff_files.size).to be >= 2 expect(assigns(:diffs).diff_files.first).to be_present end end context 'with straight mode true' do let(:from_ref) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } let(:to_ref) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' } let(:straight) { "true" } it 'the commits are empty, but the removed lines are visible as diffs' do show_request expect(response).to be_successful expect(assigns(:commits).length).to be == 0 expect(assigns(:diffs).diff_files.size).to be >= 4 end end context 'with straight mode false' do let(:from_ref) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } let(:to_ref) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' } let(:straight) { "false" } it 'the additional commits are not visible in diffs and commits' do show_request expect(response).to be_successful expect(assigns(:commits).length).to be == 0 expect(assigns(:diffs).diff_files.size).to be == 0 end end end context 'when the refs exist in different projects but the user cannot see' do let(:from_project_id) { private_fork.id } let(:from_ref) { 'improve%2Fmore-awesome' } let(:to_ref) { 'feature' } let(:whitespace) { nil } it 'does not show the diff' do show_request expect(response).to be_successful expect(assigns(:diffs)).to be_empty expect(assigns(:commits)).to be_empty end end context 'when the target project is the default source but hidden to the user' do let(:project) { create(:project, :repository, :private) } let(:from_ref) { 'improve%2Fmore-awesome' } let(:to_ref) { 'feature' } let(:whitespace) { nil } let(:request_params) do { namespace_id: project.namespace, project_id: project, from: from_ref, to: to_ref, w: whitespace, page: page, straight: straight } end it 'does not show the diff' do allow(controller).to receive(:source_project).and_return(project) expect(project).to receive(:default_merge_request_target).and_return(private_fork) show_request expect(response).to be_successful expect(assigns(:diffs)).to be_empty expect(assigns(:commits)).to be_empty end end context 'when the source ref does not exist' do let(:from_project_id) { nil } let(:from_ref) { 'non-existent-source-ref' } let(:to_ref) { 'feature' } it 'sets empty diff and commit ivars' do show_request expect(response).to be_successful expect(assigns(:diffs)).to eq([]) expect(assigns(:commits)).to eq([]) end end context 'when the target ref does not exist' do let(:from_project_id) { nil } let(:from_ref) { 'improve%2Fawesome' } let(:to_ref) { 'non-existent-target-ref' } it 'sets empty diff and commit ivars' do show_request expect(response).to be_successful expect(assigns(:diffs)).to eq([]) expect(assigns(:commits)).to eq([]) end end context 'when the target ref is invalid' do let(:from_project_id) { nil } let(:from_ref) { 'improve%2Fawesome' } let(:to_ref) { "master%' AND 2554=4423 AND '%'='" } it 'shows a flash message and redirects' do show_request expect(flash[:alert]).to eq("Invalid branch name(s): master%' AND 2554=4423 AND '%'='") expect(response).to have_gitlab_http_status(:found) end end context 'when the source ref is invalid' do let(:from_project_id) { nil } let(:from_ref) { "master%' AND 2554=4423 AND '%'='" } let(:to_ref) { 'improve%2Fawesome' } it 'shows a flash message and redirects' do show_request expect(flash[:alert]).to eq("Invalid branch name(s): master%' AND 2554=4423 AND '%'='") expect(response).to have_gitlab_http_status(:found) end end context 'when the both refs are invalid' do let(:from_project_id) { nil } let(:from_ref) { "master%' AND 2554=4423 AND '%'='" } let(:to_ref) { "improve%' =,awesome" } it 'shows a flash message and redirects' do show_request expect(flash[:alert]).to eq("Invalid branch name(s): improve%' =,awesome, master%' AND 2554=4423 AND '%'='") expect(response).to have_gitlab_http_status(:found) end end context 'when page is valid' do let(:from_project_id) { nil } let(:from_ref) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' } let(:to_ref) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } let(:page) { 1 } shared_examples 'valid compare page' do it 'shows the diff' do show_request expect(response).to be_successful expect(assigns(:diffs).diff_files.first).to be_present expect(assigns(:commits).length).to be >= 1 end end it_behaves_like 'valid compare page' it 'only loads blobs in the current page' do stub_const('Projects::CompareController::COMMIT_DIFFS_PER_PAGE', 1) expect_next_instance_of(Repository) do |repository| # This comparison contains 4 changed files but we expect only the blobs for the first one to be loaded expect(repository).to receive(:blobs_at).with( contain_exactly([from_ref, '.gitmodules'], [to_ref, '.gitmodules']), anything ).and_call_original end show_request expect(response).to be_successful end context 'when from_ref is HEAD ref' do let(:from_ref) { 'HEAD' } let(:to_ref) { 'feature' } # Need to change to_ref too so there's something to compare with HEAD it_behaves_like 'valid compare page' end context 'when to_ref is HEAD ref' do let(:to_ref) { 'HEAD' } it_behaves_like 'valid compare page' end end context 'when page is not valid' do let(:from_project_id) { nil } let(:from_ref) { '08f22f25' } let(:to_ref) { '66eceea0' } let(:page) { ['invalid'] } it 'does not return an error' do show_request expect(response).to be_successful end end end describe 'GET diff_for_path' do subject(:diff_for_path_request) { get :diff_for_path, params: request_params } let(:request_params) do { from_project_id: from_project_id, from: from_ref, to: to_ref, namespace_id: project.namespace, project_id: project, old_path: old_path, new_path: new_path } end let(:existing_path) { 'files/ruby/feature.rb' } let(:from_project_id) { nil } let(:from_ref) { 'improve%2Fawesome' } let(:to_ref) { 'feature' } let(:old_path) { existing_path } let(:new_path) { existing_path } context 'when the source and target refs exist in the same project' do context 'when the user has access target the project' do context 'when the path exists in the diff' do it 'disables diff notes' do diff_for_path_request expect(assigns(:diff_notes_disabled)).to be_truthy end it 'only renders the diffs for the path given' do expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) meth.call(diffs) end diff_for_path_request end end context 'when the path does not exist in the diff' do let(:old_path) { existing_path.succ } let(:new_path) { existing_path.succ } it 'returns a 404' do diff_for_path_request expect(response).to have_gitlab_http_status(:not_found) end end end context 'when the user does not have access target the project' do before do project.team.truncate end it 'returns a 404' do diff_for_path_request expect(response).to have_gitlab_http_status(:not_found) end end end context 'when the source and target refs exist in different projects and the user can see' do let(:from_project_id) { public_fork.id } let(:from_ref) { 'improve%2Fmore-awesome' } it 'shows the diff for that path' do expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) meth.call(diffs) end diff_for_path_request end end context 'when the source and target refs exist in different projects and the user cannot see' do let(:from_project_id) { private_fork.id } it 'does not show the diff for that path' do diff_for_path_request expect(response).to have_gitlab_http_status(:not_found) end end context 'when the source ref does not exist' do let(:from_ref) { 'this-ref-does-not-exist' } it 'returns a 404' do diff_for_path_request expect(response).to have_gitlab_http_status(:not_found) end end context 'when the target ref does not exist' do let(:to_ref) { 'this-ref-does-not-exist' } it 'returns a 404' do diff_for_path_request expect(response).to have_gitlab_http_status(:not_found) end end end describe 'POST create' do subject(:create_request) { post :create, params: request_params } let(:request_params) do { namespace_id: project.namespace, project_id: project, from_project_id: from_project_id, from: from_ref, to: to_ref } end context 'when sending valid params' do let(:from_ref) { 'awesome%2Ffeature' } let(:to_ref) { 'feature' } context 'without a from_project_id' do let(:from_project_id) { nil } it 'redirects to the show page' do create_request expect(response).to redirect_to(project_compare_path(project, from: from_ref, to: to_ref)) end end context 'with a from_project_id' do let(:from_project_id) { 'something or another' } it 'redirects to the show page without interpreting from_project_id' do create_request expect(response).to redirect_to(project_compare_path(project, from: from_ref, to: to_ref, from_project_id: from_project_id)) end end end context 'when sending invalid params' do where(:from_ref, :to_ref, :from_project_id, :expected_redirect_params) do '' | '' | '' | {} 'main' | '' | '' | { from: 'main' } '' | 'main' | '' | { to: 'main' } '' | '' | '1' | { from_project_id: 1 } 'main' | '' | '1' | { from: 'main', from_project_id: 1 } '' | 'main' | '1' | { to: 'main', from_project_id: 1 } ['a'] | ['b'] | ['c'] | {} end with_them do let(:expected_redirect) { project_compare_index_path(project, expected_redirect_params) } it 'redirects back to the index' do create_request expect(response).to redirect_to(expected_redirect) end end end end describe 'GET signatures' do subject(:signatures_request) { get :signatures, params: request_params } let(:request_params) do { namespace_id: project.namespace, project_id: project, from: from_ref, to: to_ref, straight: straight, format: :json } end let(:straight) { nil } context 'when the source and target refs exist' do let(:from_ref) { 'improve%2Fawesome' } let(:to_ref) { 'feature' } context 'when the user has access to the project' do render_views let(:signature_commit) { project.commit_by(oid: '0b4bc9a49b562e85de7cc9e834518ea6828729b9') } let(:non_signature_commit) { build(:commit, project: project, safe_message: "message", sha: 'non_signature_commit') } before do escaped_from_ref = Addressable::URI.unescape(from_ref) escaped_to_ref = Addressable::URI.unescape(to_ref) compare_service = CompareService.new(project, escaped_to_ref) compare = compare_service.execute(project, escaped_from_ref, straight: false) expect(CompareService).to receive(:new).with(project, escaped_to_ref).and_return(compare_service) expect(compare_service).to receive(:execute).with(project, escaped_from_ref, straight: false).and_return(compare) expect(compare).to receive(:commits).and_return(CommitCollection.new(project, [signature_commit, non_signature_commit])) expect(non_signature_commit).to receive(:has_signature?).and_return(false) end it 'returns only the commit with a signature' do signatures_request expect(response).to have_gitlab_http_status(:ok) signatures = json_response['signatures'] expect(signatures.size).to eq(1) expect(signatures.first['commit_sha']).to eq(signature_commit.sha) expect(signatures.first['html']).to be_present end end context 'when the user has access to the project with straight compare' do render_views let(:signature_commit) { project.commit_by(oid: '0b4bc9a49b562e85de7cc9e834518ea6828729b9') } let(:non_signature_commit) { build(:commit, project: project, safe_message: "message", sha: 'non_signature_commit') } let(:straight) { "true" } before do escaped_from_ref = Addressable::URI.unescape(from_ref) escaped_to_ref = Addressable::URI.unescape(to_ref) compare_service = CompareService.new(project, escaped_to_ref) compare = compare_service.execute(project, escaped_from_ref) expect(CompareService).to receive(:new).with(project, escaped_to_ref).and_return(compare_service) expect(compare_service).to receive(:execute).with(project, escaped_from_ref, straight: true).and_return(compare) expect(compare).to receive(:commits).and_return(CommitCollection.new(project, [signature_commit, non_signature_commit])) expect(non_signature_commit).to receive(:has_signature?).and_return(false) end it 'returns only the commit with a signature' do signatures_request expect(response).to have_gitlab_http_status(:ok) signatures = json_response['signatures'] expect(signatures.size).to eq(1) expect(signatures.first['commit_sha']).to eq(signature_commit.sha) expect(signatures.first['html']).to be_present end end context 'when the user does not have access to the project', :sidekiq_inline do before do project.team.truncate project.update!(visibility: 'private') end it 'returns a 404' do signatures_request expect(response).to have_gitlab_http_status(:not_found) end end end context 'when the source ref does not exist' do let(:from_ref) { 'non-existent-ref-source' } let(:to_ref) { 'feature' } it 'returns no signatures' do signatures_request expect(response).to have_gitlab_http_status(:ok) expect(json_response['signatures']).to be_empty end end context 'when the target ref does not exist' do let(:from_ref) { 'improve%2Fawesome' } let(:to_ref) { 'non-existent-ref-target' } it 'returns no signatures' do signatures_request expect(response).to have_gitlab_http_status(:ok) expect(json_response['signatures']).to be_empty end end end end